1.APC简介
APC是异步过程调用(Asyncroneus Procedure Call)的缩写。简单来说,每一个线程都有APC列表,其他线程想要控制这个线程的时候可以向APC列表里面插入APC实现对这个线程的控制,当这个线程被唤醒时,他就会遍历这个列表,执行需要被执行的函数。
1.1 APC寻址
APC的相关内容保存在_ETHREAD结构体头部的_KTHREAD子结构体的ApcState结构体中,这个结构体中包含了APC链表和APC状态等重要信息。
kd> dt _ETHREAD
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x1c0 CreateTime : _LARGE_INTEGER
+0x1c0 NestedFaultCount : Pos 0, 2 Bits
+0x1c0 ApcNeeded : Pos 2, 1 Bit
+0x1c8 ExitTime : _LARGE_INTEGER
+0x1c8 LpcReplyChain : _LIST_ENTRY
+0x1c8 KeyedWaitChain : _LIST_ENTRY
+0x1d0 ExitStatus : Int4B
+0x1d0 OfsChain : Ptr32 Void
+0x1d4 PostBlockList : _LIST_ENTRY
+0x1dc TerminationPort : Ptr32 _TERMINATION_PORT
+0x1dc ReaperLink : Ptr32 _ETHREAD
+0x1dc KeyedWaitValue : Ptr32 Void
+0x1e0 ActiveTimerListLock : Uint4B
+0x1e4 ActiveTimerListHead : _LIST_ENTRY
+0x1ec Cid : _CLIENT_ID
+0x1f4 LpcReplySemaphore : _KSEMAPHORE
+0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
+0x208 LpcReplyMessage : Ptr32 Void
+0x208 LpcWaitingOnPort : Ptr32 Void
+0x20c ImpersonationInfo : Ptr32 _PS_IMPERSONATION_INFORMATION
+0x210 IrpList : _LIST_ENTRY
+0x218 TopLevelIrp : Uint4B
+0x21c DeviceToVerify : Ptr32 _DEVICE_OBJECT
+0x220 ThreadsProcess : Ptr32 _EPROCESS
+0x224 StartAddress : Ptr32 Void
+0x228 Win32StartAddress : Ptr32 Void
+0x228 LpcReceivedMessageId : Uint4B
+0x22c ThreadListEntry : _LIST_ENTRY
+0x234 RundownProtect : _EX_RUNDOWN_REF
+0x238 ThreadLock : _EX_PUSH_LOCK
+0x23c LpcReplyMessageId : Uint4B
+0x240 ReadClusterSize : Uint4B
+0x244 GrantedAccess : Uint4B
+0x248 CrossThreadFlags : Uint4B
+0x248 Terminated : Pos 0, 1 Bit
+0x248 DeadThread : Pos 1, 1 Bit
+0x248 HideFromDebugger : Pos 2, 1 Bit
+0x248 ActiveImpersonationInfo : Pos 3, 1 Bit
+0x248 SystemThread : Pos 4, 1 Bit
+0x248 HardErrorsAreDisabled : Pos 5, 1 Bit
+0x248 BreakOnTermination : Pos 6, 1 Bit
+0x248 SkipCreationMsg : Pos 7, 1 Bit
+0x248 SkipTerminationMsg : Pos 8, 1 Bit
+0x24c SameThreadPassiveFlags : Uint4B
+0x24c ActiveExWorker : Pos 0, 1 Bit
+0x24c ExWorkerCanWaitUser : Pos 1, 1 Bit
+0x24c MemoryMaker : Pos 2, 1 Bit
+0x250 SameThreadApcFlags : Uint4B
+0x250 LpcReceivedMsgIdValid : Pos 0, 1 Bit
+0x250 LpcExitThreadCalled : Pos 1, 1 Bit
+0x250 AddressSpaceOwner : Pos 2, 1 Bit
+0x254 ForwardClusterOnly : UChar
+0x255 DisablePageFaultClustering : UChar
kd> dt _KTHREAD
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 MutantListHead : _LIST_ENTRY
+0x018 InitialStack : Ptr32 Void
+0x01c StackLimit : Ptr32 Void
+0x020 Teb : Ptr32 Void
+0x024 TlsArray : Ptr32 Void
+0x028 KernelStack : Ptr32 Void
+0x02c DebugActive : UChar
+0x02d State : UChar
+0x02e Alerted : [2] UChar
+0x030 Iopl : UChar
+0x031 NpxState : UChar
+0x032 Saturation : Char
+0x033 Priority : Char
+0x034 ApcState : _KAPC_STATE
+0x04c ContextSwitches : Uint4B
+0x050 IdleSwapBlock : UChar
+0x051 Spare0 : [3] UChar
+0x054 WaitStatus : Int4B
+0x058 WaitIrql : UChar
+0x059 WaitMode : Char
+0x05a WaitNext : UChar
+0x05b WaitReason : UChar
+0x05c WaitBlockList : Ptr32 _KWAIT_BLOCK
+0x060 WaitListEntry : _LIST_ENTRY
+0x060 SwapListEntry : _SINGLE_LIST_ENTRY
+0x068 WaitTime : Uint4B
+0x06c BasePriority : Char
+0x06d DecrementCount : UChar
+0x06e PriorityDecrement : Char
+0x06f Quantum : Char
+0x070 WaitBlock : [4] _KWAIT_BLOCK
+0x0d0 LegoData : Ptr32 Void
+0x0d4 KernelApcDisable : Uint4B
+0x0d8 UserAffinity : Uint4B
+0x0dc SystemAffinityActive : UChar
+0x0dd PowerState : UChar
+0x0de NpxIrql : UChar
+0x0df InitialNode : UChar
+0x0e0 ServiceTable : Ptr32 Void
+0x0e4 Queue : Ptr32 _KQUEUE
+0x0e8 ApcQueueLock : Uint4B
+0x0f0 Timer : _KTIMER
+0x118 QueueListEntry : _LIST_ENTRY
+0x120 SoftAffinity : Uint4B
+0x124 Affinity : Uint4B
+0x128 Preempted : UChar
+0x129 ProcessReadyQueue : UChar
+0x12a KernelStackResident : UChar
+0x12b NextProcessor : UChar
+0x12c CallbackStack : Ptr32 Void
+0x130 Win32Thread : Ptr32 Void
+0x134 TrapFrame : Ptr32 _KTRAP_FRAME
+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE "这个正常情况下:ApcStatePointer[0] 指向 ApcState,ApcStatePointer[1] 指向 SavedApcState。挂靠情况下:ApcStatePointer[0] 指向 SavedApcState,ApcStatePointer[1] 指向 ApcState"
+0x140 PreviousMode : Char
+0x141 EnableStackSwap : UChar
+0x142 LargeStack : UChar
+0x143 ResourceIndex : UChar
+0x144 KernelTime : Uint4B
+0x148 UserTime : Uint4B
+0x14c SavedApcState : _KAPC_STATE "备用APC队列"
+0x164 Alertable : UChar
+0x165 ApcStateIndex : UChar "当前线程是否挂靠在别的进程,正常是0,挂靠是1"
+0x166 ApcQueueable : UChar "是否可以插入APC,为真才能插入"
+0x167 AutoAlignment : UChar
+0x168 StackBase : Ptr32 Void
+0x16c SuspendApc : _KAPC
+0x19c SuspendSemaphore : _KSEMAPHORE
+0x1b0 ThreadListEntry : _LIST_ENTRY
+0x1b8 FreezeCount : Char
+0x1b9 SuspendCount : Char
+0x1ba IdealProcessor : UChar
+0x1bb DisableBoost : UChar
kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY "2个双向APC队列,用户APC和内核APC"
+0x010 Process : Ptr32 _KPROCESS "当前线程所挂靠的进程"
+0x014 KernelApcInProgress : UChar "内核APC是否正在执行"
+0x015 KernelApcPending : UChar "是否有等待执行的内核APC"
+0x016 UserApcPending : UChar "是否有等待执行的用户APC"
无论当前线程是否挂靠在其他进程中,ApcStatePointer[ApcStateIndex]都指向当前进程的ApcState。
1.2 APC结构体
APC结构体在内核叫_KAPC,其结构如下:
kd> dt _KAPC
nt!_KAPC
+0x000 Type //类型 APC类型为0x12
+0x002 Size //本结构体的大小 0x30
+0x004 Spare0
+0x008 Thread //目标线程
+0x00c ApcListEntry //APC队列挂的位置
+0x014 KernelRoutine //指向一个函数(调用ExFreePoolWithTag 释放APC占用的内存)
+0x018 RundownRoutine//略
+0x01c NormalRoutine //用户APC总入口(3环) 或者 真正的内核APC函数(0环)
+0x020 NormalContext //用户APC:真正的APC函数;内核APC:NULL ;
+0x024 SystemArgument1//APC函数的参数
+0x028 SystemArgument2//APC函数的参数
+0x02c ApcStateIndex //挂哪个队列,有四个值:0 1 2 3
+0x02d ApcMode //内核APC是0 用户APC是1
+0x02e Inserted //表示本APC是否已挂入队列 挂入前:0 挂入后 1
1.3 备用APC队列
在_KTHREAD结构体的0x14c偏移处有一个SavedApcState结构体,这个结构体也是一个_KAPC_STATE,这个地方的结构体被称为备用APC队列。当这个线程的挂靠在别的进程中时,就会用到这个_KAPC_STATE结构体。例如,当A进程的线程T1去挂靠到了B进程,假如原来APC里存的函数地址是0x12345678,如果在B进程的地址空间里里执行这个函数那么地址就是错的。因此当挂靠到别的进程中时,就会把原有的ApcState备份到这个SavedApcState中,等恢复到A进程时再恢复APC队列。
2.APC挂入过程
三环调用QueueUserAPC函数过程如下图所示:

2.1 KeInitializeApc函数
VOID KeInitializeApc
(
IN PKAPC Apc,//"KAPC指针"
IN PKTHREAD Thread,//"目标线程"
IN KAPC_ENVIRONMENT TargetEnvironment,//"0 1 2 3四种状态,这四个值就写到了ApcStateIndex里"
IN PKKERNEL_ROUTINE KernelRoutine,//"销毁KAPC的函数地址"
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//"用户APC总入口或者内核apc函数地址"
IN KPROCESSOR_MODE Mode,//"要插入用户apc队列还是内核apc队列,内核模式0,用户模式1"
IN PVOID Context//"内核APC:NULL 用户APC:真正的APC函数地址"
)
2.2 _KAPC.ApcStateIndex
这里的ApcStateIndex是_KAPC结构体里的,跟KTHREAD里那个没关系。下表为ApcStateIndex的具体意义:
| ApcStateIndex | 含义 | 插入的位置 |
|---|---|---|
| 0 | 原始环境 | ApcStatePointer[0] 指向 ApcState;ApcStatePointer[1] 指向 SavedApcState |
| 1 | 挂靠环境 | ApcStatePointer[0] 指向 SavedApcState;ApcStatePointer[1] 指向 ApcState |
| 2 | 当前环境 | 当前进程的ApcState |
| 3 | 插入APC时的当前环境 | 插入的时候,当前进程的ApcState |
3.APC执行
3.1 内核APC执行
内核的APC在线程切换的时候就会进行处理,内核APC一个while循环就处理完了。
3.2 用户APC执行
用户APC当线程从Ring0返回Ring3的时候才会执行,一般来说,在三环注入APC的时候,被注入线程中必须有某些运行了可能导致线程进入Alertable状态的函数才能成功执行。以sleep和sleepEx为例,其中sleepEx函数可以设置线程进入Alertable状态。实验如下:
1.使用如下代码,线程不会进入Alertable状态,APC将无法被执行。
#include <stdio.h>
#include <Windows.h>
HANDLE hThread1=NULL;
void func1(){
while(TRUE){
printf("I am Func1!\n");
Sleep(3000);
}
}
VOID WINAPI APCProc(LPVOID dwParam)
{
printf("I'm APC Func!!!\n");
}
int main(){
printf("I'am main Thread!\n");
hThread1 = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)func1,0,0,0);
Sleep(3000);
QueueUserAPC((PAPCFUNC)APCProc,hThread1,NULL);
getchar();
}

2.修改sleep为sleepEx,APC成功执行
#include <stdio.h>
#include <Windows.h>
HANDLE hThread1=NULL;
void func1(){
while(TRUE){
printf("I am Func1!\n");
SleepEx(3000,1);
}
}
VOID WINAPI APCProc(LPVOID dwParam)
{
printf("I'm APC Func!!!\n");
}
int main(){
printf("I'am main Thread!\n");
hThread1 = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)func1,0,0,0);
Sleep(3000);
QueueUserAPC((PAPCFUNC)APCProc,hThread1,NULL);
getchar();
}

练习
1.分析APC插入函数的流程 2.swapcontext分析看看APC什么时候执行
参考
- https://bbs.pediy.com/thread-258625.htm UserMode下的APC插入
- https://bbs.pediy.com/thread-258756.htm APC Dll注入