6

Patch diff an old vulnerability in Synology NAS

 1 year ago
source link: https://paper.seebug.org/2038/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Patch diff an old vulnerability in Synology NAS

4小时之前2023年01月04日漏洞分析

作者:cq674350529
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

之前在浏览群晖官方的安全公告时,翻到一个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个版本的描述文件,如下。

7815c09f-946f-4916-8663-8a53c074c056.png-w331s

可以看到,在6.1.7-15284 Update 3中更新的模块为libfindhostnetatalk-3.x,与对应版本发行说明中的信息一致。

c1a47cf8-4417-4051-b9ff-abbfc37c02c4.png-w331s

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

06af6f64-e5e5-4761-831c-6938ff8bbf51.png-w331s

两个版本中函数FHOSTPacketRead()内的主要差异如下,其中在6.1.7-15284 Update 3中新增加了3block

2a09f6c1-dab3-4f5f-be37-825977cbdffa.png-w331s

对应的伪代码如下。可以看到,在6.1.7-15284 Update 3中,主要增加了对变量v34的额外校验,而该变量会用在后续的函数调用中。因此,猜测漏洞与v34有关。

2dfe4e3b-f304-4fb9-9afe-2f8f7de65a5b.png-w331s

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个字段。

55e50d5f-9a47-4033-b174-391c54fafc61.png-w331s

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]

最终效果如下。

60c7fbb6-2ba6-48b8-8b70-939aaf619cee.gif-w331s

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 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2038/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK