逆向-字符串

在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,这样子就完成了一个无分支的优化。

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