windows异常分发回顾
异常的分发一共有多少轮?
一共有两轮。 KiDispatchExcption 函数的最后一个参数 FirstChance 表示当前是第几次进行异常的分发,另一个函数 RaiseException 最后一个参数也表示当前是第几次分发
通过什么可以区分当前所处的是 R0 还是 R3
在 windows 下,代码被分为了 R3 和 R0 权限,CS 段寄存器的最低两位就表示当前所处的是 3环(用户) 还是 0环(内核),可以通过 mov eax, cs + test eax, 1 区分
异常的产生方式有多少种
CPU 满足特定的条件之后,内部主动产生的异常,类似 int 3(IDT)
用户通过 RaiseException 构建 ExceptionRecord 主动抛出异常(KiDispatchException)
编译器会为用户自定义的 __try __except 添加怎样的异常处理函数
在同一个函数内,无论用户编写了多少个 SEH,编译器只会安装一个 except_handler4
当用户模式下产生异常时,SEH 函数会在什么时候被调用
int3 -> idt(3) -> _KiTrap03 -> CommonDispatchException -> KiDispatchExceptijon
-> KeUserExceptionDispatcher(3) -> RtlDispatchException(3) -> RtlpExecuteHandlerForException(3) -> except_handler4 -> except_handler4_common
-> 用户通过 _try _except 安装的异常处理函数
在 R0 中异常是如何被传递给三环调试器的
DbgkForwardException -> DbgkpSendApiMessage -> 三环调试器
R0 和 R3 的 RtlDispathException 有什么区别
KiDispatchException(0) -> RtlDispathException(0) -> SEH
KiUserExceptionDispatcher(3) -> RtlDispathException(3) -> VEH SEH UEF (VCH)
反调试与反反调试
反调试技术术语高级逆向分析技术范畴,并且涉及面异常广阔,几乎任何技术在经过精心的构造后都可以变成反调试技术
未公开的数据结构查询网站:https://www.vergiliusproject.com/
- 静态反调试:一般在调试开始时阻拦调试者,调试者只需要找到原因后可一次性突破
- 动态反调试:一般在调试过程中阻拦调试者,可在调试的过程中被频繁触发,因此需要调试者随时关注
对于所有的用户层 PEB 静态反调试,可以在程序正式的运行之前先挂起用户程序,然后修改相应的字段为非调试模式,再继续执行
BeginDebug
BeginDebug: 调试标记位
当程序被调试的时候,BegingDebugged 字段保存的是1
1 | __asm |
IsDebuggerPresent
IsDebuggerPresent:原理同上,检查 PEB.BeingDebugged 字段
1 | if (IsDebuggerPresent()) |
NTGlobalFlag
NTGlobalFlag:在当前进程处于调试状态时值为 0x70 ,可以通过判定此值来确定是否被调试
1 | int NtGlobalFlag = 0; |
_HEAP
_HEAP:没有被公开的结构体,不同版本的 NT 内核可能对这个结构体有不同的实现
ProcessHeap 的 Flags 与 ForceFlags 在正常情况下应为 2 与 0 ,处于调试状态时值会发生改变,此方法在 NT 5.X 版本以上无效
1 | int Flags = 0, ForceFlags = 0; |
ProcessDebugPort
NTQueryInformationProcess() 是一个可以同时在 R0 及 R3 运行的函数,它的主要作用是查看进程相关的各种信息
根据想要查看的信息类别不同,我们给其第二个参数 ProcessInformationClass 传的值也就不同,根据 ProcessInformationClass 的类别可知,此函数可以查看大概60余种进程相关的信息
ProcessDebugPort 可以获取目标进程的调试端口,如果目标进程未处于调试状态,此端口为0,否则为0xFFFFFFFF
1 | // 包含头文件 |
1 | int nDebugPort = 0; |
ProcessDebugObjectHandle
ProcessDebugObjectHandle 可以获取目标进程的调试对象句柄,如果未处于调试状态则获取的值为 NULL,如果当前的程序被调试了,那么保存的就是非零值
1 | HANDLE hProcessDebugObjectHandle = 0; |
ProcessDebugFlag
ProcessDebugFlag 可以获取目标进程的调试标记,如果处于调试状态其值为1,否则为0
1 | BOOL bProcessDebugFlag = 0; |
ProcessBasicInformation
ProcessBasicInformation 可以获取指定进程的父进程 PID ,我们可以将其与 Explorer.exe(资源管理器) 的PID进行对比,如果不匹配则证明此进程不是被双击运行的
1 | struct PROCESS_BASIC_INFORMATION { |
NtQuerySystemInformation
可以使用 NtQuerySystemInformation() 函数获取当前系统是否开启调试模式
1 | // 保存了和系统调试相关的属性 |
ZwSetInformationThread
可以使用 ZwSetInformationThread() 函数主动脱离调试器,使自己与调试器的调试关系分离,进而达到反调试的目的
1 | typedef enum THREAD_INFO_CLASS { |
FindWindow
FindWindow() 函数查询窗口名
1 | if (FindWindow(L"OllyDbg", NULL)) |
插件加载的原理
- 应用程序如何找到自己的插件:
所有支持插件的应用程序,都会存在一个插件路径,用户提供的就应该放置到这个路径底下,应用程序通过遍历文件的方式来确保能够找到插件。
- 插件的存在形式是什么?
插件的存在形式通常都是 dll 文件,这些文件可以是任何后缀名结尾的,并不影响插件本身的功能
- 应用程序如何识别插件路径下的模块是否是插件?
一个合格的插件,应该能够提供相应的导出函数说明当前插件的名称,版本以及能够支持的应用程序。
- 插件是如何提供功能的?
通过实现指定的导出函数可以提供相应的功能。
1 |
|
实例插件
1 |
|
OD插件的编写
1 |
|
插件测试环境
1 | 链接: https://pan.baidu.com/s/1Ycgi-D8abbE6bvKanLIVQw 提取码: gphs |