逆向-字符串
Posted 嘻嘻兮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-字符串相关的知识,希望对你有一定的参考价值。
在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,这样子就完成了一个无分支的优化。
以上是关于逆向-字符串的主要内容,如果未能解决你的问题,请参考以下文章