线程如果想结束,一定是自己执行代码把自己杀死
,不存在别人把线程结束的情况。如何改变线程的行为,提供一个函数,让他自己去调用,这个函数就是APC
,即异步过程调用,把提供的函数挂在APC队列中。
ApcListHead 两个双向链表组成 16个字节
用户APC:
内核APC:
改变线程执行流程,把给定的APC
挂载到线程的APC队列中,在某个时刻,当前线程会检查当前的函数列表,当里面有函数的时候,就会去调用。
通过NormalRoutine成员找到我所提供的APC函数 (指向apc函数)
总结:如果我们想要改变一个线程,可以先提供一个APC,然后通过 _KAPC.NormalRoutine
成员 指向我们提供的APC函数在哪里,再将APC函数地址存到 _KTHREAD.ApcState.ApcListHead
的第一个成员中(apc队列中)
当前的线程什么时候会执行所提供的APC函数
发生系统调用 异常或中断返回用户空间的时候
执行函数KiServiceExit(检查是否有apc请求,有则KiDeliverApc处理--->改变线程流程)
这个函数是系统调用 异常或中断返回用户空间的必经之路
负责执行,处理 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,则插入失败。
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
来完成
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编辑
,原因: