char* strcpy(char * destination, const char * source)
strcpy函数将source所指向的字符串拷贝到destination,拷贝内容是从source所指向的地址开始,直到遇到 为止。这就意味着,拷贝的内容有可能超过destination的空间。如果destination是通过new在堆中分配内存,将导致堆破坏。如果destination是局部变量,将导致栈破坏。不管是堆破坏还是栈破坏,最终大概率将导致程序异常崩溃,这种情况有可能会在程序运行一段时间后才出现崩溃,所以排查相当困难。强烈建议使用微软提供的安全版本strcpy_s,指定destination的空间长度。sprintf也存在同样的问题。
由于strcpy会导致栈破坏,如果source是来源于用户输入的数据,用户就可以构造特殊的数据让服务端执行,进行恶意攻击,攻击的原理是:程序在调用函数时,call指令会将下一条指令先存入栈,然后执行函数,函数执行完后,ret指令会取出之前call指令存放的下一条指令,继续执行,于是通过栈破坏,替换下一条指令为自己构造的恶意代码(比如调起cmd,执行文件删除)。
接下来,我将演示如何通过strcpy的漏洞代码,改变程序的执行流程。先看一段简单的服务端代码。
代码语言:javascript复制void ProcessData(const char data[])
{
char buf[4];
strcpy(buf, data);
}
int main()
{
const char* data = GetDataFromUserInput();
ProcessData(data);
return 0;
}
此代码片段,演示程序通过GetDataFromUserInput()函数从用户输入获得数据(可以是通过网络从前端发送过来),然后处理数据ProcessData(),该函数通过strcpy拷贝data到局部变量buf。如前面所说,strcpy调用前未对data长度进行校验,可能会导致栈破坏。
程序调用ProcessData函数返回后,要继续执行这行代码return 0; 所以在进入ProcessData()函数前需要先将return 0; 的指令存在栈上(这是由汇编指令call所做的)。进入ProcessData()后,buf是局部变量占4个字节也是在栈上,通过计算return 0的指令所在的栈上地址与buf的地址之间的偏移后,在data对应的位置填入攻击函数的地址,等strcpy执行完,下一条指令return 0;就会变成攻击函数的地址,完成拦截。
return 0的指令所在的栈上地址与buf的地址之间的偏移,怎么计算呢?对汇编代码熟悉的牛人,一眼就能计算出来。接下来,我演示下如何通过汇编代码,计算他们之间的偏移值。本人使用Visual Studio 2013调试。
这是main函数的汇编代码,我在调用ProcessData()这行代码设置断点,如下图所示。
此时,esp寄存器(指向栈顶地址)通过监视器知道是0x0034fa30
mov eax, dword ptr [data] 将data地址类型const char* 转成dword ptr,存在寄存器eax
push eax 将eax存在栈上,也就是将data地址存在栈上(函数调用前都需要将参数入栈,data是ProcessData的参数,所以得入栈),此时栈顶0x0034fa30的值是data的地址,esp变成0x0034fa2c(栈地址是从高到低,所以esp要减4)
call ProcessData 调用ProcessData,可以按F11调试进入ProcessData,call下一条指令地址是00E51725 add esp, 4
按F11进入ProcessData(),如下是ProcessData()的汇编代码。
此时通过监视器看到esp地址是0x0034fa28,比上面push eax指令执行后esp值0x0034fa2c又少了4个字节,就是因为call指令将返回指令00E51725 add esp, 4的地址00E51725存入栈上。通过内存查看器可以证实。
从上面调试可知道,函数调用返回地址存在栈0x0034fa28上。
继续调试,断点设置在call dword ptr ds:[0E54100h]这行(也就是strcpy(buf,data)这行代码),按F5执行。鼠标移到buf变量上,知道buf地址是0x0034fa20。
由此可知函数调用返回地址在栈上的位置与buf变量的位置偏移值是8(0x0034fa28 - 0x0034fa20)。于是就可以构造用户数据,完整代码如下。
代码语言:javascript复制#include <iostream>
#include<windows.h>
using namespace std;
void Attack()
{
while (true)
{
cout << "You have been attacked" << endl;
Sleep(1000);
}
}
void ProcessData(const char data[])
{
char buf[4];
strcpy(buf, data);
}
const char* GetDataFromUserInput()
{
static char data[13];
int attackAddress = (int)&Attack;
memset(data, 'a', sizeof(data));
memcpy(data 8, &attackAddress, 4);
data[12] = 0;
return data;
}
int main()
{
const char* data = GetDataFromUserInput();
ProcessData(data);
return 0;
}
GetDataFromUserInput()里面构造一个13字节的数据,内存初始化为字母a,最后一个字节设置为0(要让strcpy拷贝这13个字节,只能最后一个字节为0)。然后在偏移值8位置保存攻击函数的地址&Attack。
本演示是在Visual Studio 2013,Release配置下,关闭“启用内部函数”设置(如果不关闭,strcpy函数的汇编指令会直接内联到ProcessData()内,为了简化汇编指令,特意关闭该设置),运行结果如下图所示。