逆向-分支结构上

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-分支结构上相关的知识,希望对你有一定的参考价值。

前面把一些基本的运算都记录完了,后面开始流程结构,其流程结构主要分为分支结构和循环结构。对于流程结构的逆向识别,主要在于首先需要识别是什么,然后识别语句体(花括号上下界位置),因为确定好了这两步后,其里面的主体内容就是前面的基本运算或者又是流程结构(递归进行逐步分解)。

对于流程结构主要就以debug版为主了,因为debug版更加利于研究其结构如何,而release中除了结构以外,还混有一些的优化,对于优化的情况,会再额外对其release进行分析。

先看分支语句,其大体上可分为if和switch,因为其两者的实现机制有稍许的不同,所以这里打算分开来记录,这一篇先记录if语句。

对于if语句主要分以下三类

单分支
双分支
多分支

下面先来看一下单分支的情况

int main(int argc, char* argv[])

    if(argc)
    
        printf("if argc");
    
    printf("hello world\\r\\n");
    return 0;

对应的汇编代码,这里是Release版代码,因为debug差不多所以就选择一个观察

9:        if(argc)
00401028 83 7D 08 00          cmp         dword ptr [ebp+8],0
0040102C 74 0D                je          main+2Bh (0040103b)//为零则跳转,跳转的位置是if结束
10:       
11:           printf("if argc");
0040102E 68 2C 20 42 00       push        offset string "if argc" (0042202c)
00401033 E8 38 00 00 00       call        printf (00401070)
00401038 83 C4 04             add         esp,4
12:       
13:       printf("hello world\\r\\n");
0040103B 68 1C 20 42 00       push        offset string "hello world\\r\\n" (0042201c)

对于上面的汇编代码,如果argc为零则进行跳转,其跳转的地址就是if语句的结束(下花括号),而上花括号就是je跳转的下一行代码开始,这样子我们就可以确定了if语句块的上下界位置了。

然后对于跳转逻辑,可以发现此处的汇编逻辑代码与我们的高级代码的逻辑是相反的,因为按照if语句的规定,满足if判定的表达式才能执行if的语句块,而汇编语言的条件跳转却是满足某条件则跳转(绕过语句块不执行),这一点是和C语言是相反的。

那么如果C语言编译可以把if和else的语句块进行相互的调换,那么是不是就能和C语言的逻辑一致呢(调换后if的执行块在下面,那么条件符合就会跳转到下面执行)。

是的,这样子理论上是可行的,只是因为C语言的根据代码行的位置来决定编译后的二进制代码的地址高低的(有时会使用标号相减来得到代码段的长度),所以C编译器不能随意改变代码行在内存中的顺序。

下面简化一下单分支的汇编情况

jxx IF_END
IF_BEGIN:
	....
IF_END:   


结构特性
    条件跳转是增量跳转 - 向下跳
    跳转的目标标号上面没有jmp

确定好if语句的上下界后,然后对其条件的反条件进行还原即可。

 

下面来看双分支结构的情况

int main(int argc, char* argv[])

    if(argc)
    
        printf("if if");
    
    else
    
        printf("else else");
    
    return 0;

对应的汇编代码讲解

9:        if(argc)
0040FB98 83 7D 08 00          cmp         dword ptr [ebp+8],0
0040FB9C 74 0F                je          main+2Dh (0040fbad) 
10:       
11:           printf("if if");
0040FB9E 68 28 50 42 00       push        offset string "if if" (00425028)
0040FBA3 E8 F8 16 FF FF       call        printf (004012a0)
0040FBA8 83 C4 04             add         esp,4
12:       
13:       else
0040FBAB EB 0D                jmp         main+3Ah (0040fbba)  //这里跳转到else的结尾
14:       
15:           printf("else else");
//上面的if je跳转到这里,上面有一jmp,说明这里是一个else结构,否则是一个单分支
0040FBAD 68 8C 61 42 00       push        offset string "else else" (0042618c)
0040FBB2 E8 E9 16 FF FF       call        printf (004012a0)
0040FBB7 83 C4 04             add         esp,4
16:       
17:       return 0;
0040FBBA 33 C0                xor         eax,eax

这里双分支的情况,对比之前的单分支而言,就是jxx目标跳转的地址的上方会多一个jmp,那么此时可以确定是一个else结构,那么对于else结构的结束位置(else下花括号),就是该jmp的目标地址。

jxx IF_END
IF_BEGIN:
	....
    jmp ELSE_END
IF_END:  
ELSE_BEGIN:
	...
ELSE_END:    


结构特性
    条件跳转是增量跳转 - 向下跳
    跳转的目标标号上面有jmp

 

OK,下面需要先说一下编译器的O1和O2优化,因为这两种优化都可能会出现在Release版本中,并且对于后面的流程结构都可能会有该优化哦。

速度优先-O2方案
	对于同样的流程搞多套 -减少汇总,增加节点 (速度快,都为单分支)
体积最小-O1方案
	编译器会使用图结构展示流程,然后根据图算法归并掉一些公共节点 (分支汇总,减少节点)

对于上面的优化方案,在vc6.0的项目选项中可选择,对于Release而言默认一般是O2方案

C/C++
    Optimizations
        Maximize Speed - O2方案
        Minimize Size - O1方案

所以我们直接来看一下上例代码中的release版情况。

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 test    eax, eax
.text:00401006                 jz      short IF_END //跳转到IF的结束处
.text:00401008                 push    offset aIfIf    ; "if if"
.text:0040100D                 call    sub_401030
.text:00401012                 add     esp, 4
.text:00401015                 xor     eax, eax
.text:00401017                 retn
.text:00401018 ; ---------------------------------------------------------------------------
.text:00401018  //这里的上方没有jmp,说明是一个单分支语句
.text:00401018 IF_END:                                 ; CODE XREF: _main+6↑j
.text:00401018                 push    offset aElseElse ; "else else"
.text:0040101D                 call    sub_401030
.text:00401022                 add     esp, 4
.text:00401025                 xor     eax, eax
.text:00401027                 retn

可以发现,在Release版本中,其双分支被优化为单分支了,那么我们来还原下对于的高级语言代码,就能明白O2方案了。

int main(int argc, char* argv[])

    if(argc)
    
        printf("if if");
        return 0;  //return 0语句作为公共语句块部分,可以对if语句体和else语句体都添加该代码
    
    printf("else else");
    return 0;

看完高级语言代码后应该就明白了,其实对于O2来说,可以将公共块都塞到if和else的语句块内,这样子是等价的,因为这里刚好是一个return,所以可以省略else,也就是说该release版本对于debug版本来说,其实少掉的是一行jmp

0040FBAB EB 0D                jmp         main+3Ah (0040fbba)  //这里跳转到else的结尾

将jmp的目标地址处的全部代码替换了该jmp指令。

那么大家可能就会有疑问了,假如公共的语句内容比较多的时候,还会这么处理么?假如公共语句块比较多,那么其实这么子就化不来了,因为相当于代码有了两份,比较冗余。所以当公共语句块的代码量较多时,此时编译器又自然会使用O1方案了。

下面我们只需来改动一下源码,使其进行O1方案进行编译(外面外提优化)

int main(int argc, char* argv[])

    if(argc)
    
        printf("if if");
    
    else
    
        printf("else else");
    
    //此时下面公共语句块内容较多,防止O2优化
    printf("hello world");
    printf("hello world");
    printf("hello world");
    return 0;

Release版本

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 test    eax, eax
.text:00401006                 jz      short loc_40100F
.text:00401008                 push    offset aIfIf    ; "if if"
.text:0040100D                 jmp     short loc_401014
.text:0040100F ; ---------------------------------------------------------------------------
//这里可以看出来 else 结构出来了,因为上面有一jmp
.text:0040100F
.text:0040100F loc_40100F:                             ; CODE XREF: _main+6↑j
.text:0040100F                 push    offset aElseElse ; "else else"
.text:00401014
.text:00401014 loc_401014:                             ; CODE XREF: _main+D↑j
.text:00401014                 call    sub_401040
.text:00401019                 add     esp, 4
.text:0040101C                 push    offset aHelloWorld ; "hello world"
.text:00401021                 call    sub_401040
//.......省略后面printf打印

看完上面的汇编代码,其实如果对其进行高级代码的还原,这里C语言是模拟不出来的,因为上面的语句体中只使用了一行push,为什么呢?因为对于if和else的语句体而言,都只是一个printf,而其两者最大的区别就是参数不同,所以O1优化就将语句体的公共部分放到了最后的公共语句块中,这样子很明显就会减少了体积。

所以在还原的时候,我们需要将这部分公共的再给他还原回去,这样子就能还原回高级代码了。

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 test    eax, eax
.text:00401006                 jz      short loc_40100F
.text:00401008                 push    offset aIfIf    ; "if if"
.text:00401014                 call    sub_401040  //粘贴到一处
.text:00401019                 add     esp, 4
.text:0040100D                 jmp     short loc_401014
.text:0040100F ; ---------------------------------------------------------------------------
.text:0040100F
.text:0040100F loc_40100F:                             ; CODE XREF: _main+6↑j
.text:0040100F                 push    offset aElseElse ; "else else"
.text:00401014                 call    sub_401040  //粘贴到二处
.text:00401019                 add     esp, 4
.text:00401014
.text:00401014 loc_401014:                             ; CODE XREF: _main+D↑j
//将这两行代码复制走,还回上面的语句中
.text:0040101C                 push    offset aHelloWorld ; "hello world"
.text:00401021                 call    sub_401040
//.......省略后面printf打印

按照这里子处理后,可以发现其模样就和debug很像了,可以按其指令还原。所以有时候在遇到一些单独指令时无法还原时,考虑是不是使用了该种优化,如果使用了这种优化,那么需要把缺的这部分代码添回去即可。

到了这里,应该能明白O1和O2的优化了,对于体积优先的O1方案,也就是做了一个语句体内公共代码外提的工作,而对于速度优先的O2方案,就是做了一个语句体内添加公共代码的动作。

 

最后再来看一下多分支的情况,其实对于多分支而言,其实就是多个双分支而已,理解完上面的双分支,这个就很好理解了。

int main(int argc, char* argv[])

    if(argc == 0)
    
        printf("argc == 0");
    
    else if (argc == 1)
    
        printf("argc == 1");
    
    else if (argc == 2)
    
        printf("argc == 2");
    
    return 0;

对应的汇编代码

9:        if(argc == 0)
0040D718 83 7D 08 00          cmp         dword ptr [ebp+8],0
0040D71C 75 0F                jne         main+2Dh (0040d72d)  //跳转到if结束
10:       
11:           printf("argc == 0");
0040D71E 68 BC 2F 42 00       push        offset string "argc == 0" (00422fbc)
0040D723 E8 48 39 FF FF       call        printf (00401070)
0040D728 83 C4 04             add         esp,4
12:       
13:       else if (argc == 1)
0040D72B EB 28                jmp         main+55h (0040d755)  //0040d755 为else结束
这里if结束处的上方有一jmp,说明这里应该为 else 结构
0040D72D 83 7D 08 01          cmp         dword ptr [ebp+8],1
0040D731 75 0F                jne         main+42h (0040d742)
14:       
15:           printf("argc == 1");
0040D733 68 2C 20 42 00       push        offset string "if argc" (0042202c)
0040D738 E8 33 39 FF FF       call        printf (00401070)
0040D73D 83 C4 04             add         esp,4
16:       
17:       else if (argc == 2)
0040D740 EB 13                jmp         main+55h (0040d755)
//这里同理 应该为 else 结构
0040D742 83 7D 08 02          cmp         dword ptr [ebp+8],2
0040D746 75 0D                jne         main+55h (0040d755)
18:       
19:           printf("argc == 2");
0040D748 68 1C 20 42 00       push        offset string "else else" (0042201c)
0040D74D E8 1E 39 FF FF       call        printf (00401070)
0040D752 83 C4 04             add         esp,4
20:       
21:       return 0;
// IF0 else结束位置,IF1 else结束位置,也为IF2结束位置
0040D755 33 C0                xor         eax,eax

其实在结构上而已,和双分支是没有什么区别的,其实我们直接按双分支的套路也能还原出对应的等价高级代码

int main(int argc, char* argv[])

	if(argc == 0)
    
        printf("argc == 0");
    
    else
    
        if (argc == 1)
        
            printf("argc == 1");
        
        else
        
            if (argc == 2)
            
                printf("argc == 2");
            
        
    
    return 0;

这里还原的高级代码是和上面是等价的。好了既然多到了多分支,那么总有些不同吧,其实对于多分支的结构特点而言,就是jxx目标上一行有jmp,高亮后发现不止一个分支是同样的目标,所以在上面的汇编代码中,可以发现其jmp的地址都为0040D755,是一致的。

这里也很好理解,因为对于多分支而言,最终只能执行一个语句块,不管执行到哪个语句块,其最终都要去同一个地址处执行后面的流程代码,所以其jmp的地址会是一致的。

 下面整理下多分支的结构情况

jxx IF_END1
IF_BEGIN1:
	...
    jmp ELSE_END  //jmp目标一致
IF_END1:
ELSE_BEGIN:
IF_BEGIN2:
	...
    jmp ELSE_END  //jmp目标一致
IF_END2:


结构特性
	jxx目标上一行有jmp,并不止一个分支是同样的目标

在release版中,可能会存在上面说的O1优化,那么此时会有代码外提的动作,所以可能会导致其jmp的目标地址不一致,在还原时我们需要将外提的代码塞回去,此时多个jmp处的标号会重合,表示其相当于是跳到同一目标。

以上是关于逆向-分支结构上的主要内容,如果未能解决你的问题,请参考以下文章

HDU 3157 Crazy Circuits (有源汇上下界最小流)

BZOJ2324[ZJOI2011]营救皮卡丘 有上下界费用流

iOS逆向:循环选择指针(下)

32位汇编第五讲,逆向实战干货,(OD)快速定位扫雷内存.

bzoj2324[ZJOI2011]营救皮卡丘 最短路-Floyd+有上下界费用流

自制反汇编逆向分析工具 迭代第六版本