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 \r\n",nValue1);
int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
scanf("%d",&nValue2);
printf("值 = %d \r\n",nValue2);
nValue1 = argc;
int nValue3 = nValue1 * 3; //变量 * 常量 常量不是2的幂
scanf("%d",&nValue3);
printf("值 = %d \r\n",nValue3);
int nValue5 = nValue1 * nValue2; //变量 * 变量
scanf("%d",&nValue5);
printf("值 = %d \r\n",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 \r\n"
.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 \r\n"
.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 \r\n"
.text:004010A4 call _printf
经过上面我们调整之后,是不是我们观看汇编代码的时候就觉着顺眼了. 比如scanf.这个函数是两个参数.
那么汇编中.就要进行push 两个参数. 并且要传入地址. 观看上方汇编代码.我们得知. lea是取地址.
下面接着push.然后调用scanf完成函数功能. 这个就是流水线优化. 在以后的汇编代码还原中.一定要准确的
定位正确的汇编代码.这样才能最好的进行还原.
注意: 上面是流水线优化代码.但是我们有没有发现.其实我们提到下面.一样不影响程序结果.
2.常量*变量 /变量 * 常量 常量是2的幂 汇编代码解析
高级代码:
int nValue2 = nValue1 * 16; //变量 * 常量 其中常量是2的幂
scanf("%d",&nValue2);
printf("值 = %d \r\n",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 \r\n"
.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 \r\n"
.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 \r\n"
.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 \r\n",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 \r\n"
.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 \r\n"
.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 \r\n"
.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 \r\n",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 \r\n"
.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 \r\n"
.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 \r\n"
.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- 自身