今天详细了解一下 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
进行实现,经历过重重调用,使用 KeInitializeApc
为 APC
结构体分配内存并进行初始化,调用 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; NTSTATUS v7; char *v8; struct _KAPC *Kapc; void (__stdcall *v10)(_KAPC *, void (__stdcall **)(void *, void *, void *), void **, void **, void **); void (__stdcall *RundownRoutine)(_KAPC *); MEMORY_CACHING_TYPE v12; int AccessMode; _KTHREAD *Thread;
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; char ApcStateIndex;
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; volatile __int32 *p_ApcQueueLock; char v7; int v8; FS_INFORMATION_CLASS NewIrql; _KPRCB *Prcb;
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; _KAPC_STATE *v5; char ApcMode; _LIST_ENTRY *v7; _LIST_ENTRY *Flink; _LIST_ENTRY *v9; _LIST_ENTRY *Blink; _LIST_ENTRY *v11; _LIST_ENTRY *v12; _LIST_ENTRY *v13; unsigned int ThreadApcStateIndex; unsigned int ApcStateIndex; volatile __int32 *v16; volatile __int32 *p_ThreadLock; int v18; char v19; ULONG CurrentProcessorNumber; volatile unsigned int NextProcessor; unsigned int v22; int v23; _KPRCB *v24; char v26; char v27; int v28; __int32 v30; _WORD v31[4]; int v32;
Thread = Apc->Thread; if ( Apc->ApcStateIndex == 3 ) Apc->ApcStateIndex = Thread->ApcStateIndex; v5 = Thread->ApcStatePointer[Apc->ApcStateIndex]; ApcMode = Apc->ApcMode; if ( Apc->NormalRoutine ) { 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); } } else if ( ApcMode ) { 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 { 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; LOBYTE(ThreadApcStateIndex) = KiIpiSend((int)v31, 1u); } 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
挂到对应的队列中(挂到KAPC
的ApcListEntry
处)
- 再根据
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
|
#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->SleepEx
,WaitForSingleObject->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
|
#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
|
#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
|
参考文献