OllyDbg 反调试之 ZwQueryInformationProcess

使用工具

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

分析思路

  1. 查询微软官方文档,本次使用的 APIZwQueryInformationProcess未来可能会弃用,所以目前暂不做深入研究,有需要之时再做深入;

  2. 这次使用的反调试 API 很独特:它本身的设计初衷应该是用作异常处理,但却被CrackMe的作者用在了验证方面,不得不说,这位高人对 API 理解的很透彻,很独到,同时也提醒了我,反向思维很重要;

  3. 这里就说说本次使用的 API:SetUnhandledExceptionFilterUnhandledExceptionFilterZwQueryInformationProcess

    • 先来说说第二个 API UnhandledExceptionFilter

      这是官方文档的描述:

      An application-defined function that passes unhandled exceptions to the debugger, if the process is being debugged. Otherwise, it optionally displays an Application Error message box and causes the exception handler to be executed. This function can be called only from within the filter expression of an exception handler.

      对这段文档的理解:通过判断当前进程是否正在被调试,如果正在被调试,就把异常交给调试器,如果没有,就把异常交给进程的 UnhandledExceptionFilter 处理;

      这里就不禁产生了一个疑问:如何判断当前进程是否正在被调试?

    • 再来看第三个 API ZwQueryInformationProcess

      1
      2
      3
      4
      5
      6
      ZwQueryInformationProcess

      作用
      检索有关指定进程的信息
      参数
      ProcessInformationClass:要检索的过程信息的类型
      • 在官方说明的开始有这么一段话:

        [ZwQueryInformationProcess may be altered or unavailable in future versions of Windows. Applications should use the alternate functions listed in this topic.]

        意思是:这个 API 可能会在未来的 Windows 版本中被更改或弃用,若要开发应用,请使用本文列出的其他替代 API。

      • 这个 API 的第二参数ProcessInformationClass要检索的过程信息的类型,本身有很多可选的值,而ProcessDebugPort,值是 7是我们关注的重点:

        Retrieves a DWORD_PTR value that is the port number of the debugger for the process. A nonzero value indicates that the process is being run under the control of a ring 3 debugger.

        意思是:检索四子节长度的值,该值是该进程的调试器的端口号。非零值表示该进程正在 Ring3 调试器的控制下运行。

        也就是说,调用这个函数,在把第二参数设置为7的情况下,只要返回非零值就表示我们正在调试程序,很好很强大,同时也解释了在学习上一个 API 时留下的疑惑;

    • 最后来看看第一个 API SetUnhandledExceptionFilter

      1
      2
      3
      4
      5
      6
      SetUnhandledExceptionFilter

      作用
      设置异常捕获函数
      参数
      lpTopLevelExceptionFilter:指向顶级异常处理函数的指针,只要 UnhandledExceptionFilter 函数获得控制权且未在调试过程,该指针就会被调用
      • 这是官方对 API 的说明;

        Enables an application to supersede the top-level exception handler of each thread of a process.

        After calling this function, if an exception occurs in a process that is not being debugged, and the exception makes it to the unhandled exception filter, that filter will call the exception filter function specified by the lpTopLevelExceptionFilter parameter.

      • 这是官方对参数lpTopLevelExceptionFilter的说明,请自行理解;

        A pointer to a top-level exception filter function that will be called whenever the UnhandledExceptionFilter function gets control, and the process is not being debugged. A value of NULL for this parameter specifies default handling within UnhandledExceptionFilter.

    • 学习完这三个 API,说说自己的理解:

      1. SetUnhandledExceptionFilter可以为异常设置处理函数,参数lpTopLevelExceptionFilter指向自定义的异常处理函数,但要触发这个函数,必须满足一个条件:UnhandledExceptionFilter被调用;

      2. 同时,要想UnhandledExceptionFilter被调用,也得满足两个条件:程序没有被调试,并且这个异常没有被处理;

      3. 当同时满足以上条件时,就会触发自定义的异常处理函数来处理异常;

      4. 那么,程序是如何检测程序是否被调试呢?那就要用到ZwQueryInformationProcess了,也就是说,本次的反调试主角其实是ZwQueryInformationProcess(虽然它就要被弃用了);

  4. 下面开始进入正题:

    • 打开CrackMe看看:

      打开软件

      很常规的软件,随便输入内容并点击 Check 后,没有任何反馈;

    • 倒入OD开始分析:

      既然已经学习了需要的 API,那就不用去 API 窗口看了,直接给三个函数设置断点并做好备注:

      设置断点并做好备注

      其实地址栏已经显示了地址对应的 API 名称,不过无法保证任何时候都会显示,况且,做备注是一个好的习惯,不是吗?

      同时需要先禁用ZwQueryInformationProcess断点,否则程序会多次中断,但不会中断在理想的位置,况且按照对 API 的理解,只有UnhandledExceptionFilter调用它的时候才会检测程序是否被调试,所以,在UnhandledExceptionFilter出现后再启用也为时不晚;

    • 接着F9运行程序,程序会中断在SetUnhandledExceptionFilter

      SetUnhandledExceptionFilter

      根据 API 说明,它的参数指向自定义异常处理函数,那么就在它的参数地址上设置断点,看看程序是否会触发这个自定义函数;

      参数设置断点

    • 继续运行程序,程序主窗体弹出,输入内容并点击 Check 后,程序再次中断,这次是中断在了UnhandledExceptionFilter

      UnhandledExceptionFilter

      其实,细心的同学一眼就看到了它调用了另一个 API ZwQueryInformationProcess

      既然它都要调用ZwQueryInformationProcess了,那我们就需要去启用ZwQueryInformationProcess断点了;

    • 激活ZwQueryInformationProcess断点并运行程序,程序会再次中断:

      ZwQueryInformationProcess

      可以看到,堆栈窗口中,它的第二参数InfoClass=7,那就说明它本次执行的目的就是检测程序是否正在被调试,而它的第三参数Buffer=0012F5E4则存放的是返回值,数据窗口中跟随一下;

      数据窗口

      事实上,我们确实正在调试这个这个程序,那我们就看看它的返回值是否是非零值;

    • 既然要看它的返回值,那就不能直接运行程序了,Ctrl + F9执行到返回,同时查看数据窗口

      执行到返回

      果然,返回值是一个非零值,也就说检测到正在调试程序;

      既然检测到了正在调试程序,如何触发自定义的异常处理函数呢?当然是将返回值置 0;

      置0

      选中返回值用 00 填充,直接修改也是可以的;

    • 既然已经修改了返回值,也就是绕过了检测,那么ZwQueryInformationProcess断点也就没用了,禁用ZwQueryInformationProcess断点并运行程序,程序会中断在:为SetUnhandledExceptionFilter 的参数设置的断点位置:

      参数断点

    • 毫无悬念了,这里就是用来验证的关键代码:

      验证

      把我们输入的内容进行一系列操作之后,去和一个指定的内容做比较;

      比较

      到这里就可以理解为什么点击 Check 之后没有任何反馈了,是因为验证失败后没有任何操作;

      既然已经知道会跳转到失败,如何看到成功的弹窗呢?当然是修改ZF标志位,让JNZ不成立:

      修改

      继续向下执行,期待已久的成功弹窗:

      成功

    • 既然已经知道了它的反调试原理,但又不想每次都手动修改,那该如何绕过能,这里需要借助插件:HideDebuggerHideOD,设置如下:

      插件

      无图无真相:

      无图无真相

      插件的作用就是永远保持ZwQueryInformationProcess的参数为7时的返回值为 0;

    • 总结:不得不说,能人背后有能人,这波操作很清奇,也就是说:如果从事物的多个角度去观察,去思考,可能会有不一样的理解与收获;

      期待自己早日变得更加强大,加油 💪💪💪❗️