逆向-加减乘运算

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,这里加减乘都讲完了,下面就来简单说下编译器的窥孔优化,这是一种局部的优化方式

扫描源码,尝试使用优化手段,如常量折叠,传播等等
    扫描完代码如果没有优化则退出,优化结束
    如果有优化则代码优化(发生修改)后,循环重新回到扫描源码阶段

 

以上是关于逆向-加减乘运算的主要内容,如果未能解决你的问题,请参考以下文章

大数加减法 - java实现

用shell做个加减乘除运算

python里面的加减乘除怎么弄?求解

java中怎么将字符串(带运算符号加减乘除)转换成代数算式运算

html计算加减乘除

mongo加减乘除运算