除法运算逆向分析
- 除法运算逆向分析
- 相关数学公式
- 除数为2的幂
- 除数为负的2的幂
- 除数为非2的幂
- 除数为负的非2的幂
由于除法指令的指令周期较长,效率低,所以编译器想尽办法用其他指令组合代替除法指令。所以C/C++除法运算的逆向分析较其他运算复杂很多,在此做一下总结
相关数学公式
当b>0时有,
⌊ab⌋=⌈a−b+1b⌉
且
⌈ab⌉=⌊a+b−1b⌋
当b<0时,有,
⌊ab⌋=⌈a−b−1b⌉
且
⌈ab⌉=⌊a+b+1b⌋
1.除数为2的幂
sar指令相当于向下取整,即⌊x2n⌋,而C语言除法结果是向零取整,即[x2n]。
所以有:
[x2n]=⎧⎩⎨⎪⎪⎪⎪⎪⎪⌊x2n⌋⌈x2n⌉=⌊x+2n−12n⌋x≥0x<0
下面看例子:
//.c
printf("nVarOne/8=%d",nVarOne/8);
编译后反汇编:
;.asm
mov eax,dword ptr [ebp-4]
cdq
and edx,7
add eax,edx
sar eax,3
...
and edx,7
使得当nVarOne为负数时edx内容为2n−1,当nVarOne为正数时edx为0,最后sar eax,3
相当于除以2n。
2.除数为负的2的幂
直接给例子:
//.c
printf("nVarOne/-8=%d",nVarOne/-8);
编译后反汇编:
;.asm
mov eax, [esp+4]
cdq
and edx,7
add eax,edx
sar eax,3
neg eax
...
除了最后一句其余和上面相同, neg eax
相当于对计算结果取负。
接下来讨论难点3和4
3.除数为非2的幂
下面举例由汇编逆向推C代码:
情景1(MagicNumber⩽0x7FFFFFFF)
易得公式,
xo⇔x∗2no∗12n
令
c=2no,这个值被称为Magic Number。于是就有
xo⇔x∗2no∗12n⇔x∗c2n
_main proc near
arg_0= dword ptr 4
mov ecx,[esp+arg_0]
mov eax,38E38E39h
imul ecx
sar edx,1
mov eax,edx
shr eax,1Fh
add edx,eax
push edx
push offset Format ;"%d
call _printf
add esp,8
retn
_main endp
其中,sar edx,1
算术右移1位, shr eax,1Fh
逻辑右移31位。
首先,ecx获取参数,eax获取魔数,(edx,eax)=eax*ecx,乘积低4B的eax内容抛弃,只使用乘积高4B的edx内容,这样就相当于乘积右移32位,再加上sar edx,1
右移的1位,共右移33位,即n=33,mov eax,edx
与shr eax,1Fh
用来取得乘积结果符号位,乘积为正时eax存放00000000h,乘积位负时存放00000001h。最后,add edx,eax
的原因是:
当x<0且[xo]不为整数时,有
[xo]=⌈x∗c2n⌉=⌊x∗c2n⌋+1
(这里我认为只要x<0,编译器能够确认
x∗c2n这里必定不是整数)
∴o=2nc=23338E38E39h=8.999999……≈9,推导出C代码,
printf("%d",argc/9);
小结:
;x*(2^n/o)
mov eax,MagicNumber
imul ...
;/2^n
sar edx, ...
mov reg,edx
shr reg,1Fh
;负数调整
add edx,reg
当遇到以上指令序列,基本可判定是除法优化后的代码。MagicNumber<=7fffffffh,编译器在imul和sar之间未产生任何调整指令,故认定除数为正数。统计右移总次数确定公式中的n值,使用公式o=2nc(魔数)得到除数o的近似值。即可恢复除法原型。
情景2(MagicNumber⩽0x7FFFFFFF)
易得公式,
xo⇔x∗232+232+no−232232+n
此式中,魔数
c=232+no−232
_main proc near
arg_0= dword ptr 4
mov ecx,[esp+arg_0]
mov eax,24924925h
mul ecx
sub ecx,edx
shr ecx,1
add ecx,edx
shr ecx,2
push ecx
push offset Format ;"nVarTwo/7=%d\r\n"
call _printf
add esp,8
xor eax,eax
retn
_main endp
sub ecx,edx
,shr ecx,1
,add ecx,edx
,shr ecx,2
这4句可以用一个计算式来表示:
x−x∗c2322+x∗c23222
化简得:
x∗232+c235
∴232+n=235⇒n=3,o=232+n232+c=235232+24924925h=6.99999……≈7,推出C代码:
printf("nVarTwo/7="%d\r\n",argc/7);
小结:
mov eax,MagicNumber
mul reg
sub reg,edx
shr reg,1
add reg,edx
(shr reg,A)
如果遇到以上指令序列,基本可判定是除法优化后的代码。统计右移总次数以确定公式中的n值,使用公式o=232+n232+c(魔数)求解出除数o,即可恢复除法原型。
情景3(MagicNumber≥0x80000000)
编译器在计算MagicNumber时是作为无符号处理的,而imul指令是作为有符号处理的。所以当魔数≥0x80000000时,实际参与乘法运算的是个负数,导致魔数与数学公式上的那个“大常数”意义不一致。
当y真<0时,由补码计算公式有:y补=232−|y真|=232+y真∴y真=y补−232∴x∗y补=x∗y真+x∗232y补这里看做无符号数y无,为正数,y真为负数。
易得公式,
xo⇒x∗2no∗12n⇒(x∗(2no−232)+x∗232)∗12n即xo⇒x∗y补(y无)∗12n⇒(x∗y真+x∗232)∗12n
_main proc near
arg_0= dword ptr 4
mov esi,[esp+arg_0]
mov eax,92492493h
imul esi
add edx,esi
sar edx,2
;...负数调整
上述代码转换成公式:
(esi∗eax+232∗esi)∗1234∵c是编译器求魔数运算按公式c=2no无符号运算得到的。∴可以用公式o=2nc=23492492493h=6.999999……≈7
反推出C代码:
printf("%d",argc/7);
小结:
mov eax,MagicNumber;MagicNumber>7fffffffh
imul reg
add edx,reg
sar edx,...
mov reg,edx
shr reg,1Fh
add edx,reg
当遇到以上指令序列时,基本可判定是除法优化后的代码。当MagicNumber≥80000000h,编译器会在imul和sar之间产生调整作用的add指令,故可认定除数为正。*统计右移的总次数以确定公式中的n值,然后使用公式o=2nc(魔数)求解除数o,即可恢复除法原型。
4.除数为负的非2的幂
易得公式:
xo=x∗c∗12n(c<0)c=−2n|o|=2n|o|求补=232−2n|o|
情景1(MagicNumber≥0x80000000)
_main proc near
arg_0= dword ptr 4
mov ecx,[esp+arg_0]
mov eax,99999999h
imul ecx
sar edx,1
mov eax,edx
shr eax,1Fh
add edx,eax
push edx
push offset Format ;"%d"
call _printf
add esp,8
xor eax,eax
retn
_main endp
代码体现的表达式:
edx=ecx∗eax233|o|=2n232−c=233232−99999999h=4.999999……≈5
于是反推出C代码为:
printf("%d",argc/-5);
小结:
mov eax,MagicNumber(>=0x7fffffff)
imul reg
sar edx,...
mov reg,edx
shr reg,1Fh
add edx,reg
如遇到以上指令序列,则基本可判定是除法优化后的代码。MagicNumber≥80000000h,编译器在imul和sar之间未产生任何调整指令,故可认定除数为负。*统计右移总次数以确定公式中的n值,然后使用公式|o|=2n232−c(魔数)求解除数|o|,即可恢复除法原型。
情景2(MagicNumber⩽0x7FFFFFFF)
当MagicNumber<=7FFFFFFFFh时,除数也有可能是负数。(为什么会有这种情景?这样可以使数学式中c=2no表示的范围更大)
为了使x∗c2n中c=2no(o<0)表示更小的负数,编译器用类似3中情景3的方法,
设p=−o,(p>0)xo=−xp⇒−(x∗2np∗12n)⇒−((x∗(2np−232)+x∗232)∗12n)⇒(x∗(−(2np−232))−x∗232)∗12n⇒(x∗(232−2n|o|)−x∗232)∗12n⇒x∗c−2322n(对应代码转换的公式)−c=2np−232<0(3中情景3的y真)c=−(2np−232)=232−2np=232−2n|o|>0
_main proc near
arg_0= dword ptr 4
mov ecx,[esp+arg_0]
mov eax,6DB6DB6Dh
imul ecx
sub edx,ecx
sar edx,2
mov eax,edx
shr eax,1Fh
add edx,eax
push edx
push offset Format ;"%d"
call _printf
add esp,8
retn
_main endp
上面代码转换成公式:
edx=edx∗eax232−ecx22=ecx∗eax−232∗ecx234=ecx∗eax−232234ecxo=ecx∗eax−232234|o|=2n232−c=234232−6DB6DB6Dh=6.999999……≈7
于是反推C代码:
printf("%d",argc/-7);
小结:
mov eax,MagicNumber(<=7fffffffh)
imul reg
sub edx,reg
sar edx,...
mov reg,edx
shr reg,1Fh
add edx,reg
当遇到以上指令序列时,基本判定是除法优化后的代码。MagicNumber⩽7fffffffh,imul和sar之间有sub指令来调整乘积,故认定除数为负数。统计右移总次数以确定公式中的n值,然后使用公式|o|=2n232−c(魔数)求解除数|o|,即可恢复除法原型。
如何从汇编代码中区分正负除数?
∙ 当MagicNumber最高位为1时(≥80000000h),对于正除数,MagicNumber为原码形式,编译器会在imul和sar之间产生调整作用的add指令。如果没有,则MagicNumber为补码形式。
∙ 当MagicNumber最高位为0时(⩽7FFFFFFFh),对于负除数,编译器会在imul和sar之间产生调整作用的sub指令。
这些应作为区分负除数的重要依据。