C语言代码逆向基础--分支结构

C语言代码逆向基础–分支结构

C语言中有两种分支结构,if…else…结构和switch…case…default…结构

if/else结构分支

写一个简单的C语言例子,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>

int main()
{
int a=0,b=1,c=2;
if(a>b)
{
printf("%d \r\n",a);
}
else if(b<=c)
{
printf("%d \r\n",b);
}
else
{
printf("%d \r\n",c);
}

return 0;
}

使用VC对其进行编译,使用IDA打开DeBUG文件夹下的可执行文件,IDA自动识别main函数。

我们来看关键的反汇编代码,如下:

1
2
3
.text:00401028                 mov     [ebp+var_4], 0
.text:0040102F mov [ebp+var_8], 1
.text:00401036 mov [ebp+var_C], 2

以上三行反汇编代码为对定义变量的初始化,其余反汇编代码可以分为三部分来观察,前两段代码都有cmp/jxx/printf /jmp这些指令特征,这就是if…else…的特征所在。

观察IDA绘制的反汇编流程图

在反汇编中,C语言代码中”>”对应的是jle(小于等于则跳转),”<=”在反汇编中对应的是jg(大于则跳转),反汇编代码与C语言中比较是相反的。只有当jxx指令没有发生跳转,才执行jxx到jmp之间的指令,否则会进行跳转。

简单阐述一下上图:

C语言的if判断反汇编代码,如下

1
2
3
4
5
6
7
jle     short loc_401058
.text:00401045 mov ecx, [ebp+var_4]
.text:00401048 push ecx
.text:00401049 push offset Format ; "%d \r\n"
.text:0040104E call _printf
.text:00401053 add esp, 8
.text:00401056 jmp short loc_401084

这里如果jle(小于等于)符合条件,则跳转到loc_401058,执行else if判断语句,若不符合则执行jle到jmp之间的指令,并最后跳转到401084地址,代表这判断结束,程序结束。

else if判断反汇编代码,如下

1
2
3
4
5
6
7
.text:0040105E                 jg      short loc_401073
.text:00401060 mov eax, [ebp+var_8]
.text:00401063 push eax
.text:00401064 push offset Format ; "%d \r\n"
.text:00401069 call _printf
.text:0040106E add esp, 8
.text:00401071 jmp short loc_401084

如果jg(大于)符合条件,则跳转到401073地址处执行else语句,否则就执行其语句,最后依然跳转到401084地址,退出程序。

结构小结

if…else…结构与反汇编代码的对应结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; 初始化变量
mov xxxx,xxxx
mov xxxx,xxxx


_if:
;比较跳转
cmp xxxx,xxxx
jxx _else_if
;一系列指令(不符合执行一系列指令,符合即调转else_if)
......
jmp _if_else结束位置
_else_if:
; 比较跳转
mov xxx,xxx
cmp xxx,xxx
jxx _else(不符合执行一系列指令,符合即调转_else)
.....
jmp _if_else结束位置
_else:
;一系列指令
......

switch结构分支

编写个简单的Switch结构分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<stdio.h>

int main()
{
int nNum=0;
scanf("%d",&nNum);
switch(nNum)
{
case 1:
{
printf("1 \r\n");
break;
}
case 2:
{
printf("2 \r\n");
break;
}
case 3:
{
printf("3 \r\n");
break;
}
case 4:
{
printf("4 \r\n");
break;
}
default:
{
printf("default \r\n");
break;
}
}
return 0;
}

直接来看下关键代码,如下

Switch流程分支图如下

观察可以得到左边的四个分支为case分支,右边那个为default部分。

我们先来看下scanf()函数部分反汇编代码

1
2
3
4
5
6
.text:00401028                 mov     [ebp+var_4], 0
.text:0040102F lea eax, [ebp+var_4]
.text:00401032 push eax
.text:00401033 push offset aD ; "%d"
.text:00401038 call _scanf
.text:0040103D add esp, 8

这里的var_4就是nNum变量,首先使用mov为nNum变量赋值,然后将其地址送入ecx寄存器,然后通过调用scanf()函数,nNum接受用户输入,函数调用完后将esp恢复到调用前

接受用户输入后,就进入了Switch()分支的部分,反汇编代码如下:

1
2
3
4
5
6
7
8
9
.text:00401040                 mov     ecx, [ebp+var_4]
.text:00401043 mov [ebp+var_8], ecx
.text:00401046 mov edx, [ebp+var_8]
.text:00401049 sub edx, 1 ; switch 4 cases
.text:0040104C mov [ebp+var_8], edx
.text:0040104F cmp [ebp+var_8], 3
.text:00401053 ja short def_401058 ; jumptable 00401058 default case
.text:00401055 mov eax, [ebp+var_8]
.text:00401058 jmp ds:jpt_401058[eax*4] ; switch jump

从00401040到00401053地址之间的代码是为了对nNum变量进行判断,若nNum-1的值大于3,则跳转到401058地址(default语句),如不符合则执行Case语句。这就是流程图被分为两个部分的原因。

接下来我们来看看要执行的case分支,代码如下:

1
2
.text:00401055                 mov     eax, [ebp+var_8]
.text:00401058 jmp ds:jpt_401058[eax*4] ; switch jump

将var_8的值传递给eax寄存器,然后跳转jpt_401058[eax* 4],像是一个数组,数组下标由eax寄存器进行寻址。

jpt_401058处的内容如下:

1
2
3
4
5
04010BB jpt_401058      dd offset loc_40105F    ; DATA XREF: _main_0+48↑r
.text:004010BB dd offset loc_40106E ; jump table for switch statement
.text:004010BB dd offset loc_40107D
.text:004010BB dd offset loc_40108C
.text:004010CB align 40h

其内容为4个连续的标号地址,为40105F、40106E、40107D、40108C,分别对应4个Case的代码,通过var_8中对应的值进行访问。

结构小结

Switch结构分支通过一次比较就可分为case和default两个流程,在case分支中,通过将传入变量的值为跳转数组下标来访问对应case子分支的执行语句。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信