tmcomm驱动程序,被标记为“TrendMicro通用模块”和“TrendMicro Eyes”,快速浏览一下驱动程序,就会发现它会与来自特权用户模式应用程序进行通信,并执行与Rootkit Remover本身无关的常见操作。这个驱动程序不仅在Rootkit Buster中使用,而且在趋势科技的产品中都会发生。
接下来,我们将深入研究tmcomm驱动程序。本文,我们将集中研究寻找不同的方法来滥用驱动程序的功能,最终目标是能够执行内核代码。
TrendMicro通用模块(tmcomm.sys)
通过对对驱动程序的一个非常简短的检查,我们发现它允许特权用户模式应用程序进行通信。
驱动程序采取的第一个操作是创建一个从用户模式接受IOCTL通信的设备,驱动程序在路径\Device\TmComm上创建一个设备,并在\DosDevices\TmComm上创建一个到该设备的符号链接(可以通过\\.\Global\TmComm访问)。驱动程序在入口点初始化了整个驱动程序中使用的大量类和结构,但是,没有必要覆盖每个类和结构。
我很高兴地看到趋势科技作出了正确的决定,即限制他们设备的系统用户和管理员权限。这意味着,我们找到了可利用的代码,因为任何通信至少都需要管理特权。例如,Microsoft本身并不认为管理员对内核的限制可以保证安全,因为他们获得了大量访问权限。
TrueApi
驱动程序的一个重要组成部分是其“TrueApi”类,它在驱动程序的入口点会实现实例化。该类包含指向在整个驱动程序中使用的导入函数的指针,以下是一个逆向结构:
struct TrueApi{ BYTE Initialized; PVOID ZwQuerySystemInformation; PVOID ZwCreateFile; PVOID unk1; // Initialized as NULL. PVOID ZwQueryDirectoryFile; PVOID ZwClose; PVOID ZwOpenDirectoryObjectWrapper; PVOID ZwQueryDirectoryObject; PVOID ZwDuplicateObject; PVOID unk2; // Initialized as NULL. PVOID ZwOpenKey; PVOID ZwEnumerateKey; PVOID ZwEnumerateValueKey; PVOID ZwCreateKey; PVOID ZwQueryValueKey; PVOID ZwQueryKey; PVOID ZwDeleteKey; PVOID ZwTerminateProcess; PVOID ZwOpenProcess; PVOID ZwSetValueKey; PVOID ZwDeleteValueKey; PVOID ZwCreateSection; PVOID ZwQueryInformationFile; PVOID ZwSetInformationFile; PVOID ZwMapViewOfSection; PVOID ZwUnmapViewOfSection; PVOID ZwReadFile; PVOID ZwWriteFile; PVOID ZwQuerySecurityObject; PVOID unk3; // Initialized as NULL. PVOID unk4; // Initialized as NULL. PVOID ZwSetSecurityObject;};
查看代码,TrueApi主要用作直接调用函数的替代方法。根据我的推测,趋势科技在初始化时会缓存这些导入的函数,以避开延迟的IAT挂钩。但是,由于TrueApi是通过查看导入表来解析的,因此,如果有一个Rootkit可以将IAT挂在驱动程序载荷上,则此机制是无用的。
XrayApi
与TrueApi类似,XrayApi是驱动程序中的另一个主要类。此类用于访问多个低级设备并直接与文件系统进行交互。 XrayConfig的主要组成部分是其“config”。以下是表示配置数据的部分逆向工程结构:
struct XrayConfigData{ WORD Size; CHAR pad1[2]; DWORD SystemBuildNumber; DWORD UnkOffset1; DWORD UnkOffset2; DWORD UnkOffset3; CHAR pad2[4]; PVOID NotificationEntryIdentifier; PVOID NtoskrnlBase; PVOID IopRootDeviceNode; PVOID PpDevNodeLockTree; PVOID ExInitializeNPagedLookasideListInternal; PVOID ExDeleteNPagedLookasideList; CHAR unkpad3[16]; PVOID KeAcquireInStackQueuedSpinLockAtDpcLevel; PVOID KeReleaseInStackQueuedSpinLockFromDpcLevel; ...};
配置数据将内部/未记录的变量的位置存储在Windows内核中,例如IopRootDeviceNode,PpDevNodeLockTree,ExInitializeNPagedLookasideListInternal和ExDeleteNPagedLookasideList。根据我的经验,此类的目的是直接访问底层设备,而不是使用可能被劫持的有据可查的方法。
IOCTL请求
在了解驱动程序允许我们做什么之前,我们需要了解IOCTL请求是如何处理的。
在主调度函数中,趋势科技的驱动程序将数据连同IRP_MJ_DEVICE_CONTROL请求一起转换为我称为TmIoctlRequest的专有结构。
struct TmIoctlRequest{ DWORD InputSize; DWORD OutputSize; PVOID UserInputBuffer; PVOID UserOutputBuffer; PVOID Unused; DWORD_PTR* BytesWritten;};
IOCTL请求的组织调度的方法是通过几个“调度表”,“基本调度表”只包含一个IOCTL代码和一个相应的“子调度函数”。例如,当你发送带有代码0xDEADBEEF的IOCTL请求时,它将比较此基本调度表的每个项,并在存在具有匹配代码的表项的情况下传递数据。基本表项可以由以下结构表示:
typedef NTSTATUS (__fastcall *DispatchFunction_t)(TmIoctlRequest *IoctlRequest);struct BaseDispatchTableEntry{ DWORD_PTR IOCode; DispatchFunction_t DispatchFunction;};
调用DispatchFunction之后,通常会验证所提供的某些数据,从基本的nullptr检查到检查输入和输出缓冲区的大小。然后,这些“子调度函数”根据在用户输入缓冲区中传递的代码进行另一次查找,以找到对应的“子表项”。子表项可以表示为如下结构:
typedef NTSTATUS (__fastcall *OperationFunction_t)(PVOID InputBuffer, PVOID OutputBuffer);struct SubDispatchTableEntry{ DWORD64 OperationCode; OperationFunction_t PrimaryRoutine; OperationFunction_t ValidatorRoutine;};
在调用primary例程(它实际执行请求的操作)之前,子调度函数调用validator例程。这个例程对输入缓冲区执行“特定于操作”的验证,这意味着它对PrimaryRoutine将使用的数据执行检查。仅当ValidatorRoutine成功返回时,才会调用PrimaryRoutine。
现在,我们对IOCTL请求的处理方式有了基本的了解,下面让我们探讨一下它们允许我们执行的操作。回顾存储“子调度函数”的“基本调度表”的定义,让我们研究每个基本表项,并了解每个子调度表允许我们做什么!
IoControlCode == 9000402Bh
发现过程
第一个调度表似乎与文件系统交互,这实际上意味着“子调度表”项的代码是通过从输入缓冲区开始解引用DWORD获得的。这意味着,要指定要执行的子调度项,只需在输入缓冲区的底部设置一个DWORD,以与该项的**OperationCode**相对应。
为了让发现过程更简单,趋势科技往其中加入了了大量调试字符串,以下是我在子调度表中逆向的函数表,以及它们允许我们执行的操作。
IoControlCode == 90004027h
发现过程
这个调度表主要用于控制驱动程序的进程扫描功能,这个子调度表中的许多函数使用一个单独的扫描线程,通过文档化和未文档化的各种方法同步搜索进程。
这些IOCTL围绕着一些我称为“MicroTask”和“MicroScan”的结构,以下是逆向之后的结构:
struct MicroTaskVtable{ PVOID Constructor; PVOID NewNode; PVOID DeleteNode; PVOID Insert; PVOID InsertAfter; PVOID InsertBefore; PVOID First; PVOID Next; PVOID Remove; PVOID RemoveHead; PVOID RemoveTail; PVOID unk2; PVOID IsEmpty;};struct MicroTask{ MicroTaskVtable* vtable; PVOID self1; // ptr to itself. PVOID self2; // ptr to itself. DWORD_PTR unk1; PVOID MemoryAllocator; PVOID CurrentListItem; PVOID PreviousListItem; DWORD ListSize; DWORD unk4; // Initialized as NULL. char ListName[50];};struct MicroScanVtable{ PVOID Constructor; PVOID GetTask;};struct MicroScan{ MicroScanVtable* vtable; DWORD Tag; // Always 'PANS'. char pad1[4]; DWORD64 TasksSize; MicroTask Tasks[4];};
对于此子调度表中的大多数IOCTL,驱动程序填充的客户端都会传入MicroScan,在下一节中,我们将研究如何滥用这种信任。
逆向过程
当我最初对子分配表中的功能进行逆向工程时,我非常困惑,因为代码“看起来不正确”。它看起来像由诸如GetProcessesAllMethods之类的函数返回的MicroScan内核指针被客户端直接传递给了诸如DeleteTaskResults之类的其他函数。然后,这些函数将使用该不受信任的内核指针,并且在类的基部指定的虚拟函数表中几乎没有验证调用函数。
查看一下DeleteTaskResults子调度表项的“验证例程”,对输入缓冲区+ 0x10指定的MicroScan实例执行的唯一验证是确保它是有效的内核地址。
除了确保提供的指针位于内核内存之外,剩余的其他检查是在DeleteTaskResults中进行简单的检查,以确保微扫描的标记对象是PANS。
因为DeleteTaskResults调用MicroScan实例的虚函数表中指定的构造函数,所以要调用任意的内核函数,它要具有以下功能:
1.能够分配至少10个字节的内核内存(用于vtable和tag);
2.控制分配的内核内存来设置虚拟函数表指针和标记;
3.能够通过用户模式确定此内核内存的地址;
题为《Windows 7中的保留对象》一文讨论了如何使用Windows 7中引入的APC保留对象从用户模式分配可控内核内存。通常的想法是,你可以将Apc放在Apc 保留对象之后,ApcRoutine和ApcArgumentX对象是你在内核内存中需要的数据,然后使用NtQuerySystemInformation在内核内存中查找Apc 保留对象。这个保留对象将在一行中包含前面指定的KAPC变量,允许用户模式应用程序控制多达32字节的内核内存(64位),并知道内核内存的位置。这个技巧在Windows 10中仍然有效,这意味着我们能够满足所有三个要求。通过使用Apc 保留对象,我们可以为扫描后的结构分配至少10个字节,并完全绕过安全检查。调用任意内核指针:
虽然我在DeleteTaskResults中提供了易受攻击的代码的一个特定示例,但是我在表中用星号标记的任何函数都是易受攻击的。它们都信任由不受信任的客户端指定的内核指针,并最终在MicroScan实例的虚拟函数表中调用函数。
IoControlCode == 90004033h
发现过程
下一个子调度表主要管理我们前面讨论过的TrueApi类。
攻击过程
IoControlRegisterUnloadNotify
当我在调试字符串中看到它的名称时,我就注意到了它。使用这个子调度表函数,一个不受信任的客户端可以注册多达16个任意的“卸载例程”,这些例程在驱动程序卸载时被调用。此函数的验证器例程从不受信任的客户端缓冲区检查此指针的有效性。如果调用者来自用户模式,验证器将在不受信任的指针上调用ProbeForRead。如果调用者来自内核模式,验证器将检查它是否是有效的内核内存地址。
此函数不能立即在用户模式的攻击中使用,问题是,如果我们是一个用户模式调用者,我们必须提供一个用户模式指针,因为验证器例程使用ProbeForRead。当驱动程序卸载时,会调用这个用户模式指针,但是由于SMEP之类的缓解措施,它的作用不大。我将在后面的部分中引用这个函数,但是看到一个不受信任的用户模式客户端能够通过设计来指导一个驱动程序调用任意的指针,这确实很可怕。
IoControlCode == 900040DFh
这个子调度表用于与XrayApi交互,尽管内核中实现的扫描通常使用Xray Api,但是这个子调度表为客户机与物理驱动器进行交互提供了有限的访问。
IoControlCode == 900040E7h
发现过程
最后的子调度用于扫描各种系统结构中的挂钩,有趣的是,趋势科技检查了各种挂钩,包括对象类型,主要功能表,甚至函数内联挂钩中的挂钩。
攻击过程
是的,TMXMSCheckSystemObjectByName2看起来很糟糕,在直接看这个函数之前,下面是一些稍后使用的逆向工程结构:
struct CheckSystemObjectParams{ PVOID Src; PVOID Dst; DWORD Size; DWORD* OutSize;};struct TXMSParams{ DWORD OutStatus; DWORD HandlerID; CHAR unk[0x38]; CheckSystemObjectParams* CheckParams;};
TMXMSCheckSystemObjectByName2接受源指针、目标指针和字节大小。TMXMSCheckSystemObjectByName2调用的验证器函数检查以下内容:
关于TXMSParams结构的CheckParams对象的ProbeForRead。在CheckSystemObjectParams结构的Dst对象上执行ProbeForRead和ProbeForWrite。
事实上,这意味着我们需要传递一个有效的CheckParams结构,我们传递的Dst指针位于用户模式内存中。现在让我们看看函数本身:
虽然for循环看起来很可怕,但它所做的只是检查内核内存范围的优化方法。对于Src到Src + Size范围内的每个内存页,该函数调用MmIsAddressValid。真正复杂的部分是以下操作:
这些行接受一个不受信任的Src指针,并将大小字节复制到不受信任的Dst指针,我们可以使用memmove操作来读取任意内核指针,但是写入任意内核指针呢?问题是TMXMSCheckSystemObjectByName2的验证器要求目标是用户模式内存。幸运的是,代码中还有一个漏洞。
下一个* params-> OutSize = Size;行从我们的结构中获取Size对象,并将其放置在不受信任的OutSize对象指定的指针处。由于没有验证OutSize指向的具体内容,因此我们可以在每个IOCTL调用中写一个DWORD。需要注意的是,Src指针需要指向有效的内核内存,以支持最大字节的大小。为了满足这个需求,我只是将ntoskrnl模块的基础作为源。
使用这个任意的写原语,我们可以使用先前发现的卸载例程技巧来执行代码。尽管从用户模式调用时,验证程序会阻止我们传递内核指针,但实际上并不需要通过验证程序。取而代之的是,我们可以使用写入原语将写入驱动程序.data节内的卸载例程数组,然后放置所需的指针。
暴力执行程序
让我们看看都发生了什么,该函数有一个从0到0x10000的for循环,以4为增量递增,并检索当前索引后面的进程对象(如果有)。如果索引确实与进程匹配,则该函数检查进程的名称是否为crsss.exe。如果该进程名为csrss.exe,则最后检查该进程的会话ID为0。
EPROCESS ImageFileNam偏移
该函数接受当前进程(碰巧是系统进程,因为它是在系统线程中调用的),然后在第一个0x1000字节中搜索字符串“System”。现在的情况是,趋势科技通过在其EPROCESS结构中寻找系统进程的已知名称来强制ImageFileName对象EPROCESS结构。如果你想要一个进程的ImageFileName,只需在ProcessImageFileName类中使用ZwQueryInformationProcess…
EPROCESS Peb偏移
在这个函数中,趋势科技使用csrss进程的PID来强制处理EPROCESS结构的Peb对象。该函数使用PsLookupProcessByProcessId检索csrss进程的EPROCESS对象,使用ZwQueryInformationProcess检索PebBaseAddress。使用这些指针,它尝试从0到0x2000的每个与已知Peb指针匹配的偏移量。
ETHREAD StartAddress偏移
趋势科技在这里使用具有已知起始地址的当前系统线程对ETHREAD结构的StartAddress对象进行暴力破解。
垃圾处理
当我最初对这个函数进行反编译时,我认为IDA Pro可能简化了memset操作,因为这个函数所做的一切就是将所有TrueApi结构对象设置为零。
绕过微软的WHQL认证
到目前为止,还缺少一个具体的操作步骤来安装rootkit。
让我们看看趋势科技是如何分配内存的,在驱动程序的入口点,趋势科技通过检查主版本、次版本和操作系统的构建号来检查设备是否是一个“受支持的系统”。趋势科技这样做是因为他们硬编码了几个可以在不同版本之间改变的偏移量。
幸运的是,默认情况下,用于分配非分页内存的PoolType全局变量设置为0(NonPagedPool)。我注意到,尽管此值最初为0,但该变量仍位于.data节中,这意味着可以对其进行更改。在查看写入变量的内容时,我发现负责检查操作系统版本的同一函数在某些情况下也会设置此PoolType变量。
乍一看,如果我们的操作系统是Windows 10或更高版本,则驱动程序倾向于使用NonPagedPoolNx。从安全角度来看,这是件好事,但对我们却不利。这用于所有非分页的分配,这意味着我们必须找到一个备用的ExAllocatePoolWithTag,它具有一个硬编码的NonPagedPool参数,否则我们将无法在Windows 10上使用驱动程序分配的内存。但是,这并不是那么简单。那么对于这个if语句的第二个要求mystery iouscheck()呢?
在Windows 10上,驱动程序验证程序强制驱动程序不分配可执行内存。趋势科技没有遵守旨在保护Windows用户安全的要求,决定忽略其用户的安全性,将其驱动程序设计为绕过任何会捕获此类违规行为的测试或调试环境。
步骤如下:
1.查找驱动程序分配的任何NonPagedPool,只要你没有运行驱动程序验证程序,就可以使用将其指针存储在.data节中的任何非页面调度分配,最好选择一个不经常使用的分配。
2.使用TMXMSCheckSystemObjectByName2中的任意内核写入原语,将内核Shellcode写入内存分配中的任何位置。
3.通过注册一个卸载例程(直接在.data中)或使用90004027h分配表中提供的其他几种执行方法,执行你的Shellcode。
本文翻译自:https://billdemirkapi.me/How-to-use-Trend-Micro-Rootkit-Remover-to-Install-a-Rootkit/如若转载,请注明原文地址