PC逆向之代码还原技术,第五讲汇编中乘法的代码还原

时间:2022-06-21
本文章向大家介绍PC逆向之代码还原技术,第五讲汇编中乘法的代码还原,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

目录

PC逆向之代码还原技术,第五讲汇编中乘法的代码还原

一丶简介乘法指令

1.乘法指令

在汇编中,乘法指令使用 IMUL 或者 MUL指令. 一般有两种形式 IMUL reg,imm 这种指令格式是 reg * imm的结果 重新放到reg中. mul同上 第二种指令格式: IMUL reg,reg1,imm 这种形式是 reg1寄存器 * imm的结果.放到reg中.

IMUL MUL 一个带有I 一个没有. 这个是有符号相乘跟无符号相乘. 有符号相乘结果是有符号的.无符号相乘 结果是无符号的. 一定要注意.因为在代码还原中.可能一个有无符号没有注意就会吃大亏.博主吃过. 所以一定要注意.

2.代码还原注意问题

我们知道了汇编的乘法指令.那么为什么还要注意产生的问题.原因是这样的.乘法指令在CPU运行的时候 运行周期特别的大. 比如 x * 8 x的是任意一个变量. 8 是一个常量. 那么如果产生以下指令(当然不会产生.举个例子)

mov reg,[ebp - ?] 获得x变量的值
imul reg,8        x * 8结果重新放到reg当中.

假设这样产生的时间周期是100.那么cup就要损耗100.那么有没有什么办法可以优化,有办法.我们可以用 位运算. 我们知道8是2的3次方. 那么完全可以使用下方汇编指令来代替

shl reg,3 

shl时钟周期特别低.所以就优化了乘法.

二丶乘法的汇编代码产生的格式

通过上方我们简介了乘法的缺点(时间周期大)我们知道.乘法可以进行优化的.所以我们下方就专门讲解几种 特别的优化方式

1.高级代码观看

int main(int argc, char* argv[])
{
    int nValue1 = 3 * 4;  //常量 * 常量 
    scanf("%d",&nValue1); //放置Release优化,所以对变量取地址.这样优化就不会很厉害
    printf("值 = %d rn",nValue1);

    int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
    scanf("%d",&nValue2);
    printf("值 = %d rn",nValue2);

    nValue1 = argc;
    int nValue3 = nValue1 * 3; //变量 * 常量 常量不是2的幂
    scanf("%d",&nValue3);
    printf("值 = %d rn",nValue3);


    int nValue5 = nValue1 * nValue2; //变量 * 变量
    scanf("%d",&nValue5);
    printf("值 = %d rn",nValue5);
    
    int nValue6 = nValue5 * 3 + 12;  //常量 变量 混合运算
    return 0;
}

其实观看以上代码,我们可以总结一下乘法的几种方式 1.常量 * 常量 2.变量 * 常量 常量是2的幂 3.变量 * 常量 常量不是2的幂 4.变量 * 变量 总共4中方式.每种方式进行解析

2.乘法的汇编代码还原.

1.常量常量 汇编代码解析,以及两种新的优化方式的识别 观看过我们以前博客的童鞋应该知道. 编译器在编译的时候.有个优化选项,速度优先还是效率优先 也就是我们说的 o1 跟 o2 如果是o2模式.那么汇编代码就给我们进行最大程度的优化. 常量常量 在优化中属于常量折叠. 也就是说 常量 * 常量直接可以计算出来了. 就不会产生汇编代码了.

Debug下的汇编 Debug下的汇编并不进行优化.所以直接看着汇编代码进行优化即可.

.text:00401268                 mov     [ebp+var_4], 0Ch
.text:0040126F                 lea     eax, [ebp+var_4]
.text:00401272                 push    eax
.text:00401273                 push    offset Format   ; "%d"
.text:00401278                 call    _scanf
.text:0040127D                 add     esp, 8
.text:00401280                 mov     ecx, [ebp+var_4]
.text:00401283                 push    ecx
.text:00401284                 push    offset aD_0     ; "值 = %d rn"
.text:00401289                 call    _printf
.text:0040128E                 add     esp, 8

Release下的汇编

text:00401080 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401080 _main           proc near               ; CODE XREF: start+AFp
.text:00401080
.text:00401080 var_10          = dword ptr -10h
.text:00401080 var_C           = dword ptr -0Ch
.text:00401080 var_8           = dword ptr -8
.text:00401080 var_4           = dword ptr -4
.text:00401080 argc            = dword ptr  4
.text:00401080 argv            = dword ptr  8
.text:00401080 envp            = dword ptr  0Ch
.text:00401080
.text:00401080                 sub     esp, 10h                 开辟局部变量空间
.text:00401083                 lea     eax, [esp+10h+var_10]    注意从这里开始下方三条汇编指令
.text:00401087                 mov     [esp+10h+var_10], 0Ch    穿插的流水线优化代码.应该提到上方.
.text:0040108F                 push    eax
.text:00401090                 push    offset aD_0     ; "%d"
.text:00401095                 call    _scanf
.text:0040109A                 mov     ecx, [esp+18h+var_10]
.text:0040109E                 push    ecx
.text:0040109F                 push    offset aD       ; "值 = %d rn"
.text:004010A4                 call    _printf

在Releas汇编下.常量 * 常量 直接进行优化了. 也就是产生的汇编指令

mov [esp +10h + var_10],0ch

但是上方为什么说让我们注意三条汇编指令 原因是这里CPU又产生了优化方式,以及汇编为什么是esp寻址.而不是ebp寻址. 优化方式: 流水线优化 什么是流水线优化.流水线优化就是 A运行B,B运行C,C进行完成. 原本是这样一条线.但是这样会产生问题 原因?: 因为A在完成B的过程中. B 跟 C是不能运行的,必须等待A进行完成之后才能运行.此时就要进行优化 就是说A在做事的时候.不能占用别人时间.别人也要进行做事. 所以上方的汇编代码我们可以改变一下.不影响结果 优化方式: 平栈优化 关于平栈优化.我们有没有注意到.在使用 scanf printf这种C调用约定的函数.并没有产生Add esp,8 这种操作代码.而Debug下产生了.原因是其实已经产生了.不过可以进行统一优化.在一个函数内.我们可以计算出所有需要优化 的这种C平栈. 在函数底部进行统一的平栈即可.并不会影响程序运行.

高级代码伪代码:
   nvalue1 = 3 * 4;
   scanf(&nvalue1)
   printf(nvalue1)

.text:00401087                 mov     [esp+10h+var_10], 0Ch
.text:00401083                 lea     eax, [esp+10h+var_10]    这里使用lea 使用了eax下方使用eax这样才配套.
.text:0040108F                 push    eax
.text:00401090                 push    offset aD_0     ; "%d"
.text:00401095                 call    _scanf


.text:0040109A                 mov     ecx, [esp+18h+var_10]
.text:0040109E                 push    ecx
.text:0040109F                 push    offset aD       ; "值 = %d rn"
.text:004010A4                 call    _printf

经过上面我们调整之后,是不是我们观看汇编代码的时候就觉着顺眼了. 比如scanf.这个函数是两个参数. 那么汇编中.就要进行push 两个参数. 并且要传入地址. 观看上方汇编代码.我们得知. lea是取地址. 下面接着push.然后调用scanf完成函数功能. 这个就是流水线优化. 在以后的汇编代码还原中.一定要准确的 定位正确的汇编代码.这样才能最好的进行还原. 注意: 上面是流水线优化代码.但是我们有没有发现.其实我们提到下面.一样不影响程序结果.

2.常量变量 /变量 常量 常量是2的幂 汇编代码解析

高级代码:

int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
scanf("%d",&nValue2);
printf("值 = %d rn",nValue2);

看上边高级代码.我们知道,常量是一个2的幂. 也就是2的四次方是16.那么这种情况,底层汇编也不会使用 IMUL 指令.原因就是指令周期太长.所以进行优化. 如果是2的幂.我们完全可以进行位操作.左移一位,相当于 *2

Debug下的汇编:

.text:00401291                 mov     edx, [ebp+var_4]  这三行代码是主要代码.
.text:00401294                 shl     edx, 4
.text:00401297                 mov     [ebp+var_8], edx

.text:0040129A                 lea     eax, [ebp+var_8]
.text:0040129D                 push    eax
.text:0040129E                 push    offset Format   ; "%d"
.text:004012A3                 call    _scanf
.text:004012A8                 add     esp, 8
.text:004012AB                 mov     ecx, [ebp+var_8]
.text:004012AE                 push    ecx
.text:004012AF                 push    offset aD_0     ; "值 = %d rn"
.text:004012B4                 call    _printf
.text:004012B9                 add     esp, 8

通过Debug下的汇编.我们可以进行很好的代码还原.例如我们如果根据汇编.则可以还原高级代码为:

var_8 = var_4 << 4; //第一种还原方式. 但是可读性不好.所以我们可以进行更高的代码还原.(这个就是经验了)
var_8 = var_4 * 16; //第二种还原方式. 第二种还原方式才是真正的还原.但是他隐藏了一个2的幂.我们知道的左移4位.那么心里就要知道,左移四位.其实可以还原成 2^4次方.
上方两种还原方式都可以.不过如果还原的代码以后是很有用的.那么必须强迫自己还原为第二种方式.可以锻炼自己.也可以在逆向中学习更好的经验.

Release下的汇编:

.text:004010A9                 mov     edx, [esp+20h+var_10]
.text:004010AD                 lea     eax, [esp+20h+var_C]
.text:004010B1                 shl     edx, 4
.text:004010B4                 push    eax
.text:004010B5                 push    offset aD_0     ; "%d"
.text:004010BA                 mov     [esp+28h+var_C], edx
.text:004010BE                 call    _scanf
.text:004010C3                 mov     ecx, [esp+28h+var_C]
.text:004010C7                 push    ecx
.text:004010C8                 push    offset aD       ; "值 = %d rn"
.text:004010CD                 call    _printf

Release下的代码是有流水线优化的.我们可以自己提出代码.观看汇编上下文提出代码进行还原. 汇编代码如下:

.text:004010A9                 mov     edx, [esp+20h+var_10]
.text:004010B1                 shl     edx, 4                代码外提. edx使用,下方也接着对edx操作.进行还原
.text:004010BA                 mov     [esp+28h+var_C], edx

.text:004010AD                 lea     eax, [esp+20h+var_C]
.text:004010B4                 push    eax
.text:004010B5                 push    offset aD_0     ; "%d"

.text:004010BE                 call    _scanf
.text:004010C3                 mov     ecx, [esp+28h+var_C]
.text:004010C7                 push    ecx
.text:004010C8                 push    offset aD       ; "值 = %d rn"
.text:004010CD                 call    _printf

我们优化后的Release汇编代码.其实自己代码外提之后,跟Debug下汇编一样. 所以还原Releas下的汇编的 时候.有一个小技巧. 比如流水线优化. 我们自己提的时候. 可以观看汇编上下文. 比如上方汇编指令

mov edx,[var_10]; 如果是流水线优化.那么下方肯定跟edx寄存器无关的汇编指令.这个就是优化. 不过我们可以使用IDA打开.点中edx.那么edx就会高亮.就可以看出操作edx的汇编指令. 我们提出来. 根据上下文.只要不会影响结果就没有事.

Releas下汇编可以还原的高级代码为:

var_c = edx << 4;
var_c = edx * 16;

3.乘法的混合运算

高级代码:

nValue1 = argc;
int nValue3 = nValue1 * 3; //变量 * 常量 常量不是2的幂
scanf("%d",&nValue3);
printf("值 = %d rn",nValue3);

Debug下的汇编:

.text:004012BC                 mov     edx, [ebp+argc]
.text:004012BF                 mov     [ebp+var_4], edx
.text:004012C2                 mov     eax, [ebp+var_4]
.text:004012C5                 imul    eax, 3
.text:004012C8                 mov     [ebp+var_C], eax
.text:004012CB                 lea     ecx, [ebp+var_C]
.text:004012CE                 push    ecx
.text:004012CF                 push    offset Format   ; "%d"
.text:004012D4                 call    _scanf
.text:004012D9                 add     esp, 8
.text:004012DC                 mov     edx, [ebp+var_C]
.text:004012DF                 push    edx
.text:004012E0                 push    offset aD_0     ; "值 = %d rn"
.text:004012E5                 call    _printf
.text:004012EA                 add     esp, 8

Debug下的汇编.代码不进行优化. 因为不是2的幂.所以直接使用指令Imul指令.

Releas下的汇编

.text:004010D2                 mov     eax, [esp+30h+argc]
.text:004010D6                 mov     [esp+30h+var_10], eax
.text:004010DA                 lea     edx, [eax+eax*2]
.text:004010DD                 lea     eax, [esp+30h+var_8]
.text:004010E1                 push    eax
.text:004010E2                 push    offset aD_0     ; "%d"
.text:004010E7                 mov     [esp+38h+var_8], edx
.text:004010EB                 call    _scanf
.text:004010F0                 mov     ecx, [esp+38h+var_8]
.text:004010F4                 push    ecx
.text:004010F5                 push    offset aD       ; "值 = %d rn"
.text:004010FA                 call    _printf
.text:004010FF                 mov     edx, [esp+40h+var_C]
.text:00401103                 lea     eax, [esp+40h+var_4]
.text:00401107                 imul    edx, [esp+40h+var_10]
.text:0040110C                 push    eax
.text:0040110D                 push    offset aD_0     ; "%d"
.text:00401112                 mov     [esp+48h+var_4], edx
.text:00401116                 call    _scanf

首先Release下的汇编,乘法直接使用lea指令进行计算了. lea指令: lea是运算指令.效率还是比IMUL MUL指令周期短. 它的特点是计算地址.算数运算. 如下代码:

mov eax,[00401000]
lea eax,[00401000]

上面两个指令一个是mov 一个是lea.指令不一样,效果也不一样. mov eax,[00401000] 是获取00401000这个地址里面的值. 所以eax = [00401000] lea eax,[00401000] 是直接将00401000给eax保存了.并不获取里面的值.虽然有[]取值运算符.

指令明白了.那么观看Release下的汇编就明白了. 去掉流水线优化:

.text:004010D2                 mov     eax, [esp+30h+argc]
.text:004010D6                 mov     [esp+30h+var_10], eax

.text:004010DA                 lea     edx, [eax+eax*2]
.text:004010E7                 mov     [esp+38h+var_8], edx    更改过得代码.  

.text:004010DD                 lea     eax, [esp+30h+var_8]
.text:004010E1                 push    eax
.text:004010E2                 push    offset aD_0     ; "%d"
.text:004010EB                 call    _scanf

   
.text:004010F0                 mov     ecx, [esp+38h+var_8]
.text:004010F4                 push    ecx
.text:004010F5                 push    offset aD       ; "值 = %d rn"
.text:004010FA                 call    _printf

根据汇编代码我们可以进行还原:

.text:004010D2                 mov     eax, [esp+30h+argc]
.text:004010D6                 mov     [esp+30h+var_10], eax
这两句还原为:
nVar10 = argc;



.text:004010DA                 lea     edx, [eax+eax*2]
.text:004010E7                 mov     [esp+38h+var_8], edx    更改过得代码.  
这两句可以还原为:
edx = argc + argc * 2; 第一种方式
edx = argc * 3; 第二种方式  为什么这里是3. 原因是 argc + argc * 2;等价于就是argc *3;
因为在数学上 * 一个数.都可以用加法去替换.
比如:
2 * 3;
我们可以替换为: 2 + 2 + 2  所以我们按照第二种方式进行还原的时候.主要也是看经验.慢慢提升自己. 

4.变量*变量 高级代码:

int nValue5 = nValue1 * nValue2; //变量 * 变量
scanf("%d",&nValue5);
printf("值 = %d rn",nValue5);

Debug下的汇编:

.text:004012ED                 mov     eax, [ebp+var_4]
.text:004012F0                 imul    eax, [ebp+var_8]
.text:004012F4                 mov     [ebp+var_10], eax
.text:004012F7                 lea     ecx, [ebp+var_10]
.text:004012FA                 push    ecx
.text:004012FB                 push    offset Format   ; "%d"
.text:00401300                 call    _scanf
.text:00401305                 add     esp, 8
.text:00401308                 mov     edx, [ebp+var_10]
.text:0040130B                 push    edx
.text:0040130C                 push    offset aD_0     ; "值 = %d rn"
.text:00401311                 call    _printf
.text:00401316                 add     esp, 8

Debug下.汇编代码就很简单了.直接对着进行还原就行.如上面汇编代码我们可以还原为:

var_10 = var_4 * var_8; 

Releas下的汇编: 在Releas下.除了进行流水线优化.等必要的优化.变量 * 变量是无法进行优化了.也是直接使用指令了. 我们去掉流水线优化进行汇编代码还原即可. 有流水线的汇编代码:

.text:004010FF                 mov     edx, [esp+40h+var_C]
.text:00401103                 lea     eax, [esp+40h+var_4]   流水线代码.eax下方没有.为的就是打乱edx.避免操作edx的时候.下方指令进行等待.
.text:00401107                 imul    edx, [esp+40h+var_10]
.text:0040110C                 push    eax
.text:0040110D                 push    offset aD_0     ; "%d"
.text:00401112                 mov     [esp+48h+var_4], edx
.text:00401116                 call    _scanf
.text:0040111B                 mov     ecx, [esp+48h+var_4]
.text:0040111F                 push    ecx
.text:00401120                 push    offset aD       ; "值 = %d rn"
.text:00401125                 call    _printf
.text:0040112A                 add     esp, 40h
.text:0040112D                 xor     eax, eax
.text:0040112F                 add     esp, 10h
.text:00401132                 retn

无流水线的汇编代码:

.text:004010FF                 mov     edx, [esp+40h+var_C]
.text:00401107                 imul    edx, [esp+40h+var_10]
.text:00401112                 mov     [esp+48h+var_4], edx   去掉流水线,代码提上来.

.text:00401103                 lea     eax, [esp+40h+var_4]
.text:0040110C                 push    eax
.text:0040110D                 push    offset aD_0     ; "%d"

.text:00401116                 call    _scanf
.text:0040111B                 mov     ecx, [esp+48h+var_4]
.text:0040111F                 push    ecx
.text:00401120                 push    offset aD       ; "值 = %d rn"
.text:00401125                 call    _printf
.text:0040112A                 add     esp, 40h             平栈优化.一起进行平栈.
.text:0040112D                 xor     eax, eax
.text:0040112F                 add     esp, 10h
.text:00401132                 retn

去掉流水线.其实代码跟Debug下是一样的.一样进行还原.还原代码如下:

var_4 = var_c * var_10;

三丶乘法总结

乘法其实还是很简单的.只要掌握了以下几点.那么就没有一点问题了. 1.认识平栈优化.以及流水线优化. 自己会外提代码. 2.常量 * 常量 进行了常量折叠优化.也就是直接计算出来了.不会产生汇编代码. 3.变量常量 常量是2的幂的时候. 优化使用 shl等移位指令进行优化 3.变量 常量 常量不是2的幂 那么直接使用乘法指令了 MUL / IMUL 4.变量* 变量 + 常量 等混合运算的时候.使用 lea指令进行计算了.不会使用IMUL/MUL