首页
社区
课程
招聘
[原创]内核学习-APC机制
发表于: 2021-10-5 21:59 9364

[原创]内核学习-APC机制

2021-10-5 21:59
9364

线程如果想结束,一定是自己执行代码把自己杀死,不存在别人把线程结束的情况。如何改变线程的行为,提供一个函数,让他自己去调用,这个函数就是APC,即异步过程调用,把提供的函数挂在APC队列中。

ApcListHead 两个双向链表组成 16个字节

用户APC:
在这里插入图片描述

内核APC:

改变线程执行流程,把给定的APC挂载到线程的APC队列中,在某个时刻,当前线程会检查当前的函数列表,当里面有函数的时候,就会去调用。

通过NormalRoutine成员找到我所提供的APC函数 (指向apc函数)

总结:如果我们想要改变一个线程,可以先提供一个APC,然后通过 _KAPC.NormalRoutine成员 指向我们提供的APC函数在哪里,再将APC函数地址存到 _KTHREAD.ApcState.ApcListHead 的第一个成员中(apc队列中)

当前的线程什么时候会执行所提供的APC函数

发生系统调用 异常或中断返回用户空间的时候

执行函数KiServiceExit(检查是否有apc请求,有则KiDeliverApc处理--->改变线程流程)

这个函数是系统调用 异常或中断返回用户空间的必经之路

img

负责执行,处理 APC函数

这两个成员的结构是完全一样的

线程队列中的APC函数都是与进程相关联的,具体点说:A进程的T线程中所有的APC函数,要访问的内存地址都是A进程的。

但线程是可以挂靠到其他的进程:比如A进程的线程T,通过修改CR3( CR3是页目录基址寄存器,保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会。)改为B进程的页目录基址,就可以访问B进程的地址空间,即所谓的进程挂靠。

当T线程挂靠B进程后,APC队列中存储的仍然是原来的APC。具体点说,比如某个APC函数要读取地址为0x12345678的数据,如果此时进行读取,读到的将是B进程的地址空间,这样逻辑就错误了。

为了避免混乱,在T线程挂靠B进程时,会将ApcState中的值(apc队列)暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复

所以,SavedApcState又称为备用APC队列

在挂靠的环境下,也是可以向线程APC队列插入APC的 此时插入进ApcState队列。

A进程的T线程挂靠B进程 A是T的所属进程 B是T的挂靠进程
ApcState B进程相关的APC函数
SavedApcState A进程相关的APC函数

为了操作方便(找到apc队列方便),KTHREAD结构体中定义了一个指针数组ApcStatePointer,长度为2

正常情况下:

ApcStatePointer[0]指向ApcState

ApcStatePointer[1]指向SavedApcState

挂靠情况下:

ApcStatePointer[0]指向SavedApcState

ApcStatePointer[1]指向ApcState

ApcStateIndex用来标识当前线程处于什么状态:0正常状态 1挂靠状态

正常情况下,向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状态

用于表示是否可以向线程的APC队列中插入APC。

当线程正在执行退出的代码时,会将这个值设置为0,如果此时执行插入APC的代码,在插入函数中会判断这个值的状态,如果为0,则插入失败。

img

在这里插入图片描述

ApcStateIndex

与KTHREAD(+0x134)的属性同名,但含义不一样:

ApcStateIndex有四个值:

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

正常情况下:

挂靠情况下:

当前环境:初始化的时候,使用当前进程的ApcState

插入到当前进程的APC队列,如果没有挂靠,当前进程则是父进程,如果挂靠了,当前进程就是挂靠进程
在这里插入图片描述

在这里插入图片描述

(真正初始化的时候 是什么环境,就挂什么环境的APC)

插入的时候:(真正执行插入的时候再进行判断)

插入时的当前环境(线程随时处于切换状态)。当值为3时,在插入APC之前会判断当前线程是否处于挂靠状态 再进行APC插入

根据KAPC结构中的ApcStateIndex找到对应的APC队列

再根据KAPC结构中的ApcMode确定是用户队列还是内核队列

将KAPC挂到对应的队列中,挂到KAPC的==ApcListEntry==处

再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态

修改KAPC_STATE结构中的KernelApcPending/UserApcPending(有无内核/用户APC函数要执行)

描述:

Alertable=0,UserApcPending = 0:当前插入的APC函数未必有机会执行
Alertable=1 ,UserApcPending = 1:将目标线程唤醒(从等待链表中摘出来,并挂到调度链表)

可通过以下两个函数手动设置Alertable:

总结:内核apc直接在第五步修改KernelApcPending,用户apc修改UserApcPending需要满足一些条件

APC函数的插入和执行并不是同一个线程,具体点说:

在A线程中向B线程插入一个APC,插入的动作是在A线程中完成的,但什么时候执行则由B线程决定。所以叫异步过程调用

内核APC函数与用户APC函数的执行时间和执行方式也有区别。

在这里插入图片描述

SwapContext:在函数末判断是否有内核APC,

函数返回上一层:并未进行处理,继续返回上一层

并未进行处理,继续返回上一层:KiSwapThread()

跳转后:调用KiDeliverApc 函数执行APC函数

当要执行用户APC之前,先要执行内核APC

在进行系统调用、中断或者异常,返回ring3时,一定会调用KiServiceExit()函数

无论是内核还是用户APC最终都会调用KiDeliverApc函数

这个执行点是:只有有用户apc,内核apc才回得到执行,否则在407817处判断没有用户apc,直接进行返回,不处理内核apc。有用户apc要处理的时候,也是先处理内核apc。

1.判断第一个链表(内核APC队列)是否为空
2.判断KTHREAD.ApcState.KernelApcInProgress(是否正在执行内核APC)是否为1
3.判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)
4.将当前KAPC结构体从链表中摘除
5.执行KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间
6.将KTHREAD.ApcState.KernelApcInProgress设置为1 标识正在执行内核APC
7.执行真正的内核APC函数(KAPC.NormalRoutine)
8.执行完毕 将KernelApcInProgress改为0

如下:

如果不为空,继续此过程,循环上述步骤

1.内核APC在线程切换的时候就会执行,这就意味着,只要插入内核APC很快就会执行

2.在执行用户APC之前会先执行内核APC

3.内核APC在内核空间执行,不需要换栈,一个循环全部执行完毕。

当产生系统调用 中断或者异常,线程在返回用户空间前都会调用_KiServiceExit函数,在_KiServiceExit函数里会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数进行处理

参数为1 即处理内核也处理用户APC

在这里插入图片描述

处理用户APC要比处理内核APC复杂的多,因为用户APC函数要在用户空间执行,这里涉及到大量的换栈操作:

​ 当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器 栈的位置等等(_Trap_Frame),然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可

​ 但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程进入内核时的位置,而是返回到其他位置(真正执行APC的位置),每处理一个用户apc都会涉及到:内核->用户空间->再回到内核空间

​ 每处理一个用户APC就会涉及到:内核—>用户空间—>再回到内核空间

​ 执行用户APC最为关键的就是理解堆栈操作的细节

1.判断用户APC链表是否为空
2.判断第一个参数是为1,为1说明处理用户APC和内核APC
3.判断ApcState.UserApcPending(是否正在执行用户APC)是否为1
4.将ApcState.UserApcPending设置为0,表示正在处理用户APC
5.链表操作 将当前APC从用户队列中拆除
6.调用函数(KAPC.KernelRoutine)释放KAPC结构体内存空间
7.调用KiInitializeUserApc函数

在这里插入图片描述

​ 线程进0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame结构体中,如果要提前返回3环去处理用户APC,就必须修改_Trap_Frame结构体,因为此时Trap_Frame中存储的EIP是从三环进零环时保存的EIP,而不是用户APC函数的地址

​ 比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置,还有堆栈ESP也要修改为处理APC需要的堆栈。那原来的值怎么办?处理完APC后该如何返回原来的位置呢?

KiInitializeUserApc要做的第一件事就是备份:

​ 将原来_Trap_Frame的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成

img

CONTEXT结构体存到哪?把这个结构体和APC需要的参数,直接存到三环的堆栈里

首先esi是CONTEXT结构体里ESP的偏移,也就是三环的堆栈,然后进行4字节对齐。

接着将用户3环的栈减0x2DC个字节,此时三环的堆栈被拉伸,为什么是2DC个字节呢?

因为CONTEXT结构体的大小加上用户APC所需要的4个参数正好是2DC个字节,如下图:

在这里插入图片描述

在这里插入图片描述

此时的esi指向的是-2DC的位置,也就是上图的NormalRoutine,+10降低堆栈,将指针指向SystemArgument2

这几行代码将CONTEXT复制到了三环的堆栈

将APC函数执行时需要的4个值压入到3环的堆栈

当KiInitializeUserApc将CONTEXT和执行用户APC所需要的4个值备份到3环的堆栈时,就开始准备用户层的执行环境

首先修改段寄存器 SS DS FS GS

这个EIP就是执行用户APC时返回到3环的位置

这个位置是固定的,是一个全局变量:KeUserApcDispatcher。这个值在系统启动的时候已经赋值好了,是3环的一个函数:ntdll.KiUserApcDispatcher

然后回到3环,由KiUserApcDispatcher执行用户APC

总结:

ntdll.dll中

找到KiUserApcDispatcher函数,结合上面的堆栈图我们可以得知,esp+0x10的位置就是CONTEXT指针

当用户在3环调用QueueUserApc函数来插入apc时,不需要提供NormalRoutine,这个参数在QueueUserApc内部指定

此时的ESP指向的是NormalRoutine,pop eax将NormalRoutine赋值给了eax,然后call eax开始处理用户APC的总入口

处理完用户的APC函数之后,会调用ZwContinue,这个函数的意义在于:

1.返回内核,如果还有用户APC,重复上面的执行过程
2.如果没有需要执行的用户APC,会将CONTEXT赋值给Trap_Frame结构体,就像从来没有修改过一样,ZwContinue后面的代码不会执行,线程从哪里进0环仍然会从哪里回去

kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState         : _KAPC_STATE  //子结构体ApcState.即APC队列
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState         : _KAPC_STATE  //子结构体ApcState.即APC队列
kd> dt _KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead            //2个APC队列(双向链表) 用户APC队列和内核APC队列 ,队列存储的都是APC函数
   +0x010 Process                //线程所属进程或者所挂靠的进程 如果没有挂靠指向的地址与ethread 220成员threadprocess 指向地址相同,挂靠情况下指向挂靠的进程。
   +0x014 KernelApcInProgress    //内核APC是否正在执行
   +0x015 KernelApcPending        //是否有正在等待执行的内核APC函数 存在置为1,不存在为0
   +0x016 UserApcPending        //是否有正在等待执行的用户APC函数  存在置为1,不存在为0
kd> dt _KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead            //2个APC队列(双向链表) 用户APC队列和内核APC队列 ,队列存储的都是APC函数
   +0x010 Process                //线程所属进程或者所挂靠的进程 如果没有挂靠指向的地址与ethread 220成员threadprocess 指向地址相同,挂靠情况下指向挂靠的进程。
   +0x014 KernelApcInProgress    //内核APC是否正在执行
   +0x015 KernelApcPending        //是否有正在等待执行的内核APC函数 存在置为1,不存在为0
   +0x016 UserApcPending        //是否有正在等待执行的用户APC函数  存在置为1,不存在为0
 
 
 
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    指向我所提供的APC函数(真正想执行的函数),并不完全等于APC函数的地址。
   +0x020 NormalContext    : Ptr32 Void
   +0x024 SystemArgument1  : Ptr32 Void
   +0x028 SystemArgument2  : Ptr32 Void
   +0x02c ApcStateIndex    : Char
   +0x02d ApcMode          : Char
   +0x02e Inserted         : UChar
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    指向我所提供的APC函数(真正想执行的函数),并不完全等于APC函数的地址。
   +0x020 NormalContext    : Ptr32 Void
   +0x024 SystemArgument1  : Ptr32 Void
   +0x028 SystemArgument2  : Ptr32 Void
   +0x02c ApcStateIndex    : Char
   +0x02d ApcMode          : Char
   +0x02e Inserted         : UChar
 
 
 
 
KiServiceExit()
{
 
cmp [ kthread.apcstate.userapcpending] 0 ;检查是否有用户apc请求。。。。。不判断有无内核apc请求,有内核apc请求先处理内核的(KiDeliveApc处理),处理完了处理用户的
 
 
}
KiServiceExit()
{
 
cmp [ kthread.apcstate.userapcpending] 0 ;检查是否有用户apc请求。。。。。不判断有无内核apc请求,有内核apc请求先处理内核的(KiDeliveApc处理),处理完了处理用户的
 
 
}

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-10-8 10:18 被pyikaaaa编辑 ,原因:
收藏
免费 4
支持
分享
最新回复 (1)
雪    币: 1795
活跃值: (3995)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
好,谢谢分享
2022-9-21 19:09
0
游客
登录 | 注册 方可回帖
返回
//