APC的本质
线程是不能被杀死 挂起和恢复的,线程在执行的时候自己占据着CPU,别人怎么可能控制他呢?举个极端的例子,如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占据CPU。所以说线程如果想结束,一定是自己执行代码把自己杀死,不存在别人把线程结束的情况。
那如果想改变一个线程的行为该怎么办?可以给他提供一个函数,让他自己去调用,这个函数就是APC,即异步过程调用
APC队列
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState : _KAPC_STATE
kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead //2个APC队列 用户APC和内核APC
+0x010 Process //线程所属或者所挂靠的进程
+0x014 KernelApcInProgress //内核APC是否正在执行
+0x015 KernelApcPending //是否有正在等待执行的内核APC
+0x016 UserApcPending //是否有正在等待执行的用户APC
_KAPC_STATE
的第一个成员是一个双向链表,这个双向链表就是APC队列。
APC一共有两个,一个是用户态APC队列,一个是内核态的APC队列,里面存储的都是APC函数。
你想让线程执行某些操作的时候,就可以提供一个函数,挂到这个链表里,在某一个时刻,当前线程会检查当前的函数列表,当里面有函数的时候,就会去调用。
APC结构
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
其中最重要的是+0x01c NormalRoutine
的这个成员,通过这个成员可以找到你提供的APC函数。
APC相关函数
KiServiceExit
这个函数是系统调用 异常或中断返回用户空间的必经之路
KiDeliveApc
负责执行APC函数
备用APC队列
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState : _KAPC_STATE
+0x170 SavedApcState : _KAPC_STATE
在线程结构体0x40的位置是APC队列,在0x170的位置也有一个APC队列,这两个成员的结构是完全一样的
ApcState的含义
线程队列中的APC函数都是与进程相关联的,具体点说:A进程的T线程中所有的APC函数,要访问的内存地址都是A进程的。
但线程是可以挂靠到其他的进程:比如A进程的线程T,通过修改CR3,就可以访问B进程的地址空间,即所谓的进程挂靠。
当T线程挂靠B进程后,APC队列中存储的仍然是原来的APC。具体点说,比如某个APC函数要读取地址为0x12345678的数据,如果此时进行读取,读到的将是B进程的地址空间,这样逻辑就错误了。
为了避免混乱,在T线程挂靠B进程时,会将ApcState中的值暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复
所以,SavedApcState又称为备用APC队列
挂靠环境下的ApcState的含义
在挂靠环境下,也是可以将线程APC队列插入APC的,那这种情况下,使用的是哪个APC队列呢?
A进程的T线程挂靠B进程,A是T的所属进程,B是T的挂靠进程
- ApcState:B进程相关的APC函数
- SavedApcState:A进程相关的APC函数
在正常情况下,当前进程就是所属进程A,如果是挂靠情况下,当前进程就是挂靠进程B
其他APC相关成员
ApcStatePointer
+0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE
在KTHREAD结构体的0x168的位置的成员是一个指针,这个指针有两个成员,每一个指针都指向一个ApcState
为了操作方便,KTHREAD结构体中定义了一个指针数组ApcStatePointer,长度为2。
正常情况下:
ApcStatePointer[0]指向ApcState
ApcStatePointer[1]指向SavedApcState
挂靠情况下:
ApcStatePointer[0]指向SavedApcState
ApcStatePointer[1]指向ApcState
ApcStateIndex
+0x134 ApcStateIndex : UChar
ApcStateIndex用来标识当前线程处于什么状态:0正常状态 1挂靠状态
ApcStatePointer与ApcStateIndex组合寻址
正常情况下,向ApcState队列插入APC时:
ApcStatePointer[0]指向ApcState,此时ApcStateIndex的值为0
ApcStatePointer[ApcStateIndex]指向ApcState
挂靠情况下,向ApcState队列中插入APC时:
ApcStatePointer[1]指向ApcState,此时ApcStateIndex的值为1
ApcStatePointer[ApcStateIndex]指向ApcState
总结:
无论什么环境下,ApcStatePointer[ApcStateIndex]指向的都是ApcState,ApcState则总是表示线程当前使用的APC状态
ApcQueueable
+0x0b8 ApcQueueable : Pos 5, 1 Bit
ApcQueueable用于表示是否可以向线程的APC队列中插入APC。当线程正在执行退出的代码时,会将这个值设置为0,如果此时执行插入APC的代码,在插入函数中会判断这个值的状态,如果为0,则插入失败。
APC挂入过程
无论是正常状态还是挂靠状态,都要有两个APC队列,一个内核队列,一个用户队列。每当要挂入一个APC函数时,不管是用户队列还是内核队列,内核都要准备一个KAPC的数据结构,并且将这个KAPC结构挂到相应的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总入口 或者 真正的内核apc函数
+0x020 NormalContext //内核APC:NULL 用户APC:真正的APC函数
+0x024 SystemArgument1//APC函数的参数
+0x028 SystemArgument2//APC函数的参数
+0x02c ApcStateIndex //挂哪个队列,有四个值:0 1 2 3
+0x02d ApcMode //内核APC 用户APC
+0x02e Inserted //表示本apc是否已挂入队列 挂入前:0 挂入后 1
挂入流程
KeInitializeApc
VOID KeInitializeApc
(
IN PKAPC Apc,//KAPC指针
IN PKTHREAD Thread,//目标线程
IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四种状态
IN PKKERNEL_ROUTINE KernelRoutine,//销毁KAPC的函数地址
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//用户APC总入口或者内核apc函数
IN KPROCESSOR_MODE Mode,//要插入用户apc队列还是内核apc队列
IN PVOID Context//内核APC:NULL 用户APC:真正的APC函数
)
KeInitializeApc函数的作用就是给当前的KAPC结构体赋值
ApcStateIndex
与KTHREAD(+0x134)的属性同名,但含义不一样:
ApcStateIndex有四个值:
- 0 原始环境->插入到当前线程的所属进程APC队列,不管是否挂靠都插入到当前线程的所属进程。
- 1 挂靠环境
- 2 当前环境->插入到当前进程的APC队列,如果没有挂靠,当前进程则是父进程,如果挂靠了,当前进程就是挂靠进程
- 3 插入APC时的当前环境->线程随时处于切换状态 当值为3时,在插入APC之前会判断当前线程是否处于挂靠状态 再进行APC插入
KiInsertQueueApc
- 根据KAPC结构中的ApcStateIndex找到对应的APC队列
- 再根据KAPC结构中的ApcMode确定是用户队列还是内核队列
- 将KAPC挂到对应的队列中,挂到KAPC的ApcListEntry处
- 再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态
- 修改KAPC_STATE结构中的KernelApcPending/UserApcPending
内核APC的执行过程
APC函数的插入和执行并不是同一个线程,具体点说:
在A线程中向B线程插入一个APC,插入的动作是在A线程中完成的,但什么时候执行则由B线程决定。所以叫异步过程调用。
内核APC函数与用户APC函数的执行时间和执行方式也有区别。我们先来了解内核APC的执行过程
执行点:线程切换
- SwapContext 判断是否有内核APC
- KiSwapThread 切换线程
- KiDeliverApc 执行内核APC函数
执行点:系统调用 中断或者异常(_KiServiceExit)
当要执行用户APC之前,先要执行内核APC
KiDeliverApc函数的执行流程
- 判断第一个链表是否为空
- 判断KTHREAD.ApcState.KernelApcInProgress是否为1
- 判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)
- 将当前KAPC结构体从链表中摘除
- 执行KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间
- 将KTHREAD.ApcState.KernelApcInProgress设置为1 标识正在执行内核APC
- 执行真正的内核APC函数(KAPC.NormalRoutine)
- 执行完毕 将KernelApcInProgress改为0
总结
- 内核APC在线程切换的时候就会执行,这也就意味着,只要插入内核APC很快就会被执行
- 在执行用户APC之前会先执行内核APC
- 内核APC在内核空间执行,不需要换栈,一个循环全部执行完毕
用户APC的执行过程
当产生系统调用 中断或者异常,线程在返回用户空间前都会调用_KiServiceExit
函数,在_KiServiceExit
函数里会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数进行处理
执行用户APC时的堆栈操作
处理用户APC要比处理内核APC复杂的多,因为用户APC函数要在用户空间执行,这里涉及到大量的换栈操作:
当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器 栈的位置等等,然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可
但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程 进入内核时的位置,而是返回到其他的位置,每处理一个用户APC就会涉及到
内核—>用户空间—>再回到内核空间
KiInitializeUserApc函数分析:备份CONTEXT
线程进0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame
结构体中,如果要提前返回3环去处理用户APC,就必须修改_Trap_Frame
结构体
比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置,还有堆栈ESP也要修改为处理APC需要的堆栈。那原来的值怎么办?处理完APC后该如何返回原来的位置呢?
KiInitializeUserApc要做的第一件事就是备份:
将原来_Trap_Frame
的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成
KiInitializeUserApc函数分析:堆栈图
KiInitializeUserApc函数分析:准备用户层执行环境
- 段寄存器 SS DS FS GS
- 修改EFLAGS寄存器
- 修改ESP
- 修改EIP
总结
- 内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕
- 用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。
数分析:堆栈图
[外链图片转存中…(img-pNVmYHGw-1580550581772)]
KiInitializeUserApc函数分析:准备用户层执行环境
[外链图片转存中…(img-BLCfq8qg-1580550581781)]
- 段寄存器 SS DS FS GS
- 修改EFLAGS寄存器
- 修改ESP
- 修改EIP
总结
- 内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕
- 用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。
- 用户APC执行前会先执行内核APC
来源:CSDN
作者:鬼手56
链接:https://blog.csdn.net/qq_38474570/article/details/104136391