作者:cq674350529
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
之前在瀏覽群暉官方的安全公告時,翻到一個Critical級別的歷史漏洞Synology-SA-18:64。根據漏洞公告,該漏洞存在于群暉的DSM(DiskStation Manager)中,允許遠程的攻擊者在受影響的設備上實現任意代碼執行。對群暉NAS設備有所了解的讀者可能知道,默認條件下能用來在群暉NAS上實現遠程代碼執行的漏洞很少,有公開信息的可能就是與Pwn2Own比賽相關的幾個。由于該漏洞公告中沒有更多的信息,于是打算通過補丁比對的方式來定位和分析該公告中提及的漏洞。
環境準備
群暉環境的搭建可參考之前的文章《A Journey into Synology NAS 系列一: 群暉NAS介紹》,這里不再贅述。根據群暉的安全公告,以DSM 6.1為例,DSM 6.1.7-15284-3以下的版本均受該漏洞影響,由于手邊有一個DSM 6.1.7的虛擬機,故這里基于DSM 6.1.7-15284版本進行分析。
補丁比對
首先對群暉的DSM更新版本進行簡單說明,方便后續進行補丁比對。以DSM 6.1.7版本為例,根據其發行說明,存在1個大版本6.1.7-15284和3個小版本6.1.7-15284 Update 1、6.1.7-15284 Update 2及6.1.7-15284 Update 3。其中,大版本6.1.7-15284對應初始版本,其鏡像文件中包含完整的系統文件,而后續更新的小版本則只包含與更新相關的文件。另外,Update 2版本中包含Update 1中的更新,Update 3中也包含Update 2中的更新,也就是說最后1個小版本Update 3包含了全部的更新。
從群暉官方的鏡像倉庫中下載6.1.7-15284、6.1.7-15284-2和6.1.7-15284-3這三個版本對應的pat文件。在Update x版本的pat文件中除了包含與更新相關的模塊外,還有一個描述文件DSM-Security.json。比對6.1.7-15284-2和6.1.7-15284-3這2個版本的描述文件,如下。

可以看到,在6.1.7-15284 Update 3中更新的模塊為libfindhost與netatalk-3.x,與對應版本發行說明中的信息一致。

借助Bindiff插件對版本6.1.7-15284和6.1.7-15284 Update 3中的libfindhost模塊進行比對,如下。可以看到,主要的差異在函數FHOSTPacketRead()中。后面的其他函數很短,基本上就1~2個block,可忽略。

兩個版本中函數FHOSTPacketRead()內的主要差異如下,其中在6.1.7-15284 Update 3中新增加了3個block。

對應的偽代碼如下。可以看到,在6.1.7-15284 Update 3中,主要增加了對變量v34的額外校驗,而該變量會用在后續的函數調用中。因此,猜測漏洞與v34有關。

漏洞分析
libfindhost.so主要是與findhostd服務相關,用于在局域網內通過Synology Assistant工具搜索、配置和管理對應的NAS設備,關于findhostd服務及協議格式可參考之前的文件《A Journey into Synology NAS 系列二: findhostd服務分析》。其中,發送數據包的開始部分為magic (\x12\x34\x56\x78\x53\x59\x4e\x4f),剩余部分由一系列的TLV組成,TLV分別對應pkt_id、data_length和data。
另外,在libfindhost.so中存在一大段與協議格式相關的數據grgfieldAttribs,表明消息剩余部分的格式和含義。具體地,下圖右側中的每一行對應結構pkt_item,其包含6個字段。其中,pkt_id字段表明對應數據的含義,如數據包類型、用戶名、mac地址等;offset字段對應將數據放到內部緩沖區的起始偏移;max_length字段則表示對應數據的最大長度。
實際上,
libfindhost.so中的grgfieldAttribs,每一個pkt_item包含8個字段;而在Synology Assistant中,每一個pkt_item包含6個字段。不過,重點的字段應該是前幾個,故這里暫且只關注前6個字段。

findhostd進程會監聽9999/udp, 9998/udp, 9997/udp等端口,其會調用FHOSTPacketRead()來對接收的數據包進行初步校驗和解析。以DSM 6.1.7-15284版本為例, FHOSTPacketRead()的部分代碼如下。首先,在(1)處會校驗接收數據包的頭部,校驗通過的話程序流程會到達(2),在while循環中依次對剩余部分的pkt_item進行處理。在(2)處會從數據包中讀取對應的pkt_id,之后在grgfieldAttribs中通過二分法查找對應的pkt_item,查找成功的話程序流程會到達(3)。在(3)處會讀取對應pkt_item中的pkt_index字段,如果pkt_index=2,程序流程會到達(4)。如果v39 == pkt_id,則會執行++v36,否則在(5)處會將pkt_id賦值給v39。之后,在(6)處會根據pkt_index的值調用相應的FHOSTPacketReadXXX()。
// in libfindhost.so
__int64 FHOSTPacketRead(__int64 a1, char *recv_data, int recv_data_size, char *dst_buf)
{
v4 = a1;
// ...
remain_pkt_len = recv_data_size;
// ...
v6 = dst_buf;
memset(dst_buf, 0, 0x2F50uLL);
v7 = *(unsigned int *)FHOSTHeaderSize_ptr;
v8 = *(_DWORD *)FHOSTHeaderSize_ptr;
// ...
v37 = memcmp(recv_data, src, *(unsigned int *)FHOSTHeaderSize_ptr); // (1) check packet header
// ...
pkts_ptr = &recv_data[v7];
v33 = pkts_ptr;
v34 = remain_pkt_len - v8;
// ...
v11 = v6 + 0x74;
v12 = (char *)off_7FFFF7DD7FE0; // grgfieldAttribs
v38 = v6;
v39 = 0;
v36 = 0;
s = v11;
while ( 1 )
{
pkt_id = (unsigned __int8)*pkts_ptr; // (2) get pkt_item_id
v15 = pkts_ptr + 1;
wrap_remain_pkt_len = remain_pkt_len - 1;
v17 = 76LL;
v18 = 0LL;
wrap_pkt_id = (unsigned __int8)*pkts_ptr;
// ... try to find target pkt_item in grgfieldAttribs via binary search
pkt_index_in_table = *((_DWORD *)v21 + 1); // (3) find the target pkt_item
// ...
v31 = *((unsigned int *)v21 + 6);
if ( (_DWORD)v31 != 2 )
v31 = 1LL;
if ( pkt_index_in_table == 2 ) // index
{
if ( v39 == pkt_id ) // (4)
{
++v36; // cause out-of-bounds wirte later
}
else
{
v39 = (unsigned __int8)*pkts_ptr; // (5)
v36 = 0;
}
}
else
{
v39 = 0;
v36 = 0;
}
v24 = (*((__int64 (__fastcall **)(__int64, char *, _QWORD, char *, _QWORD, __int64, _QWORD))off_7FFFF7DD7FC0 // (6)
+ 3 * pkt_index_in_table
+ 1))(
a1,
pkts_ptr + 1,
wrap_remain_pkt_len,
&v38[*((_QWORD *)v21 + 1)], // *((_QWORD *)v21 + 1): pkt_item_offset
*((_QWORD *)v21 + 2), // *((_QWORD *)v21 + 2): pkt_item_max_len
v31,
v36);
// ...
地址off_7FFFF7DD7FC0實際指向的內容如下。其中,函數FHOSTPacketReadString()會使用傳入的第7個參數v36。另外,FHOSTPacketReadArray()內部直接調用FHOSTPacketReadString(),因此這兩個函數是等價的。
LOAD:00007FFFF7DD7FC0 off_7FFFF7DD7FC0 dq offset grgfieldParsers
LOAD:00007FFFF7DD9340 grgfieldParsers dq 0 ; DATA XREF: LOAD:off_7FFFF7DD7FC0↑o
LOAD:00007FFFF7DD9348 dq offset FHOSTPacketReadString
LOAD:00007FFFF7DD9350 dq offset FHOSTPacketWriteString
LOAD:00007FFFF7DD9358 dq 1
LOAD:00007FFFF7DD9360 dq offset FHOSTPacketReadInteger
LOAD:00007FFFF7DD9368 dq offset FHOSTPacketWriteInteger
LOAD:00007FFFF7DD9370 dq ?
LOAD:00007FFFF7DD9378 dq offset FHOSTPacketReadArray
LOAD:00007FFFF7DD9380 dq offset FHOSTPacketWriteArray
函數FHOSTPacketReadString()的部分代碼如下。正常情況下,程序流程會到達(7)處,讀取數據包中對應data_length字段,如果其值小于剩余數據包的總長度,程序流程會到達(8)。如果(8)處的條件成立,在(9)處會調用snprintf()將對應的data拷貝到內部緩沖區的指定偏移處,其中snprintf()的第1個參數為(char *)(a4 + a7 * pkt_max_length),用到了傳進來的v36/a7參數。
__int64 FHOSTPacketReadString(__int64 a1, _BYTE *a2, signed int remain_pkt_length, __int64 a4, unsigned __int64 pkt_max_length, __int64 a6, unsigned int a7)
{
// ...
if ( remain_pkt_length > 0 )
{
data_length = (unsigned __int8)*a2; // (7) get data_length
v8 = 0;
if ( remain_pkt_length > (int)data_length )
{
LOBYTE(v8) = 1;
if ( *a2 )
{
LOBYTE(v8) = 0;
if ( data_length < pkt_max_length ) // (8)
{
v8 = data_length + 1;
snprintf((char *)(a4 + a7 * pkt_max_length), (int)data_length + 1, "%s", a2 + 1); // (9) out-of-bounds write
}
}
}
// ...
回到前面的(4)/(5)處,可以發現,如果發送的數據包中包含多個對應pkt_index=0x2的pkt_item,如pkt_id=0xbc/0xbd/0xbe/0xbf,則可以觸發多次++v36。由于缺乏對v36的適當校驗,通過發送偽造的數據包,可造成后續在調用FHOSTPacketReadString()出現越界寫。進一步地,在(6)處傳遞的v38與FHOSTPacketRead()函數的第4個參數有關,而在findhostd程序中調用FHOSTPacketRead()時第4個參數為指向棧上的緩沖區,因此,利用該越界寫操作可覆蓋棧上的返回地址,從而劫持程序的控制流。
DSM 6.1.7-15284版本中的findhostd文件似乎經過混淆了,無法直接采用IDA Pro等工具進行分析,可以在gdb中dump出findhostd進程,然后對其進行分析。另外,在較新的版本如VirtualDSM 6.2.4-25556中,對應的findhostd文件未被混淆,可直接分析。
// in findhostd
__int64 handler_recv_data(__int64 a1, __int64 a2, __int64 a3)
{
// ...
int v124[3042]; // [rsp+1970h] [rbp-2F88h] BYREF
// ...
memset(v124, 0LL, 0x2F50LL); // local buffer on stack
if ( (int)FHOSTPacketRead((__int64)v113, a2, (unsigned int)a1, (__int64)v124) <= 0 )
{
// ...
另外,由于Synology Assistant客戶端對協議數據包的處理過程與findhostd類似,因此其早期的版本也會受該漏洞影響。
漏洞利用
查看findhostd啟用的緩解機制,如下,同時設備上的ASLR等級為2。其中,顯示"NX disabled",不知道是否和程序被混淆過有關。在設備上查看進程的內存地址空間映射,確實看到[stack]部分為rwxp。考慮到通用性,這里還是采用ret2libc的思路來獲取設備的root shell。
$ checksec.exe --file ./findhostd
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
由于越界寫發生在調用snprintf()時,故存在'\x00'截斷的問題。通過調試發現,利用越界寫覆蓋棧上的返回地址后,在返回地址的不遠處存在發送的原始數據包內容,因此可借助stack pivot將棧劫持到指向可控內容的地方,從而繼續進行rop。
在實際進行利用的過程中,本來是想將cmd直接放在數據包中發送,然后定位到其在棧上的地址,再將其保存到rdi寄存器中,但由于未找到合適的gadgets,故采用將cmd寫入findhostd進程的某個固定地址處的方式替代。同時,發現區域0x00411000-0x00610000不可寫(正常應該包含.bss區域?),而.got.plt區域可寫,故將cmd寫到了該區域。
root@NAS_6_1:/# cat /proc/`pidof findhostd`/maps
00400000-00411000 r-xp 00000000 00:00 0
00411000-00610000 ---p 00000000 00:00 0 # no writable permission
00610000-00611000 r-xp 00000000 00:00 0
00611000-00637000 rwxp 00000000 00:00 0 [heap]
00800000-00801000 rwxp 00000000 00:00 0
...
7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack] # executable stack?
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
最終效果如下。

One More Thing
獲取到設備的root shell后,相當于獲取了設備的控制權,比如可以查看用戶共享文件夾中的文件等。但是如何登錄設備的Web管理界面呢?這里給出一種簡單的方案:利用synouser和synogroup命令增加1個管理員用戶,然后使用新增的用戶進行登錄即可。當然,synouser命令支持直接更改現有用戶的密碼,且無需原密碼,但改了之后正常用戶就不知道其密碼了 :(
# 增加一個用戶名為cq, 密碼為cq674350529的用戶
$ synouser --add cq cq674350529 "test admin" 0 "" 31
# 查看當前管理員組中的現有用戶
$ synogroup --get administrators
# 將新增加的用戶cq添加到管理員組中,xxx為當前管理員組中的現有用戶
$ synogroup --member administrators xxx xxx cq
# 之后, 便可利用該賬戶登錄設備的Web管理界面
# 刪除新增加的用戶
$ synouser --del cq
小結
本文基于群暉DSM 6.1.7-15284版本,通過補丁比對的方式對群暉安全公告Synology-SA-18:64中提及的漏洞進行了定位和分析。該漏洞與findhostd服務相關,由于在處理接收的數據包時缺乏適當的校驗,通過發送偽造的數據包,可觸發out-of-bounds write,利用該操作可覆蓋棧上的返回地址,從而劫持程序控制流,達到任意代碼執行的目的。通常情況下,findhostd服務監聽的端口不會直接暴露到外網,故該漏洞應該是在局域網內才能觸發。
相關鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2038/
暫無評論