try-finally与try-except的区别

    • try-finally的try块中产生异常时,异常是无法被捕获, 它的特点是,无论try块中的代码是以何种方式离开的(return,break,continue,goto),finally块的代码都会被执行.
    • try-except是用于处理异常的, 只有try块中的代码触发了异常,except的过滤表达式才会被调用(注意,这里指过滤表达式,并非excep块,要区分清楚.) , 至于except块的代码是否被调用, 取决于过滤表达式的值.

    try-except过滤表达式三种结果

    • EXCEPTION_EXECUTE_HANDLER(1) : 执行except块
    • EXCEPTION_CONTINUE_SEARCH(0) : 搜索下一个异常处理
    • EXCEPTION_CONTINUE_EXECUTION(-1) : 继续执行产生异常的指令.

    在异常过滤函数中获取异常产生的地址和异常的类型
    异常过滤函数或异常处理函数的参数一般都有EXCEPTION_POINTERS结构体 ,
    这个结构体中, 有两个结构体,
    一个是CONTEXT结构体,保存的是产生异常时所有寄存器的值.
    另一个结构体是EXCEPTION_RECORD,保存有异常类型(ExceptionCode) 异常码,异常产生的地址(ExceptionAddress).
    image.png

    image.png
    image.png

    SetUnhandleExceptionFilter
    进程的任何线程中若发生一个未处理的异常,就会导致调用程序自己的异常过滤器。需要将这个过滤器的地址作为参数传递给SetUnhandledExceptionFilter。按SEH格式包装所有的线程函数
    注意:在进程被调试时,被设置的UEH异常处理函数不会被调用,因为这个差异,此机制也会被用于反调试.

    VEH 是什么?如何设置?与SEH有什么区别?
    VEH是向量化异常处理,
    使用AddVectoredExceptionHandler来设置

    VEH和SEH之间的区别

    • 先执行的
    • 是面向整个进程的, 已经设置,无论在进程的哪个位置触发了异常, 所设置的异常处理函数都被调用 .

      SEH

    • 后执行的

    • 是面向try块的, 只有try块中的代码产生了异常, 异常过滤函数才会被调用

    IDT :Interrupt Descriptor Table(中断描述符表)

    • 是一张用来存储中断服务程序的表
    • 中断可以由硬件(称为外部中断),也可以由软件产生(称为内部中断),在程序中写入int n 指令可以产生n号中断和异常(0-FFh)。每一种中断对应一个中断号,CPU 执行中断指令时,会去IDT表中查找对一个你的中断服务程序。
    • 中断是CPU 的机制,不管运行的是什么操作系统,只有是运行在x86架构,IDT结构式必然存在的(中断服务程序应该是由操作系统提供)。

    异常分发有几轮

    • 第一轮异常分发是从中断服务程序内部的CommonDispatchException()发起.
    • 第二轮异常分发是从KiUserExceptionDispatcher ()函数内部发起.

    产生了异常,是先发送给调试器处理,还是先被程序的异常处理机制处理掉

    • 需要判断产生异常的程序是当前是否被调试.
    • 如果触发异常的进程处于被调试状态,则将异常交给调试器处理,如果没有调试器(或者调试器处理不了)则将异常交给程序的异常处理机制处理.

    反调试的目的是什么?

    • a) 防止程序被调试.
    • b) 防止程序被暴力破解

    什么是静态反调试?什么是动态反调试?分别都有哪些?
    a) 静态反调试: 一般在调试开始时阻拦调试者,调试者只需找到原因后可一次性突破,检测某些固定的标志位,如利用PEB中的各个字段判断调试状态.
    b) 动态反调试: 一般在调试过程中阻拦调试者,可在调试的过程中被频繁触发,因此需要调试者随时关注,例如利用SEH机制.

    如何破解常见的反调试手段?
    a) 对于利用PEB的反调试,只需要将PEB的相应字段的值重写即可.
    b) 对于使用了未文档化的API查询信息的反调试, 只能HOOK相应的API,然后根据参数中的功能号判断API调用是否是查询调试信息的,如果是则将错误的参数通过形参输出回去.

    1. 例如:
    2. NTSTATUS WINAPI MyNtQueryInformationProcess(
    3. HANDLE ProcessHandle,
    4. PROCESSINFOCLASS ProcessInformationClass,
    5. PVOID ProcessInformation,
    6. ULONG ProcessInformationLength,
    7. PULONG ReturnLength){
    8. // 查询调试端口
    9. if(ProcessInformationClass== ProcessDebugPort){
    10. *(PDWORD*) ProcessInformation=0xFFFFFFFF;
    11. if(ReturnLength) *ReturnLength = 4;
    12. return 0;
    13. }
    14. else{
    15. // 调用原版函数:
    16. return pSrcNtQueryInformationProcess(ProcessHandle, ProcessInformationClass,
    17. ProcessInformation,
    18. ProcessInformationLength,
    19. ReturnLength);
    20. }
    21. }

    OD插件工程中,哪些函数时必须导出的
    ODBG2_Pluginquery :
    如果没有导出该函数, 则DLL将不能被OD识别, 无法加载到OD中, 此外,这个函数还用于版本检查, 检查OD版本和插件所使用的SDK版本是否一致,如果不一致, OD还是不会加载DLL

    X64dbg插件工程中, 哪些函数是必须导出的
    extern “C” declspec(dllexport) bool pluginit(PLUG_INITSTRUCT* initStruct);
    X64dbg调用该函数来检查版本.

    extern “C”
    declspec(dllexport) bool plugstop();
    当x64dbg被关掉时会调用此函数, 一般会在这里保存资源或者释放资源.

    extern “C” __declspec(dllexport) void plugsetup(PLUG_SETUPSTRUCT* setupStruct);
    当插件通过x64dbg的版本检查之后, x64dbg会调用此函数让插件进行初始化.

    创建调试会话的方式都有哪些?
    一般有三种:
    1. 使用CreateProcess加上调试标志,创建一个未运行的exe作为调试进程
    2. 使用DebugActiveProcess附加一个正在运行的进程
    3. 注册实时调试器(当进程崩溃时由系统附加) : 将调试器的路径设置到注册表:
    32位:HKEY_LOCAL_MACHINE/SOFTWARE/Wow6432Node/Microsoft/Windows NT/CurrentVersion/AeDebug/Debugger
    64位:HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/AeDebug/Debugger

    调试事件都有什么?分别是在什么时候产生的

    • REATE_PROCESS_DEBUG_EVENT: 创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件
    • CREATE_THREAD_DEBUG_EVENT:创建一个线程之后发送此类调试事件
    • EXCEPTION_DEBUG_EVENT:发生异常时发送此类调试事件。
    • EXIT_PROCESS_DEBUG_EVENT:进程结束后发送此类调试事件。
    • EXIT_THREAD_DEBUG_EVENT:一个线程结束后发送此类调试事件。
    • LOAD_DLL_DEBUG_EVENT:装载一个DLL模块之后发送此类调试事件。
    • OUTPUT_DEBUG_STRING_EVENT:被调试进程调用OutputDebugString之类的函数时发送此类调试事件。
    • RIP_EVENT:发生系统调试错误时发送此类调试事件。
    • UNLOAD_DLL_DEBUG_EVENT:卸载一个DLL模块之后发送此类调试事件。

    为什么要回复调试子系统? 能回复几种结果? 这些结果会造成什么影响?
    进程在产生调试事件后, 系统会通过发消息的方式将调试事件的信息发送给调试器, 并等待调试器回复, 如果调试器不回复, 意味着被调试的进程将不会继续运行.
    一般情况下,可以回复两种结果, 一是DBG_CONTINUE,表示继续执行., 而是 DBG_EXCEPTION_NOT_HANDLE,表示异常未处理. 处理异常事件,其余事件都必须要回复继续执行. 而异常事件下,不同的情形回复不同的代码:
    回复继续执行 , 产生异常的代码会继续被执行, 如果异常没有被处理掉,那么异常代码再次执行后必将再次产生异常.
    回复异常没有被处理, 按照异常分发流程 ,异常将会被被调试进程中的异常处理机制所处理.
    因此, 如果异常是调试器主动制造而产生的,那么调试器接收到异常之后应当先去除异常,然后再回复继续执行.异常如果是被调试程序产生的,则回复异常未被处理.

    软件断点的实现原理是什么? 实现步骤有几步?

    软件断点实际就是 int 3 指令, CPU在执行这条指令之后, 就会触发异常. 而异常被触发后,通过异常分发流程,调试器将会接收到一个异常事件. 此时, 就相当于’断点断下’到了调试器中.
    软件断点的思路是, 不管程序是怎么运行的, 如果想要程序在飞速执行的过程中精准地停在某个位置. 那么必须事先在想要断下来的位置设置断点(将int3指令的机器码写入), 这样一来程序运行到int3指令自然就会停下来.

    步骤:
    1. 将写入位置的内存分页属性设置为可读可写
    2. 备份被0xcc(int 3的机器码)覆盖的1个字节
    3. 将int3写入

    硬件断点的读写断点怎么设置?
    使用硬件断点寄存器. 可用的硬件断点寄存器总共有Dr0,Dr1,Dr2,Dr3,Dr6,Dr7, 其中,Dr0到Dr3用于保存断点的地址.Dr7保存断点中断的条件.

    Dr7是一个32位寄存器, 里面总共保存着Dr0~Dr3四个断点地址寄存器的中断条件.中断条件有三种:
    1.断点寄存器是否被启用: L0~L3
    被置1了,表示断点被启用,CPU会检测对应的DR0~DR3寄存器所记录的地址是否产生中断.
    2.断点的范围(长度): LEN0~LEN3
    记录了断点地址的命中范围, 这个范围只有以下几种: 0:1字节, 1:2字节, 2:4或8字节,3:4字节.
    断点的长度会受到断点地址的影响, 当地址是4的倍数时,只能设置4字节, 当地址是2的倍数时,可设置成2字节和4字节,当地址是1的倍数时,可以设置1/2/4字节.
    3.断点的中断类型: RW0 ~ RW3
    记录断点的中断类型, 一般有以下几种: 0:执行断点 , 1:写入断点 , 2:I/O读写断点, 3:数据读写断点
    当断点中断类型为0时, 长度必须设置为0(表示1字节的长度),否则不会有效果
    因此, 要设置读写断点,应将被访问的地址存入Dr0~Dr3中的一个, 将Dr7中对应的L0~L3设置为1,将对应的LEN0~LEN3设置为0/1/3,将对应的RW0~RW3设置为3.
    例如:
    DR0 = 0x403000
    DR7.L0 = 1
    DR7.LEN0 = 3
    DR7.RW0 = 3

    内存访问断点的原理是什么?

    内存以分页作为管理单位, 内存分页具有读,写,执行的分页属性, 如果一个内存分页中缺少了读权限,但又在这个分页上读取内容,就会产生内存访问异常. 内存访问断点正是基于这个性质而提出的. 当要下断点时,一般就是将断点地址所在的内存分页设置为没有任何分页访问属性, 这样一来, 无论是在这个分页上进行什么操作, 都会产生访问异常. 如果想要精确的设置,比如要设置地址被写入时才断下, 那么便可以只将内存分页设置为不可写即可.

    断点类型 原理 说明
    软件中断断点

    | 软件断点在X86系统中为中断指令INT 3,其二进制代码opcode是0xCC。
    当程序执行到INT 3指令时,会引发软件中断。
    操作系统的INT 3中断处理器会寻找注册在该进程上的调试处理程序。

    陷阱类异常 | 步骤: 1. 将写入位置的内存分页属性设置为可读可写
    2. 备份被0xcc(int 3的机器码)覆盖的1个字节
    3. 将int3写入 | | 内存访问断点

    | 修改断点地址所在的内存分页的访问属性

    错误类异常 | 内存以分页作为管理单位, 内存分页具有读,写,执行的分页属性, 如果一个内存分页中缺少了读权限,但又在这个分页上读取内容,就会产生内存访问异常. 内存访问断点正是基于这个性质而提出的. 当要下断点时,一般就是将断点地址所在的内存分页设置为没有任何分页访问属性, 这样一来, 无论是在这个分页上进行什么操作, 都会产生访问异常. 如果想要精确的设置,比如要设置地址被写入时才断下, 那么便可以只将内存分页设置为不可写即可. | | 硬件断点

    | 硬件断点为CPU提供给我们的断点,由4个寄存器(DR0-DR3)存放断点地址,断点属性可设置为读、写、执行,由于寄存器数量有限,只能存在4个硬件断点,这4个断点是否执行由DR7的0、2、4、6位决定,当置为1时,断点生效。

    是陷阱类异常 | 使用硬件断点寄存器. 可用的硬件断点寄存器总共有Dr0,Dr1,Dr2,Dr3,Dr6,Dr7, 其中,Dr0到Dr3用于保存断点的地址.Dr7保存断点中断的条件.

    Dr7是一个32位寄存器, 里面总共保存着Dr0~Dr3四个断点地址寄存器的中断条件.中断条件有三种:
    1.断点寄存器是否被启用: L0~L3
    被置1了,表示断点被启用,CPU会检测对应的DR0~DR3寄存器所记录的地址是否产生中断.
    2.断点的范围(长度): LEN0~LEN3
    记录了断点地址的命中范围, 这个范围只有以下几种: 0:1字节, 1:2字节, 2:4或8字节,3:4字节.
    断点的长度会受到断点地址的影响, 当地址是4的倍数时,只能设置4字节, 当地址是2的倍数时,可设置成2字节和4字节,当地址是1的倍数时,可以设置1/2/4字节.
    3.断点的中断类型: RW0 ~ RW3
    记录断点的中断类型, 一般有以下几种: 0:执行断点 , 1:写入断点 , 2:I/O读写断点, 3:数据读写断点
    当断点中断类型为0时, 长度必须设置为0(表示1字节的长度),否则不会有效果
    因此, 要设置读写断点,应将被访问的地址存入Dr0~Dr3中的一个, 将Dr7中对应的L0~L3设置为1,将对应的LEN0~LEN3设置为0/1/3,将对应的RW0~RW3设置为3.
    例如:
    DR0 = 0x403000
    DR7.L0 = 1
    DR7.LEN0 = 3
    DR7.RW0 = 3 | | 条件断点

    | 在软件断点的基础上,增加限定条件。适用于某一下断处会被多地方调用,则加上限定条件,以达到真正需要断下时触发。 | 例:[esp-4]!=某一地址 |