作者: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-152843個小版本6.1.7-15284 Update 16.1.7-15284 Update 26.1.7-15284 Update 3。其中,大版本6.1.7-15284對應初始版本,其鏡像文件中包含完整的系統文件,而后續更新的小版本則只包含與更新相關的文件。另外,Update 2版本中包含Update 1中的更新,Update 3中也包含Update 2中的更新,也就是說最后1個小版本Update 3包含了全部的更新。

從群暉官方的鏡像倉庫中下載6.1.7-152846.1.7-15284-26.1.7-15284-3這三個版本對應的pat文件。在Update x版本的pat文件中除了包含與更新相關的模塊外,還有一個描述文件DSM-Security.json。比對6.1.7-15284-26.1.7-15284-3這2個版本的描述文件,如下。

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

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

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

對應的偽代碼如下。可以看到,在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_iddata_lengthdata

另外,在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=0x2pkt_item,如pkt_id=0xbc/0xbd/0xbe/0xbf,則可以觸發多次++v36。由于缺乏對v36的適當校驗,通過發送偽造的數據包,可造成后續在調用FHOSTPacketReadString()出現越界寫。進一步地,在(6)處傳遞的v38FHOSTPacketRead()函數的第4個參數有關,而在findhostd程序中調用FHOSTPacketRead()時第4個參數為指向棧上的緩沖區,因此,利用該越界寫操作可覆蓋棧上的返回地址,從而劫持程序的控制流。

DSM 6.1.7-15284版本中的findhostd文件似乎經過混淆了,無法直接采用IDA Pro等工具進行分析,可以在gdbdumpfindhostd進程,然后對其進行分析。另外,在較新的版本如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管理界面呢?這里給出一種簡單的方案:利用synousersynogroup命令增加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服務監聽的端口不會直接暴露到外網,故該漏洞應該是在局域網內才能觸發。

相關鏈接


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