逆向-三目运算

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-三目运算相关的知识,希望对你有一定的参考价值。

首先,先简单回顾一下三目运算符(条件表达式)的格式

表达式一 ? 表达式二 : 表达式三

当表达式一的结果为真时,选择执行表达式二,否则执行表达式三。

看完这个格式,很明显这是一个有分支的结构,那么编译器会老老实实的都按分支语句进行编译么,下面我们还是需要来分情况讨论一下。

1.当表达式二或表达式三不为常量
2.表达式二或表达式三为常量
    2.1 当表达式一为0的等值比较,表达式二和表达式三差值为一
    2.2 当表达式一扩展为非0等值比较,表达式2和3扩展为其他任意常量时
    2.3 当表达式一扩展为区间比较,表达式2和3为其他任意常量时

OK,我们下面主要来看一下下面的这几种情况,首先,来看情况一,这种情况是无优化的情况,对应的汇编代码就是一个分支结构

int main(int argc, char* argv[])

	printf("%d",argc == 0 ? (int)argv : -1);
	return 0;

对应的汇编代码

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 test    eax, eax
.text:00401006                 jnz     short loc_40101D
.text:00401008                 mov     eax, [esp+argv]  ;argc为0的情况,直接拿argv
.text:0040100C                 push    eax
.text:0040100D                 push    offset unk_407030
.text:00401012                 call    sub_401040
.text:00401017                 add     esp, 8
.text:0040101A                 xor     eax, eax
.text:0040101C                 retn
.text:0040101D ; ---------------------------------------------------------------------------
.text:0040101D
.text:0040101D loc_40101D:                             ; CODE XREF: _main+6↑j
.text:0040101D                 or      eax, 0FFFFFFFFh  ;当argc不为0时,eax = -1
.text:00401020                 push    eax
.text:00401021                 push    offset unk_407030
.text:00401026                 call    sub_401040
.text:0040102B                 add     esp, 8
.text:0040102E                 xor     eax, eax
.text:00401030                 retn

这种情况没啥好记录的,所以主要看下面为常量的优化,对于常量的优化,debug和release下都是相同的。

 

首先来看2.1,这种情况一定要好好理解,因为这种情况其实可以说是下面情况的原型,后面两种都是在此基础上进行扩展的而已。先来看例子

int main(int argc, char* argv[])

    printf("%d",argc == 0 ? 0 : -1); //例一
    printf("%d",argc == 0 ? 1 : 0); //例二
    return 0;

这里的例子有2个,先来看例一的反汇编

.text:00401001                 mov     esi, [esp+4+argc]
.text:00401005                 mov     eax, esi
.text:00401007                 neg     eax  ;neg指令是对其求补(0-eax)
.text:00401009                 sbb     eax, eax  ;sbb r1,r2  相当于 r1 = r1 - r2 - CF
.text:0040100B                 push    eax

对于指令的说明写在上面的注释中了,对于sbb指令,可以发现这里是自己减去自己,那么肯定结果为一,所以决定sbb的值肯定在于CF位是多少,而CF位又取决于上一行的取补指令,下面我们来分支讨论一下CF位的情况

.text:00401007                 neg     eax       ;if eax == 0 CF = 0 else CF = 1
.text:00401009                 sbb     eax, eax  ;if CF == 0  eax = 0 else eax = -1

当eax为0时,其求补结果为0,所以CF位不会进位(CF=0),当CF不会进位时,其结果自然为零了。反之当eax不为零时,对其求补必然会产生进位,因为这里求补的本质是用零去减该数,那么很明显只有零值才不会产生进/借位。

下面来看例二

.text:00401016                 xor     ecx, ecx  ;需要注意这里要清0
.text:00401018                 test    esi, esi
.text:0040101A                 setz    cl  ;当ZF=1时,cl=1  else cl = 0
.text:0040101D                 push    ecx


setxx r8
	当条件(标志寄存器)满足,r8寄存器会被设值1

对于setxx类型的指令上面解释了,这里汇编就比较明显了,当esi的值为0时,cl为1,那么反之cl等于0。

 

下面再来看2.2的情况,这种情况其实相当于对于上面情况的扩展

int main(int argc, char* argv[])

    printf("%d",argc == 77 ? 88 : 66);
    return 0;

对应的汇编代码

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 sub     eax, 4Dh  //4dh=77
.text:00401007                 neg     eax
.text:00401009                 sbb     eax, eax  ;这里构造结果 0 和 0xFFFFFFFF(-1)
.text:0040100B                 and     al, 0EAh  //0EAh=-22
.text:0040100D                 add     eax, 58h  //58h=88
.text:00401010                 push    eax

首先对于401004处的汇编代码,减去一个77,相当于平移对齐到零值,这样子就转化为上面2.1的例一情况了,也就是减去77后,其值为零,那么必然结果为真,反之其值必然不等于77(为假)。

下面对于40100B和40100D处的代码进行分析,这里还是需要分支来讨论,毕竟上面sbb的结果就是一个分支情况

.text:00401009                 sbb     eax, eax ;eax = 0 || 0xFFFFFFFF
.text:0040100B                 and     al, 0EAh ;if eax == 0 eax = 0 else eax = 0xFFFFFFEA
.text:0040100D                 add     eax, 58h ; eax += 58h

if eax == 0
    eax = 0 + 58h = 58h = 88
else
    eax = 0xFFFFFFEA + 58h = 42h = 66

通过最后的分析也能得出当eax等于零(相当于等于77),其值为88,否则其值为66。

 

最后再来分析一下最后这种情况,这种情况其实是对上面案例的综合体现,其实通过上面的分析,可以发现编译器其实优化的特点就是构造出一个零值和一个0xFFFFFFFF值(-1),然后对其做and和add的操作。

int main(int argc, char* argv[])

    printf("%d",argc >= 66 ? 77 : 55);
    return 0;

对应的反汇编代码

.text:00401004                 xor     eax, eax  ;注意这里eax需要清0
.text:00401006                 cmp     edx, 42h ;42h=66
.text:00401009                 setl    al  ;当edx小于66时 al=1 else al=0
.text:0040100C                 dec     eax
.text:0040100D                 and     eax, 16h ;16h=22
.text:00401010                 add     eax, 37h ;37h=55
.text:00401013                 push    eax

下面我们先来找出上面所说的编译器优化的特点-构造0和-1

.text:00401009                 setl    al  ;if edx < 66 al=1 else al=0
.text:0040100C                 dec     eax ;if edx < 66 eax = 0 else eax = 0xFFFFFFFF

通过了上面两行的代码执行后,这里子就构造出了0和-1值,对于剩下的情况其实就和2.2一样了,这里简单过一下

.text:0040100C                 dec     eax  此时 eax = 0 || -1
.text:0040100D                 and     eax, 16h ;16h=22
.text:00401010                 add     eax, 37h ;37h=55

if eax == 0  ;也就是小于66的情况
    eax = 0 + 37h = 37h = 55
else ;大于等于66的情况
    eax = 16h + 37h = 4dh = 77

看完上面的分析,其实按照我们的分析还原一下

argc < 66 ? 55 : 77

可以发现,编译器生成的条件表达和源代码是相反的,不过这样也不影响代码的还原,比较只是顺序交换了一下而已。

对于这种情况,高版本还使用了CMOVXX系列的汇编指令进行优化

cmovxx S,R
	当条件(标志寄存器)满足,S=R

如对上述的代码,使用vs2015编译出的汇编如下:

.text:00401043                 cmp     [ebp+argc], 42h  //66
.text:00401047                 mov     ecx, 4Dh  //77
.text:0040104C                 mov     eax, 37h  //55
.text:00401051                 cmovge  eax, ecx  //大于等于条件满足执行
.text:00401054                 push    eax

argc >= 66 
    eax = ecx = 77 
else 
    eax = 55

所以对于高版本编译器来说,此时还原高级代码可能会更加的轻松。

以上是关于逆向-三目运算的主要内容,如果未能解决你的问题,请参考以下文章

Java中的三目运算符可能出现的问题

三目运算符

java三目运算符

c语言中,三木运算符和if语句哪个效率更高一些?

三目运算符

ARM逆向-表达式求值