• 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 
    ",nValue1);
    
    	int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
    	scanf("%d",&nValue2);
    	printf("值 = %d 
    ",nValue2);
    
    	nValue1 = argc;
    	int nValue3 = nValue1 * 3; //变量 * 常量 常量不是2的幂
    	scanf("%d",&nValue3);
    	printf("值 = %d 
    ",nValue3);
    
    
    	int nValue5 = nValue1 * nValue2; //变量 * 变量
    	scanf("%d",&nValue5);
    	printf("值 = %d 
    ",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 
    "
    .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 
    "
    .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 
    "
    .text:004010A4                 call    _printf
    

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

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

    高级代码:

    int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
    scanf("%d",&nValue2);
    printf("值 = %d 
    ",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 
    "
    .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 
    "
    .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 
    "
    .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. 变量 * 常量 常量是非2的幂

    高级代码如下:

    int main(int argc, char* argv[])
    {
    	
    	int nCount = 0;
    	scanf("%d",&nCount);
    	nCount = nCount * 15;
    
    	printf("%d",nCount);
    	return 0;
    }
    
    

    着重讲解 Release Debug版本直接对着汇编还原即可.

    .text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
    .text:00401000 _main           proc near               ; CODE XREF: start+AF↓p
    .text:00401000
    .text:00401000 var_4           = dword ptr -4
    .text:00401000 argc            = dword ptr  4
    .text:00401000 argv            = dword ptr  8
    .text:00401000 envp            = dword ptr  0Ch
    .text:00401000
    .text:00401000                 push    ecx
    .text:00401001                 lea     eax, [esp+4+var_4]
    .text:00401005                 mov     [esp+4+var_4], 0
    .text:0040100D                 push    eax
    .text:0040100E                 push    offset aD       ; "%d"
    .text:00401013                 call    _scanf
    
    .text:00401018                 mov     eax, [esp+0Ch+var_4]
    .text:0040101C                 lea     eax, [eax+eax*2]          //核心代码位置
    .text:0040101F                 lea     eax, [eax+eax*4]
    
    .text:00401022                 push    eax
    .text:00401023                 push    offset aD       ; "%d"
    .text:00401028                 mov     [esp+14h+var_4], eax
    .text:0040102C                 call    _printf
    .text:00401031                 xor     eax, eax
    .text:00401033                 add     esp, 14h
    .text:00401036                 retn
    .text:00401036 _main           endp
    

    提取出的核心汇编如下:

    .text:00401018                 mov     eax, [esp+0Ch+var_4]
    .text:0040101C                 lea     eax, [eax+eax*2]          //核心代码位置
    .text:0040101F                 lea     eax, [eax+eax*4]
    

    首先 Var_4 设为 我们的局部变量
    lea eax,[eax + eax * 2] 这个公式其实实在计算. eax + eax *2 按照数学公式可以转换为 3eax
    lea eax,[eax + eax * 4] 一样是进行计算.上面的eax我们已经知道是 3eax 带入公式则得到 3eax + 3eax * 4 ===> 提取出来 = (3 * 4)eax + 3eax = 12eax + 3eax 继续优化 => 15 eax
    此时eax我们知道是我们的局部变量. 所以求的就是 15 * 局部变量. 在高级代码中的表现形式也就是 nCount * 15

    VS2019中的优化

    .text:00401099                 mov     ecx, [ebp+var_4]
    .text:0040109C                 shl     ecx, 4
    .text:0040109F                 sub     ecx, [ebp+var_4]
    

    ecx = nCount
    ecx << 4 ====> nCount * 2^4
    sub ecx,nCount
    这种优化方式也是很巧妙的. 首先编译器尝试 + 1 + 1之后 = 16 16就可以优化为2^4次方.
    但是最终结果是
    15 .所以计算出的结果 -去自己本身. 也是*15
    公式:
    x * 2^n -x

    4.乘法的混合运算

    高级代码:

    nValue1 = argc;
    int nValue3 = nValue1 * 3; //变量 * 常量 常量不是2的幂
    scanf("%d",&nValue3);
    printf("值 = %d 
    ",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 
    "
    .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 
    "
    .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 
    "
    .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 
    ",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 
    "
    .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 
    "
    .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 
    "
    .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
    5.核心点就是 常量非2的幂的时候怎么进行的优化. 比如 可以通过lea指令 也可以转为2n- 自身

  • 相关阅读:
    Java的参数传递是值传递还是引用传递
    10张图带你深入理解Docker容器和镜像
    Java 如何有效地避免OOM:善于利用软引用和弱引用
    事务与一致性:刚性or柔性
    Java 面试题史上最强整理
    三张图秒懂Redis集群设计原理
    iOS开发笔记系列-基础4(变量与数据类型)
    iOS开发笔记系列-基础3(多态、动态类型和动态绑定)
    iOS开发笔记系列-基础2(类)
    iOS开发笔记系列-基础1(数据类型与表达式)
  • 原文地址:https://www.cnblogs.com/iBinary/p/10009710.html
Copyright © 2020-2023  润新知