逆向-三目運算

首先,先簡單回顧一下三目運算符(條件表達式)的格式

表達式一 ? 表達式二 : 表達式三

當表達式一的結果爲真時,選擇執行表達式二,否則執行表達式三。

看完這個格式,很明顯這是一個有分支的結構,那麼編譯器會老老實實的都按分支語句進行編譯麼,下面我們還是需要來分情況討論一下。

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

所以對於高版本編譯器來說,此時還原高級代碼可能會更加的輕鬆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章