逆向-分支结构上
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]营救皮卡丘 有上下界费用流