动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器

    现在的软件保护越来越厉害,从早先的加压缩壳、加密,发展到加密壳,虚拟机保护,以及扭曲变换等一系列的混淆手段,使得逆向一个软件的难度越来越大。究其根本原因,是人类天生不适合处理复杂的程序,在看过《代码大全2》之后,我发现作者一直在强调好的程序架构实际是在努力降低程序在管理上的复杂度,一个程序在运行时,至少在三个方面发生着变化,一个是空间:寄存器,内存的值在不断的变化;一个是时间,程序的运行顺序随时间推移而变化;第三个是语义上的变化,到底一段程序干了什么?它的目的是什么?它的实现机制是什么?看一行行的反汇编出来的代码都是很难搞懂的,更何况还有加壳加密这样的混淆手段在作怪,一段程序的二进制代码,再加上哪怕最简单的花指令混淆,都大大增加了复杂度,使得人脑理解它的原理变成一个艰巨的任务。
    
    混淆手段之所以成了逆向道路上的拦路虎,是在于人脑无法把许许多多条枯燥乏味的指令变成一个完整的概念,哪怕就是把两个数相加这样简单的事,编译成二进制代码,再加上花指令混淆,都很难理解它的本意,这就是加壳软件厉害的地方,许多恶意软件借助壳的帮助,逃脱了反病毒人员的逆向分析,在用户的电脑中为非作歹。(下面我把“壳”和混淆代码不加区分的使用了,少打几个字,读起来也简单。)
还有很多软件是用Java,VB这样以虚拟机为基础写的,要想在没有源代码的情况下移植到别的机器上,也是很难的事。而实际上如果能把这些二进制的程序先翻译成一种通用的中间语言,再把这种中间语言翻译成高级语言,就可以很好的利用它们来生产出更多的软件。

    为了达到更有效率的逆向以上这些软件的目的,首先要讨论的是能不能开发出这样一种程序,它能把程序中被混淆的东西暴露出来,去掉那些垃圾代码,把程序原始的面貌展现出来?
    
    我认为是可以的,因为一个很简单的事实:一个加了壳的软件,不论它的代码如何被混淆,它原有的功能不能变化,既不能多做,也不能少做,更不能做错。换句话说,从时间和空间上来看,原来这个程序的执行过程是一根线,加上壳,就多了壳这条线,两条线就像两根有塑料外皮的导线一样,再怎么交织在一起,两根电线中的电流是不会流到一起的,当线与线交织运行一定阶段后,一定要分开来各管各走,这样程序原来的功能才不会被破坏,无论是虚拟机还是扭曲变换,这条基本的定律是不会变的。
    
    然后我从上面这点推导出第二个观点是,壳这条线既然不影响程序的功能,那就是多余的东西,而多余的东西是可以被拿掉的。就是说程序在执行过程中,是可以不去走壳的这条线,也能行得通,而且走起来还快一点,事实上在我看了《编译原理》后发现编译器的代码优化就是起到去除冗余代码的作用的,随便多复杂的混淆代码,只要反汇编出来的汇编语句没有错误,把它放到编译器中优化一下,估计这些冗余代码都活不下去了。而逆向分析人员甚至都没必要去看。这样就大大节省了逆向的工作量。

    上面的这段话中有一个非常重要的条件,就是交给编译器去优化的代码必须是反汇编的时候没有错误的,而很多混淆代码使用了花指令这样的手段,使得静态反汇编出来的东西都是一团乱码,静态反编译因为不能再现程序中真实的变量值的变化,遇上跳转,CALL指令,搞不清是真是假,因为参数是未知的,比如"jmp eax"什么的,这样就大大影响了反汇编的质量,现今为止,没有什么方法能百分之百的静态反汇编成功。

    那么就让我们试试像OD这样的动态调试手段吧,动态调试可以真实地观察程序的运行情况,只要某个程序的片断跟踪过一次,基本上就得到了正确的反汇编代码,但是有三个问题,一是每个程序都有许多的分支,就像一棵树有许多的树梢,而调试器没办法一次走完所有的地方,所以反汇编出来的代码不完整;关于这个问题,可以把程序看成是一棵二叉树之类的东西,反正用个遍历算法,强制遍历一遍所有可用的分支,整个程序的反汇编就出来了吧。

    二是这些代码中有很多的循环,一个循环如果执行了1000遍,OD直接保存代码的话,就把这个循环体重复了1000遍,反汇编出来的代码又很多余。关于这个问题就要一边跟踪调试,一边在反汇编的基础上进行分析,建立基本块,循环体,子过程的结构,初步整理好代码。准备进一步的分析。

    三是OD还有一个很大的问题,就是它跟踪程序的过程不可逆的,我希望在调试程序时,程序运行到哪里,发现了问题,就反过来倒推,这要求保存程序每一步的状态,随时可以退回到前面任意的一个点上。这个问题也许可以试试建立一个数据库来保存相关信息,比如一个寄存器开始是什么值,后来这个值起了什么变化,放到了什么地方,又从什么地方取得了一个什么值,往往这些值是固定的,其实在程序中的各种变量值往往是固定的,从系统初始化开始,一步一步地搬动,加减运算什么的,一路上再怎么变,其实都是固定的数值关系,有些时候看上去好像每次运行都不一样的值,其实处理的方式是一样的,数据流是一样的,控制流也是一样的,要不然这个程序就有毛病了,好端端地运行它两次,结果第一次点“文件”菜单,出来的是“文件”菜单,第二次点变成“帮助”了,每次加减运算的结果都不一样,这样的程序没法用了,不等别人来逆向,自己先被用户抛弃了。所以,即使是系统调用,或者是用户输入,只要我们的调试器模拟输入的参数对路,那么一路上的流程是固定的,变量值的变化也是很容易计算的,关键是要能有个数据库来保存,来随时调用进行推算。

    综上所述,我们必须用动态跟踪的方法,获得跳转的间接路径,还要即时的反汇编出来,还要保存程序的运行状态,还要调用编译器优化来清除混淆代码。这里最好试试虚拟机的技术,在虚拟机中让程序运行,可以把程序和系统隔离开来,防止某些软件发现被调试后恶意地破坏我们的系统。

    这个“动态调试+静态反汇编优化”以去除混淆代码的设想背后是一个巨大的系统工程,想法本身不新鲜,网络上,论坛里,相关的资料和软件都有不少,比如虚拟机脱壳,比如可逆的调试器,但关键是它们没有拧成一股绳,没有在一个高层次的视野下统一起来。当然我只是个业余的编程爱好者,一个人做这样的事肯定是力所不能及的,但既然爱因斯坦那么看重人的想象力,所以我就先幻想一下,然后再一块砖一块砖的去搭建,这里先抛第一块砖吧。

上传的附件 PackerGeneticsTheSelfishCode.rar