除法运算逆向分析


  • 除法运算逆向分析
  • 相关数学公式
  • 除数为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,edxshr 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,edxshr ecx,1add ecx,edxshr 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指令。
这些应作为区分负除数的重要依据。