0x01 进程启动过程简介
最近正在研习毛德操那本《Windows内核情景分析》上卷,看的真是难受呀,菜鸟还是表示有压力...老大跟我说要先
过一遍然后再深入研究,慢慢啃吧。总之言归正传,在看的懂本文附源代码之前,先看看第五章 进程管理 这章吧。我这里简要概括下:
ReactOS中描述的进程启动过程如下:
1. CreateProcess打开文件创建出Section然后调用进入系统调用
2. NtCreateProcess初始化进程资源(进程内存空间,句柄表,Peb,设备描述表等等),但是进程只是一个空壳需要线程作为其执行体,所以进程在初始化的时候必须为自己创建一个线程(即我们通常说的主线程)
3. 主线程由KeInitThread函数进行初始化,并规定其受到调度时内核的启动点是PspUserThreadStartup
4. PspUserThreadStartup为主线程的Ring3执行机会创造一些环境,进程需要加载动态链接库,这个函数是家喻户晓的ntdll!LdrInitializeThunk,ReactOS并不是将TrapFrame.EIP直接修改为ntdll!LdrInitializeThunk,而是采用UserApc的方式,在内核线程受到调度回到Ring3层时,此Apc得到执行机会。
5. 一般的线程的Ring3层入口,ReactOS中描述是BaseProcessStartup(主线程)/BaseThreadStartup(非主线程),现在XP往后我们可能再也看不到这个函数了,现在我们更多看到的是ntdll!RtlUserThreadStart,这个函数同样也是修改TrapFrame.EIP的方式得到执行的,不幸的是,只要存在用户Apc那么BaseProcessStartup/BaseThreadStartup就不要想执行起来,由于每次内核返回Ring3的过程每次都会修改TrapFrame.EIP,修改为KiUserApcDispatch直到没有用户层Apc。
6. APC描述:2E中断KiSystemService退出时KiSystemServiceExit会调用KiDeliverApc,这个函数检查当前线程体(KTHREAD)的两个Apc队列,KernelMode队列的与UserMode队列的。前者调用一个循环全给执行了,后者就有些麻烦,因为涉及到Ring0->Ring3->Ring0->Ring3...重复检查的过程(详细的LZ就不多bb了,大家可以看书学习...)
0x02 进程回调的限制
1. Apc注入离不开系统回调,而Apc是线程相关的,那么就肯定要在回调中由线程Id去查找线程体(ETHREAD),我们看2000的进程回调与线程回调的调用位置,两者都是在PspCreateThread中得到执行的(进程在前线程在后):
2000在初始化线程的时候才会将EProcess.UniqueProcessId置为其句柄表的索引,后续线程创建时将不再通知进程回调。而这个时候PspCreateThread还未将当前线程体初始化完毕,特别的是EThread.GrantedAccess没被初始化,那么在进/线程中 不管是Ring3的OpenThread或者是Ring0的PsLookupThreadByThreadId都将Fail,这个算是微软的一个小bug,但是到Vista之后,也就是Win7~Win10都不存这个问题了。
2. Win7及之后的系统版本动态库加载点ntdll!LdrInitializeThunk不再使用Apc的方式启动,而是直接将TrapFrame.EIP修改为ntdll!LdrInitializeThunk,这样UserMode Apc的执行机会就自然被延迟了,那么延迟到什么时候呢? (这是64位ntdll,不过跟32也没什么差别):
ntdll!LdrInitializeThunk会调入_LdrpInitialize,在加载完导入表的一系列模块依赖之后会调用ZwTestAlert,这个函数内核情景分析也有介绍,他会将当前线程的Apc队列的UserPending重新置位,让已存在队列里的UserMode Apc得到执行(就在此次系统调用返回用户层之后),Win7以及之后的UserApc不再像2000那样,UserApc执行时机有了明确的几个点,具体大家可以深入研究
0x03 XP~Win10 APC注入方案实现
XP之前由于进程回调的限制,所以与Win7后续要分两种情况处理,看到某款安全软件的做法是采用A盾劫持KiFastCallEntry,在进程初始化加载动态链接库的过程会调用NtQuerySection询问系统模块,以这个点作为Apc注入时机,LZ只想说哪有那么麻烦,直接在模块回调通知exe加载时就可以了,而且又可以避免Apc插入到LdrInitializeThunk的UserApc之前,因为Image回调在通知exe加载的时候在这里:
b1c88c08 80644526 nt!PsCallImageNotifyRoutines+0x36
b1c88d0c 805d0e6b nt!DbgkCreateThread+0xa2
b1c88d50 805470de nt!PspUserThreadStartup+0x9d
00000000 00000000 nt!KiThreadStartup+0x16
这里正好是待创建进程的主线程刚得到调度的时候,这里线程体什么的都已初始化完毕,可以无忧进行插入。
Win7及之后由于没有那么多限制,可以直接在进程回调完成。
LZ用Insert Kernel Apc再Insert User Apc的原因在于:
1. 得到线程已经开始执行或者调度没多久的时机,这能服从先后的原则;
2. 申请ShellCode时可以不用考虑EProcess的地址空间锁的问题,32一般不会遇到,64的LoadImage可是一调用Zw*VirtualMemory就GG;
3. 避免XP或者XP之前的Apc插入过早,插到LdrInitializeThunk的UserApc之前,这样所有动态链接库都没有加载就直接LdrLoadDll 基本上必crash。
注:
1. 测试用的动态库放入C:\下,导出AiQ3Helper001函数,自己附上调试信息可以看到结果撒
2. 书写代码时基本上用的都是32/64兼容变量,没有做64位测试,如果有蓝屏现象可能也就几处小问题,大家可以自行修改哈
3. 支持XP ~ Win10,win10(BuildNumber<=14393),不要在最新版本上测试,微软出了保护...
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课