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- 自身

原文地址:https://www.cnblogs.com/iBinary/p/10009710.html