今天详细了解一下 APC 到底是怎么工作的。

APC挂入

APC 挂入的时候,内核会准备一个 _KAPC 结构体,将该结构体挂入线程的 APC 队列中。

KAPC结构体介绍

先在 windbg 中查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kd> dt _KAPC
ntdll!_KAPC
+0x000 Type : UChar
+0x001 SpareByte0 : UChar
+0x002 Size : UChar
+0x003 SpareByte1 : UChar
+0x004 SpareLong0 : Uint4B
+0x008 Thread : Ptr32 _KTHREAD
+0x00c ApcListEntry : _LIST_ENTRY
+0x014 KernelRoutine : Ptr32 void
+0x018 RundownRoutine : Ptr32 void
+0x01c NormalRoutine : Ptr32 void
+0x020 NormalContext : Ptr32 Void
+0x024 SystemArgument1 : Ptr32 Void
+0x028 SystemArgument2 : Ptr32 Void
+0x02c ApcStateIndex : Char
+0x02d ApcMode : Char
+0x02e Inserted : UChar

介绍一些比较重要的成员:

名称 含义
Type 为KOBJECTS枚举类型的ApcObject
Size 等于 KAPC 结构的大小
Thread 指向此 APC 对象所在的线程ETHREAD
ApcListEntry APC 对象被加入到线程 APC 链表中的节点对象
KernelRoutine 指向释放 APC 对象的函数指针
RundownRoutine 函数指针(可选参数),当一个线程终止时,如果它的 APC 链表中还有 APC 对象,若RundownRoutine非空,则调用它所指函数
NormalRoutine 如果是内核 APC,指向要指向的函数,如果是用户 APC 指向了所有用户 APC 都会运行的函数
NormalContext 如果是内核 APC 该参数为NULL,如果是用户 APC 该参数就是真正要执行的用户APC函数
SystemArgument1 APC 函数的参数
SystemArgument2 APC 函数的参数
AppStateIndex 说明了APC对象的环境状态,它是KAPC_ENVIRONMENT枚举类型的成员,一旦APC对象被插入到线程的APC链表中,则ApcStateIndex指示了它位于线程ETHREAD对象的哪个APC链表中
ApcMode 为0表示这是一个内核APC,为1说明这是用户APC
Inserted 指示该APC是否已被插入到线程的APC链表中

APC挂入与执行流程

QueueUserAPC->R3->R0->NtQueueApcThread->KeInitializeApc->KeInsertQueueApc->KiInsertQueueApc

其中 QueueUserAPC 这个函数位于 kernel32.dll,它会调用内核模块的 NtQueueApcThread 进行实现,经历过重重调用,使用 KeInitializeApcAPC 结构体分配内存并进行初始化,调用 KeInsertQueueApc 进行插入到指定队列,而插入最终由 KiInsertQueueApc 实现。

源码分析

NtQueueApcThreadEx

NtQueueApcThread 就是调用了一个 NtQueueApcThread,所以直接看到这个函数。

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
NTSTATUS __stdcall NtQueueApcThreadEx(
ATOM_INFORMATION_CLASS Handle,
ATOM_INFORMATION_CLASS a2,
void (__stdcall *NormalRoutine)(void *, void *, void *),
void *Context,
int a5,
int a6)
{
NTSTATUS result; // eax
NTSTATUS v7; // esi
char *v8; // ecx
struct _KAPC *Kapc; // esi
void (__stdcall *v10)(_KAPC *, void (__stdcall **)(void *, void *, void *), void **, void **, void **); // eax
void (__stdcall *RundownRoutine)(_KAPC *); // edi
MEMORY_CACHING_TYPE v12; // [esp+4h] [ebp-Ch] BYREF
int AccessMode; // [esp+8h] [ebp-8h]
_KTHREAD *Thread; // [esp+Ch] [ebp-4h] BYREF

LOBYTE(AccessMode) = KeGetCurrentThread()->PreviousMode;
result = ObReferenceObjectByHandle((HANDLE)Handle, 0x10u, (POBJECT_TYPE)PsThreadType, AccessMode, (PVOID *)&Thread, 0);
if ( result >= 0 )
{
if ( (Thread->MiscFlags & 0x2000) != 0 )
{
v7 = -1073741816;
LABEL_15:
ObfDereferenceObject(Thread);
return v7;
}
if ( a2 )
{
v7 = ObReferenceObjectByHandle((HANDLE)a2, 2u, PspMemoryReserveObjectTypes, AccessMode, (PVOID *)&v12, 0);
if ( v7 < 0 )
goto LABEL_15;
v8 = (char *)v12;
if ( _InterlockedCompareExchange((volatile signed __int32 *)v12, 1, 0) )
{
ObfDereferenceObject(v8);
v7 = -1073741584;
goto LABEL_15;
}
Kapc = (struct _KAPC *)(v8 + 4);
v10 = (void (__stdcall *)(_KAPC *, void (__stdcall **)(void *, void *, void *), void **, void **, void **))PspUserApcReserveKernelRoutine;
RundownRoutine = (void (__stdcall *)(_KAPC *))PspUserApcReserveRundownRoutine;
}
else
{
Kapc = (struct _KAPC *)ExAllocatePoolWithQuotaTag((POOL_TYPE)8, 0x30u, 0x70617350u);
if ( !Kapc )
{
v7 = -1073741801;
goto LABEL_15;
}
v10 = (void (__stdcall *)(_KAPC *, void (__stdcall **)(void *, void *, void *), void **, void **, void **))IopDeallocateApc;
RundownRoutine = (void (__stdcall *)(_KAPC *))ExFreePool;
}
KeInitializeApc(Kapc, Thread, 0, v10, RundownRoutine, NormalRoutine, 1, Context);
if ( (unsigned __int8)KeInsertQueueApc(Kapc, a5, a6, 0) )
{
v7 = 0;
}
else
{
RundownRoutine(Kapc);
v7 = -1073741823;
}
goto LABEL_15;
}
return result;
}

该函数执行流程如下:

  • 根据句柄获得线程内核对象
  • KeInitializeApc 初始化 KAPC 结构
  • KeInsertQueueApc 插入 APC

KeInitializeApc

KeInitializeApc 定义如下:

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
PKAPC __stdcall KeInitializeApc(
PKAPC Apc,
_KTHREAD *Thread,
int TargetEnvironment,
void (__stdcall *KernelRoutine)(_KAPC *, void (__stdcall **)(void *, void *, void *), void **, void **, void **),
void (__stdcall *RundownRoutine)(_KAPC *),
void (__stdcall *NormalRoutine)(void *, void *, void *),
char Mode,
void *Context)
{
PKAPC result; // eax
char ApcStateIndex; // dl

result = Apc;
ApcStateIndex = TargetEnvironment;
Apc->Type = 18;
Apc->Size = 48;
if ( TargetEnvironment == 2 )
ApcStateIndex = Thread->ApcStateIndex;
Apc->Thread = Thread;
Apc->KernelRoutine = KernelRoutine;
Apc->ApcStateIndex = ApcStateIndex;
Apc->RundownRoutine = RundownRoutine;
Apc->NormalRoutine = NormalRoutine;
if ( NormalRoutine )
{
Apc->ApcMode = Mode;
Apc->NormalContext = Context;
}
else
{
Apc->ApcMode = 0;
Apc->NormalContext = 0;
}
Apc->Inserted = 0;
return result;
}

初始化了 KAPC 这个结构,并且根据 TargetEnvironment 去修正 ApcStateIndex,线程同样也有这个字段,不过含义不一样。

含义
0 原始环境
1 挂靠环境
2 当前环境
3 插入APC时的当前环境

0和1值很好理解,跟线程结构体中该对象一致,主要就是 2 和 3 值:

当值为2的时候,插入的是当前进程的队列。什么是当前队列,是我不管你环境是挂靠还是不挂靠,我就插入当前进程的APC队列里面,以初始化APC的时候为基准。

当值为3时,插入的是当前进程的APC队列,此时有修复ApcStateIndex的操作,以插入APC的时候为基准。

KeInsertQueueApc

回到 NtQueueApcThread 函数,来看看其中调用的 KeInsertQueueApc

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
char __stdcall KeInsertQueueApc(_KAPC *Kapc, void *argument1, void *argument2, char a4)
{
_KTHREAD *Thread; // edi
volatile __int32 *p_ApcQueueLock; // ebx
char v7; // [esp+13h] [ebp-Dh]
int v8; // [esp+14h] [ebp-Ch]
FS_INFORMATION_CLASS NewIrql; // [esp+18h] [ebp-8h]
_KPRCB *Prcb; // [esp+1Ch] [ebp-4h]

Thread = Kapc->Thread;
v8 = 0;
LOBYTE(NewIrql) = KeRaiseIrqlToDpcLevel();
Prcb = KeGetPcr()->Prcb;
p_ApcQueueLock = (volatile __int32 *)&Thread->ApcQueueLock;
while ( _InterlockedExchange(p_ApcQueueLock, 1) )
{
do
{
if ( (++v8 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 )
_mm_pause();
else
HvlNotifyLongSpinWait(v8);
}
while ( *p_ApcQueueLock );
}
if ( (*(_DWORD *)&Thread->0 & 0x20) == 0 || Kapc->Inserted == 1 )
{
v7 = 0;
}
else
{
Kapc->SystemArgument1 = argument1;
Kapc->Inserted = 1;
Kapc->SystemArgument2 = argument2;
KiInsertQueueApc(Prcb, Kapc, NewIrql);
v7 = 1;
}
_InterlockedAnd(p_ApcQueueLock, 0);
KiExitDispatcher(Prcb, 0, 1, a4, NewIrql);
return v7;
}

调用流程如下:

  • 将中断等级提升至 DISPATCH_LEVEL,这个中断等级下不会做线程切换。
  • 构造好 Kapc 结构体,调用 KiInsertQueueApc 函数。

KiInsertQueueApc

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
168
169
170
171
172
173
174
175
176
177
178
char __fastcall KiInsertQueueApc(_KPRCB *PRCB, _KAPC *Apc, char IRQL)
{
_KTHREAD *Thread; // esi
_KAPC_STATE *v5; // ecx
char ApcMode; // bl
_LIST_ENTRY *v7; // ecx
_LIST_ENTRY *Flink; // edx
_LIST_ENTRY *v9; // ecx
_LIST_ENTRY *Blink; // edx
_LIST_ENTRY *v11; // ecx
_LIST_ENTRY *v12; // eax
_LIST_ENTRY *v13; // edx
unsigned int ThreadApcStateIndex; // eax
unsigned int ApcStateIndex; // ecx
volatile __int32 *v16; // ebx
volatile __int32 *p_ThreadLock; // edi
int v18; // ebx
char v19; // al
ULONG CurrentProcessorNumber; // eax
volatile unsigned int NextProcessor; // esi
unsigned int v22; // ecx
int v23; // eax
_KPRCB *v24; // eax
char v26; // [esp+Fh] [ebp-19h]
char v27; // [esp+Fh] [ebp-19h]
int v28; // [esp+10h] [ebp-18h]
__int32 v30; // [esp+18h] [ebp-10h] BYREF
_WORD v31[4]; // [esp+1Ch] [ebp-Ch] BYREF
int v32; // [esp+24h] [ebp-4h]

Thread = Apc->Thread;
if ( Apc->ApcStateIndex == 3 )
Apc->ApcStateIndex = Thread->ApcStateIndex;
v5 = Thread->ApcStatePointer[Apc->ApcStateIndex];
ApcMode = Apc->ApcMode;
if ( Apc->NormalRoutine ) // 用户模式APC
{
v26 = 1;
if ( ApcMode && (int (__stdcall *)(MEMORY_CACHING_TYPE, int, int, int, int))Apc->KernelRoutine == PsExitSpecialApc )
{
Thread->ApcState.UserApcPending = 1;
v7 = &v5->ApcListHead[ApcMode];
Flink = v7->Flink;
Apc->ApcListEntry.Flink = v7->Flink;
Apc->ApcListEntry.Blink = v7;
Flink->Blink = &Apc->ApcListEntry;
v7->Flink = &Apc->ApcListEntry;
}
else
{
v9 = &v5->ApcListHead[ApcMode];
Blink = v9->Blink;
Apc->ApcListEntry.Flink = v9;
Apc->ApcListEntry.Blink = Blink;
Blink->Flink = &Apc->ApcListEntry;
v9->Blink = &Apc->ApcListEntry;
}
}
else
{
v11 = &v5->ApcListHead[ApcMode];
v12 = v11->Blink;
v26 = 0;
while ( v12 != v11 && v12[2].Flink )
v12 = v12->Blink;
v13 = v12->Flink;
Apc->ApcListEntry.Flink = v12->Flink;
Apc->ApcListEntry.Blink = v12;
v13->Blink = &Apc->ApcListEntry;
v12->Flink = &Apc->ApcListEntry;
}
ThreadApcStateIndex = Thread->ApcStateIndex;
ApcStateIndex = Apc->ApcStateIndex;
if ( ApcStateIndex == ThreadApcStateIndex ) // 状态相同,则尝试立刻交付执行
{
LOBYTE(ThreadApcStateIndex) = (_BYTE)PRCB;
if ( Thread == PRCB->CurrentThread )
{
if ( !ApcMode && (!Thread->CombinedApcDisable || !v26 && !Thread->SpecialApcDisable) )
{
Thread->ApcState.KernelApcPending = 1;
if ( !IRQL )
{
Thread->MiscFlags |= 0x100u;
return ThreadApcStateIndex;
}
RequestSoftwareInterrupt:
LOBYTE(ApcStateIndex) = 1;
LOBYTE(ThreadApcStateIndex) = HalRequestSoftwareInterrupt(ApcStateIndex);// 触发软件中断以处理APC
}
}
else if ( ApcMode ) // 用户apc交付执行
{
LOBYTE(ThreadApcStateIndex) = Thread->State;
if ( (_BYTE)ThreadApcStateIndex == 5 ) // 线程处于等待状态
{
v27 = 0;
p_ThreadLock = (volatile __int32 *)&Thread->ThreadLock;
v18 = 0;
while ( _InterlockedExchange(p_ThreadLock, 1) )
{
do
{
if ( (++v18 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 )
_mm_pause();
else
HvlNotifyLongSpinWait(v18);
}
while ( *p_ThreadLock );
}
if ( Thread->State == 5
&& Thread->WaitMode == 1 // 用户态
&& ((*(_BYTE *)&Thread->0 & 0x20) != 0 || Thread->ApcState.UserApcPending) )
{
v19 = KiSignalThread(0, Thread, PRCB, 0xC0);// 唤醒该线程
Thread->WaitRegister.Flags |= 0x20u;
v27 = v19;
}
LOBYTE(ThreadApcStateIndex) = 0;
_InterlockedAnd(p_ThreadLock, 0);
if ( v27 )
Thread->ApcState.UserApcPending = 1;
}
}
else // 内核APC交付执行
{
Thread->ApcState.KernelApcPending = 1;
_InterlockedExchange(&v30, (__int32)PRCB);
ThreadApcStateIndex = Thread->State;
if ( ThreadApcStateIndex == 2 ) // 线程处于运行状态
{
CurrentProcessorNumber = KeGetCurrentProcessorNumberEx(0);
ApcStateIndex = Thread->NextProcessor;
if ( CurrentProcessorNumber == ApcStateIndex )// 如果当前线程调度的下一个处理器为本处理器,则立刻交付执行
goto RequestSoftwareInterrupt;
NextProcessor = Thread->NextProcessor;
v31[0] = 1;
v31[1] = 1;
v32 = 0;
v22 = (unsigned int)KiProcessorIndexToNumberMappingTable[NextProcessor] >> 6;
v23 = KiProcessorIndexToNumberMappingTable[NextProcessor] & 0x3F;
if ( v22 )
v31[0] = v22 + 1;
*(&v32 + v22) |= KiMask32Array[v23];
v24 = KeGetPcr()->Prcb;
++v24->IpiSendSoftwareInterruptCount; // 统计Ipi发送次数
LOBYTE(ThreadApcStateIndex) = KiIpiSend((int)v31, 1u);// 否则向该处理器发出Ipi中断用于通知该处理器执行该线程的APC。
}
else if ( ThreadApcStateIndex == 5 ) // 线程处于等待状态
{
v28 = 0;
v16 = (volatile __int32 *)&Thread->ThreadLock;
while ( _InterlockedExchange(v16, 1) )
{
do
{
if ( (++v28 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 )
_mm_pause();
else
HvlNotifyLongSpinWait(v28);
}
while ( *v16 );
}
if ( Thread->State == 5
&& !Thread->WaitIrql
&& !Thread->SpecialApcDisable
&& (!Apc->NormalRoutine || !Thread->KernelApcDisable && !Thread->ApcState.KernelApcInProgress) )
{
KiSignalThread(0, Thread, PRCB, 0x100);// 唤醒该线程
Thread->WaitRegister.Flags |= 0x10u;
}
LOBYTE(ThreadApcStateIndex) = 0;
_InterlockedAnd(v16, 0);
}
}
}
return ThreadApcStateIndex;
}

总结一下该函数的流程:

  • 根据KAPC结构中的ApcStateIndex找到对应的APC队列
  • 再根据KAPC结构中的ApcMode确定是用户队列还是内核队列
  • KAPC挂到对应的队列中(挂到KAPCApcListEntry处)
  • 再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态
  • 修改KAPC_STATE结构中的KernelApcPending/UserApcPending

当插入 APC 时线程为睡眠且可唤醒(alertable),那么 APC 立刻被执行,否则只会被插入到 APC 队列中。

在队列中的 APC 会在线程进入 alertable 状态后全部交付执行。

实验

实验1

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
// APCTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>

void shellcode1() {
printf("shellcode1 execute\n");
}

void shellcode2() {
printf("shellcode2 execute\n");
}

int main()
{
HANDLE hThread = GetCurrentThread();
printf("helloworld\n");
QueueUserAPC((PAPCFUNC)shellcode1, hThread, 0);
QueueUserAPC((PAPCFUNC)shellcode2, hThread, 0);
for (int i = 0; i < 5; i++) {
printf("[%d]prepare to insert apc %d\n", time(NULL), i);
Sleep(1000);
}
return 0;
}

向当前线程插入 APC 后,继续运行。

1
2
3
4
5
6
helloworld
[1739719424]prepare to insert apc 0
[1739719425]prepare to insert apc 1
[1739719426]prepare to insert apc 2
[1739719427]prepare to insert apc 3
[1739719428]prepare to insert apc 4

可以看到,线程到死都没有执行成功 APC,因为不满足上面分析的 APC 执行的条件。

要么插入时执行,要求插入时线程处于可唤醒状态。

要么插入后执行,要求插入后线程某一刻处于可唤醒状态。

实验2

很多跟等待相关的,加了 Ex 后缀的函数,其基本跟另外一个参数有关,就是 alertable,例如 Sleep->SleepExWaitForSingleObject->WaitForSingleObjectEx

这里我们把 Sleep 替换为 SleepEx 并把 alertable 设置为 True

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
// APCTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>

void shellcode1() {
printf("shellcode1 execute\n");
}

void shellcode2() {
printf("shellcode2 execute\n");
}

int main()
{
HANDLE hThread = GetCurrentThread();
printf("helloworld\n");
QueueUserAPC((PAPCFUNC)shellcode1, hThread, 0);
QueueUserAPC((PAPCFUNC)shellcode2, hThread, 0);
for (int i = 0; i < 5; i++) {
printf("[%d]prepare to insert apc %d\n", time(NULL), i);
SleepEx(1000,1);
}
return 0;
}

APC 在第一次 SleepEx 便全部交付完成,并且也没有实际休眠一秒,而是立刻被唤醒。

1
2
3
4
5
6
7
8
helloworld
[1739719959]prepare to insert apc 0
shellcode1 execute
shellcode2 execute
[1739719959]prepare to insert apc 1
[1739719960]prepare to insert apc 2
[1739719961]prepare to insert apc 3
[1739719962]prepare to insert apc 4

实验3

来演示在 SleepEx 的过程中插入 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
// APCTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>

void shellcode1() {
printf("shellcode1 execute\n");
}

void shellcode2() {
printf("shellcode2 execute\n");
}

void routine() {
for (int i = 0; i < 5; i++) {
printf("[%d]routine %d\n", time(NULL), i);
SleepEx(10000, 1);
}
}

int main()
{
HANDLE hThread = GetCurrentThread();
HANDLE hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)routine, NULL, 0, NULL);
printf("helloworld\n");
for (int i = 0; i < 5; i++) {
printf("[%d]prepare to insert apc %d\n", time(NULL), i);
QueueUserAPC((PAPCFUNC)shellcode1, hThread2, 0);
SleepEx(1000,1);
}

return 0;
}

可以看到,线程自己的 SleepEx 并没有生效,因为在插入 APC 时线程就被立刻唤醒且执行 APC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
helloworld
[1739770840]prepare to insert apc 0
[1739770840]routine 0
shellcode1 execute
[1739770840]routine 1
[1739770841]prepare to insert apc 1
shellcode1 execute
[1739770841]routine 2
[1739770842]prepare to insert apc 2
shellcode1 execute
[1739770842]routine 3
[1739770843]prepare to insert apc 3
shellcode1 execute
[1739770843]routine 4
[1739770844]prepare to insert apc 4
shellcode1 execute

参考文献