第一个话题是内核APC源码剖析,算是笔记吧!第二个话题主要是当时分析的程序,经常启动错误报告机制,所以探讨了一番!
内核APC源码剖析
一、与APC相关的结构
1.APC属于内核对象,所有的内核对象都有一个结构体 -- 1:1(数据模型)
APC内核对象
1: kd> dt _KAPC
nt!_KAPC
+0x000 Type : UChar //对象类型
+0x001 SpareByte0 : UChar //结构体对齐 -- 说明Type是ushort
+0x002 Size : UChar //内核对象结构体大小(对齐大小)
+0x003 SpareByte1 : UChar //结构体对齐
+0x004 SpareLong0 : Uint4B
+0x008 Thread : Ptr64 _KTHREAD //APC所属的线程
+0x010 ApcListEntry : _LIST_ENTRY //APC挂载链表
+0x020 KernelRoutine : Ptr64 void //后续补充
+0x028 RundownRoutine : Ptr64 void
+0x030 NormalRoutine : Ptr64 void
+0x038 NormalContext : Ptr64 Void
+0x040 SystemArgument1 : Ptr64 Void
+0x048 SystemArgument2 : Ptr64 Void
+0x050 ApcStateIndex : Char //指向激活的APC
+0x051 ApcMode : Char //相当于KernelMode
+0x052 Inserted : UChar //标志APC是否插入链表
1.APC用于特定的线程,一个线程具有多个APC -- 1:N
线程中相关内核重要结构
1: kd> dt _KTHREAD
nt!_KTHREAD
+0x050 ApcState : _KAPC_STATE //激活的APC
+0x088 ApcQueueLock : Uint8B //APC队列锁
+0x100 ApcQueueable : Pos 5, 1 Bit //是否开启APC队列
+0x1c4 KernelApcDisable : Int2B //禁用内核APC
+0x1c6 SpecialApcDisable : Int2B //禁用特殊APC
+0x1f0 ApcStateIndex : UChar //指向激活的APC
+0x230 ApcStatePointer : [2] Ptr64 _KAPC_STATE //APC数组指针
+0x240 SavedApcState : _KAPC_STATE //备份APC所用
1: kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY //0表示内核 1 表示用户
+0x020 Process : Ptr64 _KPROCESS //当前进程
+0x028 KernelApcInProgress : UChar //内核APC正在处理
+0x029 KernelApcPending : UChar //有挂起的内核APC
+0x02a UserApcPending : UChar //有挂起的内核APC
先解释一下:什么是激活的APC?
激活的APC是指当前可以被交付的APC。首先APC是属于线程的,线程又属于进程,但是线程所属的这个进程不一定亲生的,比如挂靠到另一个进程中去。
如果当一个线程挂靠到另一个进程中去,那么此时线程一般都不会交付原来进程环境中的APC,那么就要让这个APC进行备份一下。所以,在线程挂靠到另一个进程中时,它首先会将当前+0x050 ApcState保存到+0x240 SavedApcState字段中,同时ApcStatePointer[_KAPC.ApcStateIndex]指向的就SavedApcState。
KTHREAD.ApcStateIndex指向的ApcState。
谨记:ApcState总是被激活的那一个,也就是被执行的那一个。
可能比较绕,简而言之KAPC中的ApcStateIndex代表的是它所处的环境(或者它想要的环境),而KTHREAD中的ApcStateIndex指向的当前线程的环境。
二、APC的类别区分
由于这篇不设计用户APC,所以不概述用户APC的特征!
普通内核APC:
NormalRoutine !=0 && ApcMode ==0
特殊内核APC:
NormalRoutine ==0 && ApcMode ==0 && NormalContext==0 (三无)
三、与APC相关的函数剖析
KeInitializeApc:初始化内核APC
VOID
KeInitializeApc (
IN PRKAPC Apc, //得自己分配拟空间
IN PRKTHREAD Thread, //指明线程
IN KAPC_ENVIRONMENT Environment, //指明KAPC.ApcStateIndex
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,
IN KPROCESSOR_MODE ApcMode OPTIONAL,
IN PVOID NormalContext OPTIONAL
)
这里剖析一下Environment:
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment,
InsertApcEnvironment
} KAPC_ENVIRONMENT;
其实OriginalApcEnvironment代表的是,你想往当前线程插入,什么意思呢?其实就是“嫁鸡随鸡,嫁狗随狗”,不管是当前线程是挂靠了还是没挂靠,我都只认现在。而AttachedApcEnvironment代表的是往挂靠的线程插入,它的意思就是我不管你挂靠了还没挂靠,我都认为是你挂靠了! -- 这种就承担了一种风险,如果它没有挂靠,那么再也执行不了!而CurrentApcEnvironment和InsertApcEnvironment,它们俩更像是代表了当前代码所处的阶段。比如CurrentApcEnvironment代表的就是正在执行KeInitializeApc函数。而InsertApcEnvironment代表是正在执行KiInsertQueueApc(是Ki不是Ke)。
前两种像是天生的,后两种就是后生的(延迟)...
KeInsertQueueApc:插入APC
BOOLEAN
KeInsertQueueApc (
IN PRKAPC Apc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2,
IN KPRIORITY Increment
)
以KiInsertQueueApc函数为分界线,上半部分其实就只做了两件事:
一件是,提升IRQL,只让时钟中断和IPI(核间中断)打断它。
另一件,判断所属的线程是否开启了APC队列。--- 这里引出一个问题,如果一个线程没有开启APC队列后,那么它怎么结束自己呢?或者说是没有办法通过APC结束掉自己。
众人都知KiDeliverApc派发APC,殊不知我在KiInsertQueueApc中已经开始偷偷筹划。
接下来就探一探KiInsertQueueApc。
KiInsertQueueApc:实际插入APC的函数
VOID
FASTCALL
KiInsertQueueApc (
IN PKAPC InApc,
IN KPRIORITY Increment
)
此时我们要考虑两个问题:
第一个问题:
SpecialApcDisable为什么会等于FALSE?其实纵观之前代码,我们发现没有一处修改这里,这说明这个变量有可能是别人禁用APC的标志。
我举一个例子:
KeEnterGuardedRegionThread函数内部会对这个标志进行修改。
这就解释了MSDN上所描述APC关于互斥体的那段。可见SpecialApcDisable体现的也是临界区的用途。
第二个问题:
KiUnwaitThread只会将线程从等待网上摘下来,并不能立马变为就绪线程,而是延迟就绪线程,所以并不能去派发APC,那么如何去派发呢?
这时就要说出KiExitDispatcher(LockHandle.OldIrql)的作用了,它第一个功能是降低IRQL到之前的等级,其次万一KiInsertQueueApc内核唤醒的是等待线程(术语:延迟就绪线程),还会为这个延迟就绪的线程选择合适的处理器,从而去派发。
谨记:此处的派发时通过SwapContext,切换线程时候的才会有执行KiDeliverApc的时机。
KiDeliverApc:派发APC
VOID
KiDeliverApc (
IN KPROCESSOR_MODE PreviousMode,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame
)
这里是普通内核APC和特殊内核APC的一些区别吧:
1. 特殊内核APC和普通内核APC的KernelRoutine是执行在APC_LEVEL等级,也就是操作系统给APC_LEVEL的接口。
2. 普通内核APC的NormalRoutine是执行在PASSIVE_LEVEL,这是操作系统给用于的PASSIVE_LEVEL接口,并且在KernelRoutine中可以控制NormalRoutine。
但是这里降低IRQL会引入一个新的问题,那么就是重入问题:也就是说可能有多个线程执行到这里。会造成意想不到的问题。所以操作系统在降低IRQL之前加了一句,Thread->ApcState.KernelApcInProgress = TRUE;这就与前面的if判断条件想呼应,同时KernelApcInProgress 是每个线程都具备的,所以就替你解决了重入问题。
在win7之后加入了GateWait机制:
简单的叙述一下GateWait机制,GateWait和Wait都是等待它们的区别是什么呢?
其实GateWait是比Wait更加细化的等待。Wait模式可以将堆栈或者所属的进程被换到内存外去,但是GateWait就不可以,而且GateWait一般是以循环的方式消耗,这一点可以看出,GateWait就是偏向于短时间的等待。
Vista前后的调试机制变化
一、引言
在分析样本的时候或者调试程序时,都会弹出一个错误的对话框,熟悉而陌生,这次研究一下为何会弹出对话框。
其实这个是Windows设计的Error Report机制。当程序自身无法处理异常时,最终会由OS接管,并弹出错误对话框。但是这种机制在Vista前后是不一样的。
二、Vista之前
1. 首先运行一个有Debug的程序,会弹出一个错误对话框,这个错误对话框就是MS的Error Report机制,那么下面就从谁启动的错误对话框方面去探究:
首先来研究这个问题,谁启动的错误对话框?
通过Process Explorer可以发现,错误对话框是有由一个dwwin.exe程序弹出来的,那么问题就转化到了谁启动了dwwin.exe。使用windbg附加到程序中,打印线程堆栈,可以观察到如下堆栈:
根据异常的处理流程可知,首先这个程序没有定义任何异常处理部分,即没有自己的SEH、VEH等。所以最终会调用OS定义的UnhandledExceptionFilter。上面堆栈也应验了这个流程。
随后它调用了faultrep!ReportFault程序,以及faultrep!ReportFaultDWM+0x14cf,然后就进入了等待状态。由此猜测,贴近等待函数的函数帧,必然内部启动dwwin.exe,由此为线索开始排查。
由于faultrep存在OAMP优化,所以不能通过name+offset的方式进行反汇编,所以要通过ub retAddress。
并通过在IDA中查找此模块地址,可以找到它的原始名字是:StartDWException
接下来对其uf,查看整个函数体,寻找CreateProcess的特征,或者在IDA中直接查看。
紧接着对其第一个参数进行查看,观察它启动了什么东西?
由此可知,dwwin.exe是由faultrep!StartDWException启动的。
1. 其次当点击错误对话框的Debug按钮时,会弹出一个事后调试器,那么这个是怎么启动的呢?
此时就会有两种可能:
1. faultrep启动了及时调试器。
2. UnhandledExceptionFilter启动了事后调试器。
为了缩小研究范围,可以尝试对faultrep进行改名,然后运行一个错误程序。改名的时候要同时修改dllcache目录和system32目录中的,如果只修改了system32中的,OS会直接将dllcache中的备份向其拷贝一份。
经过测试,发现并不会开启事后调试器。因此,可以断定是UnhandledExceptionFilter内部启动了事后调试。对UnhandledExceptionFilter进行反汇编,查看创建进程时其ebx,指向了什么字符串?
小结:
通过上述分析可知,UnhandledExceptionFilter线程上下文肩负其了加载faultrep.dll,创建错误对话框,启动事后调试器,以及结束进程。如果当这个线程的出现一点问题的时候,那么程序就会静默退出,因此Vista之后,对齐进行了改进。
二、Vista之后
随便找个Win7系统,运行之前的Debug程序,打印其堆栈,会发现,再也看不到了faultrep模块,那么此时的错误对话框是谁弹出的呢?
运行Process Explorer,重新运行错误程序会发现,svchost启动了WerFault.exe,这个exe就是错误对话框。
那么它是如何启动的?启动windbg附加到svchost进程中,查看其堆栈信息。
只能发现启动了一个LPC,无法看到一些敏感的函数,因此使用IDA加载该DLL,并在其搜索faultrep.dll,会找到一个地方。
因此,svchost会加载faultrep.dll,并初始化CrashReport机制, 随后会创建进程API CreateProcessW,对其进行交叉引用,会发现它创建了werfault.exe。
接下来就探究点击了错误对话框的Debug按钮后,还是由UnhandledExceptionFilter启动事后调试器吗?
首先对WerFault.exe和UnhandledExceptionFilter函数的CreateProcessW进行下断点,然后点击Debug按钮,会发现,事后调试器是WerFault.exe创建起来的。
由此可见,为了可以保证事后调试器的稳定启动,在Vista之后,将其迁移到了WerFault.exe。但是通过IDA反汇编UnhandledExceptionFilter,会发现一个一模一样的函数。因此得出Vista之后兼容了之前的设计思想,并添加了一个双重保险。
小结:
UnhandledExceptionFilter先会通知系统svchost进程(说明系统进程监听了异常端口),加载faultrep.dll,并弹出错误对话框,其次,又会回到 UnhandledExceptionFilter函数中,当用户点击Debug后,会首先会从WerFault.exe中创建事后调试器,如果创建失败,就从UnhandledExceptionFilter中创建事后调试器并最后也负责进程的结束任务。
三、事后调试器(JIT)
事后调试器(Postmortem Debug),即JIT。注册事后调试器是在注册表的如下图路径注册:
如果没有注册事后调试器的话,那么有如下子键:
Auto:代表是否自动启动,1表示自动,0表示手动
UserDebuggerHotKey:代表的是即时调试器的快捷键
如果要注册事后调试器,则在当前路径下创建一个Debugger子键,并填充调试器路径,或者利用调试器的快捷命令(如Windbg:使用Windbg -I)。
当没有Debugger子键时,UnhandledExceptionFilter函数会创建没有Debug按钮的对话框。
如果有Debugger子键的话,并且Auto = 1,时,那么会创建一个带有Debug按钮的对话框。有Debugger子键,Auto = 0时,会立即拉起调试器。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2021-8-22 09:51
被烟花易冷丶编辑
,原因: