如何使用Trend Micro的Rootkit Remover安装Rootkit

2020-05-19 01:01:44

在最近的一个项目中,我不得不研究检测rootkit的方法,以及捕捉它们的最有效措施。当我问到这个问题时,我问到,rootkit的现有解决方案是什么,它们是如何工作的?我的搜索最终让我找到了TrendMicro RootkitBuster,它将自己描述为“一个扫描隐藏文件、注册表项、进程、驱动程序和主引导记录(MBR)以识别和删除rootkit的免费工具”。

它吹嘘的特点肯定引起了我的注意。他们声称检测到了几种Rootkit用来将自己钻进机器的技术,但它是如何在引擎盖下工作的,我们会滥用它吗?我决定通过逆向工程找出应用程序本身的核心组件,这至少可以说是把我带入了一个永远给我留下伤疤的兔子洞代码洞中。

开始冒险时,启动应用程序会收到Process Hacker发出的奇妙警告,称已经安装了新的驱动程序。

我们已经有了一个良好的开端,我们得到了一份趋势科技的“共同驱动程序”,这绝对是值得研究的东西。除了该驱动程序正在安装之外,这个友好的窗口也打开了,提示我接受趋势科技的用户协议。

我现在还没有心情把自己的灵魂交给魔鬼,特别是因为条款中包含了一个条款,声明“您同意不尝试对…进行反向工程、反编译、修改、翻译、反汇编、发现源代码或创建派生作品。“。

值得庆幸的是,在我接受任何条款之前,趋势科技已经在我的机器上部署了他们的软件。有趣的是,当我试图通过右键单击应用程序并按下“关闭窗口”来退出该过程时,它完全避开了许可协议,转到扫描仪的主屏幕,尽管我选择了“我不接受许可协议的条款”选项。谢谢趋势科技!

当我启动应用程序时,我注意到一个快速的命令提示符闪烁。事实证明,这是一个7-Zip自解压二进制文件的结果,它将其余的应用程序组件解压到%temp%\RootkitBuster。

Tmcomm驱动程序,标签为“TrendMicro Common Module”和“Trend Micro Eyes”。快速浏览该驱动程序表明,它接受了来自特权用户模式应用程序的通信,并执行了非特定于rootkit Remover本身的常见操作。该驱动程序不仅在Rootkit Buster中使用,而且在趋势科技的整个产品线中都有实施。

在接下来的部分中,我们将深入研究tmcomm驱动程序。我们将把研究重点放在寻找滥用驱动程序功能的不同方法上,最终目标是能够执行内核代码。我决定不检查tmrkb.sys,因为虽然我确定它是易受攻击的,但它似乎只用于rootkit Buster。

让我们从基本驱动程序开始我们的冒险之旅,它似乎不仅用于这个rootkit Remover实用程序,而且还用于其他几个趋势科技产品。正如我在上一节中所说的,非常简单地浏览一下驱动程序就会发现,它确实允许来自特权用户模式应用程序的通信。

驱动程序采取的第一个操作之一是创建一个设备来接受来自用户模式的IOCTL通信。驱动程序在路径\Device\TmComm创建一个设备,并在\DosDevices\TmComm(可通过\\.\global\TmComm访问)创建指向该设备的符号链接。驱动程序入口点初始化了在整个驱动程序中使用的大量类和结构,但是,出于我们的目的,没有必要涵盖每个类和结构。

我很高兴看到趋势科技做出了正确的决定,将他们的设备限制为系统用户和管理员。这意味着,即使我们确实发现了可利用的代码,因为任何通信都至少需要管理权限,所以业界中有相当大一部分人不会认为它是一个漏洞。例如,Microsoft本身并不将管理员到内核视为安全边界,因为他们获得了大量的访问权限。然而,这并不意味着趋势科技驱动程序中的可利用代码将毫无用处。

驱动程序的一个很大的组件是它的“TrueApi”类,它在驱动程序的入口点期间实例化。该类包含指向在整个驱动程序中使用的导入函数的指针。下面是一个颠倒的结构:

struct TrueApi{Byte Initialized;PVOID ZwQuerySystemInformation;PVOID ZwCreateFile;PVOID unk1;//初始化为NULL。PVOID ZwQueryDirectoryFile;PVOID ZwClose;PVOID ZwOpenDirectoryObjectWrapper;PVOID ZwQueryDirectoryObject;PVOID ZwDuplicateObject;PVOID unk2;//初始化为NULL。PVOID ZwOpenKey;PVOID ZwEnumerateKey;PVOID ZwEnumerateValueKey;PVOID ZwCreateKey;PVOID ZwQueryValueKey;PVOID ZwQueryKey;PVOID ZwDeleteKey;PVOID ZwTerminateProcess;PVOID ZwOpenProcess;PVOID ZwSetValueKey;PVOID ZwDeleteValueKey。PVOID ZwQuerySecurityObject;PVOID unk3;//初始化为空PVOID unk4;//初始化为空PVOID ZwSetSecurityObject;};

查看代码,TrueApi主要用作直接调用函数的替代方法。我有根据的猜测是,趋势科技在初始化时缓存这些导入的函数,以避免延迟的IAT挂钩。但是,由于TrueApi是通过查看导入表来解析的,因此如果存在在驱动程序加载时挂钩IAT的rootkit,则此机制是无用的。

与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 ExInitial.。

配置数据存储Windows内核中内部/未记录变量的位置,如IopRootDeviceNode、PpDevNodeLockTree、ExInitializeNPagedLookasideListInternal和ExDeleteNPagedLookasideList。我猜测这门课的目的是直接访问低级设备,而不是使用可能被劫持的有文档记录的方法。

在我们进入驱动程序允许我们做什么之前,我们需要了解IOCTL请求是如何处理的。

在主调度函数中,Trend Micro驱动程序将IRP_MJ_DEVICE_CONTROL请求中的数据转换为我称为TmIoctlRequest的专有结构。

Trend Micro组织IOCTL请求调度的方式是有几个“调度表”。“基本调度表”简单地包含IOCTL代码和相应的“子调度功能”。例如,当您发送代码为0xDEADBEEF的IOCTL请求时,它将比较此基本调度表的每个条目,如果存在具有匹配代码的表条目,则传递数据。基表条目可以由以下结构表示:

在调用DispatchFunction之后,它通常会验证所提供的一些数据,范围从基本的nullptr检查到检查输入和输出缓冲区的大小。然后,这些“子分派函数”根据用户输入缓冲区中传递的代码进行另一次查找,以找到相应的“子表条目”。子表条目可以用下面的结构表示:

在调用实际执行所请求操作的PrimaryRoutine之前,子调度函数调用ValidatorRoutine。此例程在输入缓冲区上执行“特定于操作”的验证,这意味着它对PrimaryRoutine将使用的数据执行检查。只有当ValidatorRoutine成功返回时,才会调用PrimaryRoutine。

现在我们已经基本了解了IOCTL请求是如何处理的,让我们来看看它们允许我们做什么。回顾存储“子调度函数”的“基本调度表”的定义,让我们研究一下每个基表条目,并找出每个子调度表允许我们做什么!

第一个分派表似乎与文件系统交互,但这实际上意味着什么?首先,“子调度表”条目的代码是通过从输入缓冲区的起始处取消引用DWORD来获得的。这意味着要指定要执行哪个子分派条目,只需在输入缓冲区的底部设置一个DWORD,使其与条目的**OperationCode**相对应。

为了让我们的生活更轻松,趋势科技方便地包括了大量的调试字符串,通常可以让我们了解函数的作用。下面是我在此子调度表中颠倒的功能以及它们允许我们执行的操作。

此调度表主要用于控制驱动程序的进程扫描功能。此子调度表中的许多函数使用单独的扫描线程,通过各种方法(有记录的和未记录的)同步搜索进程。

如果系统“受支持”,则返回TRUE(无论它们是否具有用于您的构建的硬编码偏移量)。

这些IOCTL围绕着一些我称之为“微任务”和“微扫描”的结构。以下是反向工程构建的结构:

struct MicroTaskVtable{PVOID构造器;PVOID新节点;PVOID删除节点;PVOID插入;PVOID插入之后;PVOID插入之前;PVOID第一个;PVOID下一个;PVOID移除;PVOID RemoveHead;PVOID RemoveTail;PVOID unk2;PVOID IsEmpty;};struct MicroTask{MicroTaskVtable*vtable;PVOPVOID self2;//ptr自身。DWORD_PTR unk1;PVOID内存分配器;PVOID CurrentListItem;PVOID PreviousListItem;DWORD ListSize;DWORD unk4;//初始化为NULL。字符列表名称[50];};struct MicroScanVtable{PVOID Constructor;PVOID GetTask;};struct MicroScan{MicroScanVtable*vtable;DWORD Tag;//Always';PANS';。字符PAD1[4];DWORD64任务大小;微任务任务[4];};

对于此子调度表中的大多数IOCTL,驱动程序填充的客户端传入一个MicroScan。在下一节中,我们将研究如何滥用这种信任。

当我最初对此子调度表中的函数进行反向工程时,我非常困惑,因为代码“看起来不正确”。它看起来像是客户端将GetProcessesAllMethods等函数返回的MicroScan内核指针直接传递给DeleteTaskResults等其他函数。然后,这些函数将采用这个不受信任的内核指针,并且在类的基础上指定的虚拟函数表中几乎没有验证调用函数。

查看DeleteTaskResults子分派表条目的“验证例程”,在输入缓冲区+0x10处指定的MicroScan实例上执行的唯一验证是确保它是有效的内核地址。

除了确保提供的指针在内核内存中之外,唯一的其他检查是在DeleteTaskResults中进行简单检查,以确保MicroScan的标记成员是PANS。

由于DeleteTaskResults调用在MicroScan实例的虚拟函数表中指定的构造函数,要调用任意内核函数,我们需要:

能够分配至少10字节的内核内存(用于vtable和tag)。

控制分配的内核内存来设置虚拟函数表指针和标签。

幸运的是,当涉及到从用户模式分配和控制内核内存时,我的一位导师Alex Ionescu能够为我指明正确的方向。2010年的一份HackInTheBox杂志上有一篇由Matthew Jurczyk撰写的名为“在Windows7中保留对象”的文章。本文讨论了使用Windows7中引入的APC保留对象从用户模式分配可控内核内存。总体思路是,您可以使用ApcRoutine和ApcArgumentX成员作为内核内存中所需的数据将APC排队到APC保留对象,然后使用NtQuerySystemInformation在内核内存中查找APC保留对象。此保留对象将在一行中包含以前指定的KAPC变量,从而允许用户模式应用程序控制多达32字节的内核内存(在64位上),并知道内核内存的位置。如果您想了解更多,我强烈建议您阅读这篇文章。

这个技巧在Windows10中仍然有效,这意味着我们能够满足所有三个要求。通过使用APC保留对象,我们可以为MicroScan结构分配至少10个字节,并完全绕过不充分的检查。结果是什么呢?调用任意内核指针的能力:

虽然我在DeleteTaskResults中提供了一个易受攻击的代码的具体示例,但我在表中用星号标记的任何函数都是易受攻击的。它们都信任不受信任的客户端指定的内核指针,并最终调用MicroScan实例的虚拟函数表中的函数。

当我在调试字符串中看到它的名称时,这个函数引起了我的注意。使用此子调度表函数,不受信任的客户端可以注册多达16个在卸载驱动程序时调用的任意“卸载例程”。此函数的验证器例程检查来自不可信客户端缓冲区的该指针的有效性。如果调用方来自用户模式,则验证器对不受信任的指针调用ProbeForRead。如果调用方来自内核模式,则验证器将检查它是否为有效的内核内存地址。

此函数不能立即用于用户模式下的利用漏洞攻击。问题是,如果我们是用户模式调用者,则必须提供用户模式指针,因为验证器例程使用ProbeForRead。当驱动程序卸载时,将调用此用户模式指针,但由于SMEP等缓解措施,它不会做太多事情。我将在后面的小节中引用此函数,但是看到一个不可信的用户模式客户端能够指示驱动程序调用任意指针的设计真的很可怕。

此子调度表用于与XrayApi交互。虽然Xray Api通常由内核中实现的扫描使用,但此子调度表为客户端与物理驱动器交互提供了有限的访问权限。

最后的子分派用于扫描各种系统结构中的挂钩。看到Trend Micro检查在对象类型、主要函数表甚至函数内联挂钩中包括挂钩的各种挂钩是很有趣的。

是的,TMXMSCheckSystemObjectByName2就像听起来那么糟糕。在直接查看函数之前,下面是后面使用的几个反向工程结构:

struct CheckSystemObjectParams{PVOID Src;PVOID DST;DWORD Size;DWORD*Outsize;};struct TXMSParams{DWORD OutStatus;DWORD HandlerID;Char unk[0x38];CheckSystemObjectParams*CheckParams;};

TMXMSCheckSystemObjectByName2接受源指针、目标指针和字节大小。为TMXMSCheckSystemObjectByName2调用的验证器函数检查以下内容:

本质上,这意味着我们需要传递一个有效的CheckParams结构,并且我们传递的DST指针位于用户模式内存中。现在让我们看一下函数本身:

尽管for循环看起来可能很可怕,但它所做的只是检查一系列内核内存的优化方法。对于Src到Src+Size范围内的每个内存页,该函数调用MmIsAddressValid。真正可怕的部分是以下操作:

这些行采用不受信任的Src指针,并将大小字节复制到不受信任的dst指针…。哎呀。我们可以使用memmove操作来读取任意内核指针,但是写入任意内核指针又如何呢?问题是TMXMSCheckSystemObjectByName2的验证器要求目标是用户模式内存。幸运的是,代码中还有另一个错误。

下一行*params->;outsize=size;从我们的结构中获取size成员,并将其放在由不受信任的outsize成员指定的指针处。没有对特大号指向什么进行验证,因此我们可以在每个IOCTL调用中最多编写一个DWORD。需要注意的是,Src指针需要指向最大字节大小的有效内核内存。为了满足这个要求,我刚刚传递了ntoskrnl模块的基础作为源。

使用这个任意的写原语,我们可以使用前面找到的卸载例程技巧来执行代码。尽管如果我们从用户模式调用,验证器例程会阻止我们传入内核指针,但我们实际上不需要通过验证器。相反,我们可以使用WRITE原语写入驱动程序的.Data部分内的卸载例程数组,并放置所需的指针。

通常情况下,我喜欢在我的博客文章中坚持严格的安全性,但这个驱动程序让我打破了这个传统。在本节中,我们不会讨论驱动程序的安全问题,而是全球数百万趋势科技客户使用的可怕代码。

让我们来看看这里发生了什么。该函数有一个从0到0x10000的for循环,递增4,并检索当前索引后面的进程对象(如果有)。如果索引确实与某个进程匹配,则该函数检查该进程的名称是否为csrss.exe。如果进程名为csrss.exe,则最终检查该进程的会话ID是否为0。来吧,伙计们,确实有记录在案的api可以枚举内核…中的进程。暴力有什么意义?

当我第一次看到这段代码时,我不确定我看到的是什么。该函数接受当前进程(恰好是系统进程),因为这是在系统线程中调用的,然后它在第一个0x1000字节中搜索字符串“system”。现在发生的是…。Trend Micro通过在其EPROCESS结构中查找系统进程的已知名称,对EPROCESS结构的ImageFileName成员进行暴力攻击。如果需要进程的ImageFileName,只需使用带有ProcessImageFileName类…的ZwQueryInformationProcess。

在此函数中,Trend Micro使用csrss进程的PID来暴力强制EPROCESS结构的PEB成员。该函数使用PsLookupProcessByProcessId检索csrss进程的EPROCESS对象,并使用ZwQueryInformationProcess检索PebBaseAddress。使用这些指针,它会尝试与已知PEB指针匹配的从0到0x2000的每个偏移量。如果您只能使用ZwQueryInformationProcess,就像您已经使用…一样,那么查找PEB成员的偏移量又有什么意义呢

在这里,Trend Micro使用具有已知起始地址的当前系统线程来暴力强制ETHREAD结构的StartAddress成员。不需要查找原始偏移量的另一种情况。ZwQueryInformationThread有一个半文档化的类,名为ThreadQuerySetWin32StartAddress,它为您提供线程的起始地址。

当我最初反编译此函数时,我认为IDA Pro可能会简化Memset操作,因为此函数所做的一切就是将所有TrueApi结构成员设置为零。我决定带上。

..