反汇编代码中的优化方式
一丶优化方式
1.1 前言
在我们学习反汇编的时候.很多人都以为反汇编很难. 其实不然. 学什么都是有技巧的.
而想要锻炼我们的反汇编能力. 第一就是要进行多练. 第二就是熟悉底层原理. 第三就是熟悉套路.
往后几篇 会把C与C++的反汇编形式体现出来. 如果写的不对的话请批评指正.
1.2优化方式分类
汇编中的加法 减法 乘法 除法 取模 等等 都是有优化方式.以及有套路的.
优化方式分为以下几种
- 常量折叠
- 常量传播
- 变量去除
- 归并优化
- Cpu流水线优化
- 数学变换
- 不可达分支优化
- 代码外提优化
遇到新的优化再说
那么着重介绍一下上面优化方式所代表的意思.
优化的前提是在 Release下切开启O2选项优化速度的前提 Debug版本也会优化但是更多的是方便程序员调试.所以在不影响调试的前提下才会进行优化.
1.3 常量折叠
有以下例子:
1 2 3 4 5 | int n = 0 ;
int m = 1 ;
printf( "%d" , 7 + 8 );
printf( "%d" ,n + 6 );
printf( "%d" ,n + m);
|
所谓常量折叠就是 在编译前 所遇到的常量.是可以进行计算的.那么就会优化为一个常量值
例如上面的 7 + 8 不会产生add指令 而是在程序编译后直接成为 15(0xF)
以VC6.0与VS2019分别编译 为什么要两个编译器. 目的就是让大家知道.这个套路不管是几十年前的6.0还是现如今的Vs2019 都是一样的. 可能会有些稍许不同.但是绝不影响你的逆向.以及反汇编.而恰巧这才是真正的核心所在.包括gcc编译也是一样.
Vc6.0 保留核心
1 2 3 4 5 6 7 8 9 10 | .text: 00401000 push 0Fh
.text: 00401002 push offset aD ; "%d"
.text: 00401007 call _printf
.text: 0040100C push 6
.text: 0040100E push offset aD ; "%d"
.text: 00401013 call _printf
.text: 00401018 push 1
.text: 0040101A push offset aD ; "%d"
.text: 0040101F call _printf
.text: 00401024 add esp, 18h
|
可以看到 7+8在反汇编的形势下直接变为了 0xF. 也就是10进制的15
Vs2019
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .text: 00401040 sub_401040 proc near ; CODE XREF: start - 8D ↓p
.text: 00401040 push 0Fh
.text: 00401042 push offset unk_417A8C
.text: 00401047 call sub_401010
.text: 0040104C push 6
.text: 0040104E push offset unk_417A8C
.text: 00401053 call sub_401010
.text: 00401058 push 1
.text: 0040105A push offset unk_417A8C
.text: 0040105F call sub_401010
.text: 00401064 add esp, 18h
.text: 00401067 xor eax, eax
.text: 00401069 retn
.text: 00401069 sub_401040 endp
|
可以看到唯一不同的就是高版本 IDA没有识别出sig库.所以调用的都成了 sub_401010 而低版本函数已经认出来了.
还有就是文件体积变大了. vc6.0 编译出来28kb 2019编译出来98kb. 你品.
上面例子足以说明什么是常量折叠 含义就是常量会在编译器给你计算出来
1.4常量传播
常量传播也叫做常量扩散 指的就是 变量在写入或者读取的时候没有传递内存地址(&)也没有传指针或者引用来修改值的时候就会发生常量传播
大白话讲就是 你没有修改我变量的代码.那么这个变量我就可以认为是常量了.
以上面高级代码为例子
1 2 | int n = 0 ;
printf( "%d" ,n + 6 );
|
那么进行常量传播之后. n因为没有对其修改.也没有对其进行传地址的操作. 所以编译器就会把它变为常量了.
那么上面的代码就会产生如下代码
1 2 | int n = 0 ;
printf( "%d" , 0 + 6 );
|
而看到这里想必大家应该明白了. 0 + 6 又符合常量折叠. 所以代码继续变化
1 2 | int n = 0 ;
printf( "%d" , 6 );
|
以上面汇编为例子
1 2 3 | .text: 0040100C push 6
.text: 0040100E push offset aD ; "%d"
.text: 00401013 call _printf
|
想必大家知道这里为啥是 push 6 了. 这里进行了两次优化 一次是常量传播,一次是常量折叠
1.5变量去除
变量去除指的就是你程序中定义了变量但是没有对其进行修改. 然后进行常量传播,常量折叠一步一步给优化掉
还是以高级代码为例子
1 2 3 | int n = 0 ;
int m = 1 ;
printf( "%d" ,n + m);
|
程序首先发现 n m 两个变量相加. 但是看了一下上面. 发现没有对其进行修改.所以代码就会变为如下
而0+1符合常量折叠.所以最终代码就变为了.
对应反汇编
1 2 3 | .text: 00401018 push 1
.text: 0040101A push offset aD ; "%d"
.text: 0040101F call _printf
|
1.6 归并优化
归并优化,如果可以一起优化那么我就一起优化.
我们知道 printf 属于C调用约定. 所以需要外平栈 而且他是可变参.通过你push参数个数的不同.外平栈的大小也会相应改变
比如:
1 2 3 4 | .text: 00401018 push 1
.text: 0040101A push offset aD ; "%d"
.text: 0040101F call _printf
add esp, 8
|
但是我们在上面的汇编代码中.并没有看到 add esp,8 而是直接看到了 add esp,0x18
原因是什么. 在你调用printf的时候.而下面你又调用了相同的几个 printf ,printf 都是C调用约定.
所以我就一起给你平了
所以代码就有如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .text: 00401040 sub_401040 proc near ; CODE XREF: start - 8D ↓p
.text: 00401040 push 0Fh
.text: 00401042 push offset unk_417A8C
.text: 00401047 call sub_401010
.text: 0040104C push 6
.text: 0040104E push offset unk_417A8C
.text: 00401053 call sub_401010
.text: 00401058 push 1
.text: 0040105A push offset unk_417A8C
.text: 0040105F call sub_401010
.text: 00401064 add esp, 18h
.text: 00401067 xor eax, eax
.text: 00401069 retn
.text: 00401069 sub_401040 endp
|
一个参数是4个字节. 所以累计总共push了6个参数. 4 * 6 = 24个字节.所以平栈也需要24个字节
24恰巧是 十六进制的 0x18
1.7Cpu流水线优化
Cpu流水线优化其实说白了就是打乱指令执行顺序,而不影响原有功能. 这个在我们反汇编的时候需要注意.正常的汇编代码都是平平整整顺顺序序 让人一眼看的很舒服,而且反汇编出高级代码也很快.
这里以简单的汇编为例子 因为我要写很多代码才会遇到流水线优化.这里我模拟一下
正常的汇编代码指令顺序
1 2 3 4 5 6 7 | xor eax,eax
xor ebx,ebx
xor ecx,ecx
mov eax, 1
add eax, 2
mov ebx,eax
mov ecx, 3
|
打乱的流水线
1 2 3 4 5 6 7 | xor eax,eax
mov eax, 1
xor ecx,ecx
add eax, 2
mov ecx, 3
xor ebx,ebx
mov ebx,eax
|
汇编代码就很简单.
我们着重看一下打乱的汇编.
在没打乱之前代码平平整整.打乱之后发现很多汇编进行了穿插
比如
在Cpu执行 mov eax,1的时候.可以直接执行 xor ecx,ecx 这样的好处是下一行汇编不依赖于上一行汇编.
之前的指令是下一行指令依赖于上一行指令. 那么Cpu 如果在执行第二行的时候发现你依赖于上一行汇编.那么就会等待.
第一行执行了 mov eax,1 那么第二行又使用了eax. 那么第二行执行的时候就要等待第一行.
而打断的好处就是 我执行第一行的时候也可以执行第二行而且不会影响你的结果. 也可以提升速度
流水线优化需要你细品.
当你品完之后再看下打乱的汇编
1 2 3 4 5 6 7 | xor eax,eax
mov eax, 1
xor ecx,ecx
add eax, 2
mov ecx, 3
xor ebx,ebx
mov ebx,eax
|
是不是发现很顺眼了. 那么当你还原的时候完全可以按照自己的意愿来恢复汇编进行还原
1 2 3 4 5 6 7 8 9 | xor ecx,ecx
mov ecx, 3
xor eax,eax
mov eax, 1
add eax, 2
xor ebx,ebx
mov ebx,eax
|
是不是就很简单了.
1.8 数学变换优化
数学变换优化: 如果操作的数是无意义的.那么就会进行优化.
1 2 3 4 5 6 | i = 10 ;
b = 11 ;
i = b + 0 ;
i = b - 0 ;
i = b * 3 ;
i = i / 3 ;
|
那么以上高级代码直接进行优化.优化为
1.9 不可达分支优化
不可达分支则是分支永远都不会走.那么也不会产生汇编代码.也没有存在的意义
1 2 3 4 5 6 7 8 9 | a = 10 ;
if (a = = 10 )
{
xxxx
}
else
{
xxxxx
}
|
上面已经知道a就是个常量.值就是10.那么会走if块. 而 else永远不会走.那么就会将else优化掉. 当然实际情况中代码肯定很多.不会像我一样简单的写一个 a = 10 去判断.
2.0代码外提优化
所谓代码外提一般是在循环的时候进行优化.循环体内部没有产生修改此变量的代码.就会进行优化
1 2 3 4 5 6 7 | int x = xxx;
while (x > y / 3 )
{
xxx....
x - - ;
}
|
循环体内部并没有操作y/3. 所以这个值都会放到外面执行
则会优化为
1 2 3 4 5 6 | t = y / 3 ;
while (x > t)
{
xxxxx....
x - - ;
}
|
而t变量很可能也会经过上面的几种优化变为了寄存器变量
二丶去掉优化方式
代码混淆与优化是对立的.所以学习下优化也方便我们更好的"人肉"优化混淆代码
去掉优化的方式说一下
上面所说.都是编译器已经识别到了你的程序没有传地址传指针等. 所以我们想办法就不让他优化.
简单的函数如果你什么也不做也会给你优化掉的.
1 2 3 4 5 6 7 | int n = 10 ;
int m = 0 ;
scanf( "%d" ,&n);
scanf( "%d" ,&m);
int c = n + m;
scanf( "%d" ,&c);
printf( "%d%d%d" ,&n,&m,&c)
|
我们使用了很多scanf 以及printf 并对其变量取地址 那么编译器就不知道我们会不会修改变量的值了.
那么就不会给我们优化了.此时查看汇编代码就能看到真实的最小限度的优化代码了.
三丶总结
上面的几种优化方式,虽然很简单.但是我们也必须要掌握以及了解的. 因为后面的反汇编 代码还原 会有更多的优化. 而那些优化在配合 上面所说的优化就会让你感觉很难. 或者不知道汇编为什么那样做. 而你了解了这些 在单独看各自的优化就会明白.也会豁然开朗.
这些在我博客上已经写过.这里发出来也是为了让新会员重新了解一下反汇编. 高手能复习. 新人能学习.
CPP代码很简单.本次不提交了
[课程]Android-CTF解题方法汇总!
最后于 2020-9-21 11:58
被TkBinary编辑
,原因: 流水线优化位置,操作数手写有误.修改了