首先,得先会正向开发的选择和循环结构,不会正向,如何逆向呢?
收藏一句话:开发的水平决定了你逆向的高度,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:
特点分析:
- 如果不跳转,那么会执行到jmp处,jmp直接跳转到END处
- 如果跳转,则会直接跳过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
总结:
- 根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址
- 根据条件跳转指令所在的地址,可以得到循环语句块的结束地址
- 条件跳转的逻辑与源码相同
while 语句
例子如下:
首先绿色代码块是用于while条件判断,jge(大于等于)说明当x >=y时则跳出循环(到0x0040B81C),上面青色代码块对应的是printf,黄色代码块对应着的就是i++,最后无条件跳转到 0x40B7F8循环判断。
最后的一块青色代码块是用于堆栈的现场还原,此时while循环已经结束了。
WHILE_BEGIN:
影响标志位的指令
jxx WHILE_END ;条件成立跳转到循环语句块结尾处
......
jmp WHILE_BEGIN ;跳转到条件比较处
WHILE_END:
总结:
- 根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址
- 根据jmp 指令所跳转到的地址,可以得到循环语句块的起始地址
- 在还原while 比较时,条件跳转的逻辑与源码相反
for 语句
for循环的汇编代码可能相对于上面两个复杂一点点,首先青色代码块是只会执行一次的,因为他对应着的是for语句里的初始化,后面他会jmp无条件跳转到紫色代码块,这个代码块就是用于第一次for循环的条件判断(cmp....jge.....),如果条件判断成立,那么for循环结束到绿色代码块,否者继续往下执行蓝色代码块(printf),后面有个jmp会无条件跳转到黄色代码块,进行i++,以及for循环循环判断。
总结:
- 第一个jmp 指令之前为赋初值部分
- 第一个jmp 指令所跳转的地址为循环条件判定部分起始
- 判断条件后面的跳转指令条件成立时跳转的循环体外面
- 条件判断跳转指令所指向的地址上面有一个jmp jmp地址为for语句第三部分的起始位置(即上面 i++)