|
|
|
|
|
|
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
现在进入架构核对表时间,先写程序关键部位算法和大致的框架: 基本的调试过程是这样的:
一:在虚拟机或类似的空间,加载这个程序。
(目的:提高对被调试程序的控制力)
二:静态反汇编一个片断,直到遇到第一个控制转移为止,这个控制转移包括call,ret,jmp,jx这类;这样得到一个基本块。
(目的:这是防止花指令对静态反汇编的anti)
三:把每一条指令送到分解器中分解,得到 变量流 ,但是不优化它,直接送到虚拟机进行操作,记录结果。然后循环这个过程到块尾。
*******(易受攻击点-1,作者会发散大量的虚假信息,造成数据流庞大,
反制因素:
1 因为这条线路现在才走了一遍,所以虚假信息流的增大是有限的,
2 要增大这个数据流,在第一遍的阶段,没有循环,所以要求该程序的体积也相应庞大,效率降低
3 一个基本块越长,无用信息抵消的可能性越大,所以最后形成的数据流的体积是有限的
)
四:继续执行以上二、三两步,若转移变成了循环,就一直运行到第一个出口,也就是第一次脱离这条循环线路的跳转。
(如果第一个循环就是死循环,那这个程序还能用吗?)
*******(易受攻击点-2,作者会让多条虚假信息会并到某个循环,以很低的代价制造大量的虚假数据流
对策:
1 优化这个循环中的垃圾指令,减少运行时间
2 观察这个循环所处理的数据是不是可以归纳成组,是的话按数组对待,集中处理数据流,减少虚假数据流的发生
3 如果这个循环处理的数据不可以归纳成组,说明出现了一个新的组,组建一个新的数据结构对应它
)
*******(易受攻击点-3,有一个开始我没有想对的问题,Ptero说得对,确实存在不可归约的流图,也就是说从循环外向循环内转移的可能是始终存在的,我画了个图,证明一个循环是否是一个编译理论上说的唯一入口点的循环,在局部是看不出来的。
对策,
1 多线程符号执行,在每一个分支点同时向下跟踪
2 出错就返回,重新开始分析,并且鉴于这种恶劣的设计不太可能是编译器的设计,给它记上个污点再说。
3 凭人的经验判断了,比如这种非嵌套的循环很不自然,但往往存在一些模式,比如在这种嵌套里要么有anti
要么是在循环体内存在一些模式之类,毕竟这种非结构的程序多半是人写的。
4 在虚拟机中第一遍跟踪时只记录指令序列,不分析,直到壳把控制权交换给程序,就比如以画个窗体为例。
在这个基础上,分析前述的指令,就绕过了这个NP完全问题)
)
五:分析这个循环的操作对象,记录循环的条件,循环操作的对象,把这些数据整合成一个或几个数据结构。
六:优化循环,再次根据循环的特征,分析这个循环的数据流,得到能继续导向下一阶段的数据流。
七:反复迭代上面的过程,直到最优质的数据流出现,把它从原来的指令流中提取出来。
|
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
把x86的指令化作程序的基本数据,就三个 复制,计算,控制转移 把x86的指令化作程序的基本流程形式,就三个 顺序,循环,条件分支 把x86的数据组织形式确定为一种 索引 在以上三个基本出发点,建立各种推理规则 把壳的作者对人的体力和耐心的考验转化为对规则的检验和对机器性能的考验。 不过刚才我核对了《代2》上前期准备的几个核对表,好多叉叉,还要继续准备啊。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
另外一个关于时间的话题是,可能我破解一个程序,一天十小时不够,怎么办? 那我就一次对付一个,比如今天我要对付的是为什么它要检测出调试器,我一层一层跟踪到出问题的时候,然后以这个点为污点,随后回溯,因为每一个分支点都有记录,每一个数据结构都能查询,所以一直看啊看,直到爆破它,然后关机睡觉。 第二天起来,昨天的弯路就不走了,程序的状态已经保持,随时恢复到我要的地方。一个自动脱壳的脚本或者补丁也积累起来了。 关键是这样之后,每次作者都要在每一行代码上好好下功夫变形,因为我们的程序积累了日志,下次遇上同样的anti,我把两份记录拿出来看看,是不是很好的参照呢? 写得不好的OD脚本很容易被作者找到漏洞,而且别人也很难看出他为什么这样写,结果遇上壳一变化就不行了。所以这种破解的经验是很宝贵,但是稍显原始。 程序可以帮我们从繁琐的低级反anti中解脱出来,即使为此多花几天功夫分析,人的信心也不会轻易被消磨,至少你不用一天到晚看自己记录的参考在哪里,或者一次次重新启动程序,等它运行到指定点了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
关于第一个问题,我已经说了,我眼里的数据只有三种类型,就是无索引的,有索引的,和索引。 在这种观点下,我不在乎这个变量在寄存器还是内存,还是什么别的地方,我只看他被访问的方式,只要它曾经被索引了,只要它的索引还在,就一定储存在一个数据结构中,而这些数据结构就是一个个多级的结构,结构化的东西可以从宏观上去组织储存,没必要一个个元素去单独分析。所以在我的程序眼里没有什么pushad,popad,堆栈只是一种特定的索引区域而已。 所以第二个问题也很简单,我不是对每一个数据做索引,只要它一旦被确定是一个数据结构的一部分,就只要对首地址索引了。 有个小问题是循环,其实循环是动态调试最容易确定的事了,昨晚我又好好看了一遍编译原理,确认了这一点,而且有意思的是在编译器眼里根本没有子程序这件事,让我大大欣慰了,也就是说,只要确认一个循环,就确认至少一个数据结构,所以循环越多,我们对数据结构了解就越清楚。我们可以在循环里插个桩,省得调用调试器了,建议你看看valgrind,它的实现方式和我的设想几乎一样,可惜的是它只支持linux,并且它没有对机器指令携带的数据流进行细分,当然,把它移植一下,再写个插件,我认为是完全可行的。 最后还是效率,同样的,你可以看一下valgrind的实现,整个程序每一条指令都单步加分析,做内存检查,速度比原来慢50倍。所以,要想让我的程序慢到10小时,我只要做到让壳慢到6分钟就OK了。 除此之外,因为识别循环是这么简单的事,所以,要想增加程序的复杂度,必须使用switch结构,并使用不相关的代码重复进行分支,并在每一次分支的时候,进行保存原有索引的工作,这就拖慢了程序的运行。 怎样确认一条信息流是虚假的?我想了有三种策略,前向,当下和后向。 先看当下,我们知道一个程序总有一些基本的功能,比如画个窗体,按钮,接收鼠标响应,这是程序固有的功能,而且在api的对应中也是很清楚地,只要在眼下运行到这个地方,知道它是有效的,那就可以回溯到前面找那个stolen oep 了。 然后是前向,比如我已经在正常的程序中,现在转VMP,所以我就紧紧抓住正确的信息流。 最后是后向,我们已经走到了陷阱里,并且知道问题出在那一条信息流上了,所以它是个污点,后向追溯,就可以了。 为什么一条假的信息流和真的信息流是永远不会真正混合在一起的? 因为有个老外教授用数学方法证明了,在程序中,只要给定过程和结果,就一定能逆推出条件。 还有一点是关于社会工程学的,我是永远无法和像你们这样的高手并肩分析什么二进制流的,我相信大多数破解爱好者也差不多。但是我的程序可以啊。如果来个时髦的云计算,或者最不济,云储存,那就人多力量大了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
现在考虑保存这些变量的数据结构,首先是有关单个变量,我们需要掌握它的哪些方面的信息? 1 变量的名称:不管这个变量在哪个寄存器上,或者哪个标志位上,它都有一个独一无二的id。 变量的副本是没有新名称的,只是要记录在一个数组里,指出这个副本的其他信息。 2 变量的值: 一个变量的值首先要考虑是不是可变的,一个变量的值的变化是指,当它来自一个随机源,又没有其他副本时,与另 一个变量发生计算后,结果被填入了这个变量的位置, 当不满足以上的这个要求时,一般考虑是用一个新的变量来记录这个新的值。 一个有副本的变量,不论什么时候被填入新值,都是生成了新变量。 以上两条的典型场景: 比如在第一条指令上,EAX其值为2,那么我们就说有一个叫id_1的变量生成了。(无索引)
然后第二条,mov ecx,eax ,我们说id_1现在有了一个副本。
然后第三条,mov eax,1,我们说现在有了一个新变量,id_2,它的值是1。(无索引)
然后第四条,mov ecx,eax,我们说现在id_1死亡了,这条线索归入中止,以后再走这条线路时,一看它是僵尸,就不为它生成新变量了。
而id_2,现在有了副本。(无索引)
(1) 因为id_1死了,所以要在分析结果中,进行规约:
首先要倒推,ecx中是id_1的最后一个副本,用id_2的副本取代它。
mov ecx,eax
(2) 发生过一次有效规约,往往意味着还可能有其他规约,程序自动进入下一次规约:
继续倒推,在第一句中的ecx是id_1最后副本的唯一前驱,所以去掉,因为这次去掉同
时取消了id_1的出生,所以id_1的流程彻底终止,整体消亡了。
nop
mov eax,1
mov ecx,eax (这里在显示的时候别忘了注释它们的真实值)
(3) 发生过一次有效规约,往往意味着还可能有其他规约,程序自动进入下一次规约:
因为这里是在程序头,可能情况和后面程序中的稍有差异,也许要分别制定位于程序头和中间的不同规约策略。
就这例而言,因为eax从未被使用,又写入了一个固定值,所以调整程序头的eip,指向下一条指令,同时eax中自动置1,
这时候因为eax成为了起始量,所以从原来的数量流上脱开,id_2的两个副本分道扬镳。现在有两个变量
id_2和id_3
(4) 又发生了有效规约,所以继续扫描可以规约的内容,这次调整eip到下一条指令,同时ecx置1,变量流现在有两条,id_2,id_3。
(5) 现在整个程序的起点调整到第三条指令后,eax和ecx中间是两条新数据流的起点。以上这些过程可以用一些简单的规则,反复进行迭代,得到最后不变的那个结果。不过这里(3)和(4)的分析乱了一点,应该是和(1)(2)的规约同步的,也许最后我应该用多线程来处理规约问题? 让我们来提取一些必要的信息。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
我就是在写程序啊。只不过在看《代码大全2》前我都是先写代码,看过《代2》后,改成先写文档了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
算一算一共有多少个寄存器变量 eax ebx ecx edx esi edi esp ebp
| | | | | | | |
ax bx cx dx si di sp bp
| | | |
ah,al bh,bl ch,cl dh,dl
|___________________________| |_________| |_________|
16个寄存器变量 4个寄存器 4个寄存器 -----------24个
变量 变量
cs ds es ss fs gs
|__________________________________|
6个寄存器变量 --------------------------------------------30个
eip
|
ip
2个寄存器变量 --------------------------------------------------------32个
运算结果标志位
SF ZF AF PF CF
|_______________|
5个寄存器变量 ---------------------------------------------------------37个
方向寄存器
DF
1个寄存器变量 ----------------------------------------------------------38个
需要有特权才能控制的寄存器暂时不考虑。 这里有一个至少38位的位图,如果一个指令是影响了其中的n个地方的话,就要考虑生成n个变量。 但是前后两条指令之间会有一个冲销的效果,比如由第一条指令计算产生的标志位其实是个无索引的量,如果第二条指令重置了它,那么这个量就死了。一个死亡的量,其信息流中断了,就不需要保存它了,如果我们是采用滞后保存信息流的策略的话,呵呵,它连数据库都没机会进了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
smc并不能隐藏信息,并且,如果分析下来数据的移动与原来的指令地址是重合的,并没有增加什么信息流,反而是增加了曝光无用的信息流的机会,因为作者在这个过程中面临了两难选择: 1 丢弃虚假的信息流,这样就一下子为分析器指明了方向,因为程序自动中断的那些信息流,会一直留在数据库的记忆中,下次经过时,就不鸟它了。 2 保存虚假的信息流,但是代价是让自己的程序更复杂,运行时花费的内存和时间过多,引起用户的不满。 这就像打桥牌,作者是庄家,我和用户是东手和西手,我们在两个方向上挤压庄家,逼迫他要么放弃建立自己的长套花色,要么失去对将牌的控制。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
linxer的虚拟脱壳机我也用过,而且我在前面的跟帖中也提供了 国外使用虚拟机脱壳的实例,这个已经有很多了。但我要说的是我不是在脱数据,我是在分离壳的信息流,还原原程序的信息流。 怎样建立一个可以既可以全面覆盖信息流又能同时高效的规约信息流的数据模型,是解决问题的关键,我的一个想法是位图。比如在每条指令的背后,是各种各样的寄存器和内存,对不对?那我可以建立一个位图,比如简单的说: _________________ |1|0|1|1|0|0|1|0| ----------------- 这里的每一位对应一个寄存器或者标志位,或者其他必要的信息,当然这样也许还不能全面的反映信息,但至少当你在分析这条指令所有的信息流的时候,可以拿这条指令的位图先和上一条指令的去比对,有些无用的信息流立即被剔除了。不会再参与到后继的分析中去了。还可以想出其他根据上下文规约信息流的规则,这些规则可以设计得尽量全面高效,让多余的信息流控制在可以接受的范围内。 第二,我并不是要实时的调试程序,我是先读取程序的一个片段,然后一条条的做分析,直到控制转移指令,然后再运行到我指定的某个断点。所以我不需要为每条指令下断点。而且,作用范围仅在这样的基本块里的花指令就不需要人去识别,简化了。 另外,对于系统调用,当然不用追随下去了。 第三,所谓的变形循环,当然要做到识别很难,我也还没有去想控制流的分析,到底构建这个理论的基石是什么。 比如说我设想任何一个地址上的指令,只要它没发生变化而被执行了第二次,就是循环的入口,在上一次执行它到这一次执行之间的所有指令构成了一个循环。 当然它同时也可能是个子程序,但是在这个入口没有被来自其他地方的指令调用之前,我都当它是循环。 而循环,本身不产生多余的信息流,循环的调用子程序也不是产生更多信息流的地方。 只有switch,才是产生信息流的一大来源。 而switch,是一个很低效的结构,要想制造出更多的信息流,就要switch更多,而因为我只追踪一条可执行的线路,所以,信息流的产生是可以控制的。 最后 效率永远是个问题,但好在就算到最后证明我的这条路行不通,也对任何人都没有伤害,在没有用的事情上伤脑筋是我最喜爱的消遣了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
我是这样开始程序设计的,就是要为我的程序找到几块牢固的基石,然后从这样几条定理出发,一步步构建逻辑的体系。 首先,我的理解:汇编指令不是程序逻辑的基石,因为大部分汇编指令,都可以向下,继续细分为一组微操作,这些微操作既可以是并行的,也可以是串行的,就像电路板上的电路一样,(其实电脑还是由电路组成的)。这样划分的结果,并不一定是说增加了我们要处理的变数。有时候这样一分析,其实就化繁为简了,比如就功能而言,所有的微操作放在一起,只有三种功能: 1 传送,其实更准确的说是复制,因为毕竟源操作数没有变。 它的含义从源的角度来看,就是制造了一个数据流的分支。 而如果从目的地的角度来看,如果本来不是被任何一个变量所占据,那就是NULL被填充了。 或者本来这里只有一个没有副本的变量,那就是把这个变量NULL了。 或者这里原来的变量有其他副本,那就是这个变量的一条数据流分支被终止了。 再一般的来说,传送:是信息流的延续。 2 计算,有加减的,有布尔的, 这两类运算有没有互相转化的可能,我的数学水平太低,暂时还搞不清楚。 但运算本身的概念很清楚,就是我们产生了一个新的变量,这个变量和它的制造者之间信息流上的关系,决定了后继的信息流的数量和性质。 要注意的是,计算本身是没有延续数据流功能的。 一般的说,计算:是信息流的制造。 3 控制转移 比如jmp,其实是在把数据传递到eip中,然后改变了程序的控制流程。 这个动作既是在延续信息流,同时也是划分控制流的基石。 如果说传送是延续信息流,计算是制造信息流,那么控制转移,就是在把信息流结构化。 以上这些功能并不是针对某一条汇编指令来说的,而是深入到指令内部,比如考虑到指令同时对标志位的影响等,每个标志位也是一个变量。 什么是信息流结构化? 从程序结构上看:变量只有三种,索引,有索引的和无索引的。 什么是无索引的变量? 比如我在程序编译时就固定的常数,它是硬编码在程序中的;又比如 inc eax,这里至少隐含了一个无索引的变量,其值为1,考虑到标志位,就有更多; 又比如 add eax,1。这个立即数也是硬编码的,所以也是无索引的。 什么是有索引的变量? 就是需要以另外一个变量为因子,才能找到的变量。比如堆栈什么的,可以这么看,push 就是传送了一个变量,外加计算了那个索引变量(栈指针),所以以后看到push,就没有push了,只有这两个基本的数据流。 一个有索引变量,可以有多个索引指向它,但如果这些索引变量都被破坏了,而它本身又不在寄存器中,同时也没有了副本,就说它死亡了,一条信息链也随之终止。 如果是一个有索引的变量,那么和它同索引的变量,就成了一种有结构的变量。比如数组变量。具有相同索引的变量走过同一个基本块时,只有第一个被分析,其他的只记录运算结果。这就是信息流的结构化规约。 一个变量如果起到了间接寻址其他变量的作用的,就是索引。 |
|
|
|
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
我知道Ptero的意思了,主要是担心机器的资源被大量虚假的信息流来淹没,那么我们来看看到底这所谓的信息流是怎么回事,我说说的信息流是指一段代码,只产生一次信息分析,这样假设整个程序比如说10M大,写在程序中的指令比如说100000行,OK,我也只完整地产生了100000条指令的信息流的记录,每条指令只在第一次遇到时分析信息流,这样不算循环的话,假如平均每一条指令改变了10条信息流,那么就是100000*10=1000000条信息流。我承认这不是全部,因为还有循环啊,任何一条指令走第二遍时,都会成倍地增加信息流,这是巨大的负担。 好在我们还有控制流,这就是说我不管高级语言有多少种形式,我眼里的程序结构只有三种,顺序,分支和循环,从这个不变的基石出发,我们再来看所谓的数据,就可以知道数据的处理,其实都是结构化的,程序要做的只是,要分辨出几个基本的结构,然后形成一个视图放在人面前,人要处理的是结构化的信息,比如要我们搞清楚程序中单个数值的意义是很难的,但是放在一个循环中,我们立即就知道这里有个数组之类的,然后因为处理数组需要使用一个指针来索引,所以对数据流的分析立即就简化了,我不需要再去研究这里有什么细微的数据流了,只要记录这个数组有多少元素被处理了就是了。 现在是第一个问题,比如一个加密解密的过程,是的,有许多的信息流产生了,其实,许多信息流,从一开始就可以被判定是无意义的,为什么?还是那句话,比如我在程序中这样写: int a=1; int b=1; int c; c=a+b; 你一眼就看出来,固定数据,经过固定的处理,一定是固定的结果。 然后我改: int a=1; int b; int c; int e; int d=1; b=gets(); a=a+d; c=a+b; e=c; c=a; 这个程序模拟一个简单的解密过程,我们看到b接收一个真正要解密的数据,然后在a是解密的key---2,解密的结果是c。 在解密后,程序试图擦除这个解密结果,所以在c中间塞了那个key,这个key在这里是当垃圾用了。 但是,按照我说的分析数据流的方法,程序是不会被迷惑的,因为它已经把e当做数据流的后继了,这条信息链没有中断。 这里的c其实只和b有关,因为a是固定的信息流的产物,我们的分析器是走一步看一步的它看到在 c=a+b这里的时候,还是把信息流分为两路,到e=c的时候,它创建了副本,形成了有用信息流的新分支。 当我告诉它去掉固定变形的时候,直接把c=a+b,改成c=2+b,这样,这个key就是明文了。 美妙的事情是,这一切根本不需要人来动脑筋的。 现在让我们把难度调高一档,假设这个key是随机产生的,在这个情况下程序是这样 int a; int b; int c int e; int d=1; a=random(); b=gets(); a=a+d; c=a+b; e=c; c=a; 运行这个程序,你会发现程序解码错误了,为什么呢?因为一个简单的事实,密码解密之后,一定要和原文是一样的。 所以我们再动动脑筋,我们可以产生一个随机数,把它加到那个真正的key上,然后,和输入的密文相加,最后再减去那个随机数。 int a; int b; int c; int e; int d=2; a=random(); b=gets(); a=a+d; c=a+b; e=c; e=e-a; c=a; 这就需要我们来分析了,当然这个时候机器会提示我们,虽然迷惑数是随机产生的,而且在做 c=a+b 的时候, a 和 b 发生了短路,但是等一会儿做 e=e-a 的时候,短路似乎被解除了。我们的智能分析器缺乏足够的经验,不知道到底要不要按解除短路来处理,但它会提示我们相关的信息,让我们人来确定,而一旦我们确定一个点是“污点”,那整条信息链的前端每一步就回溯上去当固定值处理了。 这样,剩下的就是真正的信息流和key了。 我们再把难度调高一点 这次我们去掉 e 这个变量,情况如何呢?你一定发现程序走不下去了,是的,它到了壳里,却回不了正常的程序流程了。这就是为什么任何壳在处理自己的代码时一定要把原来的数据放在一个它能找到的地方,并且保持指向这个地方的指针,或者指向这个指针的指针,一直抓在自己的寄存器里。 那么,基础的理论看上去不错,但是实现的时候要受到机器性能的制约,如何看待这个问题? 一 人脑 vs 电脑 我知道有许多高手为自己能人肉分析壳而自豪,但是要指出的是,在达到这个水平的过程中,他们必然经过了一个漫长而陡峭的学习曲线,这个时间因素有没有考虑进去呢? 第二,即使你是再高超的人肉脱壳机,也免不了被壳的作者牵着鼻子,你所有积累的关于脱壳的经验都是一种艺术,但这种艺术难以传授,无法大规模的传播,甚至于你能不能在不同的时间,不同的程序上再现你的这种艺术,都是未知数,取决于很多天知道的因素。 第三,大多数像我这样低水平的破解者可能都有这样的想法,既希望能脱壳,又希望是自己而不是专家写的脚本来完成这个工作,因为别人写的脚本毕竟是死的,比如我在脱asprotect的壳时,就遇上很大的困难。是的。我很笨,而且我像大家一样,认为自己的生命是很宝贵,而这点盯着屏幕的时间,我情愿去做更有意义的事。 二 人脑 vs 人脑 我的意思是电脑分析确实很慢,而且我呢,又是个又懒又蠢的家伙,分析二进制流很没劲,但是设计程序的是一件有意义的事。一般来说逆向一个程序要比写一个程序难上N倍,所以我永远不能逆向一个强壳,但是我可以努力把自己的程序写好,如果加壳和脱壳是一场战争的话,我要的是在我选的地方,按照我选的方式来战斗。 我希望我的程序分析器虽然慢,但要达到努力这样一个水平:让壳的作者明白,想让我慢到没法再慢,那他也要慢到没有人要他加过壳的软件,这就是我选的战斗方式。 研究我自己的算法,而不是分析别人的陷阱,是我选的战场。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
关于在机器自动分析中人脑的作用,请不要对我产生误解,我不是说有了机器程序自动分析,就不需要人的参与了。恰恰相反,我把自动分析的程序当做是我的助手,比如当它追踪到壳程序调用widows的API的时候,它就会在这个地方停下来,通知我,提供给我一些相关信息。比如我一看,什么?它要调用messagebox显示这里有调试器? 那么我把这条分支就记作一个污点,现在分析程序掌握了这个信息,它就会调出先前储存的信息流,告诉我在哪里,是什么数据引发了程序转向这个污点,走到这个污点,中间经过了哪些分支点,我判断一下,告诉它下次重来的时候小心点就是了。程序同时把导致污点的整个信息流判定为浊流,以后凡是和这条浊流扯上关系的统统戴上标记。这就叫启发式收缩判断。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
about question 2 when you read a value from windows api like file or registry . we can identify our program label it a constant or varible , as yourself determine , the human's brain is much more capable handle this kind information than machine. because the windows API is same every where, and though the packer writer may cosplay the API, after all , it always run into the kernel. and may the packer writer made a driver in it . but we can make our driver in the kernel first. in this way ,the packer can't invoke an api hiding from us. and since we trace its every step. when it invokes the api . we deside if the data read in a solid value or on the fly. "complexity may exceed the capability of contemporary CPU and memory" that why we must found a way , to structrue our datastruct and algorithm effecientlly. like we only analysize a loop one time. not 1000 times. |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
about question 3 every value coded in is a "constant", the concept of "constant" is that its value is solid and every time you run the program , when it's here , it gets the same value. a constant replaces a constant , is opening a new data chain, and when we patch it , we surely can find the way through the old chain out and deploy the new one. about question 1 it's a compiler design issue. it's called SSA--“static single-assignment form”. which means every time you move a data from here to there, you built a copy for it . and every time you change one data. you killed a copy of it. the copys information , i think we the better keep them in a database. about question 2 if a value which has only one copy and you change it. it means you just give this varible a new value , and you can found it is a node in the data chain, after a long time processing. the way it looks may change, but since its the cause of the result, we can trace back from the result, through every proceeding procedure, back to it. the difficult thing is we must find the encrypt key. another factor in the chain. there are two datachains in the program ,one is the encrypted data and two is the decrypt key. as we see what happens in winrar, you could hardly decrypt the data without a key. but in packed program as long as it can run , surely it embeded the key in the same program, and all we have to do is finding it. |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
要搞懂混淆代码,道路只有一条,就是要弄清每一条汇编指令的意思,然后是一段一段指令块的作用,追踪数据的流向,把真正属于源程序的指令提取出来。要分析每一条汇编指令的作用,不能仅仅看数据的变化,还要看它在上下文中所处的作用,不能简单的建立一棵树,画一张图,列一张表,单一的数据结构是无法同时记录和追踪控制流和数据流的。这就需要我们回到最底层,要比汇编语言更底层,去理解每一条指令的语义,然后又要在全局的视角下,从联系上下文来确定这种指令之间的语义关系。汇编指令和机器码是一一对应的,但不是说汇编指令就是组成程序结构的最小单位,在每一条汇编指令的下面,还有更基本的语义单位。 比如这样一条指令 add dst, src 表面上看只有两个操作数,其实里面藏了好多的变量,假设两个操作数各是两个变量,那么他们的计算结果就是第三个变量,这第三个变量会覆盖原来的一个变量,如果被覆盖的变量没有一个可用的副本,那么新的变量和被覆盖的变量形成了最直接的关系之一,就是替代,一个变量被另一个变量替代,是一个数据流和控制流不可中断的部分,就像一节链条一样,拿掉了,程序就进行不下去了,所以它们前后之间是有关联的。 参与加法运算的原来两个变量 dst 和 src 之间又是什么关系呢?回答是不一定有关系,尽管 add 指令同时用到了他们两个,但它们究竟有没有在程序流程中连接起来,要看它们的前身。这里可以引入一个变量的类型和生存周期的概念。一个数据要么是程序事先设定好的固定值,要么是随机生成的输入值,每个新生的变量都开启了一段程序的流程,在程序运行的某个时间点上,同时有多个流程,靠这些数据分别前后连接起来。比如dst和src,这里有两条数据流,它们各是一条数据流上的一个环节。假如这里的src这个变量,在它的流程中的每一代祖先都是固定值,那么它和运算的结果就没有任何关系,所以,它和参与运算的另一个数dst也就没有什么关系。我们不能因为src和dst参与了同一个运算,就说这里两条数据流连接在一起了,它们还是各管各,就像两条有塑料皮的导线碰到一起,相互之间不会短路一样。比如: 固定值起源1 -----> 中间固定值n -----> src --------------------------> 固定值后继1
+ -----> 新的固定值起源 ----> 固定值后继_new
固定值起源1 -----> 中间固定值n -----> dst --------------------------> 固定值后继2
(例1) 如果两个操作数都是来自于固定值,那么这里的计算结果等于是产生一个新的事先设好的固定值,也就是新生了一条数据流。它和原来的任何一条数据流都没关系了。 -----------------------这是创设新固定值的关系------------------------------------ 如果两个操作数中有一个是起源于随机输入的,那么计算结果就是它的后代,它们是同一条数据流上的先后两节链条,后代的属性也是具有随机输入的血统的。如果这个父辈操作数没有其他副本,这两个操作数就是同一个变量,在做记录的时候,不是新建一个变量,而是把这里看做是变量的值改变了,或者说脏了,这个变量也就是一个活跃的变量。比如:
固定值起源1 -----> 中间固定值n -----------------> src --------------> 固定值后继1
+
| 随机属性
随机值起源1 -----> 中间保持随机性n --->| dst ----> dst'-----> 随机值后继1
| 没有副本
(例2) -----------------------这是一对一的连接关系-------------------------------------- 或者反过来: 随机值起源1 -----> 中间保持随机性n ----> src --------------------------> 随机值后继1
|
+ |
|
固定值起源1 -----> 中间固定值n --------> dst other_fork_new ------> 随机值后继2
(例3) -----------------------这是 1分n 的分支关系------------------------------------ 这里可以看到如果源的随机值路线没被覆盖,一定是新生成一个随机值的数据流。 但是,事情没有这么简单,假设原来被覆盖的随机性变量还有其他存活可用的副本,显然这里的先后关系就不是替代,而是生成关系,第三个变量是新生的,从这个变量开始,这条数据流又多了一条分支。比如这样: 固定值起源1 -----> 中间固定值n -------------------> src --------------------------> 固定值后继1
+
| 随机属性 |--------dst ----------------> 随机值后继1
随机值起源1 -----> 中间保持随机性n ----->| dst-----|
| 有副本 |--------other_fork_new -----> 随机值后继2
(例4) -----------------------这也是 1分n 的分支关系---------------------------------- 假设父母双方都来自随机性的血统, 随机值起源1 -----> 中间保持随机性n -------------> src --------------------------> 随机值后继1
+ |
|
| 随机属性 |
随机值起源1 -----> 中间保持随机性n --->| dst ----> dst'----------------> 随机值后继2
| 没有副本
(例4) -----------------------这是短路原来的数据流------------------------------------ 好了,这时候两条随机线路短路了,假设在将来的某个时候,我们知道了整个随机值数据流1是完全没必要的多余数据流,那么相应的流程2也是多余的。但不能反过来说流程2是多余的,所以后继1也是多余的。 但如果dst有副本,那么情况略有不同: 随机值起源1 -----> 中间保持随机性n -------------> src --------------------------------> 随机值后继1------
+ |
|
| 随机属性 |--------dst ----------|------> 随机值后继2
随机值起源1 -----> 中间保持随机性n --->| dst------| |
| 有副本 |--------other_fork_new -----> 随机值后继3----
(例5) -----------------------这是短路原来的数据流------------------------------------ 在这里若是后继1是多余的,我们只可拿掉后继3。 但是事情不算完,从汇编指令的手册上看,即使是两个操作数相加,也有可能改变标志寄存器的位。因此,这里要引申的说,一次简单的运算后,还要考虑产生了标志位上的新变量,假如结果是随机的,那么标志位上的变量也是随机的,所以可以说除了第一个例子外,从计算结果开始,都形成了原来的随机数据流的一组新的分支。 |----------------
|----------------
|----------------
随机数据流 ---------|
|----------------
|----------------
|----------------
因为现代加壳软件的趋势是指令混用,就是在运用指令时,不是从设计者的本意出发,遵循基本的教科书的规范,而是灵活地发掘指令的功能,比如拿 CALL 当 JMP 之类的,如果老老实实的从汇编指令的字面上分析,就要吃大亏了。 成组的分支虽然看上去有路径爆炸的危险,但是,我们可以运用一定的策略来回溯地消除这些分支,比如这条指令运行之后,又来了一次 add dst, rsc,那么就有可能抛弃那些多余的分支了。而如果有壳的作者试图在标志位上做文章的话,因为我们记录了数据流的每一条路径,所以顺藤摸瓜就容易了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
我一直和你有同样的想法,可惜在第一步反编译的时候,就无法保证反编译的结果100%正确,到目前为止,静态反编译依然不是花指令的对手。是的,有的人可以做到人肉反混淆,但你说的真好,生命诚可贵。然后我们要看看为什么人肉那么难? 这是因为每一条汇编指令都蕴含了比它表面显示出来的简单形式更丰富的信息,比如这个指令 add dst, src 表面看只有两个变量参加运算,其实作者在那里偷笑了。因为这个运算的结果,它包含的信息是非常的多。它有可能修改了标志位,也许作者根本不是要加法的结果,他要的是用标志位当判断条件,又或者这个标志判断也是假的,其实他再减掉那src,又把dst恢复了,这就是单指令多义性。这样的例子不胜枚举,什么拿call当jmp,等会儿又拿jmp当call,还有时候push是保存中间结果,有时候又是填充垃圾的,对于作者来说他只要写个宏,再想做前者的地方,放一个,想做后者的时候放另外一个,在编译后,这个提示的信息丢失了,所以分析者就要去猜了。 当然,也不能说猜,对于人来说,他第一个想到的就是从上下文去找参照,就像我考英语的时候一样,奶奶的,那些出题的未免太变态了吧,尤其是阅读分析,为什么不认识的单词那么多啊?见到生字,我想的就是从句子中找线索,见到难句,我就从段落里寻端倪。 同样道理,无论作者如何把真正的信息隐藏在哪里,它一定有一条运行的流程在那里,他不能在本质上改变这个算法的结果,所以他无法修改实现这个算法的信息。最多把它拉得长一些,中间穿过的弯路多些,多并发几条无用的信息流,好像在动物世界里看到的那样,凶猛的动物去捕猎,而猎物成群结队一起在那里打转,于是猛兽就失去了目标。 人的脑筋是不适应在微观层次上去捕捉信息的,正像我看片子,一般来说200M左右的美剧,看得就蛮舒服了。同样的,我们的大脑习惯于忽略无规律的信息。如果你让VMP的作者自己来脱壳,也不见得就比这里的版主和其他高手强,但是当我们去分析他的壳的时候,他是居高临下,占尽了优势的。 那么,如何扭转这种被动的局面呢?我想最好的办法是把每一条汇编指令分解成更小的信息单位。有人说编程其实就是把东西在内存里搬来搬去,我把这些东西前后连起来,当我发现某个跳转不正确时,我就知道前面哪些指令和它是有关系的,而哪些指令对它是没有影响的。我把从正常的代码段执行到虚拟机的这条线索串起来,就算不还原,也足以简化了。因为虚拟机的执行线路其实是个固定的冗余,一定可以清除的。 为什么虚拟机和花指令以及扭曲变化等等代码膨胀的混淆都是固定冗余呢?因为我们知道一个正常的程序,它每次要做的功能都是一样的,它的功能是程序逻辑的体现,比如我设计个计算器,那每次计算2+2一定要等于4,对不对?而如果我对这条指令进行冗余,那就一定要保证有个地方去冗余,这个冗余和去冗余必须严格对应,所以说它的数值和逻辑是严格固定对应。而在这个冗余产生,膨胀,消除的整个过程中,我们的信息一直在那里,这条信息的生命链从来没有中断过,信息本身也不会改变,该是2+2的,就一定是2+2。这个数量关系怎么可能丢掉呢?在变形过程中一丢掉,那就再也找不回来了。 所以看到了这点,我想结论就很清楚了,让程序帮助我们来追踪信息流,这样我们就和壳的作者站在差不多的高度了,让机器和机器去绕好了。记录信息流要消耗大量的时间和空间是不是?但等电脑头晕时,我可以启发它啊,我再看看它给我整理出来的信息流,作为人的优势立马体现出来了,我们也许对细微的信息流不敏感,但是找特征,找规律,那是机器及不上的,只要有信息流的记录做参考,然后该爆破就爆破,该还原就还原。如果我也错了,那也不要紧,机器有记录啊,让它倒退到记录点重新来过好了。 因此,与其跟在VMP作者的后面,见到一个变形的虚拟机就写一个对应的脚本,不如,从宏观着眼,在理论的高度上去对抗,从微观着手,用数学的方法来破解,那就事半功倍了。 |
|
|
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
这两天一直在想的是如何把汇编指令以数据流为导向分解为更小的单位,恰好这里看到一篇文章,谈的是类似的事,和我的思路几乎是一致的。 基于二进制翻译的仿真器关键技术研究 |
操作理由
RANk
{{ user_info.golds == '' ? 0 : user_info.golds }}
雪币
{{ experience }}
课程经验
{{ score }}
学习收益
{{study_duration_fmt}}
学习时长
基本信息
荣誉称号:
{{ honorary_title }}
勋章
兑换勋章
证书
证书查询 >
能力值
[/QUOTE]