C语言选择结构与循环结构逆向分析

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Wang_1997/article/details/78671484

首先,得先会正向开发的选择和循环结构,不会正向,如何逆向呢?

收藏一句话:开发的水平决定了你逆向的高度,en....很有道理

编译器:VC++ 6.0

 

学习之前需要先了解几个汇编指令:

JMP 指令:

指令格式:MOV EIP,寄存器/立即数   简写为   JMP 寄存器/立即数

无条件跳转,修改EIP的值。

CMP指令:

指令格式:CMP  R/M,R/M/IMM

该指令是比较两个操作数,实际上,它相当于SUB指令,但是相减的结构并不保存到第一个操作数中。

只是根据相减的结果来改变零标志位的,当两个操作数相等的时候,零标志位置1。

JCC条件跳转指令:

这些指令不必去记哦,因为一般看到英语缩写大概就能猜出来了:

J : JMP  跳转

E : Equal 相等

N : Not

Z : 零标志ZF(Zero Flag)                    零标志ZF用来反映运算结果是否为0

S : 符号标志SF(Sign Flag)                符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同

P: 奇偶标志PF(Parity Flag)              奇偶标志PF用于反映运算结果中“1”的个数的奇偶性

O: 溢出标志OF(Overflow Flag)         溢出标志OF用于反映有符号数加减运算所得结果是否溢出

B: Below 低于

A: Above 超过

L: Lesser 小的

G: Greater 大的

选择结构

    if选择结构

IF...ELSE...语句的反汇编判断:

首先,给出如下逆向代码我们来分析:

 

00401108 8B 45 08             mov         eax,dword ptr [ebp+8]
0040110B 3B 45 0C             cmp         eax,dword ptr [ebp+0Ch]
0040110E 7D 0B                jge         Function+2Bh (0040111b)
00401110 8B 4D 0C             mov         ecx,dword ptr [ebp+0Ch]
00401113 89 0D 64 7C 42 00    mov         dword ptr [res (00427c64)],ecx
00401119 EB 09                jmp         Function+34h (00401124)
0040111B 8B 55 08             mov         edx,dword ptr [ebp+8]
0040111E 89 15 64 7C 42 00    mov         dword ptr [res (00427c64)],edx
00401124 5F                   pop         edi

 

 

首先看第一行和第二行汇编代码,可以分析该函数具有两个参数,存在ebp+8(假设变量a)和ebp+0xC(假设变量b)中。第三行代码对俩参数进行比较(cmp),更改标志位。

第四行代码jge(大于等于)根据标志位进行跳转。如果不跳转,继续往下执行但会碰到第9行的jmp跳转到了第9行代码;如果跳转,可以看出来他跳转到了第7行代码然后往下执行;

那么很明显可以分析出来,如果a大于等于b,则会执行第7,8俩行代码,否者执行4,5俩行代码。

7,8行代码的意思就是把变量a放入全局变量res中;4,5行代码的意思就是把变量b放入全局变量res中。

由于逆向代码中的选择判断逻辑都是与正向反着来的,所以我们可以得出如下正向代码:

 

int res = 0;
void Function(int a,int b) {
	if(a < b) {
		res = b;
	} else {
		res = a;
	}
}

汇编代码格式如下:

 

 

IF_BEGIN:			
	先执行各类影响标志位的指令		
	jxx ELSE_BEGIN		
	......		
IF_END:			
	jmp END		
	ELSE_BEGIN:		
	......		
	ELSE_END:		
END:			

特点分析:

 

  1. 如果不跳转,那么会执行到jmp处,jmp直接跳转到END处
  2. 如果跳转,则会直接跳过jmp END处的代码,直接执行后面的代码

总结:

第一个jxx跳转的地址前面有一个jmp ,可以判断是if...else...语句

IF...ELSE IF...ELSE IF..多分支语句的反汇编判断:

如果懂了上面的单分支判断,那么这个就简单了,此处不做记录了,直接给出如下总结:

 

    switch选择结构

在开发中,switch语句还是很常见的,因为在某些情况下,switch的执行效率是比if语句快的多的。[某些情况]是哪些情况呢?下面我们来分析一下

 

首先我们看如下代码:

 

	switch(a) {
		case 1:
			printf("%d\n",1);
			break;
		case 2:
			printf("%d\n",2);
			break;
		default:
			printf("sorry");
			break;
	}

再观察其反汇编代码:

 

 

0040D578 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D57B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D57E 83 7D FC 01          cmp         dword ptr [ebp-4],1
0040D582 74 08                je          Function+2Ch (0040d58c)
0040D584 83 7D FC 02          cmp         dword ptr [ebp-4],2
0040D588 74 13                je          Function+3Dh (0040d59d)
0040D58A EB 22                jmp         Function+4Eh (0040d5ae)
0040D58C 6A 01                push        1
0040D58E 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D593 E8 58 02 00 00       call        printf (0040d7f0)
0040D598 83 C4 08             add         esp,8
0040D59B EB 1E                jmp         Function+5Bh (0040d5bb)
0040D59D 6A 02                push        2
0040D59F 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D5A4 E8 47 02 00 00       call        printf (0040d7f0)
0040D5A9 83 C4 08             add         esp,8
0040D5AC EB 0D                jmp         Function+5Bh (0040d5bb)
0040D5AE 68 6C 2F 42 00       push        offset string "sorry" (00422f6c)
0040D5B3 E8 38 02 00 00       call        printf (0040d7f0)

很明显,这个写法和if else 的写法没有啥区别。那么我们多添加几个case呢?代码如下:

 

 

	switch(a) {
		case 101:
			printf("%d\n",101);
			break;
		case 102:
			printf("%d\n",102);
			break;
		case 103:
			printf("%d\n",103);
			break;
		case 104:
			printf("%d\n",104);
			break;
		default:
			printf("sorry");
			break;
	}

查看其反汇编代码:

 

 

49:       switch(a) {
0040D888 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D88B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D88E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D891 83 E9 65             sub         ecx,65h
0040D894 89 4D FC             mov         dword ptr [ebp-4],ecx
0040D897 83 7D FC 03          cmp         dword ptr [ebp-4],3
0040D89B 77 4E                ja          $L593+11h (0040d8eb)
0040D89D 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D8A0 FF 24 95 09 D9 40 00 jmp         dword ptr [edx*4+40D909h]
50:           case 101:
51:               printf("%d\n",101);
0040D8A7 6A 65                push        65h
0040D8A9 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D8AE E8 3D FF FF FF       call        printf (0040d7f0)
0040D8B3 83 C4 08             add         esp,8
52:               break;
0040D8B6 EB 40                jmp         $L593+1Eh (0040d8f8)
53:           case 102:
54:               printf("%d\n",102);
0040D8B8 6A 66                push        66h
0040D8BA 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D8BF E8 2C FF FF FF       call        printf (0040d7f0)
0040D8C4 83 C4 08             add         esp,8
55:               break;
0040D8C7 EB 2F                jmp         $L593+1Eh (0040d8f8)
.......

观察其第2行与第10行之间的这些代码,很明显与上面的代码不一样了,重点我们来分析下这2到10行代码。

 

第2,3,4行代码一起讲,这三行的代码意思是将函数参数(ebp+8)放入堆栈(ebp-4)中,最后放入寄存器ecx中。

然后第5行代码意思是 减去(sub)0x65,问题来了,0x65是什么呢?将其转化十进制可以发现正好是 101,而101正好是我们这几个连续数中最小的一个。

第6,7,8行代码的意思是将其减去101的值重新放入堆栈中,并将其值与3进行比较,如果比3大就跳转到地址0x0040d8eb。

为什么是与3比较呢?看我们的正向代码,我们case的选项只有 101,102,103,104,而104-103后等于3,那么大于3跳转很明显就是跳转到default的地方(由于汇编代码过程,上述代码未给出)。

如果不大于3呢,那么意思就是说可以在case中找到喽,我们来看看汇编中如何找的呢?

第9行代码将堆栈中的值(上面减掉的值)取出放入edx中,第10行代码是重点哦,它jmp到了[edx*4+40D909h],那么结果是什么呢?我们使用内存窗口查看一下。

在内存窗口我们可以发现,它生成了一张大表,可以说是地址跳转表,重新推理一下,假设是101,那么减去0x65后,edx其值就是0,0+0x40D909也就是0x40D909,查看内存窗口的其值为 0x0040D8A7,所以他就跳转到了上面图片中51行代码的下面位置。

所以说,由于有了这张大表,其执行的效率肯定会快很多的。

既然上面说有大表,那么难道还有小表?对滴,看如下代码

 

	switch(a) {
		case 101:
			printf("%d\n",101);
			break;
		case 102:
			printf("%d\n",102);
			break;
		case 109:
			printf("%d\n",109);
			break;
		case 110:
			printf("%d\n",110);
			break;
		default:
			printf("sorry");
			break;
	}

我将其中连续的一段删除,case匹配只剩下 101,102,109,110。然后查看其汇编代码

 

 

49:       switch(a) {
0040D888 8B 45 08             mov         eax,dword ptr [ebp+8]
0040D88B 89 45 FC             mov         dword ptr [ebp-4],eax
0040D88E 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D891 83 E9 65             sub         ecx,65h
0040D894 89 4D FC             mov         dword ptr [ebp-4],ecx
0040D897 83 7D FC 09          cmp         dword ptr [ebp-4],9
0040D89B 77 56                ja          $L593+11h (0040d8f3)
0040D89D 8B 45 FC             mov         eax,dword ptr [ebp-4]
0040D8A0 33 D2                xor         edx,edx
0040D8A2 8A 90 25 D9 40 00    mov         dl,byte ptr  (0040d925)[eax]
0040D8A8 FF 24 95 11 D9 40 00 jmp         dword ptr [edx*4+40D911h]
50:           case 101:
51:               printf("%d\n",101);
0040D8AF 6A 65                push        65h
0040D8B1 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D8B6 E8 35 FF FF FF       call        printf (0040d7f0)
0040D8BB 83 C4 08             add         esp,8
52:               break;
0040D8BE EB 40                jmp         $L593+1Eh (0040d900)
53:           case 102:
54:               printf("%d\n",102);
0040D8C0 6A 66                push        66h
0040D8C2 68 1C 20 42 00       push        offset string "%d\n" (0042201c)
0040D8C7 E8 24 FF FF FF       call        printf (0040d7f0)
0040D8CC 83 C4 08             add         esp,8
55:               break;
0040D8CF EB 2F                jmp         $L593+1Eh (0040d900)
56:           case 109:
57:               printf("%d\n",109);
0040D8D1 6A 6D                push        6Dh
0040D8D3 68 1C 20 42 00       push        offset string "%d\n" (0042201c)

相同的地方就不多说了,主要看第9~12行代码。

 

第9行代码,eax寄存器里面存储的是sub 0x65(十进制为101)后的值

第10行代码,edx 清零

第11,12行代码解释见下图

大概流程根据其减去的数值查小表,再根据小表查大表,详情见下面

 

0(101) -查小表-> 0x0040d925(00) --[edx*4 + 0x40D911] 查大表--> 0x0040D911 -> case 101
1(102) -查小表-> 0x0040d926(01) --[edx*4 + 0x40D911] 查大表--> 0x0040D915 -> case 102
2(103) -查小表-> 0x0040d927(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
3(104) -查小表-> 0x0040d928(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
4(105) -查小表-> 0x0040d929(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
5(106) -查小表-> 0x0040d92A(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
6(107) -查小表-> 0x0040d92B(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
7(108) -查小表-> 0x0040d92C(04) --[edx*4 + 0x40D911] 查大表--> 0x0040D921 -> default
9(109) -查小表-> 0x0040d92D(02) --[edx*4 + 0x40D911] 查大表--> 0x0040D919 -> case 109
10(110) -查小表-> 0x0040d92E(03) --[edx*4 + 0x40D911] 查大表--> 0x0040D91D -> case 110

上面的 edx中的值就是对应前面小表中查询出的结果(00,01,04 ...,02,03)
 

 

循环结构

 

    do...while 语句

 

看如下例子:

do ...while 语句的反汇编代码还是比较好理解的,因为和程序本身的逻辑一致,如图看汇编,其褐色代码块就是对应printf语句,橙色代码块对应着x++,最后青蓝色代码块这对应着最后的条件判断,如果成立,则会往上跳转,重复循环代码。

 

DO_BEGIN地址:		
		
	影响标志位的指令	
	jxx DO_BEGIN	

总结:

 

  1. 根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址
  2. 根据条件跳转指令所在的地址,可以得到循环语句块的结束地址
  3. 条件跳转的逻辑与源码相同

    while 语句

例子如下:

首先绿色代码块是用于while条件判断,jge(大于等于)说明当x >=y时则跳出循环(到0x0040B81C),上面青色代码块对应的是printf,黄色代码块对应着的就是i++,最后无条件跳转到 0x40B7F8循环判断。

最后的一块青色代码块是用于堆栈的现场还原,此时while循环已经结束了。

 

WHILE_BEGIN:						
	影响标志位的指令					
	jxx WHILE_END ;条件成立跳转到循环语句块结尾处					
	......					
	jmp WHILE_BEGIN ;跳转到条件比较处					
WHILE_END:						

总结:

 

  1. 根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址
  2. 根据jmp 指令所跳转到的地址,可以得到循环语句块的起始地址
  3. 在还原while 比较时,条件跳转的逻辑与源码相反

    for 语句

for循环的汇编代码可能相对于上面两个复杂一点点,首先青色代码块是只会执行一次的,因为他对应着的是for语句里的初始化,后面他会jmp无条件跳转到紫色代码块,这个代码块就是用于第一次for循环的条件判断(cmp....jge.....),如果条件判断成立,那么for循环结束到绿色代码块,否者继续往下执行蓝色代码块(printf),后面有个jmp会无条件跳转到黄色代码块,进行i++,以及for循环循环判断。

总结:

  1. 第一个jmp 指令之前为赋初值部分
  2. 第一个jmp 指令所跳转的地址为循环条件判定部分起始
  3. 判断条件后面的跳转指令条件成立时跳转的循环体外面
  4. 条件判断跳转指令所指向的地址上面有一个jmp jmp地址为for语句第三部分的起始位置(即上面 i++)

 

展开阅读全文

没有更多推荐了,返回首页