这段时间没啥事干,就断断续续把 Windows 的 APC机制给分析了一下,发现许多地方还是比较有趣的,例如:用户特殊APC一开始是插入到内核APC链表中的,然后再通过它的 kernelroutine
将APC插回用户APC链表。
还发现了我调试的这个win10版本在用户特殊APC执行的时候的存在的BUG(快速跳转)。
APC的作用我这里就略了,可以百度一下。
Windows 10版本(同以前文章的版本):
废话少说,正文开始:
APC结构篇
APC插入篇
APC插入篇总结
APC执行篇
APC执行篇总结
APC玩法篇
1.R3 注册用户特殊APC
2.R0 通过内核特殊APC读取进程内存
文档pdf版本和相关资料下载:
kthread.ApcState 指向 _KAPC_STATE
ApcListHead:
ApcListHead[0] 指向 内核APC链表
ApcListHead[1] 指向 用户APC链表
当 ApcListHead.Flink == ApcListHead.Blink == &ApcListHead.Flink 时,APC链表为空
否则 ApcListHead.Flink = & _KAPC.ApcListEntry,ApcListHead.Blink = & _KAPC.ApcListEntry
Process:
线程所属或者所挂靠的进程
InProgressFlags:
是否有APC正在执行 :
第0位置1:内核APC正在执行
第1位置1:特殊APC正在执行
KernelApcPending:
是否有内核APC正在等待
UserApcPendingAll:
用户APC正在等待
第0位置1:用户特殊APC正在等待
第1位置1:用户普通APC正在等待
我们就不讨论QueueUserAPC
这个函数了,因为他最后是调用NtQueueApcThread
进入R0的。
QueueUserAPC调用栈如下:
所以呀,我们直接 干R0的NtQueueApcThread
就行啦!!
我们可以发现!NtQueueApcThread
里面只是调用了NtQueueApcThreadEx
。
说明NtQueueApcThread
就是一个被阉割功能的函数了hhhh。
注意1:当用户APC触发时,返回的一定是R3的ntdll!KiUserApcDispatcher
,然后ntdll!KiUserApcDispatcher
再调用ApcRoutine
。
注意2:QueueUserApc
在插入APC调用NtQueueApcThread
时,参数ApcRoutine
是 ntdll!RtlDispatchAPC
,并不是我们提供给QueueUserApc
的函数指针。我们的函数指针需要由ntdll!RtlDispatchAPC
再次分发执行。
及 NtQueueApcThread
的第二个参数为ntdll!RtlDispatchAPC
,第三个参数才是我们我们提供给QueueUserApc
的函数指针。
QueueUserApc
简直就是NtQueueApcThreadEx
的二次阉割函数。()
我们继续分析NtQueueApcThreadEx
:
这个函数首先会判断一些参数是否正确,例如:会判断我们给的ApcRoutine地址合不合理。不允许给0。
说明从R3调用函数插入用户APC,APC的NormalRoutine
不能为0,非常可惜。少了很多玩法。
可以看出,如果(flag != 0 && flag != 1)
,那么就会执行这段代码。
这个类型的APC不同于一般的用户APC(用户普通APC 和 用户特殊APC)
主要的区别就是 _KAPC的地址空间是通过特殊手段申请和释放的,并且 KernelRoutine 和 RundownRoutine 和一般的用户APC不同。
可以看出:
1、一般的用户APC的_KAPC
都是通过ExAllocatePoolWithQuotaTag
申请,且大小为0x58。
2、用户普通APC的 KernelRoutine = SC_ENV::Free
;而用户特殊APC的 KernelRoutine = KeSpecialUserApcKernelRoutine
;
3、用户普通APC和用户特殊APC的 RundownRoutine = ExFreePool
;
如果插入失败!则会调用RundownRoutine
释放申请的 _KAPC 的内存。
填充_KAPC的各个成员。
这里补充一个非常离谱的设定:
通过NtQueueApcThreadEx
插入用户APC时:
当初始用户普通APC时,传入a7 = 1,用户特殊APC时,传入a7 = 0。
然后ApcMode
的值竟然由 a7 来确定!!??
那么说明我们注册的 用户特殊APC,它的 ApcMode 竟然是 0 !!
也就是说待会插入时,是插入的内核APC链表。(详情见下文)
这个函数主要作用就是上锁,填充APC的两个参数指针。
然后调用函数将APC插入 _KTHREAD._KAPC_STATE
(内核APC和用户APC都是通过这个函数插入!)。
然后解锁。
上锁部分:(略)
填充参数指针、插入APC部分:
KiDeliverApc
把这个用户特殊APC视作内核APC,并执行它的KernelRoutine = KeSpecialUserApcKernelRoutine
,
KeSpecialUserApcKernelRoutine
会把这个用户特殊APC重新插入到用户APC链表内。
详情请点击这里。
这个函数的功能就是将 _KAPC 插入 _KTHREAD._KAPC_STATE
。
用户APC 插入 _KAPC.ApcListHead[1]
内核APC插入_KAPC.ApcListHead[0]
_KTHREAD.ApcStateIndex
表示这个线程当前是否附加到其他进程。0:没有、1:附加到了其他进程
_KTHREAD.ApcState
是这个线程当前需要执行的APC。
_KTHREAD.SaveApcState
是这个线程的备份APC。
当需要插入的 _KAPC.ApcState != 0
时,直接插入线程的ApcState。
当需要插入的_KAPC.ApcState == 0 && 需要插入的线程未附加到其他进程
时,直接插入线程的ApcState。
当需要插入的_KAPC.ApcState == 0 && 需要插入的线程已经附加到其他进程
时,插入线程的SaveApcState。
关于_KTHREAD.ApcStateIndex
、_KTHREAD.ApcState
、_KTHREAD.SaveApcState
三者的关系:
设:
有两个进程:进程A、进程B。和一个线程A_T是属于进程A的。
此时:A_T(_KTHREAD).ApcStateIndex = 0
。
接下来,线程A_T将要执行KeStackAttachProcess
附加到进程B,那么会发生:(代码节选自KiAttachProcess
)
将原有的ApcState备份到SaveApcState,然后将用户APC清空,再将 ApcState置1。当然了,解除附加状态的时候,会把SaveApcState恢复到ApcState,然后将ApcState置0(代码就不贴图了)。
插入APC,插入方式分为两类(插入链表头部、插入链表尾部):
用户普通APC 和 内核普通APC 和 用户特殊APC第一次 的插入方式:
插入链表头部。
注意!!!一开始用户特殊APC的 ApcMode = 0,也就是说,这个时候,用户特殊APC 插入的是内核APC链表!
用户特殊APC第一次插入也是通过这段代码插入,插入到内核APC链表。
那用户特殊APC插入到了内核APC链表,这不是乱套了吗??!!!其实并没有,
阿三哥这个地方整了一手骚操作:还记得 用户普通APC的 KernelRoutine = KeSpecialUserApcKernelRoutine
吗。
它会将这个APC重新插入用户APC链表。(下文详解)
用户特殊APC!第二次!插入方式:
(KeSpecialUserApcKernelRoutine
会将APC重新插入到 用户APC链表)
将 ApcState.UserApcPendingAll or 1
后,将此APC插入链表尾部
内核特殊APC插入方式:
插入链表尾部。
用户普通APC 和 用户特殊APC 最终都插入到用户 APC 链表中。
内核普通APC 和 内核特殊APC 最终都插入到内核 APC 链表中。
用户普通APC 和 内核普通APC 最终总是插入到 APC 链表的头部。
用户特殊APC 和 内核特殊APC 最终总是插入到 APC 链表的尾部。
用户特殊APC 一开始插入到 内核APC链表中,然后再取出来插入到 用户APC链表。
用户特殊APC会很快执行,不需要等待线程变为可接警状态。
用户普通APC的_KAPC.KernelRoutine = SC_ENV::Free
。
而用户特殊APC的 KernelRoutine = KeSpecialUserApcKernelRoutine
。
用户普通APC和用户特殊APC的 RundownRoutine = ExFreePool
。
当APC插入失败时,会调用RundownRoutine
。
首先知道想要执行插入的APC,必须先通过KiDeliverApc
来处理。
通常,当线程在进行一些特殊操作(R0返回R3、睡眠、之类的)时,就会调用这个函数,可以通过IDA的交叉引用窗口查询一下。
这里挑一个典型的案例,用户调用NT函数后从R0返回R3的时候:(代码节选自KiSystemCall64
结尾附近)<a name="while_ua"></a>
满足条件((_KTHREAD.ApcState.UserApcPendingAll & 3) != 0
)
即:有用户APC等待执行时,进入。
KiInitiateUserApc
调用 KiDeliveApc(1)
内核APC会在一轮KiDeliverApc
内全部调用,
用户APC一轮KiDeliverApc
只能选出一个,添信息加入用户堆栈。
上图通过while循环,可将全部需要执行的用户APC信息添加入用户堆栈(详情见下文),再交给R3ntdll!KiUserApcDispatcher
处理。
代码我就不贴了,太多了,我会另外提供一个文本,文本内是注释过后的KiDeliverApc
代码,可以看细节。
这里贴大致的流程图,然后简单说明一下就行:
CurrentThread->ApcState.InProgressFlags = 1
执行KernelRoutine
,
执行NormalRoutine
。
CurrentThread->ApcState.InProgressFlags = 0
CurrentThread->ApcState.InProgressFlags = 2
执行KernelRoutine
CurrentThread->ApcState.InProgressFlags = 0
对于内核APC来说:
KernelRoutine
这个函数的意义不那么清晰,有时候是一些功能,或者只有一条 return 指令的函数,有时候又是释放KAPC的内存。
NormalRoutine
就是我们想要让线程执行的函数啦。
内核APC处理的非常快!因为内核代码中有许多地方都会调用KiDeliverApc(0)
(仅执行内核APC,这也是为什么用户特殊APC能快速从内核APC链表中取出重新插入用户APC链表的原因。)
还记得在 APC插入 篇中,提到的:
用户普通APC的KernelRoutine = SC_ENV::Free
;
所以对用户普通APC来说:执行这个函数KernelRoutine = SC_ENV::Free
相当于把选中的KAPC释放掉。
对于用户APC(不管是用户普通APC还是用户特殊APC)来说,
需要将NormalRoutine
下放到R3的ntdll!KiUserApcDispatcher
。
所以他们的执行NormalRoutine
的流程是一样一样的!
下面的图是调试用户特殊APC的时候截的,所以函数名字是 SpecialUserApc
,
但是不用在意,都一样的,不影响。
作用就是设置陷阱帧的各个值,然后拓展用户堆栈的空间,将对应参数填入用户堆栈。使得待会从KiSystemCall64
返回R3时,返回到设定的地址。一定要记得是拓展了用户堆栈空间,这和多个用户特殊APC的执行有关系!
(代码节选)这一段代码的赋值结果,和 下一张图的 堆栈是对应起来的。
我们在相同线程的ntdll!KiUserApcDispatcher
处下断点,中断后 堆栈 和 调用栈(三个参数是内核地址,这是BUG点击查看):
看调用栈中还保留着ntdll!NtQueueApcThreadEx
,待会要通过 ZwContinue
返回此处。<a name="userapc_next"></a>
代码节选:
通过KiUserCallForwarder
把栈中的参数放入寄存器 jmp进我们设置的NormalRoutine
,:
看三个参数值 和上图堆栈中的一模一样。
那么参数a7用哪了?上文已经说了,a7的值:
当是用户普通APC时,a7 = 1,用户特殊APC时,a7 = 0。
上图中调用 ZwContinue
时,第二个参数就是 a7。第一个参数是内核帮我们准备好的 Context
(这个_CONTEXT的地址就是刚从R0返回到R3ntdll!KiUserApcDispatcher
时 RSP 的值),方便我们直接返回到 ntdll!NtQueueApcThreadEx
中 syscall 的下一行。(或者方便调用下一个用户特殊APC)
看图:
0x00007ffd33262224
就是 ntdll!NtQueueApcThreadEx
中 syscall 的下一行(上面也有图)
关于 a7 的作用:
当是用户普通APC时,a7 = 1,用户特殊APC时,a7 = 0。
当a7作为ZwContinue
的第二个参数传入时,填入0说明不将线程设置为可警醒,填入1说明将线程设置为可警醒。
与 SleepEx
的第二个参数有异曲同工之妙。
浅逆一下 NtContinue
NtContinue
会再调用KiContinueEx
,NtContinueEx
内有一段:
那为什么 用户普通APC执行后要将线程设置警醒,而用户特殊APC就不用呢??
这个机制和 多个用户APC的执行 有关,点击查看详情!
对用户特殊APC来说,有一个非常有趣()的机制:
我们知道用户特殊APC的 KernelRoutine = KeSpecialUserApcKernelRoutine
那我们分析一下,KeSpecialUserApcKernelRoutine
,关于用户特殊APC的一切都清楚了。
内核APC处理的非常快!因为内核代码中有许多地方都会调用KiDeliverApc(0)
(仅执行内核APC,这也是为什么用户特殊APC能快速从内核APC链表中取出重新插入用户APC链表的原因。)
(这个函数有BUG!详情看下文!)
红圈对应红圈说明,绿圈对应绿圈说明.....
上文中说到:内核APC是很快就能执行的
那么进入KiDeliverApc
后,因为用户特殊APC插入到的是内核APC链表,所以就被选中执行。
会先执行 KernelRoutine = KeSpecialUserApcKernelRoutine
:
把外部的NormalRoutine置0
,让KiDeliverApc
认为这是内核特殊APC,不执行我们设置的NormalRoutine
。
然后再将 此APC的以 用户特殊APC 的身份重新插入到 用户APC链表。(这里在上文已经讲过了)
因为 第二次插入用户特殊APC时,将Thread.ApcState.UserApcPendingAll 置 有值
,因此接下来执行到
KiDeliverApc(1)
时,就会执行这个APC。
(注意是KiDeliverApc(1)
不是KiDeliverApc(0)
,第一个参数置0的话KiDeliverApc
不执行用户APC)
第一次插入用户特殊APC,插入到内核APC链表,调用栈:
第二次插入用户特殊APC,插入到用户APC链表,调用栈:<a name="bug"></a>
注意,我发现我调试的这个版本,这里是有BUG的,它在KeSpecialUserApcKernelRoutine
内重新创建一个KAPC时,传入参数是NormalContext 、SystemArgument1、SystemArgument2
它们在内核里面的地址,并不是值!(可惜的是在新版本的Windows中修复了这个BUG),所以导致KAPC内NormalContext 、SystemArgument1、SystemArgument2
都为内核地址,这些内核地址会传入R3,所以,如果执行了需要参数用户特殊APC函数,就会触发内存访问异常。(仅用户特殊APC会触发这个BUG)
BUG版本:
无BUG版本:
同用户普通APC,点击此跳转。
关于用户特殊APC执行后不需要设置线程警醒,而用户普通APC执行后需要设置线程警醒的原因:<a name="原因"></a>
在用户APC执行的执行过程中,不论选中的是普通APC还是特殊APC,总会先将 ApcState.UserApcPendingAll.UserApcPeding 置 0
。告诉()操作系统已经没有用户普通APC在执行了。
所以在执行完选中的用户APC后,需要通过NtContinue
调用TestAlertThread
判断是否还有用户APC尚未执行。
TestAlertThread代码节选:
所以对用户普通APC来说:
即便使用while循环调用 KiDeliverApc(1)
处理了用户APC(例如这张图),再返回R3进入ntdll!KiUserApcDispatcher
后, 只能执行一个用户普通APC(因为没有用户特殊APC的话,这个while内的指令只执行了一次)。
而对于用户特殊APC来说:<a name="特殊APC栈"></a>
流程图我已经说了:在KiDeliverApc
里面就通过一个while循环来判断用户APC链表中是否还存在用户特殊APC。
如果还存在用户特殊APC,那么会重新将ApcState.UserApcPendingAll.SpecialUserApcPeding 置 1
。
若使用while循环调用 KiDeliverApc(1)
处理用户APC(例如这张图),就会重复在用户堆栈中添加相应用户特殊APC的信息(因为这个while内的指令执行了多次),在返回R3,进入ntdll!KiUserApcDispatcher
后,可配合 ZwContinue
一次性执行多个用户特殊APC。
例:
当用户APC链表中存在3个用户特殊APC时:
通过whiel
循环调用KiDeliverApc(1)
处理后,返回R3ntdll!KiUserApcDispatcher
时的堆栈:(_context仅展示部分)
当返回R3时,此时RIP定位到ntdll!KiUserApcDispatcher
,RSP定位到红色部分,准备处理第一个APC(红色部分)。
处理完第一个APC后(红色部分),通过ZwContinue
再次跳转到ntdll!KiUserApcDispatcher
,同时RSP也定位到了黄色部分,处理第二个APC(黄色部分)。
处理完第二个APC后,又通过ZwContinue
再次跳转到ntdll!KiUserApcDispatcher
,同时RSP也定位到了青色部分,处理第三个APC(青色部分)。
处理完第三个APC后,已经没有APC要执行了,就通过ZwContinue
返回到原本RIP的下一行,同时RSP也恢复到灰色部分。
自此,三个用户特殊APC执行完成。线程也就可以干自己的事情了。
注意这种情况只有用户特殊APC才会有!!!
整理了一下
APC的执行总是从链表表尾开始,所以特殊APC执行的比普通APC要早。
内核特殊APC的 _KAPC.NormalRoutine
为空。
不论是内核APC还是用户APC,选中后总要先执行_KAPC.KernelRoutine
。
调用一次KiDeliverApc
,就会把全部内核APC执行。
调用一次KiDeliverApc
,只能选出一个用户普通APC,然后将相关信息添加到用户堆栈中。
用户APC总是在从R0返回R3的时候执行。
用户普通APC只能在一个一个分开的时间段执行。而用户特殊APC是一次性连续全部执行。
用户普通APC只能等待线程可接警时才能执行,而用户特殊APC不需要,它可以快速执行。
NtQueueApcThreadEx
这个函数竟然是ntdll
导出的!微软还把它藏起来不让用了是吧?!
示例代码:
有其他进程的线程句柄的话,也能向其他进程插入 用户特殊APC。
内核特殊APC,是能执行的APC中最早执行的,因此可以用它来做些事情。(不仅限于读内存)
项目地址:Kernel-Special-APC-ReadProcessMemory
因为我电脑上没有网络游戏,所以还没测试过读取受保护进程的内存。但是理论上是可读的。
更详细的信息请点击项目地址查看。
代码仅作学习与交流使用,请勿用作非法用途!!!!!
无需获取进程句柄,无需挂靠进程(线程),无需切换CR3,让目标进程自己将内存交出来。
因为没有创建新的线程在目标进程上下文,所以,读取内存的时间相比挂靠进程(线程)的方式要慢上许多。(但也可以接受)
读取速度测试(Read speed test):
KernelSpecialAPC :
读取 1000000 次长度为 30 字节的内存所需时间为:21985 ms
ReadProcessMemory:
读取1000000 次长度为 30 字节的内存所需时间为:890 ms
当进程全部线程被挂起时,无法读取内存!
10.0
.
17763
版本
17763
如果网络文档内的“跳转”无法使用,推荐阅读PDF版本
链接:https:
/
/
pan.baidu.com
/
s
/
1owtKjfL80f1WbQj4blIoKQ
提取码:ICEY
如果网络文档内的“跳转”无法使用,推荐阅读PDF版本
链接:https:
/
/
pan.baidu.com
/
s
/
1owtKjfL80f1WbQj4blIoKQ
提取码:ICEY
ntdll!_KAPC_STATE
+
0x000
ApcListHead : [
2
] _LIST_ENTRY
+
0x020
Process : Ptr64 _KPROCESS
+
0x028
InProgressFlags : UChar
+
0x028
KernelApcInProgress : Pos
0
,
1
Bit
+
0x028
SpecialApcInProgress : Pos
1
,
1
Bit
+
0x029
KernelApcPending : UChar
+
0x02a
UserApcPendingAll : UChar
+
0x02a
SpecialUserApcPending : Pos
0
,
1
Bit
+
0x02a
UserApcPending : Pos
1
,
1
Bit
ntdll!_KAPC_STATE
+
0x000
ApcListHead : [
2
] _LIST_ENTRY
+
0x020
Process : Ptr64 _KPROCESS
+
0x028
InProgressFlags : UChar
+
0x028
KernelApcInProgress : Pos
0
,
1
Bit
+
0x028
SpecialApcInProgress : Pos
1
,
1
Bit
+
0x029
KernelApcPending : UChar
+
0x02a
UserApcPendingAll : UChar
+
0x02a
SpecialUserApcPending : Pos
0
,
1
Bit
+
0x02a
UserApcPending : Pos
1
,
1
Bit
ntdll!_KAPC
+
0x000
Type
: UChar
+
0x001
SpareByte0 : UChar
+
0x002
Size : UChar
+
0x003
SpareByte1 : UChar
+
0x004
SpareLong0 : Uint4B
+
0x008
Thread : Ptr64 _KTHREAD
+
0x010
ApcListEntry : _LIST_ENTRY
+
0x020
KernelRoutine : Ptr64 void
+
0x028
RundownRoutine : Ptr64 void
+
0x030
NormalRoutine : Ptr64 void
+
0x020
Reserved : [
3
] Ptr64 Void
+
0x038
NormalContext : Ptr64 Void
+
0x040
SystemArgument1 : Ptr64 Void
+
0x048
SystemArgument2 : Ptr64 Void
+
0x050
ApcStateIndex : Char
+
0x051
ApcMode : Char
+
0x052
Inserted : UChar
ntdll!_KAPC
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2023-2-9 12:46
被icey_编辑
,原因: