首页
社区
课程
招聘
[原创]内核APC源码剖析以及Vista之后调试机制的变化分析
2021-4-14 19:36 7259

[原创]内核APC源码剖析以及Vista之后调试机制的变化分析

2021-4-14 19:36
7259

第一个话题是内核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代表的是往挂靠的线程插入,它的意思就是我不管你挂靠了还没挂靠,我都认为是你挂靠了!  -- 这种就承担了一种风险,如果它没有挂靠,那么再也执行不了!CurrentApcEnvironmentInsertApcEnvironment它们俩更像是代表了当前代码所处的阶段。比如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 被烟花易冷丶编辑 ,原因:
上传的附件:
收藏
点赞4
打赏
分享
最新回复 (6)
雪    币: 1126
活跃值: (2041)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oday小斯 2021-4-15 16:16
2
0
感谢分享
雪    币: 32406
活跃值: (18810)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2021-4-16 14:07
3
0
能不能将文章整理一下,直接放到帖子里?
方便阅读,同时方便论坛搜索。
雪    币: 1226
活跃值: (1605)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
烟花易冷丶 2021-4-18 10:15
4
0
kanxue 能不能将文章整理一下,直接放到帖子里? 方便阅读,同时方便论坛搜索。
好的,我整理一下
雪    币: 239
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
abel346 2021-6-24 18:02
5
0
楼主PDF内容是不是打算发篇小论文的,感觉写的很好,再附加点实例代码就好看的更明白了。
雪    币: 1226
活跃值: (1605)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
烟花易冷丶 2021-6-24 18:40
6
0
abel346 楼主PDF内容是不是打算发篇小论文的,感觉写的很好,再附加点实例代码就好看的更明白了。
一点心得而已...论文不至于 hhh,我一直想写一篇关于内存的设计的
雪    币: 189
活跃值: (2406)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
杰克王 2021-8-24 18:51
7
0
感谢分享
游客
登录 | 注册 方可回帖
返回