C++反汇编与逆向分析技术揭秘(第2版)
上QQ阅读APP看书,第一时间看更新

5.1 if语句

if语句是分支结构的重要组成部分。if语句的功能是先对运算条件进行比较,然后根据比较结果执行对应的语句块。if语句只能判断两种情况:“0”为假值,“非0”为真值。如果判断结果为真值,则进入语句块内执行语句;如果判断为假值,则跳过if语句块,继续运行程序的其他语句。需要注意的是,if语句转换的条件跳转指令与if语句的判断结果是相反的。我们以代码清单5-1为例,逐步展开对if语句的分析。

代码清单5-1 if语句构成(Debug版)

// C++ 源码
#include <stdio.h>
int main(int argc, char* argv[]) {
  if (argc == 0) {
    printf("argc == 0");
  }
  return 0;
}

//x86_vs对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  cmp     dword ptr [ebp+8], 0
00401007  jnz     short loc_401016             ;如果argc!=0,则跳转到if结束块代码
{
00401009  push    offset aArgc0
0040100E  call    printf
00401013  add     esp, 4                       ;if语句块代码
}
00401016 loc_401016:
00401016  xor     eax, eax                     ;if结束块代码
00401018  pop     ebp
00401019  retn

//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略

//x64_vs对应汇编代码讲解
0000000140001000  mov     [rsp+10h], rdx
0000000140001005  mov     [rsp+8], ecx
0000000140001009  sub     rsp, 28h
000000014000100D  cmp     dword ptr [rsp+30h], 0
0000000140001012  jnz     short loc_140001020  ;如果argc!=0,则跳转到if结束块代码
{
0000000140001014  lea     rcx, aArgc0
000000014000101B  call    sub_140001090        ;if语句块代码
}
0000000140001020  xor     eax, eax             ;if结束块代码
0000000140001022  add     rsp, 28h
0000000140001026  retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略

代码清单5-1中if的比较条件为“argc == 0”,如果成立,即为真值,则进入if语句块内执行语句。但是,转换后的汇编代码使用的条件跳转指令JNE判断结果为“不等于0跳转”,这是为什么呢?按照if语句的规定,满足if判定的表达式才能执行if的语句块,而汇编语言的条件跳转却是满足某条件则跳转,绕过某些代码块,这一点与C语言是相反的。

既然这样,那为什么C语言编译器不将else语句块提到前面并把if语句块放到后面呢?这样汇编语言和C语言中的判定条件不就一致了吗?

因为C语言是根据代码行的位置决定编译后二进制代码地址高低的,也就是说,低行数对应低地址,高行数对应高地址,所以有时会使用标号相减得到代码段的长度。鉴于此,C语言的编译器不能随意改变代码行在内存中的顺序。

根据这一特性,如果将if语句中的比较条件“argc == 0”修改为“if(argc > 0)”,则其对应的汇编语言使用的条件跳转指令会是“小于等于0”。

代码清单5-2 if语句大于0比较(Debug版)

// C++ 源码
#include <stdio.h>
int main(int argc, char* argv[]) {
  if (argc > 0){
    printf("%d\n", argc);
  }
  return 0;
}

//x86_vs对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  cmp     dword ptr [ebp+8], 0
00401007  jle     short loc_40101A             ;如果argc<=0,则跳转到if结束代码块
{
00401009  mov     eax, [ebp+8]
0040100C  push    eax
0040100D  push    offset unk_412160
00401012  call    sub_401060
00401017  add     esp, 8                       ;if语句块代码
}
0040101A loc_40101A:                           ;if结束代码块
0040101A  xor     eax, eax
0040101C  pop     ebp
0040101D  retn

//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略

//x64_vs对应汇编代码讲解
0000000140001000  mov     [rsp+10h], rdx
0000000140001005  mov     [rsp+8], ecx
0000000140001009  sub     rsp, 28h
000000014000100D  cmp     dword ptr [rsp+30h], 0
0000000140001012  jle     short loc_140001024  ;如果argc<=0,则跳转到if结束代码块
{
0000000140001014  mov     edx, [rsp+30h]
0000000140001018  lea     rcx, unk_1400122C0
000000014000101F  call    sub_140001090        ;if语句块代码
}
0000000140001024  xor     eax, eax             ;if结束代码块
0000000140001026  add     rsp, 28h
000000014000102A  retn

//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略

分析代码清单5-1和代码清单5-2,可以总结出if语句的转换规则:在转换成汇编代码后,由于当if比较结果为假时,需要跳过if语句块内的代码,因此使用了相反的条件跳转指令。

将4.2.2节的代码清单4-21与代码清单5-2进行对比分析可知,代码结构特征十分相似,但使用的条件跳转指令不同。由此可见,在反汇编时,表达式短路和if语句这两种分支结构的实现过程都是一样的,很难在源码中对它们进行区分。

总结

如果遇到以上指令序列,可高度怀疑它是一个由if语句组成的单分支结构,根据比较信息与条件跳转指令,找到与跳转条件相反的逻辑,即可恢复分支结构原型。由于循环结构也会出现类似代码,因此在分析过程中还需要结合上下文。