Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html
软件调试
1.调试程序如何与被调试程序
2. 调试事件的采集
3. 调试事件的处理流程
4. 异常的调试流程
5. 软件断点
6. 内存断点
7. 硬件断点
8. 单步异常与单步步过
9. 硬件HOOK
1.调试程序如何与被调试程序
1)API建立存在两种方式
①CreateProcess
②DebugActiveProcess
2) 调试器如何创建调试对象 - Kernel32!DebugActiveProcess函数分析
① DbgUiConnectToDbg函数分析:
其函数分析如下:简单的就是调用zw函数进零环,在零环创建一个内核对象然后返回三环。
其内核模块创建的具体细节,可以搜索nt!CreateDebugObject函数。
② DebugObject数据结构如下
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent;
FAST_MUTEX Mutex;
LIST_ENTRY EventList;
ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;
③ DebugObject 创建完之后放在那里
如何我们查看Kernel32!DebugActiveProcess函数时,我们发现虽然调用DbgUiConnectToDbg创建一个DEBUG_OBJECT,但我们并没有看到该结构体。
其返回值是一个是否调用成功,那存放在哪里了呢?我们查看其zwCreateDebugObject的反汇编,如下:
我们可以得出,其存放在当前调试器线程的 Teb+0xF24 的位置中。
3)被调试进程与调试进程建立关系 - Kernel32!DebugActiveProcess函数分析
①DbgUiConnectToDbg函数分析:
我们之前分析过该函数通过调用 DbgUiConnectToDbg() 创建调试对象,现在我们分析其是如何与被调试进程建立联系的。
分析如下,很明显调用一个DbgUiDebugActiveProcess()函数来建立联系的。
② ntdll!DbgUiDebugActiveProcess函数分析:
分析如下图所示,这样再结合我们对于调试器的行为,则很好理解。
③ Nt!NtDebugActiveProcess函数分析:
如下图,进入内核之后,先做的就是通过三环句柄来获取进程对象与调试对象,然后调用一个函数,在函数内部将其关联起来。
④nt!DbgkpSetProcessDebugObject函数分析
该函数内容很多,但很容易看出核心操作就是将进程的DebugPort中传入DEBUG_OBJECT的地址,如下所示:
4)总结:
要牢记大体流程,通过 DebugActiveProcess 来进行具体分析。
首先,先调用内核创建一个调试对象,返回三环句柄,将句柄存储在Teb+0xF24中;之后将被调试程序的句柄与调试对象句柄一起传入零环中,在零环_EPROCESS.DebugPort中。
值得注意的是:调试器是通过句柄与被调试对象建立连接的,而被调试程序是在内核中建立连接的,一个_TEB,一个_EPROCESS,这是建立的关键区别。
2. 调试事件的采集
1)调试事件在哪儿
如下:其是_DEBUG_OBJECT结构体,看存在一个 EventList,其就是一个调试事件链表。
被调试程序各种事件发往调试对象的EventList中,然后由调试器接收并进行处理,其大体流程就是这个样子。
typedef struct _DEBUG_OBJECT {
KEVENT EventsPresent;
FAST_MUTEX Mutex;
LIST_ENTRY EventList; +0x30
ULONG Flags;
} DEBUG_OBJECT, *PDEBUG_OBJECT;
2)何为调试事件?
被调试进程所做的任何一件事,难道都要报告被调试器吗?当然不是,其存在如下七种调试事件:
typedef enum _DBGKM_APINUMBER
{
DbgkmExceptionApi = 0, // 异常
DbgkmCreateThreadApi = 1, // 创建线程
DbgkmCreateProcessApi = 2, // 创建进程
DbgkmExitThreadApi = 3, // 退出线程
DbgkmExitProcessApi = 4, // 进程退出
DbgkmLoadDllApi = 5, // 映射DLL
DbgkmUnloadDllApi = 6, // 反映射DLL
DbgkmErrorReportApi = 7, // 内部错误(已废弃)
DbgkmMaxApiNumber = 8, // 这组常量的最大值 (已废弃)
} DBGKM_APINUMBER;
3)如何生成调试事件?(调试事件采集函数)
我们下面介绍一组函数,其是Dbg**函数,其在各个活动事件的必经之路上。
当被调试程序正在这些事件的一种,其都会经过这些函数,然后其检查是否处于被调试状态,如果处于就发送事件到链表中。
① Nt!DbgKpSendApiMessage函数分析
该函数首先要判断该线程是否要暂停,然后调用DbgkpQueueMessage,该函数将生成调试事件,然后加入到调试对象链表中。
其中我们可以得出一个反调试思路:这里是线程暂停的关键地方,我们可以保护我们的程序,hook该地方,当程序执行到这里,如果发现是我们的进程,则不暂停,
这样,其就算接收到我们的调试事件了,也不会暂停我们的程序。
② Nt!DbgKpQueueMessage函数分析
该函数主要目的是生成调试事件,根据传入过来的有关信息来填写,之后使用KeSetEvent。
其中的详细过程可以在《等待对象》,那一节查看。
3. 调试事件的处理流程
现在,被调试进程的所有调试事件都会发送到DebugEventList中,我们在编写调试器时,使用一个循环来循环遍历是否产生新的消息。
void main() {
DebugActiveProcess/CreateProcess;
while(Alive){
WaitForDebugEvent(&debugEvent,WaitTime);
switch(DebugEvent.dwDebugEventCode)
{ .... }
ContinueDebugEvent();
}
}
1)WaitForDebugEvent、ContinueDebugEvent函数
循环遍历该事件链表,如果存在事件就会取出来,处理完之后然后等待进一步执行。
当处理完成之后,调用ContinueDebugEvent让被调试程序恢复执行,这个很好理解。
2)发送虚假信息
我们在调试过程时,会看见发送虚假的调试对象,这是为什么呢?
很简单,我们及时以ActiveDebugProcess附加的方式调试进程,也可以看到其进程的线程以及各种模块加载的情况。
这样很奇怪,进程模块加载已经完成了,为什么还能接收到加载信息呢,其实就是依赖于这组函数。
当以附加的形式添加信息时,其会发送虚假的信息给调试器,让其可以显示出调试信息,但其不会中断,这很容易理解。
4.异常的调试流程
如下图,用户层出现的异常调试流程,之前我们提到过异常处理流程,可其是如何发送给三环的调试器呢?
注意③⑤,其调用的函数DbgKForwardException函数,该函数就是异常的收集函数,之后的流程我们上面已经分析过。
5. 软件断点
硬件断点,众所周知是CC指令,INT 3 中断,0x80000003异常码。
我们查看 Trap03,很明显看出其执行流程,我们之后学过异常和处理程序,接下来就知道怎么做了。
6. 内存断点
内存断点本质就是调用 VirtualProtectEx 函数来修改页的属性(PTE),将其属性修改为写,当进行读的时候,会触发异常,然后执行异常的分发流程。
其内存断点触发时执行 Trap0E 号中断函数,我们分析该中断,发现其也是调用CommonDispatchException函数,0xc0000006来触发异常。
7. 硬件断点
1)CPU的Dr调试寄存器
硬件断点本质调用CPU的Dr0~7,8个调试寄存器。
其相关属性如下图,其中Dr0~Dr3存储断点地址,Dr4~Dr5保存,Dr6~Dr7设置Dr0~Dr4相关寄存器的属性。
因为断点地址只记录在Dr0~Dr3中,因此硬件断点最多只能记录四个。
2)硬件断点的处理程序
硬件断点如果被检测,则会走Trap01中断,触发0x80000004中断(软件断点为0x8000003中断),其派发如下图
3)硬件断点的单步异常与TF位产生的单步异常区分
硬件断点产生的是单步异常,通过TF位也会产生单步异常。(单步异常我们之后会讲)
那操作系统到底如何区分其单步异常是如何产生的呢?答案是通过Dr6寄存器的有关属性(B0~B4)。
4)代码写硬件断点的时机
调用GetThreadContext()和SetThreadContext(),获取线程上下文和设置线程上下文来,然后对Dr寄存器进行有关设置。
有一点需要注意的,在调用SetThreadContext(),应该暂停其他线程,否则可能出现设置错误。
8. 单步异常与单步步过
1)单步异常:
①单步异常的实现:
我们在使用调试器时肯定使用过单步异常(OD的F7),如果让我们来设置,肯定执行一行代码来设置软件断点或硬件断点。
但是CPU在设计之初已经考虑过我们这个需求了,其Eflag.TF位就是用来标识的,CPU每运行一次代码,会检查该标志位。
如果将其设置为1,其就会触发一次单步异常,走Trap01中断(与硬件断点的执行顺序是一样的)。
②单步异常的代码实现:
先设置软件断点,将被调试线程断开,调用GetThreadContext()和SetThreadContext()来修改TF位后继续运行。
2)单步步过
①单步步过的实现:
单步步过是调试器编写者去实现,并不会像单步步入一样可以通过修改TF位实现。
根据反汇编引擎动态计算,如果下一条指令是CALL类指令,则在其再下一条指令设置断点,然后运行。
因此,通过硬件断点或软件断点都可以实现,自己有自己的思路就好。
②通过反调试干掉单步步过:
根据单步步过的反调试思路,我们可以设置大量嵌套无用函数,在里面动态修改返回地址,最后并不会执行call函数的下一个地址,
而单步步过在call下一个地址下断点,这样该程序就会跑飞,很好理解这种原理。
9.硬件HOOK
其核心原理就是修改线程的Dr寄存器,然后再加入一个VEH异常处理函数。
其中VEH是全局异常,我们在自己的进程添加,在其他线程也可以获取到。
我们在VEH异常处理程序中先判断当前EIP是否是要hook的地址,如果是则执行hook代码,之后再还原进去,其实就是这个样子,很好理解。