C++反汇编学习笔记1——选择判断语句

逆向第一篇,两年前写的,欢迎大家吐槽!

转载请注明出处。

1.流程控制语句的识别

If…else…语句比较简单,这里就不做详细说明。这里对switch语句和循环语句进行判断。

1.1 switch语句

1.1.1 case语句块不大于3条(模拟if…else…结构,也有可能不止3条,表示比较少)

此时,switch语句块反汇编的代码与if…else…的相差无几,只是switch语句将所有条件跳转都放到了一起并与case语句块分开,而if…else…则是每一条跳转语句后都跟着语句块,反汇编都比较简单,这里不再赘述。下面举出一个例子:

8:  switch(i){

0042B3F6 mov         eax,dword ptr [i] 

0042B3F9 mov         dword ptr[ebp-0D0h],eax 

//取出i的值

0042B3FF  cmp        dword ptr [ebp-0D0h],1  //和1比较

0042B406  je         test_switch_if+5Ch (42B41Ch)  //条件成立跳转到case1

0042B408  cmp        dword ptr [ebp-0D0h],3  //和3比较

0042B40F je          test_switch_if+6Bh(42B42Bh)  //条件成立跳转到case3

0042B411  cmp        dword ptr [ebp-0D0h],64h  //和100比较

0042B418  je         test_switch_if+7Ah (42B43Ah)  //条件成立跳转到case100

0042B41A jmp         test_switch_if+87h(42B447h)  //否则跳转至switch结束处

     9:   case 1:

    10:      printf("i == 1");

0042B41C push        offset string "i== 1" (47DC80h) 

0042B421  call       @ILT+4100(_printf) (42A009h) 

0042B426  add        esp,4 

//调用printf函数

11:       break;

//跳转至switch结束处,下面两个结构相同

0042B429  jmp        test_switch_if+87h (42B447h) 

    12:   case 3:

    13:      printf("i == 3");

0042B42B  push       offset string "i == 3" (47DC78h) 

0042B430  call       @ILT+4100(_printf) (42A009h) 

0042B435  add        esp,4 

    14:      break;

0042B438  jmp        test_switch_if+87h (42B447h) 

    15:   case 100:

    16:      printf("i == 100");

0042B43A push        offset string "i== 100" (47DC6Ch) 

0042B43F call        @ILT+4100(_printf) (42A009h) 

0042B444  add        esp,4 

    17:      break;

    18:   }

 

1.1.2 有序线性表的switch

当case的最大值和最小值之间差距较小时使用这种方法优化,一半差距不会大于10(VC++6.0似乎是不大于7,下面会看到和VS2010编译器的区别)。

直接看例子:

26: switch(nIndex)

0042B4C6 mov         eax,dword ptr[nIndex] 

0042B4C9 mov         dword ptr[ebp-0D0h],eax 

0042B4CF  mov        ecx,dword ptr [ebp-0D0h]

//到此为止的三行是取出i的值

0042B4D5  sub        ecx,1 

//这里的减1则是为了与线性表的对齐,线性表第一个为0

0042B4D8  mov        dword ptr [ebp-0D0h],ecx 

0042B4DE  cmp        dword ptr [ebp-0D0h],6 

//这里和6比较是因为case最大值为7,减1后为6,若超过最大值则直接跳出语句或跳向default

0042B4E5  ja         $LN1+0Dh (42B54Ch)

0042B4E7  mov        edx,dword ptr [ebp-0D0h] 

0042B4ED  jmp        dword ptr  (42B59Ch)[edx*4] 

//决定跳向哪个case

    27:   {

    28:   case 1:

    29:      printf("nIndex == 1");

0042B4F4 push        offset string"nIndex == 1" (47DCECh) 

0042B4F9 call        @ILT+4100(_printf) (42A009h) 

0042B4FE  add        esp,4

//Printf函数的调用

    30:      break;

0042B501  jmp        $LN1+0Dh (42B54Ch)

//跳向结束或是default语句块,以下的case都类似

    31:   case 2:

    32:      printf("nIndex == 2");

0042B503  push       offset string "nIndex == 2" (47DCDCh) 

0042B508  call       @ILT+4100(_printf) (42A009h) 

0042B50D  add        esp,4 

    33:      break;

0042B510  jmp        $LN1+0Dh (42B54Ch) 

    34:   case 3:

    35:      printf("nIndex == 3");

0042B512  push       offset string "nIndex == 3" (47DCCCh) 

0042B517  call       @ILT+4100(_printf) (42A009h) 

0042B51C add         esp,4 

    36:      break;

0042B51F jmp         $LN1+0Dh(42B54Ch) 

    37:   case 5:

    38:      printf("nIndex == 5");

0042B521  push       offset string "nIndex == 5" (47DCBCh) 

0042B526  call       @ILT+4100(_printf) (42A009h) 

0042B52B  add        esp,4 

    39:      break;

0042B52E  jmp        $LN1+0Dh (42B54Ch) 

    40:   case 6:

    41:      printf("nIndex == 6");

0042B530  push       offset string "nIndex == 6" (47DCACh) 

0042B535  call       @ILT+4100(_printf) (42A009h) 

0042B53A add         esp,4 

    42:      break;

0042B53D  jmp        $LN1+0Dh (42B54Ch) 

    43:   case 7:

    44:      printf("nIndex == 7");

0042B53F push        offset string"nIndex == 7" (47DC9Ch) 

0042B544  call       @ILT+4100(_printf) (42A009h) 

0042B549  add        esp,4 

    45:      break;

    46:   }

在这种情况下编译器会生成一张地址表来帮助寻找case,如下所示:

0x0042B59C  f4 b4 42 00

0x0042B5A0  03 b5 42 00

0x0042B5A4  12 b5 42 00

0x0042B5A8 4c b5 42 00

0x0042B5AC  21 b5 42 00

0x0042B5B0  30b5 42 00

0x0042B5B4  3f b5 42 00

这里的每个元素代表着一个case的地址,例如f4 b4 4200地址是0x0042b4f4,再看看书上面case1的地址发现是一样的,依次类推。你还会看到那些没有的元素,就是第四个元素4c b5 42 00,这里就把没有的case都定为default或是switch的结束地址。

以上是debug版本的程序反汇编的结果,release版本的差不多,就是在每个case的最后一句有所不同,因为这个程序在switch语句后就直接返回了且每个case都以break结尾所以编译器决定减少一个跳转,直接在case结束后返回以节约时间提高效率。

1.1.3 难以构成线性表的switch

当两个case值相差较大采取上面的方法会浪费许多空间,而采取以下索引表的结构则不会浪费太多。下面是一个简单的例子:

switch(i){

0042B5F6 mov         eax,dword ptr [i] 

0042B5F9 mov         dword ptr[ebp-0D0h],eax 

0042B5FF  mov        ecx,dword ptr [ebp-0D0h] 

//到此为止的三行是取出i的值

0042B605  sub        ecx,1

//这里的减1则是为了与线性表的对齐,线性表第一个为0

0042B608  mov        dword ptr [ebp-0D0h],ecx 

0042B60E  cmp        dword ptr [ebp-0D0h],18h 

0042B615  ja         $LN1+0Dh (42B692h)

//若超过最大值则直接跳转到switch结束处

0042B617  mov        edx,dword ptr [ebp-0D0h] 

0042B61D  movzx      eax,byte ptr  (42B6F0h)[edx] 

0042B624  jmp        dword ptr  (42B6D0h)[eax*4]  //根据输入的元素决定跳转到哪一个case

    56:   case 1:

    57:      printf("i == 1");

0042B62B  push       offset string "i == 1" (47DC80h) 

0042B630  call       @ILT+4100(_printf) (42A009h) 

0042B635  add        esp,4 

//以上是调用printf函数

    58:      break;

0042B638  jmp        $LN1+0Dh (42B692h) 

//直接跳到switch结束处

//下面的每个case都是一样的结构

    59:   case 2:

    60:      printf("i == 2");

0042B63A push        offset string "i== 2" (47DD1Ch) 

0042B63F call        @ILT+4100(_printf) (42A009h) 

0042B644  add        esp,4 

    61:      break;

0042B647  jmp        $LN1+0Dh (42B692h) 

    62:   case 3:

    63:      printf("i == 3");

0042B649  push       offset string "i == 3" (47DC78h) 

0042B64E  call       @ILT+4100(_printf) (42A009h) 

0042B653  add        esp,4 

    64:      break;

0042B656  jmp        $LN1+0Dh (42B692h) 

    65:   case 5:

    66:      printf("i == 5");

0042B658  push       offset string "i == 5" (47DD10h) 

0042B65D  call       @ILT+4100(_printf) (42A009h) 

0042B662  add        esp,4 

    67:      break;

0042B665  jmp        $LN1+0Dh (42B692h) 

    68:   case 6:

    69:      printf("i == 6");

0042B667  push       offset string "i == 6" (47DD08h) 

0042B66C call        @ILT+4100(_printf) (42A009h) 

0042B671  add        esp,4 

    70:      break;

0042B674  jmp        $LN1+0Dh (42B692h) 

    71:   case 7:

    72:      printf("i == 7 ");

0042B676  push       offset string "i == 7 " (47DD00h) 

0042B67B  call       @ILT+4100(_printf) (42A009h) 

0042B680  add        esp,4 

    73:      break;

0042B683  jmp        $LN1+0Dh (42B692h) 

    74:   case 25:

    75:      printf("i == 25");

0042B685  push       offset string "i == 255" (47DDF0h) 

0042B68A call        @ILT+4100(_printf) (42A009h) 

0042B68F add         esp,4 

    76:      break;

这里最关键的是这一句:

0042B624 jmp         dword ptr  (42B6D0h)[eax*4]

也就是选择跳转到哪一个case的语句。在这种情况下,编译器会为switch建立两张表,一张是前面类似的地址表,一张则是索引表。下面来看一下具体内容是什么:

0x0042B6D0  2bb6 42 00

0x0042B6D4  3a b6 42 00

0x0042B6D8  49b6 42 00

0x0042B6DC  58b6 42 00

0x0042B6E0  67b6 42 00

0x0042B6E4  76b6 42 00

0x0042B6E8  85b6 42 00

0x0042B6EC  92b6 42 00

上面的这个就是case地址表,里面存着每个case对应的地址。如第一个2b b6 42 00,它的地址就是0x0042b62b,这正是case 1所在的地址,这就是为什么前面的代码要将i-1了,这就是地址表的第0号元素。后面的就很简单了,如果是3那么找到线性表的第三个元素内容49 b6 42 00,对应地址为0x0042b649,这就是case 3的地址。但是这里的没有case 4,所以第四个元素的地址就是default或是switch的结束地址。但是要如何找到地址表呢?这里通过一个索引表来找到:

0x0042B6F0  00 01 02 07 03 04 05 07 07 07 07 07 07 07 07 07 07 07 0707 07 07 07 07 06

说是索引不如说是地址表的下标。如果i=3,减去1之后是2,找到索引表中下标为2的内容是02,再由02去地址表中找到地址49 b6 42 00即0x0042b649,这就是要跳转的地址,接下去就很简单了,不必多说。你还会看到那些没有的索引,比如10,这里就把没有的case索引都定为地址表中的最后一个元素,这就是default或是switch的结束地址。

这里总共有25个元素,共占资源7*4+25=53个字节,若是全部按照地址表来存储则需25*4=100个字节。这样存储明显就减少了很多的存储空间。当然这也并非全是好处,由于索引表中只用一个字节存储索引,故最大只能有256个索引,比这还大的需要下面一种方法优化。

1.1.4降低判定树的高度

还是和上面一样先看例子:

85:   switch(i){

0042B746  mov        eax,dword ptr [i] 

0042B749  mov        dword ptr [ebp-0D0h],eax 

//取出i的值

0042B74F cmp         dword ptr[ebp-0D0h],23h 

0042B756  jg         test_switch_tree+7Eh (42B78Eh) 

//和35比较,大于则跳转至42B78Eh

0042B758  cmp        dword ptr [ebp-0D0h],23h 

0042B75F je          $LN6+0Fh(42B7EDh) 

//等于35的跳转至case35处

0042B765  mov        ecx,dword ptr [ebp-0D0h] 

0042B76B  sub        ecx,2 

//case值与地址表中的元素对齐,所以要减2

0042B76E  mov        dword ptr [ebp-0D0h],ecx 

0042B774  cmp        dword ptr [ebp-0D0h],8 

0042B77B  ja         $LN6+4Bh (42B829h) 

//大于10(-2后大于8就相当于大于10)则跳转至default处

0042B781  mov        edx,dword ptr [ebp-0D0h] 

0042B787  jmp        dword ptr  (42B874h)[edx*4]

在小于10的case中利用前面提到过的线性表进行优化,此处即为线性表的寻址

0042B78E  cmp        dword ptr [ebp-0D0h],25h 

0042B795  je         $LN6+1Eh (42B7FCh) 

//和37比较,相等则跳至case37处

0042B797  cmp        dword ptr [ebp-0D0h],29Ah 

0042B7A1 je          $LN6+2Dh (42B80Bh)

//和666比较,相等则跳至case666处

0042B7A3 cmp         dword ptr[ebp-0D0h],2710h 

0042B7AD  je         $LN6+3Ch (42B81Ah) 

//和10000比较,相等则跳至case10000处

0042B7AF  jmp        $LN6+4Bh (42B829h) 

//跳至default处

    86:   case 2:

    87:      printf("i == 2\n");

0042B7B1  push       offset string "i == 2\n" (47DCF8h) 

0042B7B6  call       @ILT+4100(_printf) (42A009h) 

0042B7BB  add        esp,4 

    88:      break;

0042B7BE  jmp        $LN6+58h (42B836h) 

    89:   case 3:

    90:      printf("i == 3\n");

0042B7C0 push        offset string "i== 3\n" (47DC90h) 

0042B7C5 call        @ILT+4100(_printf) (42A009h) 

0042B7CA  add        esp,4 

    91:      break;

0042B7CD  jmp        $LN6+58h (42B836h) 

    92:   case 8:

    93:      printf("i == 8\n");

0042B7CF  push       offset string "i == 8\n" (47DD74h) 

0042B7D4  call       @ILT+4100(_printf) (42A009h) 

0042B7D9  add        esp,4 

    94:      break;

0042B7DC  jmp        $LN6+58h (42B836h) 

    95:   case 10:

    96:      printf("i == 10\n");

0042B7DE  push       offset string "i == 10\n" (47DD68h) 

0042B7E3  call       @ILT+4100(_printf) (42A009h) 

0042B7E8  add        esp,4 

    97:      break;

0042B7EB  jmp         $LN6+58h (42B836h) 

    98:   case 35:

    99:      printf("i == 35\n");

0042B7ED  push       offset string "i == 35\n" (47DD5Ch) 

0042B7F2 call        @ILT+4100(_printf) (42A009h) 

0042B7F7 add         esp,4 

   100:      break;

0042B7FA  jmp        $LN6+58h (42B836h) 

   101:   case 37:

   102:      printf("i == 37\n");

0042B7FC  push       offset string "i == 37\n" (47DD50h) 

0042B801  call       @ILT+4100(_printf) (42A009h) 

0042B806  add        esp,4 

   103:      break;

0042B809  jmp        $LN6+58h (42B836h) 

   104:   case 666:

   105:      printf("i == 666\n");

0042B80B  push       offset string "i == 666\n" (47DD44h) 

0042B810  call       @ILT+4100(_printf) (42A009h) 

0042B815  add        esp,4 

   106:      break;

0042B818  jmp        $LN6+58h (42B836h) 

   107:   case 10000:

   108:      printf("i == 10000\n");

0042B81A push        offset string "i== 10000\n" (47DD38h) 

0042B81F call        @ILT+4100(_printf) (42A009h) 

0042B824  add        esp,4 

   109:      break;

0042B827  jmp        $LN6+58h (42B836h) 

   110:   default:

   111:      printf("default\n");

0042B829  push       offset string "default\n" (47DD2Ch) 

0042B82E  call       @ILT+4100(_printf) (42A009h) 

0042B833  add        esp,4 

   112:      break;

在这里,由于case值相差过大,因此采取类似于二叉树的结构来进行查找。首先判断是否大于35,之后分成两部分进行判断,小于35的由于比较接近因此采用线性查找法,而大于35的部分则采用if...else...模拟法即使用第一种优化方式进行优化。但是在我所购买的书中是使用VC++6.0编译器,所以它的代码反汇编出的结果看出无论是大于35还是小于35的部分都使用了if...else...模拟法进行优化。

以上是用Visual Studio 2010反汇编器对Deubg版本进行的反汇编,下面可以利用IDA Pro反汇编Release版本并查看其结构,具体如下:

C++反汇编学习笔记1——选择判断语句_第1张图片

这是简单的结构图。

C++反汇编学习笔记1——选择判断语句_第2张图片

放大之后可以看到里面都有代码,利用这个分析将会事半功倍。

以上是其主要的结构流程,可以看到和二叉树的形状非常类似, Release版本的具体代码和Debug版本的差不多,具体还是像之前那样分析就可以了。


你可能感兴趣的:(C++逆向,逆向,汇编,黑客,c++)