反汇编(Disassembly) 即把目标二进制机器码转为汇编代码的过程,该技术常用于软件破解、外挂技术、病毒分析、逆向工程、软件汉化等领域,学习和理解反汇编对软件调试、系统漏洞挖掘、内核原理及理解高级语言代码都有相当大的帮助,软件一切神秘的运行机制全在反汇编代码里面。
寻找OEP
1.使用编译器 VS 2013 Express 版本,写代码,并关闭基地址随机化,编译采用debug版。
2.寻找OEP,x64dbg 载入,默认停在,回车跟进。
call <__tmainCRTStartup> 再次回车跟进。
下方看到一个安全cookie检测,主要防止在目标堆栈中瞎搞,而设计的安全机制。编译器开启后,每次运行都会生成随机cookie,结束时会验证是否一致,防止瞎搞,老版本编译器中不存在这个选项,一开始开发人员也没想那么多,后来瞎搞的人多了,才加上的,主要是害怕自己的windows被玩坏。
继续向下看代码,看到以下代码就到家了,call 0x0041113B 跟进去就是main.
数值类型变量: 数值类型默认在32位编译器上是4字节存储的。
#include <stdio.h>
int main(int argc, char* argv[])
{
int x = 10, y = 20, z = 0;
printf("%d\n", x + y);
return 0;
}
反汇编结果如下,如果在vc6下mov ecx,0xC 原因 int=4 3*4
=0xC
004113CC | 8DBD 1CFFFFFF | lea edi,dword ptr ss:[ebp-0xE4] | 取出内存地址
004113D2 | B9 39000000 | mov ecx,0x39 | 指定要填充的字节数
004113D7 | B8 CCCCCCCC | mov eax,0xCCCCCCCC | 将内存填充为0xcccccc
004113DC | F3:AB | rep stosd | 调用rep指令,对内存进行初始化
004113DE | C745 F8 0A000000 | mov dword ptr ss:[ebp-0x8],0xA | 对变量x初始化为10
004113E5 | C745 EC 14000000 | mov dword ptr ss:[ebp-0x14],0x14 | 对变量y初始化为20
004113EC | C745 E0 00000000 | mov dword ptr ss:[ebp-0x20],0x0 | 对变量c初始化为0
004113F3 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 取出10并放入eax
004113F6 | 0345 EC | add eax,dword ptr ss:[ebp-0x14] | 与20相加得到结果付给eax
004113F9 | 8BF4 | mov esi,esp | 保存当前栈帧
004113FB | 50 | push eax |
004113FC | 68 58584100 | push consoleapplication1.415858 | 415858:"%d\n"
00411401 | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411407 | 83C4 08 | add esp,0x8 |
除了整数类型外,浮点数也是一种数值运算方式,将上方代码稍微修改后,编译并查看其反汇编代码.
#include <stdio.h>
int main(int argc, char* argv[])
{
double x = 1.05, y = 20.56, z = 0;
z = x + y;
printf("%f\n", z);
return 0;
}
观察以下汇编代码,会发现前面的初始化部分完全一致,但后面则使用了xmm0寄存器,该寄存器是专门用于运算浮点数而设计的浮点运算模块默认大小就是64位,其将内存中的值取出来并放入xmm0中进行中转,然后复制到堆栈中,等待最后调用addsd命令完成对浮点数的加法运算,并将运算结果回写到缓冲区,最后调用打印函数实现输出.
004113CC | 8DBD 10FFFFFF | lea edi,dword ptr ss:[ebp-0xF0] |
004113D2 | B9 3C000000 | mov ecx,0x3C |
004113D7 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
004113DC | F3:AB | rep stosd |
004113DE | F2:0F1005 70584100 | movsd xmm0,qword ptr ds:[<__real@3ff0cccccccccccd>] | 将内存地址中的值取出并放入xmm0中
004113E6 | F2:0F1145 F4 | movsd qword ptr ss:[ebp-0xC],xmm0 | 将数据放入堆栈中存储
004113EB | F2:0F1005 80584100 | movsd xmm0,qword ptr ds:[<__real@40348f5c28f5c28f>] |
004113F3 | F2:0F1145 E4 | movsd qword ptr ss:[ebp-0x1C],xmm0 |
004113F8 | F2:0F1005 60584100 | movsd xmm0,qword ptr ds:[<__real@0000000000000000>] |
00411400 | F2:0F1145 D4 | movsd qword ptr ss:[ebp-0x2C],xmm0 |
00411405 | F2:0F1045 F4 | movsd xmm0,qword ptr ss:[ebp-0xC] | main.c:6
0041140A | F2:0F5845 E4 | addsd xmm0,qword ptr ss:[ebp-0x1C] | 对浮点数进行相加操作
0041140F | F2:0F1145 D4 | movsd qword ptr ss:[ebp-0x2C],xmm0 | 结果放入堆栈等待被打印
00411414 | 8BF4 | mov esi,esp |
00411416 | 83EC 08 | sub esp,0x8 |
00411419 | F2:0F1045 D4 | movsd xmm0,qword ptr ss:[ebp-0x2C] |
0041141E | F2:0F110424 | movsd qword ptr ss:[esp],xmm0 |
00411423 | 68 58584100 | push consoleapplication1.415858 | 415858:"%f\n"==L"春\n"
00411428 | FF15 14914100 | call dword ptr ds:[<&printf>] |
0041142E | 83C4 0C | add esp,0xC |
字符与字符串变量:
#include <stdio.h>
int main(int argc, char* argv[])
{
char x = 'a', y = 'b',z = 'c';
printf("x = %d --> y = %d --> z = %d", x,y,z);
return 0;
}
反汇编结果如下,观察发现字符型的表现形式与整数类型基本一致,只是在数据位大小方面有所区别,如上int类型使用dword作为存储单位,而字符类型则默认使用byte
形式存储。
004113CC | 8DBD 1CFFFFFF | lea edi,dword ptr ss:[ebp-0xE4] |
004113D2 | B9 39000000 | mov ecx,0x39 |
004113D7 | B8 CCCCCCCC | mov eax,0xCCCCCCCC |
004113DC | F3:AB | rep stosd |
004113DE | C645 FB 61 | mov byte ptr ss:[ebp-0x5],0x61 | 第一个字符
004113E2 | C645 EF 62 | mov byte ptr ss:[ebp-0x11],0x62 | 第二个字符
004113E6 | C645 E3 63 | mov byte ptr ss:[ebp-0x1D],0x63 | 第三个字符
004113EA | 0FBE45 E3 | movsx eax,byte ptr ss:[ebp-0x1D] | 拷贝第三个字符
004113EE | 8BF4 | mov esi,esp | esi:__enc$textbss$end+109
004113F0 | 50 | push eax |
004113F1 | 0FBE4D EF | movsx ecx,byte ptr ss:[ebp-0x11] | 拷贝第二个字符
004113F5 | 51 | push ecx |
004113F6 | 0FBE55 FB | movsx edx,byte ptr ss:[ebp-0x5] | 拷贝第一个字符
004113FA | 52 | push edx |
004113FB | 68 58584100 | push consoleapplication1.415858 | 415858:"x = %d --> y = %d --> z = %d"
00411400 | FF15 14914100 | call dword ptr ds:[<&printf>] | 打印输出
00411406 | 83C4 10 | add esp,0x10 |
虽然使用的是byte存储一个字符,但是每一个字符在分配上还是占用了12个字节的空间4x3=12,编译器并没有因为它是一个字符而区别对待,同样是采用了4字节的分配风格.
反汇编第一种形式的字符串类型,发现首先会从常量字符串中ds:[0x415858]
取出前四个字节子串,并将其压入堆栈中,然后再循环后四个字节子串并压栈,最后取出第一个字符串的堆栈地址并输出打印,该方法只适用于小字符串.
004113E8 | A1 58584100 | mov eax,dword ptr ds:[0x415858] | 在常量字符串中取出数据 "hell"
004113ED | 8945 E8 | mov dword ptr ss:[ebp-0x18],eax | 将常量前面的hell压栈
004113F0 | 8B0D 5C584100 | mov ecx,dword ptr ds:[0x41585C] | 继续压栈 "o lyshark"
004113F6 | 894D EC | mov dword ptr ss:[ebp-0x14],ecx |
004113F9 | 8B15 60584100 | mov edx,dword ptr ds:[0x415860] | 00415860:"shark"
004113FF | 8955 F0 | mov dword ptr ss:[ebp-0x10],edx |
00411402 | 66:A1 64584100 | mov ax,word ptr ds:[0x415864] | 00415864:L"k"
00411408 | 66:8945 F4 | mov word ptr ss:[ebp-0xC],ax | 最后将剩下的一个字单位压栈
0041140C | 8BF4 | mov esi,esp | main.c:8
0041140E | 8D45 E8 | lea eax,dword ptr ss:[ebp-0x18] | 取出hell的内存地址作为首地址使用
00411411 | 50 | push eax |
00411412 | 68 68584100 | push consoleapplication1.415868 | 415868:"短字符串: %s\n"
00411417 | FF15 14914100 | call dword ptr ds:[<&printf>] | 打印输出
0041141D | 83C4 08 | add esp,0x8 |
反汇编长字符串与字符串指针,由于这两个类型具有很强的对比性所以放在一起,第1种字符串数组存储,可以看到内部是通过拷贝内存实现的,而第2种指针方式则是直接引用常量地址,从效率上来说指针效率更高,因为没有拷贝造成的性能损失.
004113E5 | 8945 FC | mov dword ptr ss:[ebp-0x4],eax |
004113E8 | B9 06000000 | mov ecx,0x6 | 循环拷贝6次
004113ED | BE 58584100 | mov esi,0x415858 | 字符串常量地址
004113F2 | 8D7D DC | lea edi,dword ptr ss:[ebp-0x24] | 取出初始化的内存空间首地址
004113F5 | F3:A5 | rep movsd | 循环拷贝6次
004113F7 | 66:A5 | movsw | 单独拷贝最后的y字符
004113F9 | C745 D0 78584100 | mov dword ptr ss:[ebp-0x30],0x415858 | 取字符串常量地址
00411400 | 8BF4 | mov esi,esp | main.c:8
00411402 | 8D45 DC | lea eax,dword ptr ss:[ebp-0x24] | 通过指针指向字符串首地址
00411405 | 50 | push eax |
00411406 | 68 98584100 | push consoleapplication1.415898 | 415898:"长字符串: %s\n"
0041140B | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411411 | 83C4 08 | add esp,0x8 |
00411414 | 3BF4 | cmp esi,esp |
00411416 | E8 1BFDFFFF | call 0x411136 | 此处忽略,堆栈检测仪
0041141B | 8BF4 | mov esi,esp | main.c:9
0041141D | 8B45 D0 | mov eax,dword ptr ss:[ebp-0x30] | 取地址直接打印常量
00411420 | 50 | push eax |
00411421 | 68 A8584100 | push consoleapplication1.4158A8 | 4158A8:"字符串指针: %s\n"
00411426 | FF15 14914100 | call dword ptr ds:[<&printf>] |
0041142C | 83C4 08 | add esp,0x8 |
常量折叠/常量传播: 在Release模式下,编译器会对常量取值进行优化以提高程序效率,通常在编译前遇到常量,都会进行计算,得出一个新的常量值.
#include <stdio.h>
int main(int argc, char* argv[])
{
int x = 1, y = 2,z = 3;
printf("常量+常量: %d\n", 10 + 25);
printf("变量+变量: %d\n", x + y);
printf("常量+变量: %d\n", 100 + x + y + z);
return 0;
}
观察Release模式下的汇编代码,下方代码不难看出,程序会将可以提前计算的常量值进行计算,并将该结果压栈,从而可以直接Push常量节约运算资源,另外常量折叠优化通常伴随有常量传播,如果两个变量在程序中从来都没有被修改过,也没有传入过参数,为了提升运行效率在VS2013编译器中会将这种变量自动的优化为常量.
00B11000 | 56 | push esi | main.c:4
00B11001 | 8B35 9020B100 | mov esi,dword ptr ds:[<&printf>] | 获取printf基地址,并存入esi
00B11007 | 6A 23 | push 0x23 | 0x23 = 10 + 25 的结果
00B11009 | 68 0021B100 | push consoleapplication1.B12100 | B12100:"常量+常量: %d\n"
00B1100E | FFD6 | call esi |
00B11010 | 6A 03 | push 0x3 | 0x3 = x + y 的结果
00B11012 | 68 1021B100 | push consoleapplication1.B12110 | B12110:"变量+变量: %d\n"
00B11017 | FFD6 | call esi |
00B11019 | 6A 6A | push 0x6A | main.c:8
00B1101B | 68 2021B100 | push consoleapplication1.B12120 | B12120:"常量+变量: %d\n"
00B11020 | FFD6 | call esi |
00B11022 | 83C4 18 | add esp,0x18 |
00B11025 | 33C0 | xor eax,eax | main.c:9
00B11027 | 5E | pop esi |
00B11028 | C3 | ret | main.c:10
窥孔优化: 一种很局部的优化方式,编译器仅仅在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点对一些认为可能带来性能提升的转换规则,或者通过整体的分析,通过指令转换,提升代码性能。
这个窥孔,你可以认为是一个滑动窗口,编译器在实施窥孔优化时,就仅仅分析这个窗口内的指令。每次转换之后,可能还会暴露相邻窗口之间的某些优化机会,所以可以多次调用窥孔优化,尽可能提升性能
基本的乘除法: 乘法与除法与加减法相同也有一组专用汇编指令,只不过乘除法的计算方式与加减法稍有不同,在Debug版与Release版中的表现也不同.
#include <stdio.h>
int main(int argc, char* argv[])
{
int x = 10, y = 34,z = 22;
printf("x*y*z 结果是: %d\n", x*y*z);
int a = 1000, b = 20;
printf("a/b 结果是: %d\n", a / b);
return 0;
}
针对基本的乘除法的反汇编代码如下所示,第一个就是乘法的计算方式,第二个则是除法的计算方式.
计算乘法是应遵循:
如果乘数与被乘数都是8位,则把al做乘数,结果放在ax中.
如果乘数与被乘数都是16位,将把ax做乘数,结果放在eax中.
如果乘数与被乘数都是32位,将把eax做乘数,结果放在edx:eax中.
004113DE | C745 F8 0A000000 | mov dword ptr ss:[ebp-0x8],0xA | x = 0xA => 10
004113E5 | C745 EC 22000000 | mov dword ptr ss:[ebp-0x14],0x22 | y => 0x14 => 34
004113EC | C745 E0 16000000 | mov dword ptr ss:[ebp-0x20],0x16 | z => 0x20 => 22
004113F3 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | 参数移动
004113F6 | 0FAF45 EC | imul eax,dword ptr ss:[ebp-0x14] | 执行 eax * y = eax
004113FA | 0FAF45 E0 | imul eax,dword ptr ss:[ebp-0x20] | 执行 eax * z = eax
004113FE | 8BF4 | mov esi,esp |
00411400 | 50 | push eax | push eax * y * z
00411401 | 68 58584100 | push consoleapplication1.415858 | 415858:"x*y*z 结果是: %d\n"
00411406 | FF15 14914100 | call dword ptr ds:[<&printf>] | 输出乘法最终结果
0041140C | 83C4 08 | add esp,0x8 |
计算除法是应遵循:
如果除数为8位则被除数为16位,则结果的商存放与al中,余数存放ah中.
如果除数为16位则被除数为32位,则结果的商存放与ax中,余数存放dx中.
如果除数为32位则被除数为64位,则结果的商存放与eax中,余数存放edx中.
cdq指令扩展标志位 edx存储高位:eax存储低位
00411416 | C745 D4 E8030000 | mov dword ptr ss:[ebp-0x2C],0x3E8 | a = 0x3E8 => 1000
0041141D | C745 C8 14000000 | mov dword ptr ss:[ebp-0x38],0x14 | b = 0x14 => 20
00411424 | 8B45 D4 | mov eax,dword ptr ss:[ebp-0x2C] | eax = ebp-0x2c => 0x3E8
00411427 | 99 | cdq | 把eax的第31bit复制到edx的每个bit上
00411428 | F77D C8 | idiv dword ptr ss:[ebp-0x38] | 除法 eax = [ebp-0x2C]/[ebp-0x38] => eax/0x14
0041142B | 8BF4 | mov esi,esp |
0041142D | 50 | push eax | 输出eax里面的值,就是除法结果.
0041142E | 68 70584100 | push consoleapplication1.415870 | 415870:"a/b 结果是: %d\n"
00411433 | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411439 | 83C4 08 | add esp,0x8 |
上方代码中所展示的都是基于Debug版本的编译方式,可以说该版本没有经过任何优化,所以乘除法是通过计算后得到的结果,下面这段代码是Release版本代码,你可以清楚地看出代码中并没有任何与计算乘除法有关的指令,这是因为编译器在编译的时候提前将结果计算出来并打成了常量值,这有助于提高程序的运算效率.
002B1000 | 68 381D0000 | push 0x1D38 | main.c:4
002B1005 | 68 00212B00 | push consoleapplication1.2B2100 | 2B2100:"x*y*z 结果是: %d\n"
002B100A | FF15 90202B00 | call dword ptr ds:[<&printf>] |
002B1010 | 6A 32 | push 0x32 | main.c:10
002B1012 | 68 14212B00 | push consoleapplication1.2B2114 | 2B2114:"a/b 结果是: %d\n"
002B1017 | FF15 90202B00 | call dword ptr ds:[<&printf>] |
002B101D | 83C4 10 | add esp,0x10 |
002B1020 | 33C0 | xor eax,eax | main.c:11
002B1022 | C3 | ret | main.c:12
递增/递减运算符: 递增递减运算符也是C语言中最常用的运算元素.
#include <stdio.h>
int main(int argc, char* argv[])
{
int x = 10;
printf("x+1 = > %d\n", x + 1);
printf("x+=2 => %d\n", x -= 2);
printf("++x => %d\n", ++x);
printf("x++ => %d\n", x++);
return 0;
}
如下就是递增递减的代码片段,可以看出这几种的差别并不大,这里直接省略了.
004113DE | C745 F8 0A000000 | mov dword ptr ss:[ebp-0x8],0xA | 10
004113E5 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | eax = 10
004113E8 | 83C0 01 | add eax,0x1 | eax = eax+1
004113EB | 8BF4 | mov esi,esp |
004113ED | 50 | push eax |
004113EE | 68 58584100 | push consoleapplication1.415858 | 415858:"x+1 = > %d\n"
004113F3 | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411403 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | eax = 10
00411406 | 83E8 02 | sub eax,0x2 | eax = eax-2
00411409 | 8945 F8 | mov dword ptr ss:[ebp-0x8],eax |
0041140C | 8BF4 | mov esi,esp |
0041140E | 8B4D F8 | mov ecx,dword ptr ss:[ebp-0x8] |
00411411 | 51 | push ecx |
00411412 | 68 68584100 | push consoleapplication1.415868 |
00411417 | FF15 14914100 | call dword ptr ds:[<&printf>] |
复合运算符: 复合运算就是运算符中嵌套另一个运算表达式,该表达式就是符合运算表达式.
#include <stdio.h>
int main(int argc, char* argv[])
{
int x = 10, y = 34,z = 22;
int a = (x + z + y);
int b = a * 3;
printf("当前b= %d\n", b);
return 0;
}
Debug反汇编代码如下,如果是Release那么这些变量也会被计算出结果并赋予一个常量,所以这里只能使用Debug版查看.
004113DE | C745 F8 0A000000 | mov dword ptr ss:[ebp-0x8],0xA | 10
004113E5 | C745 EC 22000000 | mov dword ptr ss:[ebp-0x14],0x22 | 34
004113EC | C745 E0 16000000 | mov dword ptr ss:[ebp-0x20],0x16 | 33
004113F3 | 8B45 F8 | mov eax,dword ptr ss:[ebp-0x8] | eax = 10
004113F6 | 0345 E0 | add eax,dword ptr ss:[ebp-0x20] | eax = eax + 33
004113F9 | 0345 EC | add eax,dword ptr ss:[ebp-0x14] | eax = eax + 34
004113FC | 8945 D4 | mov dword ptr ss:[ebp-0x2C],eax | mov [ebp-0x2C], 10+33+34
004113FF | 6B45 D4 03 | imul eax,dword ptr ss:[ebp-0x2C],0x3 | imul eax,77,03 => 77乘3结果存入eax
00411403 | 8945 C8 | mov dword ptr ss:[ebp-0x38],eax | 赋值到临时变量
00411406 | 8BF4 | mov esi,esp | main.c:9, esi:__enc$textbss$end+109
00411408 | 8B45 C8 | mov eax,dword ptr ss:[ebp-0x38] |
0041140B | 50 | push eax |
0041140C | 68 58584100 | push consoleapplication1.415858 | 415858:"当前b= %d\n"
00411411 | FF15 14914100 | call dword ptr ds:[<&printf>] |
00411417 | 83C4 08 | add esp,0x8 |
多目运算符:
int main(int argc, char* argv[])
{
unsigned int temp;
scanf("%d",&temp);
printf("%d\r\n",temp == 0 ? 0:-1); // 针对有符号数
printf("%d\r\n",temp == 0 ? 1:0); // 针对无符号数
printf("%d\r\n",temp >= 1 ? 35:98); // 大于等于
return 0;
}
针对有符号数
0040F979 |. 8B4D FC mov ecx, dword ptr [ebp-4]
0040F97C |. F7D9 neg ecx
0040F97E |. 1BC9 sbb ecx, ecx
0040F980 |. 51 push ecx ; /<%d>
0040F981 |. 68 802E4200 push 00422E80 ; |format = "%d"
0040F986 |. E8 45FFFFFF call printf ; \printf
0040F98B |. 83C4 08 add esp, 8
针对无符号数
0040F990 |. 837D FC 00 cmp dword ptr [ebp-4], 0
0040F994 |. 0F94C2 sete dl
0040F997 |. 52 push edx ; /<%d>
0040F998 |. 68 802E4200 push 00422E80 ; |format = "%d"
0040F99D |. E8 2EFFFFFF call printf ; \printf
0040F9A2 |. 83C4 08 add esp, 8
大于等于符号
0040F9A5 |. 837D FC 01 cmp dword ptr [ebp-4], 1
0040F9A9 |. 1BC0 sbb eax, eax
0040F9AB |. 83E0 3F and eax, 3F
0040F9AE |. 83C0 23 add eax, 23
0040F9B1 |. 50 push eax ; /<%d>
0040F9B2 |. 68 802E4200 push 00422E80 ; |format = "%d"
0040F9B7 |. E8 14FFFFFF call printf ; \printf
0040F9BC |. 83C4 08 add esp, 8