逆向-分支结构下

Posted 嘻嘻兮

tags:

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

上一篇说完IF结构,这一篇就接着总结分支结构中的switch,对于switch结构,编译器共有四种方案提供选择,我们不必去关心编译器何时会选择何种结构,因为这个完全取决于编译器作者,所以我们只需能识别出这四种方案是switch结构即可。

对于switch结构而言,不管哪种方案,其主要分两部分

引导代码
case块集合

对于不同的方案,其case块集合是不变的,主要在于其引导方式的变化。

首先,来看方案一,这是最基础的情况,在vs系列中,其触发的条件是case在三个以内(由编译器决定)。

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

    switch(argc)
    
        case 1:
            printf("case 1");
            break;
        case 2:
            printf("case 2");
            break;
        default:
            printf("default"); //注意此时没有break,表示default执行完继续执行
        case 3:
            printf("case 3");
            break;
    
    return 0;

首先,先来看一下上面的代码,其实对于switch而言,其与if结构最大的一个不同是当case块中没有break时,需要顺序执行,那么这意味着switch的结构并不会同if结构一样。

对应的汇编代码

9:        switch(argc)
10:       
//引导部分
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 83 7D FC 01          cmp         dword ptr [ebp-4],1
0040D722 74 0E                je          main+32h (0040d732)
0040D724 83 7D FC 02          cmp         dword ptr [ebp-4],2
0040D728 74 17                je          main+41h (0040d741)
0040D72A 83 7D FC 03          cmp         dword ptr [ebp-4],3
0040D72E 74 2D                je          main+5Dh (0040d75d)
0040D730 EB 1E                jmp         main+50h (0040d750)  //上面都没比较成功则跳转到default
//case块集合部分
11:           case 1:
12:               printf("case 1");
0040D732 68 24 20 42 00       push        offset string "case 1" (00422024)
0040D737 E8 34 39 FF FF       call        printf (00401070)
0040D73C 83 C4 04             add         esp,4
13:               break;
0040D73F EB 29                jmp         main+6Ah (0040d76a)  //break 执行完跳转到switch结束
14:           case 2:
15:               printf("case 2");
0040D741 68 BC 2F 42 00       push        offset string "argc == 0" (00422fbc)
0040D746 E8 25 39 FF FF       call        printf (00401070)
0040D74B 83 C4 04             add         esp,4
16:               break;
0040D74E EB 1A                jmp         main+6Ah (0040d76a)
17:           default:
18:               printf("default"); //注意此时没有break,表示default执行完继续执行
0040D750 68 2C 20 42 00       push        offset string "argc == 1" (0042202c)
0040D755 E8 16 39 FF FF       call        printf (00401070)
0040D75A 83 C4 04             add         esp,4  //此处没有jmp,说明default执行完会顺序执行下面case 3
19:           case 3:
20:               printf("case 3");
0040D75D 68 1C 20 42 00       push        offset string "else else" (0042201c)
0040D762 E8 09 39 FF FF       call        printf (00401070)
0040D767 83 C4 04             add         esp,4
21:               break;
22:       
23:       return 0;
0040D76A 33 C0                xor         eax,eax

在上面的汇编代码中,引导部分主要就是用于比较case值,当匹配到对应的值时,跳转到对应的case块中,因为case块中有break存在,此时就会产生jmp的语句(执行完跳转到switch结束),如果没有jmp,说明该case块没有break,此时会接着顺序执行,也就是因为这个特点,需要把所有的case块都集中在一起,否则无法做到没有无break时顺序执行。

对于switch的边界问题其实还是比较好识别的,因为这个特点比较明显,主要的是如何识别switch结束位置,这里switch结束的位置可以在case块中找,因为如果有break,那么其jmp的位置一般就是switch结束位置了。

在有些代码中,可能没有default,那么上面在引导部分都不符合条件时,就会直接jmp到switch结束的位置。

明白了switch的机制后,也就可以明白这种方案,其实在时间上来说,与if语句其实并没有显著的优点,毕竟这里再引导部分也是用if一个个进行比较的。

 

下面再来看方案二,方案二使用的是地址表引导的方案。对于之前的引导部分,其可以细分为三个部分,并不是简简单单的使用jxx进行比较。

引导部分
	表达式求值
	范围检查
	查表并转移

下面来观察下面这段的反汇编进行观察

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

    switch(argc)
    
    case 19:
        printf("case 10");
        break;
    case 12:
        printf("case 1");
        break;
    default:
        printf("default");
        break;
    case 13:
        printf("case 3");
        break;
    case 17:
        printf("case 3");
        break;
    case 20:
        printf("case 3");
        break;
    case 16:
        printf("case 3");
        break;
    
    return 0;

对应的反汇编代码

9:        switch(argc)
10:       
//表达式求值部分
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D721 83 E9 0C             sub         ecx,0Ch
0040D724 89 4D FC             mov         dword ptr [ebp-4],ecx
//范围检查
0040D727 83 7D FC 08          cmp         dword ptr [ebp-4],8
0040D72B 77 28                ja          $L858+0Fh (0040d755)
//查表并转移
0040D72D 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D730 FF 24 95 B1 D7 40 00 jmp         dword ptr [edx*4+40D7B1h]
11:       case 19:
12:           printf("case 10");
0040D737 68 24 20 42 00       push        offset string "case 10" (00422024)
0040D73C E8 2F 39 FF FF       call        printf (00401070)
0040D741 83 C4 04             add         esp,4
13:           break;
0040D744 EB 58                jmp         $L866+0Dh (0040d79e)
14:       case 12:
15:           printf("case 1");
0040D746 68 BC 2F 42 00       push        offset string "case 2" (00422fbc)
0040D74B E8 20 39 FF FF       call        printf (00401070)
0040D750 83 C4 04             add         esp,4
16:           break;
0040D753 EB 49                jmp         $L866+0Dh (0040d79e)
17:       default:
18:           printf("default");
0040D755 68 2C 20 42 00       push        offset string "argc == 1" (0042202c)
0040D75A E8 11 39 FF FF       call        printf (00401070)
0040D75F 83 C4 04             add         esp,4
19:           break;
0040D762 EB 3A                jmp         $L866+0Dh (0040d79e)
//.....此处省略部分代码
29:       case 16:
30:           printf("case 3");
0040D791 68 1C 20 42 00       push        offset string "else else" (0042201c)
0040D796 E8 D5 38 FF FF       call        printf (00401070)
0040D79B 83 C4 04             add         esp,4
31:           break;
32:       
33:       return 0;
0040D79E 33 C0                xor         eax,eax

归根结底这个机制其实就是查表机制,表中其实存着对应的case的地址,那么通过其索引就能跳转到对应的位置,下面我们对引导的三个部分细细来讨论一下

表达式求值,对于这部分而言,我们可以知道该case的最小值。拿上面那段程序来说,我们的case的最小值为12,那么对于数组而言,其下标的最小值肯定是0,那么对于数组的0~11项是不是就浪费了呢,所以这里使用了坐标平移的办法,也就是说最小值case 12对应其下标0。

好了,明白了上面的对应关系后,如何得出case的最小值

0040D721 83 E9 0C             sub         ecx,0Ch

在这行汇编代码中,我们发现其case值减去了一个值,其实这个值就是最小值,也就是0xC(十进制12)。

下面再来看范围检查这一部分,对于范围检查,因为可知其case的最大值,因为对于范围检查而言肯定是对地址越界检查,那么将地址表的最大项加上我们上面得出的case最小值就是case最大值。

0040D727 83 7D FC 08          cmp         dword ptr [ebp-4],8
0040D72B 77 28                ja          $L858+0Fh (0040d755)
//这里使用的是无符号比较,所以当其值为负数也会不符合条件(很大的正数)而跳转

通过这里的汇编代码,我们可以得知其case的最大值为12 + 8 = 20。

好了,最后就是到了查表并跳转的部分了。这里的话我们主要解决一个问题,那就是当case值不存在时,数组表中填什么呢?如case 14就不存在,那么对于上图中的下标为三填写什么呢,这里我们需要看一下内存,补充一下上表即可得出结论

    

通过实践可以得出其地址表中空缺的地址会填写default的地址,如果没有default呢?那么其实就会填写switch end处的地址,这样子的就通过查表就可以满足当case值不存在时执行default了。

 

OK,下面该来说方案三了,对于方案三而言,其实算是对于上面的优化,对于引导部分,其多了一个步骤

引导部分
    表达式求值
    界限检查
    查索引部分-此步骤相比方案二是多出来的
    查表并转移

下面先来说说方案二的问题,通过上面的分析,我们可以知道在地址表中,对于不存在的case项会填写default的地址,那么假设在一组序列中,其不存在的case很多这么办?如下面的代码

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

	switch(argc)
    
        case 23:
            printf("hello1");
            break;
        case 89:
            printf("hello2");
            break;
        case 47:
            printf("hello3");
            break;
        case 72:
            printf("hello2");
            break;
        case 22:
            printf("hello3");
            break;
        case 56:
            printf("hello4");
            break;
    
    return 0;

我们来简单计算一下空间成本

数组项数* 4 = (89-23 + 1)*4 = 67 * 4 =  268

有什么办法可以节省一下空间么,那么此时需要多一张索引表即可解决问题,其索引表每一项占一字节,用于记录地址表的索引位置,相当于之前的直接查地址表,现在需要先查找索引表得到索引,然后在根据索引表查地址,算是一个以时间换空间的方案吧。

 大概的思想就和左边的表一样,这里左边的地址表与上面代码中的不符,因为正常情况,一般是case值有多少项,其地址表就会有多少项,注意default地址也会在里面(只有一份)。这样子很明显因为索引值只占一字节,所以其总大小肯定比上面小的。

下面再来看一下反汇编代码

9:        switch(argc)
10:       
//表达式求值
0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D721 83 E9 16             sub         ecx,16h  //这里可以得出case最小值为22
//界限检查
0040D724 89 4D FC             mov         dword ptr [ebp-4],ecx
0040D727 83 7D FC 43          cmp         dword ptr [ebp-4],43h //case最大值 22 + 0x43=89
0040D72B 77 6A                ja          $L864+0Dh (0040d797)
//查索引部分
0040D72D 8B 45 FC             mov         eax,dword ptr [ebp-4]
0040D730 33 D2                xor         edx,edx
0040D732 8A 90 C6 D7 40 00    mov         dl,byte ptr  (0040d7c6)[eax]  //在索引表取值,注意这里是一字节
//查表并转移
0040D738 FF 24 95 AA D7 40 00 jmp         dword ptr [edx*4+40D7AAh] //根据索引表值在到地址表中获取地址跳转
11:           case 23:
12:               printf("hello1");
0040D73F 68 24 20 42 00       push        offset string "case 10" (00422024)
0040D744 E8 27 39 FF FF       call        printf (00401070)
0040D749 83 C4 04             add         esp,4
13:               break;
0040D74C EB 49                jmp         $L864+0Dh (0040d797)
14:           case 89:
15:               printf("hello2");
0040D74E 68 BC 2F 42 00       push        offset string "argc == 0" (00422fbc)
0040D753 E8 18 39 FF FF       call        printf (00401070)
0040D758 83 C4 04             add         esp,4
16:               break;
0040D75B EB 3A                jmp         $L864+0Dh (0040d797)
//.....这里省略部分代码
26:           case 56:
27:               printf("hello4");
0040D78A 68 1C 20 42 00       push        offset string "case 3" (0042201c)
0040D78F E8 DC 38 FF FF       call        printf (00401070)
0040D794 83 C4 04             add         esp,4
28:               break;
29:       
30:       return 0;
0040D797 33 C0                xor         eax,eax

其原理就是中间使用了一字节的索引表进行间接查询地址,好了上面说了索引表的每一项都只占一字节,然而对于索引表还有一个就是最多只能存储256项,因此索引表只能存储256项索引编号,那么问题来了,当case最小值和case最大值的间隔大于255时,那么索引表就无法进行索引了。

 

所以,针对上面的问题,方案四就来了,方案四使用的是二叉平衡树加混合的方案。至于如何混,下面我们先来看一下例子

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

	switch (argc)
	
        case 3:
            printf("hello 3");
            break;
        case 58:
            printf("hello 7");
            break;
        case 153:
            printf("hello 3");
            break;
        case 250:
            printf("hello 5");
            break;
        case 256:
            printf("hello 3");
            break;
        case 300:
            printf("hello 5");
            break;
        case 317:
            printf("hello 3");
            break;
        default:
            printf("default");
            break;
	
	return 0;

在这个例子中,很明显其case最小和最大的差值已经255,所以此时使用平衡树的方案,下面我们对上面的数值来构建一颗平衡树

对于二叉平衡树而言,其查找速度也会快很多了,其查找规则就是如果比根节点的值小,那么往左边找,大的话就往右边找。我们下面来看一下反汇编代码是否是这样呢?

0040D718 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D71B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D71E 81 7D FC FA 00 00 00 cmp         dword ptr [ebp-4],0FAh //250
0040D725 7F 23                jg          main+4Ah (0040d74a) //大于走右边
0040D727 81 7D FC FA 00 00 00 cmp         dword ptr [ebp-4],0FAh
0040D72E 74 64                je          main+94h (0040d794) //根节点值
//下面是左子树部分
0040D730 83 7D FC 03          cmp         dword ptr [ebp-4],3     //3
0040D734 74 31                je          main+67h (0040d767)
0040D736 83 7D FC 3A          cmp         dword ptr [ebp-4],3Ah  //58
0040D73A 74 3A                je          main+76h (0040d776)
0040D73C 81 7D FC 99 00 00 00 cmp         dword ptr [ebp-4],99h  //158
0040D743 74 40                je          main+85h (0040d785)
0040D745 E9 86 00 00 00       jmp         main+0D0h (0040d7d0)  //都不符合到default
//下面是右子树部分
0040D74A 81 7D FC 00 01 00 00 cmp         dword ptr [ebp-4],100h   //256
0040D751 74 50                je          main+0A3h (0040d7a3)
0040D753 81 7D FC 2C 01 00 00 cmp         dword ptr [ebp-4],12Ch   //300
0040D75A 74 56                je          main+0B2h (0040d7b2)
0040D75C 81 7D FC 3D 01 00 00 cmp         dword ptr [ebp-4],13Dh  //317
0040D763 74 5C                je          main+0C1h (0040d7c1)
0040D765 EB 69                jmp         main+0D0h (0040d7d0)  //都不符合到default

看完上面的汇编代码,其实可以发现第一次是对了,查找的是根节点,大于则跳转到右子树,等于说明找到了,剩下的就左子树情况,对于左右子树而言,可以发现其是顺序查找的,按照我们上面的平衡二叉树,应该也是要先查找中值呀?

其实对于方案四来说,先是使用平衡二叉树的方案,使用完二叉树方案,可以发现其左右子树的节点并未超过三个,符合方案一(由编译器决定),所以左右子树使用的是方案一。

  此时如果再增加一个多个值,那么其可能又会由方案一转变为方案或者方案三。

至于如何选择方案就是编译器做的事,我们只需知道不管case情况如何复杂,其最终肯定在这四种方案间混合。对于方案四而言,在还原时候不用去关心jg或者gl,因为对于二叉树查找而言,要么比较大于,然后比较等于,剩下的情况肯定就是小于了,所以在比较时我们只需把重心放在je或者jne上,因为此时cmp的值可能就是对应的case值。

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

分支限界法C++ 学习&练习

分支限界法C++ 学习&练习

分支限界法C++ 学习&练习

分支界限

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

逆向-分支结构上