逆向-加减乘运算
Posted 嘻嘻兮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-加减乘运算相关的知识,希望对你有一定的参考价值。
后面打算记录一下逆向识别的学习过程,也就是把每个C语言中的基础关键点都反汇编,然后对比观察。虽然说这里的每一步都是很简单,但是就算简单也还是得看,毕竟每个程序都是由这些一个个简单的点组合而成,当进行反汇编还原时,也就是将反汇编一点点拆分成这若干块的小知识点。
好了,首先看最基础的四则运算,也就是加减乘除,当然这篇博客并不包括除,除法的话下面博客再记录吧(因为优化有些复杂了,怕篇幅过长)。
首先对于最简单的运算来说,就是不包括一些复杂混合运算,大体上分应该就以下几种
变量?常量
常量?常量
变量?变量
首先来看一下加的情况
int main(int argc, char* argv[])
int nValueOne = 5;
int nValueTwo = 6;
nValueOne + nValueTwo; //无效语句
nValueOne = nValueTwo + 2; //变量+常量
nValueOne = 5 + 6; //常量+常量
nValueTwo = argc + nValueTwo; //变量+变量
printf("%d %d",nValueOne,nValueTwo);
return 0;
那么首先来观察一下Debug的情况
mov dword ptr [ebp-4],5
mov dword ptr [ebp-8],6
//nValueOne = nValueTwo + 2;
mov eax,dword ptr [ebp-8]
add eax,2
mov dword ptr [ebp-4],eax
//nValueOne = 5 + 6;
mov dword ptr [ebp-4],0Bh
//nValueTwo = argc + nValueTwo;
mov ecx,dword ptr [ebp+8]
add ecx,dword ptr [ebp-8]
mov dword ptr [ebp-8],ecx
mov edx,dword ptr [ebp-8]
push edx
mov eax,dword ptr [ebp-4]
push eax
可以看出来,在Debug中,常量+常量的方式编译器直接给出了结果(编译器的优化-常量折叠),其余的都按照汇编语句还原即可。对于编译器的一些优化,我们下面先来看完Release后再谈
mov eax,[esp+argc]
add eax,6
push eax
push 0Bh //常量折叠+常量传播
使用了Release后,这代码瞬间变得精简了好多,下面那就先来说一说编译器的一些优化
1. 常量折叠
x = 3 + 4
此时3和4都是常量,其结果可预见,必定为3,所以没有必要产生指令。
2. 常量传播
x = 3 + 5
y = x
首先x的值是可以直接确定的,也就是8,那么所以下一行代码y的赋值其结果也是可预见的,所以直接生成y=8即可。
3. 减少变量
x = i * 3;
y = j * 3;
if(x > y) //此后x和y没有再引用
//...
由于后面的代码中不会再引用x和y,所以可以直接去掉x和y,直接生成 if (i > j)即可
4.复写传播
x = a;
//... 此处未改变x值
y = x + b;
与常量传播很像,只是目标变成了变量,由于中间未改变x的值,所以可直接用变量a代替x,即 y = a + b。
除了以上一些优化外,还有一些其他的优化,到时候就遇到了再分析吧。在Relase中,可能还会将一些减法转化为加法(加-x),还有当出现重复赋值的时候,上句赋值会被删除,如上面代码中的nValueOne这个变量,连续中间未修改的进行赋值操作,那么上一次的赋值操作肯定是无用的语句。
好了,再来看上面Relase中的结果应该就比较清楚了,首先,由于第一处的变量+常量的语句是无效语句,所以都没有生成反汇编代码。而第二处使用了常量折叠,直接计算出了0xB。而后因为需要打印这个结果,有使用了常量传播,使得最后直接push结果即可。
看完加的情况再来看看减的情况,其实减的情况和加很像,因为上面的优化规则都是通用的
int main(int argc, char* argv[])
int a = 10;
int b = 5;
scanf("%d",&b);
int c = b - 9;
argc = 10 - b;
b = argc - c;
printf("%d %d %d",b,c,argc);
return 0;
下面分析对比Debug和Release情况
Debug下
//int c = b - 9;
mov ecx,dword ptr [ebp-8]
sub ecx,9
mov dword ptr [ebp-0ch],ecx
//argc = 10 - b;
mov edx,0ah
sub edx,dword ptr [ebp-8]
mov dword ptr [ebp+8],edx
//b = argc - c;
mov eax,dword ptr [ebp+8]
sub edx,dword ptr [ebp-0ch]
mov dword ptr [ebp-8],eax
Relsese下
mov edx,[esp+0ch+var_4]
mov eax,0Ah
sub eax,edx //argc = 10 - b; 这里使用了两寄存器化的情况,一般debug只会一寄存器化
lea ecx,[edx-9] //int c = b - 9; 使用lea指令,效率更高
mov edx,eax
sub edx,ecx //b = argc - c;
这里的话主要看分析一下lea指令吧,lea其本意是对其取地址,但是由于效率有些高,所以有时候会用于做一些基本运算,你想想看,[0x12345678]是用于取0x12345678中的内容的,那么对其取地址是不是就是等于0x12345678。那么对于上面的指令其实就是 ecx = edx - 9。注意lea也不是万能的,因为其lea是有一定的格式的,所以当运算不符合其格式时,当然是用不了的。
OK,下面再来分析一下乘的情况
乘法运算对应的汇编指令有有符号IMUL和无符号MUL两种。不过就单独对于乘法来说,在vs系列上我发现就算定义的都是无符号,不过最终生成的汇编指令都是IMUL,这里因为IMUL可以支持多操作数(结果32位),而mul只有单操作数。
这里对于乘来说,我们需要细分一下乘以常量的情况,因为此时编译器有不同的优化,分别是乘以2的幂和乘以非2的幂
int main(int argc, char* argv[])
int nVarOne = argc;
int nVarTwo = nVarOne;
printf("%d",nVarOne * 29); //乘以非2的幂
printf("%d",nVarOne * 16); //乘以2的幂
printf("%d", 5 * 6); //常量*常量
printf("%d",nVarOne * nVarTwo); //变量乘变量
printf("%d",nVarOne * nVarTwo + 9); //混合
return 0;
看完源码,其实对于Debug下来说,我们基本上可以猜出来,首先第三个printf结果常量折叠肯定是结果,而第二个我们可以使用左移来代替乘法。
//nVarOne * 29
mov edx,dword ptr [ebp-4]
imul edx,edx,1Dh
//nVarOne * 16
mov eax,dword ptr [ebp-4]
shl eax,4
// 5 * 6
push 1Eh
//nVarOne * nVarTwo
mov ecx,dword ptr [ebp-4]
imul ecx,dword ptr [ebp-8]
//nVarOne * nVarTwo + 9
mov edx,dword ptr [ebp-4]
imul edx,dword ptr [ebp-8]
add edx,9
下面再来看一下Release下的版本
//nVarOne * 29
mov esi,[esp+4+argc]
lea eax,ds:0[esi*8] //esi = esi * 8
sub eax,esi //eax = esi * 7
lea ecx,[esi+eax*4] //ecx = esi + esi * 7 * 4 = esi * 29
//nVarOne * 16
mov edx,esi
shl edx,4
//5 * 16
push 1Eh
//nVarOne * nVarTwo 编译器发现都是变量 argc
imul esi,esi
//nVarOne * nVarTwo + 9 上面的结果+9
add esi,9
对比看来,release下优化的还是有点多的,这里着重注意下第一个,release会把非乘以2的幂也给优化了,使用了多条lea指令,当然当这个数过大时,可能lea指令就需要多条了,这时当划不来时就还是会上imul指令的。
OK,这里加减乘都讲完了,下面就来简单说下编译器的窥孔优化,这是一种局部的优化方式
扫描源码,尝试使用优化手段,如常量折叠,传播等等
扫描完代码如果没有优化则退出,优化结束
如果有优化则代码优化(发生修改)后,循环重新回到扫描源码阶段
以上是关于逆向-加减乘运算的主要内容,如果未能解决你的问题,请参考以下文章