首页
社区
课程
招聘
[原创]动态调试与静态反汇编合一,运用虚拟机技术创建可逆向运行的调试器
发表于: 2010-8-22 21:00 139108

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

2010-8-22 21:00
139108
收藏
免费 7
支持
分享
最新回复 (124)
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
26
贴一下GCC的优化测试,这是开头我发过的程序,这里用x ++ 模拟了 inc eax

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main (){

  int x = 1;
  x ++;
  x ++;
  x ++;
  x ++;

  printf ("%d",x);

}

第一次在编译的时候选择不优化,在调试器中反汇编如下

Temporary breakpoint 1, 0x004013c3 in main ()
(gdb) disassemble
Dump of assembler code for function main:
   0x004013c0 <+0>:     push   %ebp
   0x004013c1 <+1>:     mov    %esp,%ebp
=> 0x004013c3 <+3>:     and    $0xfffffff0,%esp
   0x004013c6 <+6>:     sub    $0x20,%esp
   0x004013c9 <+9>:     call   0x401a20 <__main>
   0x004013ce <+14>:    movl   $0x1,0x1c(%esp)
   0x004013d6 <+22>:    incl   0x1c(%esp)
   0x004013da <+26>:    incl   0x1c(%esp)
   0x004013de <+30>:    incl   0x1c(%esp)
   0x004013e2 <+34>:    incl   0x1c(%esp)

   0x004013e6 <+38>:    mov    0x1c(%esp),%eax
   0x004013ea <+42>:    mov    %eax,0x4(%esp)
   0x004013ee <+46>:    movl   $0x403064,(%esp)
   0x004013f5 <+53>:    call   0x401c90 <printf>
   0x004013fa <+58>:    leave
   0x004013fb <+59>:    ret
   0x004013fc <+60>:    add    %al,(%eax)
   0x004013fe <+62>:    add    %al,(%eax)
End of assembler dump.
(gdb)

可以看到它果然是四个 incl   0x1c(%esp)
奇怪啊,它用esp当变量用

然后我用优化开关O2,这个是最通用的选项

Temporary breakpoint 2, 0x004013c3 in main ()
(gdb) disassemble
Dump of assembler code for function main:
   0x004013c0 <+0>:     push   %ebp
   0x004013c1 <+1>:     mov    %esp,%ebp
=> 0x004013c3 <+3>:     and    $0xfffffff0,%esp
   0x004013c6 <+6>:     sub    $0x10,%esp
   0x004013c9 <+9>:     call   0x401a10 <__main>
   0x004013ce <+14>:    movl   $0x5,0x4(%esp)
   0x004013d6 <+22>:    movl   $0x403064,(%esp)
   0x004013dd <+29>:    call   0x401c80 <printf>
   0x004013e2 <+34>:    leave
   0x004013e3 <+35>:    ret
   0x004013e4 <+36>:    add    %al,(%eax)
   0x004013e6 <+38>:    add    %al,(%eax)
   0x004013e8 <+40>:    add    %al,(%eax)
   0x004013ea <+42>:    add    %al,(%eax)
   0x004013ec <+44>:    add    %al,(%eax)
   0x004013ee <+46>:    add    %al,(%eax)
End of assembler dump.
(gdb)

优化完成,这里用一句代替了四句,看起来舒服多了。可以设想,有很多花指令建立在用成对指令对冲的基础上来变形,在gcc优化面前就难混了。

也有很多混淆代码,是建立这样一种机制:在保存正确代码后,运行一段无关程序流程的垃圾代码,再回来执行正确代码。作为一个人,不得不痛苦地在这种盲肠里打转,很辛苦啊。

照书上说的,编译器优化的基本功能就是:

合并已知量

删除多余运算

删除死代码

可以预计,将来,让壳的作者和gcc的作者对掐,是逆向分析者的一大福音。
2010-8-24 05:25
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
27
这两天一直在想的是如何把汇编指令以数据流为导向分解为更小的单位,恰好这里看到一篇文章,谈的是类似的事,和我的思路几乎是一致的。

基于二进制翻译的仿真器关键技术研究
上传的附件:
2010-8-25 12:40
0
雪    币: 222
活跃值: (478)
能力值: ( LV11,RANK:188 )
在线值:
发帖
回帖
粉丝
28
我想说的是,没意义。
反编译出来,然后努力改成可以编译通过的格式,接着努力做一个从这个改好的可以编译的结果中优化掉无用代码的编译器,大家都在做直接从反编译出来的东西里面取出无用东西的脚本。

VM真花费时间,生命诚可贵,VM全当黑盒吧。跳转回去的地址,把可以看明白的部分改成跳往正确流程不就可以了。VM里的功能,用CE搜索改变了的字节来解决罢。
2010-8-25 14:49
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
29
    我一直和你有同样的想法,可惜在第一步反编译的时候,就无法保证反编译的结果100%正确,到目前为止,静态反编译依然不是花指令的对手。是的,有的人可以做到人肉反混淆,但你说的真好,生命诚可贵。然后我们要看看为什么人肉那么难?

    这是因为每一条汇编指令都蕴含了比它表面显示出来的简单形式更丰富的信息,比如这个指令

    add   dst,  src

    表面看只有两个变量参加运算,其实作者在那里偷笑了。因为这个运算的结果,它包含的信息是非常的多。它有可能修改了标志位,也许作者根本不是要加法的结果,他要的是用标志位当判断条件,又或者这个标志判断也是假的,其实他再减掉那src,又把dst恢复了,这就是单指令多义性。这样的例子不胜枚举,什么拿call当jmp,等会儿又拿jmp当call,还有时候push是保存中间结果,有时候又是填充垃圾的,对于作者来说他只要写个宏,再想做前者的地方,放一个,想做后者的时候放另外一个,在编译后,这个提示的信息丢失了,所以分析者就要去猜了。

    当然,也不能说猜,对于人来说,他第一个想到的就是从上下文去找参照,就像我考英语的时候一样,奶奶的,那些出题的未免太变态了吧,尤其是阅读分析,为什么不认识的单词那么多啊?见到生字,我想的就是从句子中找线索,见到难句,我就从段落里寻端倪。

    同样道理,无论作者如何把真正的信息隐藏在哪里,它一定有一条运行的流程在那里,他不能在本质上改变这个算法的结果,所以他无法修改实现这个算法的信息。最多把它拉得长一些,中间穿过的弯路多些,多并发几条无用的信息流,好像在动物世界里看到的那样,凶猛的动物去捕猎,而猎物成群结队一起在那里打转,于是猛兽就失去了目标。

    人的脑筋是不适应在微观层次上去捕捉信息的,正像我看片子,一般来说200M左右的美剧,看得就蛮舒服了。同样的,我们的大脑习惯于忽略无规律的信息。如果你让VMP的作者自己来脱壳,也不见得就比这里的版主和其他高手强,但是当我们去分析他的壳的时候,他是居高临下,占尽了优势的。

    那么,如何扭转这种被动的局面呢?我想最好的办法是把每一条汇编指令分解成更小的信息单位。有人说编程其实就是把东西在内存里搬来搬去,我把这些东西前后连起来,当我发现某个跳转不正确时,我就知道前面哪些指令和它是有关系的,而哪些指令对它是没有影响的。我把从正常的代码段执行到虚拟机的这条线索串起来,就算不还原,也足以简化了。因为虚拟机的执行线路其实是个固定的冗余,一定可以清除的。

    为什么虚拟机和花指令以及扭曲变化等等代码膨胀的混淆都是固定冗余呢?因为我们知道一个正常的程序,它每次要做的功能都是一样的,它的功能是程序逻辑的体现,比如我设计个计算器,那每次计算2+2一定要等于4,对不对?而如果我对这条指令进行冗余,那就一定要保证有个地方去冗余,这个冗余和去冗余必须严格对应,所以说它的数值和逻辑是严格固定对应。而在这个冗余产生,膨胀,消除的整个过程中,我们的信息一直在那里,这条信息的生命链从来没有中断过,信息本身也不会改变,该是2+2的,就一定是2+2。这个数量关系怎么可能丢掉呢?在变形过程中一丢掉,那就再也找不回来了。

    所以看到了这点,我想结论就很清楚了,让程序帮助我们来追踪信息流,这样我们就和壳的作者站在差不多的高度了,让机器和机器去绕好了。记录信息流要消耗大量的时间和空间是不是?但等电脑头晕时,我可以启发它啊,我再看看它给我整理出来的信息流,作为人的优势立马体现出来了,我们也许对细微的信息流不敏感,但是找特征,找规律,那是机器及不上的,只要有信息流的记录做参考,然后该爆破就爆破,该还原就还原。如果我也错了,那也不要紧,机器有记录啊,让它倒退到记录点重新来过好了。

    因此,与其跟在VMP作者的后面,见到一个变形的虚拟机就写一个对应的脚本,不如,从宏观着眼,在理论的高度上去对抗,从微观着手,用数学的方法来破解,那就事半功倍了。
2010-8-25 18:48
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
30
要搞懂混淆代码,道路只有一条,就是要弄清每一条汇编指令的意思,然后是一段一段指令块的作用,追踪数据的流向,把真正属于源程序的指令提取出来。要分析每一条汇编指令的作用,不能仅仅看数据的变化,还要看它在上下文中所处的作用,不能简单的建立一棵树,画一张图,列一张表,单一的数据结构是无法同时记录和追踪控制流和数据流的。这就需要我们回到最底层,要比汇编语言更底层,去理解每一条指令的语义,然后又要在全局的视角下,从联系上下文来确定这种指令之间的语义关系。汇编指令和机器码是一一对应的,但不是说汇编指令就是组成程序结构的最小单位,在每一条汇编指令的下面,还有更基本的语义单位。

    比如这样一条指令

    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,那么就有可能抛弃那些多余的分支了。而如果有壳的作者试图在标志位上做文章的话,因为我们记录了数据流的每一条路径,所以顺藤摸瓜就容易了。
2010-8-25 18:54
0
雪    币: 1946
活跃值: (248)
能力值: (RANK:330 )
在线值:
发帖
回帖
粉丝
31
头脑风暴啊123456
2010-8-25 21:38
0
雪    币: 452
活跃值: (72)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
33
This is a novel topic. I have some questions.
Sorry I cannot type Chinese. I hope you are able to understand my English.

Question 1:
How do you know if a variable has a backup? A variable can not only be stored in reg/mem, but also in file/registry. In the latter case, you have to identify Windows API relating to IO, right? Besides, a variable can be encrypted before stroing, and decrypted before using.

Question 2:
If a variable is encrypted before backing up, what is the complexity to find the correlation between decrypted value and original value?
For example:
enc+dec:
var1--->(encryption1)--->enc1--->(decryption1)--->var1
enc+enc:
var1--->(encryption1)--->enc1--->(enctyption2)--->enc2
unanalyzed algorithms:
var1--->(algorithm1)--->var2--->(algorithm2)--->var3


if algorithm1=AES and algorithm2=MD5, then var3 is not a backup of var1
but if algorithm1=AES and algorithm2=inverse_AES, then var3 is a backup of var1.
Theoretically it is possible to analyzed the flow of the algorithm, but the complexity may exceed the capability of contemporary CPU and memory, and you may consume all the memory on you machine before you get the result.

Question 3:
Your algorithm depends on identifying variables and constants. BUT, how can you tell if src is a constant?
Consider the following example:
00401000    B8 40C54801    mov eax, 0148C540

Is src constant? Maybe yes, maybe no.
The following SMC can change it easily:
mov dword ptr [00401001], 12345678

And this SMC can hide itself deeply, transformed, maybe in a gardian thread.
Similarly, the imm value 12345678 cannot be granted as constant.
The imm value at 00401001 can also be modified by a gardian process, or even in R0 kernel.
In such SMC case, it is extreamly hard to trace its origin, and you have to assume there is no constant at all.

How do you think?
2010-8-25 22:50
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
34
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.
2010-8-26 05:16
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
35
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.
2010-8-26 05:44
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
36
关于在机器自动分析中人脑的作用,请不要对我产生误解,我不是说有了机器程序自动分析,就不需要人的参与了。恰恰相反,我把自动分析的程序当做是我的助手,比如当它追踪到壳程序调用widows的API的时候,它就会在这个地方停下来,通知我,提供给我一些相关信息。比如我一看,什么?它要调用messagebox显示这里有调试器?
那么我把这条分支就记作一个污点,现在分析程序掌握了这个信息,它就会调出先前储存的信息流,告诉我在哪里,是什么数据引发了程序转向这个污点,走到这个污点,中间经过了哪些分支点,我判断一下,告诉它下次重来的时候小心点就是了。程序同时把导致污点的整个信息流判定为浊流,以后凡是和这条浊流扯上关系的统统戴上标记。这就叫启发式收缩判断。
2010-8-26 06:09
0
雪    币: 452
活跃值: (72)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
37
Thanks for your answer.

For my question 1 & 2, I agree it's theoretically possible to determine the data flow.

My concern is that, the complexity may be huge, because when analyzing API and enc/dec algorithm, a lot of other variables are introduced and you need a huge database to store them. Think about IDA... just statically disasm and analysis, it's already slow.

For your answer to question 3, I agree your idea, but how you can you tell when a new chain should be generated? And how can you trace its origin?

You can monitor several hotspots, but not for the entire memory. There is no extra space to store the states of every memory blocks.

Also memory breakpoint is not available. Think about OD...If memory breakpoint is triggered too often, then most CPU resources are wasted on filtering out the false alarm. If I hide the patch operation among 1,000,000 normal memory accesses, how are you able to detect it?

Hmm..the only solution I can come up with is to use remote debugging or VM debugging, with a huge memory to store the state of each memory block of the debugee.

Do you have better ideas?
2010-8-26 06:19
0
雪    币: 452
活跃值: (72)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
38
明白你的意思了。
这样的话,就算SMC,代码改了,你的程序还能保留修改之前的状态。
那么多态变形呢?每次调用messagebox的时候代码都不一样,你查看之前的追踪记录,到下次运行的时候又不一样了。你想让它自动断下来是不可能了。
如果需要人工干预才能把数据流merge到一起的话,就得先把多态引擎搞定,然后才能分析API,是不是?如果这样的话,完全可以在多态引擎里面嵌入虚假的或者冗余的API调用,让你不停地create新的data chain,阻止你merge,最后就。。。
2010-8-26 07:17
0
雪    币: 202
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
39
不懂啊,膜拜下
2010-8-26 11:26
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
40
我知道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倍,所以我永远不能逆向一个强壳,但是我可以努力把自己的程序写好,如果加壳和脱壳是一场战争的话,我要的是在我选的地方,按照我选的方式来战斗。

我希望我的程序分析器虽然慢,但要达到努力这样一个水平:让壳的作者明白,想让我慢到没法再慢,那他也要慢到没有人要他加过壳的软件,这就是我选的战斗方式。

研究我自己的算法,而不是分析别人的陷阱,是我选的战场。
2010-8-26 15:44
0
雪    币: 1844
活跃值: (35)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
41
我等你的好消息
2010-8-26 16:14
0
雪    币: 6075
活跃值: (2236)
能力值: (RANK:1060 )
在线值:
发帖
回帖
粉丝
42
2010-8-26 16:39
0
雪    币: 338
活跃值: (103)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
43
期待更多转头
2010-8-26 17:22
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
44
[QUOTE=forgot;851072][/QUOTE]

该图片仅限百度用户交流使用,我登陆了百度也没用。
2010-8-26 17:33
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
45
我是这样开始程序设计的,就是要为我的程序找到几块牢固的基石,然后从这样几条定理出发,一步步构建逻辑的体系。

首先,我的理解:汇编指令不是程序逻辑的基石,因为大部分汇编指令,都可以向下,继续细分为一组微操作,这些微操作既可以是并行的,也可以是串行的,就像电路板上的电路一样,(其实电脑还是由电路组成的)。这样划分的结果,并不一定是说增加了我们要处理的变数。有时候这样一分析,其实就化繁为简了,比如就功能而言,所有的微操作放在一起,只有三种功能:

1 传送,其实更准确的说是复制,因为毕竟源操作数没有变。

  它的含义从源的角度来看,就是制造了一个数据流的分支。

  而如果从目的地的角度来看,如果本来不是被任何一个变量所占据,那就是NULL被填充了。
                           或者本来这里只有一个没有副本的变量,那就是把这个变量NULL了。
                           或者这里原来的变量有其他副本,那就是这个变量的一条数据流分支被终止了。

  再一般的来说,传送:是信息流的延续。
                           
2 计算,有加减的,有布尔的,

    这两类运算有没有互相转化的可能,我的数学水平太低,暂时还搞不清楚。
   
    但运算本身的概念很清楚,就是我们产生了一个新的变量,这个变量和它的制造者之间信息流上的关系,决定了后继的信息流的数量和性质。

    要注意的是,计算本身是没有延续数据流功能的。

    一般的说,计算:是信息流的制造。

3 控制转移

    比如jmp,其实是在把数据传递到eip中,然后改变了程序的控制流程。

    这个动作既是在延续信息流,同时也是划分控制流的基石。

    如果说传送是延续信息流,计算是制造信息流,那么控制转移,就是在把信息流结构化。

以上这些功能并不是针对某一条汇编指令来说的,而是深入到指令内部,比如考虑到指令同时对标志位的影响等,每个标志位也是一个变量。

   
什么是信息流结构化?

从程序结构上看:变量只有三种,索引,有索引的和无索引的。

什么是无索引的变量?

比如我在程序编译时就固定的常数,它是硬编码在程序中的;又比如 inc eax,这里至少隐含了一个无索引的变量,其值为1,考虑到标志位,就有更多;
又比如 add eax,1。这个立即数也是硬编码的,所以也是无索引的。

什么是有索引的变量?
就是需要以另外一个变量为因子,才能找到的变量。比如堆栈什么的,可以这么看,push 就是传送了一个变量,外加计算了那个索引变量(栈指针),所以以后看到push,就没有push了,只有这两个基本的数据流。

一个有索引变量,可以有多个索引指向它,但如果这些索引变量都被破坏了,而它本身又不在寄存器中,同时也没有了副本,就说它死亡了,一条信息链也随之终止。

如果是一个有索引的变量,那么和它同索引的变量,就成了一种有结构的变量。比如数组变量。具有相同索引的变量走过同一个基本块时,只有第一个被分析,其他的只记录运算结果。这就是信息流的结构化规约。

一个变量如果起到了间接寻址其他变量的作用的,就是索引。
2010-8-26 19:28
0
雪    币: 222
活跃值: (478)
能力值: ( LV11,RANK:188 )
在线值:
发帖
回帖
粉丝
46
LZ大牛,做个指令虚拟执行机才是第一步的啊 /:^]
linxer的虚拟脱壳机很猛的
2010-8-26 22:21
0
雪    币: 452
活跃值: (72)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
47
The performance issue is still what I'm worrying about.

Let's consider the timing at first and assume that you have sufficient large buffer to store the data flows extracted from the code.

How to collect information form instructions? I think it is necessary to trace through every instruction, and there are 2 ways of doing it:
setting TF to 1 in debugger at every single instruction, or anaylzing the target in VM. Either way is much slower than running without debugging or VM, especially with loops. You don't want to wait for hours before a messagebox prompts out, do you?

You may claim that the analyzer can skip the loop when it is recognized. But what if it needs assistance from human brain to detect a well-transformed loop? It may eat up all the CPU resources on a deliberately-made hard-to-detect loop.
2010-8-26 23:49
0
雪    币: 1769
活跃值: (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
48
linxer的虚拟脱壳机我也用过,而且我在前面的跟帖中也提供了
国外使用虚拟机脱壳的实例,这个已经有很多了。但我要说的是我不是在脱数据,我是在分离壳的信息流,还原原程序的信息流。

怎样建立一个可以既可以全面覆盖信息流又能同时高效的规约信息流的数据模型,是解决问题的关键,我的一个想法是位图。比如在每条指令的背后,是各种各样的寄存器和内存,对不对?那我可以建立一个位图,比如简单的说:

_________________
|1|0|1|1|0|0|1|0|
-----------------


这里的每一位对应一个寄存器或者标志位,或者其他必要的信息,当然这样也许还不能全面的反映信息,但至少当你在分析这条指令所有的信息流的时候,可以拿这条指令的位图先和上一条指令的去比对,有些无用的信息流立即被剔除了。不会再参与到后继的分析中去了。还可以想出其他根据上下文规约信息流的规则,这些规则可以设计得尽量全面高效,让多余的信息流控制在可以接受的范围内。

第二,我并不是要实时的调试程序,我是先读取程序的一个片段,然后一条条的做分析,直到控制转移指令,然后再运行到我指定的某个断点。所以我不需要为每条指令下断点。而且,作用范围仅在这样的基本块里的花指令就不需要人去识别,简化了。

另外,对于系统调用,当然不用追随下去了。

第三,所谓的变形循环,当然要做到识别很难,我也还没有去想控制流的分析,到底构建这个理论的基石是什么。

比如说我设想任何一个地址上的指令,只要它没发生变化而被执行了第二次,就是循环的入口,在上一次执行它到这一次执行之间的所有指令构成了一个循环。

当然它同时也可能是个子程序,但是在这个入口没有被来自其他地方的指令调用之前,我都当它是循环。

而循环,本身不产生多余的信息流,循环的调用子程序也不是产生更多信息流的地方。

只有switch,才是产生信息流的一大来源。

而switch,是一个很低效的结构,要想制造出更多的信息流,就要switch更多,而因为我只追踪一条可执行的线路,所以,信息流的产生是可以控制的。

最后
效率永远是个问题,但好在就算到最后证明我的这条路行不通,也对任何人都没有伤害,在没有用的事情上伤脑筋是我最喜爱的消遣了。
2010-8-27 05:09
0
雪    币: 452
活跃值: (72)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
49
If you use static analysis only, it cannot prevent patching or SMC, and you have the risk to lose the trace.

BTW, I'm not criticizing your algorithm. I just want to figure out if it is feasible, given limited CPU and memory resource, and also of course time. If yes, it can also be applied to virus analysis, or to build a polymorphism engin based on the data flow, etc.

Hope you can realize your idea. Good luck.
2010-8-27 06:01
0
雪    币: 150
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
50
麻烦国人还是说中文吧~,英语像我这种不好好学习英文的人,实在看不懂....
您要是国人,就麻烦您了。,也省得我们还得 google翻译~~~~。
最后说不定语法还不对~~
2010-8-27 06:13
0
游客
登录 | 注册 方可回帖
返回
//