strcpy汇编代码分析


前言

在学习Windows异常机制的时候,编译书中的测试代码后发现源代码中调用了strcpy但是在ida或者od中却没有看到类似call的指令调用函数,仔细分析后发现实际上编译器通过简短的几个指令完成的整个复制过程。以前没有发现这种情况,所以拿出来记录一下。

c语言源码

#include <windows.h>
char shellcode[] = "\x90\x90\x90...";
DWORD MyExceptionhandler(void)
{
    printf("got an exception, press Enter to kill proces!\n");
    getchar();
    ExitProcess(1);
}

void test(char * input)
{
    char buf[200];
    int zero=0;
    __asm int 3;
    __try{
        strcpy(buf, input);
        zero=4/zero;
    }
    __except(MyExceptionhandler()){}
}
int main(void)
{
    test(shellcode);
}

这段代码实际上是通过一个缓冲区溢出来修改存储在栈上的异常处理函数句柄,完成程序流程的劫持。具体原理及过程不属于本文的内容,所以这里也不再详细的介绍。

编译环境说明:

Windows 2000, Visual C++ 6.0, 默认编译选项下的release版本

编译后的程序

在分析实际代码之前,先回顾一下几个汇编指令

几个汇编指令

REP, REPNE, REPNZ, REPE, REPZ 重复字符串操作前缀

按计数寄存器 ((E)CX) 中指定的次数重复执行字符串指令,或是重复到 ZF 标志不再满足指定的条件。REP 前缀一次只能应用于一条字符串指令

在每次迭代之后,REPE、REPNE、REPZ 及 REPNZ 前缀还会检查 ZF 标志的状态,如果 ZF 标志未处于指定的状态,则终止重复循环。

各前缀的终止条件如下:

重复前缀 终止条件1 终止条件2
REP ECX=0
REPE/REPZ ECX=0 ZF=0
REPNE/REPNZ ECX=0 ZF=1

SCASB指令

cmp byte ptr [edi],al  //对标志位的影响相当于sub指令
//同时还会修改寄存器EDI的值:如果标志DF为0,则 inc EDI;如果DF为1,则 dec EDI

SCASB指令配合REP指令,可以完成对字符串长度的计算或者定位到特定的字符

movsb, movsw, movsd指令

从源地址向目的地址传送数据,这里需要注意默认的源地址和目的地址

指令 源地址 目的地址 作用 ESI(SI) EDI(DI)
MOVSB DS:SI ES:DI 传送一个字节 加减1 加减1
MOVSW DS:SI ES:DI 传送一个字 加减1 加减1
MOVSD DS:SI ES:DI 传送一个双字 加减1 加减1

当DF=0 时,表示正向传送,传送之后SI和DI(或者ESI和EDI)的值会增加;

当DF=1 时,表示反向传送,传送之后SI和DI(或者ESI和EDI)的值会减小;

OD中的汇编代码

0040104A   C745 FC 00000000 MOV DWORD PTR SS:[EBP-4],0
00401051   8D95 20FFFFFF    LEA EDX,DWORD PTR SS:[EBP-E0]
00401057   8B7D 08          MOV EDI,DWORD PTR SS:[EBP+8]
0040105A   83C9 FF          OR ECX,FFFFFFFF
0040105D   33C0             XOR EAX,EAX
0040105F   F2:AE            REPNE SCAS BYTE PTR ES:[EDI]
00401061   F7D1             NOT ECX
00401063   2BF9             SUB EDI,ECX
00401065   8BC1             MOV EAX,ECX
00401067   8BF7             MOV ESI,EDI
00401069   8BFA             MOV EDI,EDX
0040106B   C1E9 02          SHR ECX,2
0040106E   F3:A5            REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
00401070   8BC8             MOV ECX,EAX
00401072   83E1 03          AND ECX,3
00401075   F3:A4            REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]

上述代码片段仅是strcpy函数对应的部分,但从OD里面得到的汇编代码来看,实际上也还容易理解,去参数之后,计算源字符串的长度,然后按字复制到目的地址,余下的单字节的字符在按照字节复制到目的地址。

在IDA中打开后,汇编代码就进一步变得“精简”了

IDA中的汇编代码

.text:0040104A                 mov     [ebp+ms_exc.registration.TryLevel], 0
.text:00401051                 lea     edx, [ebp+var_E0] ;目的地址
.text:00401057                 mov     edi, [ebp+arg_0] ;源字符串
.text:0040105A                 or      ecx, 0FFFFFFFFh  
;ecx寄存器用作记录源字符串的长度因为rep指令的潜在操作会对ecx做减操作所以这里通过大值做减法然后取非获得字符串长度
.text:0040105D                 xor     eax, eax
;eax清零因为字符串都是以'\x00'结尾而rsacsb是和al做比较当要匹配特定字符时将ax寄存器的值指定为特定值即可
.text:0040105F                 repne scasb
.text:00401061                 not     ecx  ;取非之后ecx的值就是源字符串的长度了
.text:00401063                 sub     edi, ecx
.text:00401065                 mov     eax, ecx
.text:00401067                 mov     esi, edi
.text:00401069                 mov     edi, edx
.text:0040106B                 shr     ecx, 2   ;因为先按字传送所所以这里字符串长度除以2
.text:0040106E                 rep movsd
.text:00401070                 mov     ecx, eax
.text:00401072                 and     ecx, 3
.text:00401075                 rep movsb    ;这里是对字符串长度为奇数的情况处理即将单出来的字符按字节传送

个人人为,在IDA中看到的汇编代码更加精简,不过如果对这些指令不够熟悉的话,在阅读上会有不少难处。

编译器采用这种形式避免了函数栈帧的产生,在一定程度上可以加快程序执行速度吧(瞎猜的。。。。)