OllyDbg 反调试之 ProcessHeap & NTGlobalFlag

使用工具

  • OllyDbg 1.10原版,简称OD
  • OD插件HideDebugger
  • OD 汉化插件均来自互联网;
  • CrackMe来自互联网,仅供学习使用;
  • 文中特殊数字均是HEX,为了书写方便采用DEC

分析思路

  1. 使用搜索引擎搜寻良久并未找到通俗易懂的说明,在CTF Wiki上发现了描述,但无奈自己功力尚浅,无法透彻理解,遂将链接奉上,感兴趣的同学可以去学习一下NtGlobalFlagHeap Flags

  2. 文中使用的CrackMe无特定要求,可以使用任意一款;

  3. 了解一下两个标志,以下描述引用自CTF Wiki

    1
    2
    3
    4
    5
    6
    7
    NtGlobalFlag

    描述
    1. 在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置.
    2. 该字段的默认值为 0.
    3. 当调试器正在运行时, 该字段会被设置为一个特定的值.
    4. 尽管该值并不能十分可信地表明某个调试器真的有在运行, 但该字段常出于该目的而被使用

    呃,至于ProcessHeap,实在是总结不出具体的描述性语言,感兴趣的同学去CTF Wiki看吧;

  4. 接下来就用实例来说明一下这两个标志的用法:

    • 首先选择一个CrackMe倒入OD

      之前学习IsDebuggerPresent的时候学习到:如果程序倒入OD之后没有运行,那么EBX寄存器指向的位置就是IsDebuggerPresent检测的位置,用代码表示就是:

      1
      2
      3
      4
      MOV EAX,DWORD PTR FS:[18]       ; 将 FS:[18] 中的地址拷贝到 EAX
      MOV EAX,DWORD PTR DS:[EAX+30] ; 将 EAX 中的地址加 0x30 之后的地址拷贝到 EAX
      MOVZX EAX,BYTE PTR DS:[EAX+2] ; 将 EAX 中的地址,存储的第 2 个字节的数据拷贝到 EAX
      RETN ; 返回

      在这里简单计算一下,就当作复习之前的知识了,以这个CrackMe为例:

      为例子

      手动计算一下IsDebuggerPresent要检测的位置:

      1. MOV EAX,DWORD PTR FS:[18]FS 寄存器从第 18 位开始,存放的是该寄存器从起始位置依次向后的地址,即第 18 位存放的是起始位置的地址,也就是说,在这里,FS:[18] == 7FFDF000

      2. MOV EAX,DWORD PTR DS:[EAX+30]:这个很好计算嘛,[EAX+30] == 7FFDF030,使用Ctrl + G跟随一下这个地址:

        计算并跟随

        7FFDF030中存储的地址不正是EBX中的地址嘛;

    • 回顾完之前的内容,开始学习本次内容的第一个标志NtGlobalFlag

      根据对NtGlobalFlag描述可知:在 32 位机器上, NtGlobalFlag 字段位于 PEB(进程环境块)0x68 的偏移处;

      首先,什么是 PEB?查询了许久之后又引出了 TEB,不太理解,但找到一些感觉描述很通俗的话:

      • TEB:即 Thread Environment Block ,它记录相关线程的信息,每一个线程都有自己的 TEB,每个 TEB 都有自己的 TIB(Thread Information Block),即线程信息块,每当创建一个线程,系统均会为每个线程分配 TEB,而且 TEB 永远放在 FS 寄存器指定的数据段的 0 偏移处;

      • PEB:即(Process Envirorment Block Structure),英文翻译过来就是进程环境信息块,在你进行打开这个程序或以调试方式打开这个程序,那么操作系统会对你这个进程的进程环境块的一些标志设置一系列的属性,如果你使用系统的调试方法,那么系统就会把相对应的标志位给设置上;

        1. TEB 结构体位于 FS 段选择符所指的段内存的起始地址处,也就是 0 偏移处;

        2. PEB 成员位于距 TEB 结构体 Offset 30 的位置,也就是 0x30 偏移处;

        3. 那么就有两种获取 PEB 的方法:

          • 既然 TEB 位于 FS 寄存器 0 偏移处,在本文中就是7FFDF000,而 PEB 位于距 TEB 结构体 0x30 偏移处,那么是不是可以直接理解为:PEB 位于FS 寄存器 0x30 偏移处,即FS:[30]就是 PEB,在本文中就是7FFDF030
          • TEB 位于 FS 寄存器 0 偏移处,在本文中就是7FFDF000,如何获取FS 寄存器 0 偏移处即文中的7FFDF000呢?当然是FS:[18]了,既然拿到 TEB 本身,如何获取 PEB 呢?方法就和上面复习的一样了;
      • 既然找到了 PEB 的位置,而根据对NtGlobalFlag的定义得知,它位于 PEB 0x68 偏移处;

        本文中 PEB 为7FFDF030地址中的数据7FFD8000,也就是初始EBX指向的地址,再偏移 0x68 计算为7FFD8068,也就是说,如果7FFD8068地址的数据不为 0,则表示检测到正在调试程序:

        NtGlobalFlag

        整理一下代码的实现:

        1
        2
        3
        4
        5
        MOV EAX,DWORD PTR FS:[18]       ; 将 FS:[18] 中的地址拷贝到 EAX
        MOV EAX,DWORD PTR DS:[EAX+30] ; 将 EAX 中的地址加 0x30 之后的地址拷贝到 EAX
        MOV EAX,DWORD PTR DS:[EAX+68] ; 将 EAX 中的地址加 0x68 之后的数据拷贝到 EAX
        OR EAX,EAX ; 是否为 0
        RETN ; 返回

        验证一下:

        验证NtGlobalFlag

    • 再来说说第二个标志ProcessHeap

      对这个标志的了解少之又少,暂时没有查到直接关联的资料,只能大概总结一下:

      1. 位于 PEB 偏移 0x18 处,本文中 PEB 是7FFD8000,偏移 0x18 之后是7FFD8018

      2. 把偏移后的地址中的 DWORD 长度的数据作为地址,偏移 0x10,即对本文的7FFD8018中的数据00140000偏移 0x10;

      3. 如果这个位置的数据不为 0,即本文的00140010不为 0,则表示检测到正在调试程序:

        ProcessHeap

        整理一下代码的实现:

        1
        2
        3
        4
        5
        6
        MOV EAX,DWORD PTR FS:[18]       ; 将 FS:[18] 中的地址拷贝到 EAX
        MOV EAX,DWORD PTR DS:[EAX+30] ; 将 EAX 中的地址加 0x30 之后的地址拷贝到 EAX
        MOV EAX,DWORD PTR DS:[EAX+18] ; 将 EAX 中的地址加 0x18 之后的数据拷贝到 EAX
        MOV EAX,DWORD PTR DS:[EAX+10] ; 将 EAX 中的地址加 0x10 之后的数据拷贝到 EAX
        OR EAX,EAX ; 是否为 0
        RETN ; 返回

        验证一下:

        验证ProcessHeap

    • 两个标志分析完毕,接下来就要说说如何绕过了,不能每次都手动修改吧;

      这里需要借助两个OD插件HideDebuggerHideOD

      设置如下图:

      插件设置

      细心的同学会发现,这里的设置和上节课一模一样,是的,设置相同,但是重点不同;

      虽然之前设置了某些选项,但只是照猫画虎,不知其意,相信学完这节内容,就会理解这样设置的意图;

    • 验证一下插件是否生效:

      程序倒入OD不要运行,在数据窗口中跟随EBX

      前两个绕过

      可以在数据窗口直观的看到,IsDebuggerPresentNtGlobalFlag检测的位置已经成功置 0,绕过检测,至于ProcessHeapCtrl + G跟随地址查看:

      最后一个绕过

  5. 聊完两个标志,再来聊聊另外一个知识点:一个崩溃:

    一个崩溃不是说 Windows 的 API 又有特殊的用途或参数,或者说是开发人员思路清奇把 API 用出了花儿,而是OD的 bug:当被调试的程序通过 OutputDebugString 输出超长的一串调试字符串的时候,OllyDbg 无法处理导致崩溃;