专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
51好读  ›  专栏  ›  看雪学苑

Win10 x64 APC的分析与玩法

看雪学苑  · 公众号  · 互联网安全  · 2023-03-10 18:01

正文

请到「今天看啥」查看全文



我们继续分析NtQueueApcThreadEx:


NtQueueApcThreadEx:

NTSTATUS __fastcall NtQueueApcThreadEx(        void *ThreadHandle,            //需要插入的线程句柄        BOOLEAN flag,                //0:用户普通APC  1:用户特殊APC        __int64 ApcRoutine,            //需要执行的APC函数指针        __int64 NormalContext,        //需要执行的APC函数的第零个参数        __int64 SystemArgument1,    //需要执行的APC函数的第一个参数        __int64 SystemArgument2)    //需要执行的APC函数的第二个参数


这个函数首先会判断一些参数是否正确,例如:会判断我们给的ApcRoutine地址合不合理。不允许 给0。


说明从R3调用函数插入用户APC,APC的NormalRoutine不能为0,非常可惜。少了很多玩法。


内部分析1(节选):

可以看出,如果(flag != 0 && flag != 1),那么就会执行这段代码。

这个类型的APC不同于一般的用户APC(用户普通APC 和 用户特殊APC)
主要的区别就是 _KAPC的地址空间是通过特殊手段申请和释放的,并且 KernelRoutine 和 RundownRoutine 和一般的用户APC不同。


内部分析2(节选):

可以看出:

1、一般的用户APC的_KAPC都是通过ExAllocatePoolWithQuotaTag申请,且大小为0x58。

2、用户普通APC的 KernelRoutine = SC_ENV::Free;而用户特殊APC的 KernelRoutine = KeSpecialUserApcKernelRoutine;

3、用户普通APC和用户特殊APC的 RundownRoutine = ExFreePool;
如果插入失败!则会调用RundownRoutine释放申请的 _KAPC 的内存。


KeInitializeApc:


填充_KAPC的各个成员。
这里补充一个非常离谱的设定:
通过NtQueueApcThreadEx插入用户APC时:
当初始用户普通APC时,传入a7 = 1,用户特殊APC时,传入a7 = 0。
然后ApcMode的值竟然由 a7 来确定!
那么说明我们注册的 用户特殊APC,它的 ApcMode 竟然是 0 !
也就是说待会插入时,是插入的内核APC链表。(详情见下文)


KeInsertQueueApc:


这个函数主要作用就是上锁,填充APC的两个参数指针。
然后调用函数将APC插入 _KTHREAD._KAPC_STATE(内核APC和用户APC都是通过这个函数插入!)。
然后解锁。

上锁部分:(略)

填充参数指针、插入APC部分:
KiDeliverApc把这个用户特殊APC视作内核APC,并执行它的KernelRoutine = KeSpecialUserApcKernelRoutine,
KeSpecialUserApcKernelRoutine会把这个用户特殊APC重新插入到用户APC链表内。


KiInsertQueueApc:


这个函数的功能就是将 _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会很快执行,不需要等待线程变为可接警状态。

用户普通APC的_KAPC.KernelRoutine = SC_ENV::Free。

而用户特殊APC的 KernelRoutine = KeSpecialUserApcKernelRoutine。

用户普通APC和用户特殊APC的 RundownRoutine = ExFreePool。

当APC插入失败时,会调用RundownRoutine。










请到「今天看啥」查看全文