逆向-字符串

在C語言中,字符串其實就是一個特殊的數組,一個以零結尾的字符數組而已,所以對於定位字符串中的字符的話可以參考上一篇博客-數組。這篇博客主要用於記錄字符串的一些操作函數,以便於在逆向識別的時候可以順利的還原爲函數。這裏因爲在release版下其字符串操作函數會內嵌彙編,也就是說並不是使用call來調用函數,所以我們需要來逆向識別一下。

所以下面我們討論的都是release版下的情況,並且使用的編譯器爲vc6.0,爲什麼使用這款編譯器呢,因爲這款編譯器編譯出來的字符串操作函數是有無分支優化的,而對於高版本的vs來說,使用的就是我們平常的邏輯(循環處理函數),高版本比較好識別。雖然vc6.0版本比較老了,就算我們平常遇不到,但是對於無分支優化的手段還是很值得我們去學習的,體會一下彙編的藝術。

下面我們主要來看一下以下幾個函數的實現

strlen
strcpy
memcpy
strcmp

先來看第一個strlen

int main(int argc, char* argv[])
{
    return strlen("Hello World!\n");
}

對應的彙編代碼

.text:00401000                 push    edi
.text:00401001                 mov     edi, offset aHelloWorld ; "Hello World!\n"
.text:00401006                 or      ecx, 0FFFFFFFFh  //相當於ecx=-1,無符號下爲最大的整數
.text:00401009                 xor     eax, eax //eax=0
.text:0040100B                 repne scasb  //字符串掃描函數,當ecx不爲0或者和al比較不相等時繼續
.text:0040100D                 not     ecx
.text:0040100F                 dec     ecx
.text:00401010                 pop     edi
.text:00401011                 mov     eax, ecx
.text:00401013                 retn

對於上面的彙編代碼先簡單的分析一下,可以發現其並未用到循環,但是從其字符串掃描函數來看,其相當於做了一個循環(處理器有優化),對於上面的字符串掃描函數,也就是說當遇到字符串的結尾字符0時便會退出,而在掃描過程中ecx也會隨之一直減。說到這裏其實大家心裏應該就有個概念了,此時減去的ecx的個數應該就是字符串長度,但是此時ecx也包括了最後的結尾0,而字符串長度是不包含結尾0的,所以在最後一處又使用dec減一獲取正確的字符串長度。

ecx = 0xFFFFFFFF - len - 1 (末尾0)
ecx = -2-len
len = -2-ecx
len = -2 + neg(ecx)
len = -2 + not(ecx) + 1
len = not(ecx) - 1

以上就是具體的推倒過程,也就是爲什麼最後需要取反減一。

 

下面再來看strcpy函數

int main(int argc, char* argv[])
{
    strcpy(argv[0],"Hello World!\n");
    return;
}

對應的彙編代碼

.text:00401000                 push    esi
.text:00401001                 push    edi
.text:00401002                 mov     edi, offset aHelloWorld ; "Hello World!\n"
.text:00401007                 or      ecx, 0FFFFFFFFh
.text:0040100A                 xor     eax, eax
.text:0040100C                 mov     edx, [esp+8+argv]
.text:00401010                 repne scasb
.text:00401012                 not     ecx  //這裏ecx = sizeof "xxx"
.text:00401014                 sub     edi, ecx  //edi復位
.text:00401016                 mov     eax, ecx
.text:00401018                 mov     esi, edi
.text:0040101A                 mov     edi, [edx]
.text:0040101C                 shr     ecx, 2  //>>2相當於除以4
.text:0040101F                 rep movsd  //4字節拷貝
.text:00401021                 mov     ecx, eax
.text:00401023                 and     ecx, 3 //這裏就是%4
.text:00401026                 rep movsb //剩餘的按照字節拷貝
.text:00401028                 pop     edi
.text:00401029                 pop     esi
.text:0040102A                 retn

可以看出來,strcpy首先使用了strlen的彙編代碼求出長度,因爲一旦長度已知,那麼拷貝多少就自然可以確定了,那麼仔細和上面的strlen的彙編代碼觀察,可以發現其少了最後一行dec的代碼,這裏爲什麼可以缺省呢?通過上面strlen的分析可知,其dec減一的目的是去除最後的零結尾字符。那麼對於strcpy拷貝函數而言,我們拷貝的時候是不是需要連最後的結尾字符零也需要拷貝呢,所以這裏相當於求的是字符串的size。

獲取其size後,在拷貝字符串時做了一個優化,在平常的邏輯中,我們只需寫一個for循環一個一個字節拷貝即可,這的優化直接使用的4字節進行拷貝,因爲有可能其size並不一定爲4的整數倍,所以最後求一個餘數按字節拷貝。

等價的高級代碼如下

    int size = strlen("Hello World!\n") + 1; //+1是需要拷貝結尾0
    int count = size / 4; //先按四字節拷貝,計算需要拷貝多少次
    for(int i=0;i < count;++i)
    {
        //... 四字節拷貝
    }
    count = size % 4; //剩餘未拷貝的字節數
    for(int i=0;i < count;++i)
    {
        //... 一字節拷貝
    }

可以發現,這樣子拷貝的循環次數很明顯會比單字節單字節拷貝少的多。

 

下面再來看一下memcpy函數,其實明白了上面的函數,這個就很好理解了,因爲其套路差不多

int main(int argc, char* argv[])
{
    memcpy(argv[0],argv[1],argc);
    return;
}

對應的彙編代碼

.text:00401000                 mov     eax, [esp+argv]
.text:00401004                 mov     ecx, [esp+argc] //argc就是需要拷貝的總個數
.text:00401008                 push    esi
.text:00401009                 push    edi
.text:0040100A                 mov     esi, [eax+4]
.text:0040100D                 mov     edi, [eax]
.text:0040100F                 mov     eax, ecx
.text:00401011                 shr     ecx, 2  //除以4計算拷貝次數
.text:00401014                 rep movsd
.text:00401016                 mov     ecx, eax
.text:00401018                 and     ecx, 3 //剩餘按單字節拷貝
.text:0040101B                 rep movsb
.text:0040101D                 pop     edi
.text:0040101E                 pop     esi
.text:0040101F                 retn

可以發現這裏的套路和上面的strcpy一模一樣,所以就不多說了。

 

下面來一下最後這個strcmp函數

int main(int argc, char* argv[])
{
    return strcmp(argv[0],argv[1]);
}

對於這個函數,我們需要額外的注意一下其返回值,先使用msdn來查看一下文檔

可以看到對於字符串一小於字符串二,則其值小於零,而大於則返回大於零。

那麼其vs編譯器的產品中,其如果小於,則返回-1,大於則返回1。所以當我們寫代碼時考慮兼容性時,判斷條件需要注意。

int res = strcmp(argv[0],argv[1]);
if(res < 0)
    //...小於
else if(res > 0)
    //...大於
else
    //...相對


切不可如下編碼
if(res == -1)
    //...小於
else if(res == 1)
    //...大於
else
    //...相對

如果寫成了下面那種方式,那就跟着微軟混吧。好了我們先來說一個標準的情況,其實對於標準的情況而言,其返回值是很好設計的

return argv[0][i]-argv[0][i] 
//當第i位不相等時直接返回其差值即可,如果argv[0][i]大於argv[0][i],那麼可以確保結果大於0,反之同理

那麼對於微軟的編譯器,其返回的是一個定值,也就是-1和1,普通情況我們想到的就是使用if判斷了,下面來看一下反彙編代碼來觀察是否這樣

.text:00401000                 mov     eax, [esp+argv]
.text:00401004                 push    ebx
.text:00401005                 push    esi
.text:00401006                 mov     esi, [eax+4]  //arv[1]
.text:00401009                 mov     eax, [eax]  //arv[0]
.text:0040100B
.text:0040100B loc_40100B:                             ; CODE XREF: _main+2D↓j
.text:0040100B                 mov     dl, [eax]
.text:0040100D                 mov     bl, [esi]
.text:0040100F                 mov     cl, dl
.text:00401011                 cmp     dl, bl
.text:00401013                 jnz     short loc_401034  //不相等則跳轉到loc_401034進行比較
.text:00401015                 test    cl, cl
.text:00401017                 jz      short loc_40102F //相等並且其值爲0說明兩個字符串都結尾了
.text:00401019                 mov     dl, [eax+1] //同樣的套路,這裏相當於一個循環裏面比較連續的兩個字符
.text:0040101C                 mov     bl, [esi+1]
.text:0040101F                 mov     cl, dl
.text:00401021                 cmp     dl, bl
.text:00401023                 jnz     short loc_401034
.text:00401025                 add     eax, 2
.text:00401028                 add     esi, 2
.text:0040102B                 test    cl, cl
.text:0040102D                 jnz     short loc_40100B
.text:0040102F
.text:0040102F loc_40102F:                             ; CODE XREF: _main+17↑j
.text:0040102F                 pop     esi
.text:00401030                 xor     eax, eax
.text:00401032                 pop     ebx
.text:00401033                 retn
.text:00401034 ; ---------------------------------------------------------------------------
.text:00401034 //這裏是不相等返回的情況,可以發現是一個無分支的優化
.text:00401034 loc_401034:                             ; CODE XREF: _main+13↑j
.text:00401034                                         ; _main+23↑j
.text:00401034                 sbb     eax, eax
.text:00401036                 pop     esi //這裏是流水線的調整,不影響結果,主要是上行代碼和下行代碼
.text:00401037                 sbb     eax, 0FFFFFFFFh
.text:0040103A                 pop     ebx
.text:0040103B                 retn

下面就具體來看一下這個無分支的優化,其實對於研究這類的無分支優化,只需分情況拿來討論即可。

由於cmp的比較會影響其cf位,如果小於則cf=1,否則cf=0
// .text:00401030                 sbb     eax, eax  // if cf == 0 eax = 0,else eax = -1
// .text:00401032                 or      eax, 1   //  if cf == 0 eax = 1,else eax = -1

可以發現在cf爲0的情況下,最終eax的值爲1,而cf爲1的情況下,eax的值一直爲-1,這樣子就完成了一個無分支的優化。

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