我的知识记录

digSelf

C++逆向:除法的非常规优化及其识别与还原

2021-08-21
C++逆向:除法的非常规优化及其识别与还原

本文涉及到的是除法的f非常规优化方法,包括:MagicNum溢出的情况和有符号数除以非2的幂的两种特殊情况。测试环境与编译选项均与上一篇常C/C++除法的常规优化及其识别与还原的情况相同。

具体的知识点包括:

  • 无符号数除以非2的幂的特殊情况
  • 有符号数除以非2的幂的两种特殊情况
    • 除数是正数,但是MagicNum为负
    • 除数是负数,但是MagicNum为正

无符号数除以非2的幂的特殊情况

测试代码:

int main(int argc, char* argv[])
{
	printf("%d", (unsigned int)argc / 7);
	return 0;
}

整理后对应的反汇编代码:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 mov     ecx, [esp+arg_0]
.text:00401004                 mov     eax, 24924925h
.text:00401009                 mul     ecx
.text:0040100B                 sub     ecx, edx
.text:0040100D                 shr     ecx, 1
.text:0040100F                 add     ecx, edx
.text:00401011                 shr     ecx, 2
.text:00401014                 push    ecx
.text:00401015                 push    offset s->D     ; "%d"
.text:0040101A                 call    printf
.text:0040101A
.text:0040101F                 add     esp, 8
.text:00401022                 xor     eax, eax
.text:00401024                 retn
.text:00401024
.text:00401024 _main           endp

在常规优化中,其代码定式应该是:

mov eax, MagicNum
mul x
shr edx, n

而在上述代码中,操作却不一致,这是为什么?上述操作又是在做什么?可以根据上述代码写出数学表达式进行化简来得到其结果,设ecx = arg0C = 24924925h ,有:

result=ecxecx×C2322+ecx×C23222 result = \frac{\frac{ecx - \frac{ecx \times C}{2^{32}}}{2} + \frac{ecx \times C}{2^{32}}}{2^2}

化简可得到:

result=232×ecx+ecx×C235=ecx×232+C235=ecx×M>>35 result = \frac{2^{32} \times ecx + ecx \times C }{2^{35}} = ecx \times \frac{2^{32} + C}{2^{35}} = ecx \times M >> 35

于是可以发现,其实就是编译器在计算MagicNum的时候,发现得到的结果用4个字节存放不下,多了一个进位出来,也就是2322^{32}​ 。所以在后序计算结果的时候,由于MagicNum是低4字节的结果,所以需要进行调整,在计算的时候需要先加上这个进位才能正确的得到结果。而它之所以拆分为上述情况,是因为要将除法转为周期更短的移位运算,进行了等价变换。

还原时需要将MagicNum加上2322^{32} 再进行还原即可。

有符号数除以非2的幂的两种特殊情况

出现这两种特殊情况的原因是,由于编译器在计算MagicNum的时候,是按照无符号数来进行运算的,但是在有符号数除法中需要使用有符号命令,此时得到的MagicNum可能会为负数,因此需要做调整;同理,其MagicNum本应该是负数,但是在计算过程中却被视为正数,因此也需要做调整,故有以下两种情况。

除数是正数,但是MagicNum为负

测试代码:

int main(int argc, char* argv[])
{
	printf("%d", argc / 7);
	return 0;
}

对应的反汇编结果为:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 mov     ecx, [esp+arg_0]
.text:00401004                 mov     eax, 92492493h		; MagicNum为负数,因为大于0x7fff, ffff
.text:00401009                 imul    ecx
.text:0040100B                 add     edx, ecx				; 对比之前的有符号数除以非2的幂多了这个操作
.text:0040100D                 sar     edx, 2
.text:00401010                 mov     eax, edx
.text:00401012                 shr     eax, 1Fh
.text:00401015                 add     edx, eax
.text:00401017                 push    edx
.text:00401018                 push    offset s->D     ; "%d"
.text:0040101D                 call    printf
.text:0040101D
.text:00401022                 add     esp, 8
.text:00401025                 xor     eax, eax
.text:00401027                 retn
.text:00401027
.text:00401027 _main           endp

将上述操作所描写的数学公式写出来并化简,可以得到以下式子:

result=ecx×C232+ecx22=C×ecx+232×ecx234=ecx×232C234=ecx×M>>34 result = \frac{\frac{ecx \times -C}{2^{32}} + ecx}{2^2} = \frac{-C \times ecx + 2^{32} \times ecx}{2^{34}}= ecx \times \frac{2^{32} - C}{2^{34}} = ecx \times M >> 34

其中M=232CM = 2^{32} - C,即:M是对C取补之后的结果,真是的MagicNum应该为正值。

其多添加的这一句add edx, ecx其实就是对无符号整数乘以有符号数做调整,而还原方法是直接按照正数的方法进行还原即可。

除数是负数,但是MagicNum为正

测试代码为:

int main(int argc, char* argv[])
{
	printf("%d", argc / -7);
	return 0;
}

反汇编结果为:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 mov     ecx, [esp+arg_0]
.text:00401004                 mov     eax, 6DB6DB6Dh		; MagicNum为正数
.text:00401009                 imul    ecx
.text:0040100B                 sub     edx, ecx				; 多出了一个减法操作
.text:0040100D                 sar     edx, 2
.text:00401010                 mov     eax, edx
.text:00401012                 shr     eax, 1Fh
.text:00401015                 add     edx, eax
.text:00401017                 push    edx
.text:00401018                 push    offset s->D     ; "%d"
.text:0040101D                 call    printf
.text:0040101D
.text:00401022                 add     esp, 8
.text:00401025                 xor     eax, eax
.text:00401027                 retn
.text:00401027
.text:00401027 _main           endp

同样,将上述运算写为数学表达式:

result=ecx×C232ecx22=ecx×C232×ecx234=ecx×C232234=ecx×M>>34 result = \frac{\frac{ecx \times C}{2^{32}} - ecx}{2^2} = \frac{ecx \times C - 2^{32} \times ecx}{2^{34}} = ecx \times \frac{C - 2^{32}}{2^{34}} = ecx \times M >> 34

其中M=232+CM = -2^{32} + C,即:真正的M是一个负值。

其还原方法还是先求补,再还原。

整体总结

总结一下识别方法:

  1. 在识别时,先观察是否有加1做调整的操作,如果有且运算指令使用的是有符号运算指令,则被除数是有符号数。
  2. 查看MagicNum是否是负数(大于等于0x8000,0000即为负数)。如果是负数,且未见加调整,则除数是负数;如果是正数,且有减调整,则除数是负数,需要先求补再进行还原;同理,如果是负数,且有加调整,除数是正数;如果是正数,且未有减调整,则除数是正数

对于还原方法,首先判断除数是正数还是负数来进行还原:

  1. 正数还原除数

2nM \frac{2^n}{M}

  1. 负数还原除数

2n232M \frac{2^n}{2^{32} - M}

  1. 溢出还原除数

2n232+M \frac{2^n}{2^{32} + M}

  • 0