OllyDbg 反调试之综合练习

使用工具

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

分析思路

  1. 本次内容涉及了小部分脱壳以及一个OD无法处理的异常int 68

  2. 简单的脱壳和int 68说明:

    要想看到int 68异常,需要用到HideDebuggerHideOD插件,不然程序运行不到那里就退出了,至于两个插件的配置,翻阅上篇文章,这里不再赘述;

    • 首先,这个程序很奇怪,没有打开OD的情况下程序也打不开,眼疾手快的同学可以在任务管理器窗口发现打开程序的同时有个进程一闪而过;

    • 在开始之前,需要设置一下OD,为了能捕获所有异常,需要设置如下:

      设置OD

    • 既然打不开,那还等什么,直接倒入OD分析:

      倒入后,会弹出了弹窗,而这个弹窗正说明了这个程序有壳:

      有壳

      关闭弹窗继续;

      奇怪

      如果在这里能发现异常,说明基础知识还是比较扎实的;

      是的,第二行代码POPAD就很奇怪,往上翻也只有一行空指令,奇怪在哪里呢?POPAD是弹栈的意思,也就是说,这条指令会把 8 个通用寄存器的数据从栈中弹出,恢复到程序压栈前的状态,而前面却没有对应的压栈指令PUSHAD,所以很奇怪;

    • 既然POPAD前面有一行空指令,那岂不是可以直接写一个PUSHAD就解决了,我不但这么想了,而且还这么做了,不过立马就后悔了,原因无他,程序倒入OD没有运行的话,本就处于EP位置,在EP前面写代码,如何运行?被自己的灵机一动蠢哭了;

    • 不过怀疑归怀疑,如何确定POPAD这个指令有问题呢,继续运行程序:

      程序中断

      程序中断,在OD的最下方给出了原因,是因为有一个异常:访问违规,也就是说PUSH 474988这行指令无法压栈,无法压栈的原因是什么呢?应该是堆栈地址不对,就好比去澡堂子洗澡,给你的是 183 号衣柜的钥匙,而你非要放入 96 号衣柜,这肯定是放不进去的;

      如何证实这个猜想呢?去内存窗口看看:

      堆栈区间

      可以看到,系统分配给堆栈的地址是0012C000开始,大小4000,加法运算一下,刚好00130000,也就是说,堆栈的区间是0012C000 - 0012FFFF

      回过头来看ESP的地址,ESP指向堆栈顶端嘛:

      ESP

      很明显,PUSH 474988这行指令要压栈的地址,超出了堆栈的范围,所以才会引发异常;

    • 既然找到了问题,那问题的根源是什么?POPAD,有可能是它改变了ESP的地址,从而导致后面的异常,重载程序:

      未运行

      观察POPAD前,ESP的数据,然后运行程序:

      运行后

      此时明显看到,ESP指向的地址,已经不在堆栈范围内,所以这个压栈指令会产生一个异常;

    • 那么如何修改绕过这个异常呢?

      既然POPAD前,ESP指向的地址在堆栈范围内,那如果不让它弹栈岂不是没有影响了,那就让它入栈PUSHAD

      pushad

      然后运行程序,那个异常没有出现,看来已经绕过了,然鹅,程序却展示了一堆”乱码”:

      乱码

      汇编窗口任意位置右键分析 > 从模块中删除分析,然后乱码就消失了:

      解码

    • 兴奋之余扫了一眼OD左下角,又是一个访问违规

      又一个访问违规

      而它就是我们今天要了解的第二个异常INT 68,具体的描述也没有在搜索引擎中找到,暂且就知道它是一个OD无法处理的异常;

      至于绕过方法很简单,就是将这行指令改为NOP即可:

      改为nop

      然后继续运行程序,发现程序正常运行了:

      程序运行

  3. 反调试小记:

    既然绕过脱壳和int 68异常,程序就正常运行了,那还反调试啥?别忘了,前面为了看到int 68异常,我们使用了两个插件,而如果没有这两个插件呢?

    • HideDebuggerHideOD插件从OD移除并重启OD,然后将程序倒入OD

    • 倒入程序后,Ctrl + N查看 API 列表:

      函数列表

      可以看到,函数不多而且没有什么特殊的,但GetProcAddress却在其中,这就很值得思考了,是不是函数都被隐藏了,果断给GetProcAddress设置断点并做好备注;

    • 然后运行程序,程序会中断多次,而GetProcAddress会获取到所有隐藏或间接调用的函数,把认为可疑的函数设置断点并做好备注:

      断点做备注

      可以看到有三个断点被禁用了,这是经过思考之后的决定:

      1. GetProcAddress:既然程序中断的位置已经改变,那就说明所有隐藏函数获取完毕,这个断点就没有意义了:“飞鸟尽良弓藏”;
      2. CloseHandle:关闭句柄,用处不大;
      3. OpenProcess:获取句柄,这个用途范围太大,况且,获取了就是要操作,直接拦截关闭岂不更香;
    • 获取所有隐藏函数后, 程序中断在CreateToolhelp32Snapshot:这个 API 是获取进程列表的所有程序的进程快照,并返回快照句柄供其他 API 使用,Ctrl + F9执行到返回;

    • 既然已经获取了进程快照,那接下来应该就是调用Process32FirstProcess32Next来分析每个进程了吧?

      F8单步执行程序,程序又来到了未解密的代码段,右键分析 > 从模块中删除分析解码:

      分析代码

      Ctrl + A分析一下解码后的代码,确实发现了Process32First,呃,不过不在一个代码块,打脸是不是来的有点快;

    • 继续F8单步执行程序,程序跳转,来到了一个新的代码段,这里看上去非常可疑:

      非常可疑

      有一处用OpenProcess获取句柄然后用TerminateProcess关闭程序,还有两个结构很类似的代码结构,继续往下翻,数一数,共有 6 处相同的结构:

      很一致

    • 既然觉得可疑,那就单步执行分析一下代码:

      没有打脸,它确实调用了Process32FirstProcess32FirstW,二者的区别是:Process32FirstWUnicode;

      调用Process32First

      继续运行程序,当然不会退出程序,因为进程列表的第一个进程肯定不是OD:

      进程列表

    • 然后多次运行程序,6 个结构相同的代码段依次比较:

      6个比较

    • 比较完会干什么?当然是调用Process32Next获取下一个进程的信息:

      Process32Next

    • 继续向下执行,毫无悬念,又会回到 6 个循环组成的大循环,直到进程快照的所有进程比较完毕;

    • 这里分析的差不多了,接下来迫不及待的想知道所有进程比较完毕后会做什么?很简单,无论它比较的结果是什么,都不让它结束程序,所以,把这里的JNZ都改成JMP,同样是 6 个:

      JMP

    • 继续运行程序,直到所有进程比较完毕:

      PostQuitMessage

      程序中断在了PostQuitMessage,发送了退出程序的信息,这里就不能向下执行了,跟随它到调用的地方去看看:

      反汇编窗口跟随

      来到反汇编窗口,发现代码段很短:但很重要

      代码段很短

      如果这个跳转成立,就不会调用PostQuitMessage,那就直接修改为JMP即可,甚至可以不用管它上面那个关键CALL是做什么的,这是为什么?很简单,因为这么半天了,连程序窗口都没有看到,如果允许程序调用PostQuitMessage就彻底没戏了,所以,在这个跳转上设置断点;

    • 接着,重载并运行程序到最后设置的断点,需要先修改POPAD,接着修改大循环的JNZJMP,然后才能到达最后这个跳转的断点:

      • 重载程序的时候,会有提示弹窗:

        重载弹窗

        这是因为,重载程序后,程序被加密了,我们在程序领空设置的断点无法识别,这个弹窗是提示我们程序领空设置的断点被禁用了:

        断点禁用

      • 在修改完POPAD并运行程序后,程序会中断在CreateToolhelp32Snapshot,这时就可以启用程序领空的断点了;

      • 运行到关键跳转的断点,修改JEJMP达到绕过PostQuitMessage的目的:

        绕过PostQuitMessage

    • 接着F9运行程序,程序中断在INT 68,这个简单,直接修改为NOP

      NOP填充

    • 继续运行程序,期待已久的程序界面终于出现了:

      终于出现

      至于界面卡顿,禁用所有断点就正常喽;