利用空字节写入漏洞攻击 Synology DiskStation

admin 2025年4月24日15:41:36评论0 views字数 7727阅读25分45秒阅读模式

【翻译】Exploiting the Synology DiskStation with Null-byte Writes 

在 Synology DS1823xs+ NAS 上实现 root 权限的远程代码执行

2024 年 10 月,我们参加了 Pwn2Own Ireland 2024 并成功利用 Synology DiskStation DS1823xs+ 实现了 root 权限的远程代码执行。该漏洞已被修复,编号为 CVE-2024-10442。

DiskStation 是 Synology 公司广受欢迎的 NAS(网络附加存储)产品线。在过去的 Pwn2Own 比赛中,它曾多次被成功利用,不过在上一年的 Pwn2Own Toronto 2023 中未被攻破。而在 2024 年的爱尔兰站比赛中,出现了三个成功的利用案例,均使用了不同的漏洞。

本文将详细介绍我们研究 Synology DiskStation 并为比赛编写漏洞利用程序的经验。

利用空字节写入漏洞攻击 Synology DiskStation
准备在 Pwn2Own Ireland 2024 上对 Synology DiskStation 发起攻击

审查 Synology 软件包

如前所述,过去一两年 Pwn2Own 比赛中没有针对 Synology DiskStation 的成功利用案例。2024 年,ZDI 决定将一些非默认但由 Synology 开发的第一方软件包纳入比赛范围:

对于 Synology DiskStation 目标,以下软件包将被安装并纳入比赛范围:

  • MailPlus
  • Drive
  • Virtual Machine Manager(虚拟机管理器)
  • Snapshot Replication(快照复制)
  • Surveillance Station(监控中心)
  • Photos(照片)

"软件包"是通过 DiskStation Manager 中的 Package Center 可以轻松安装在设备上的可选附加应用程序/服务等。

对我们来说,这意味着更大的攻击面。由于这是这些软件包首次被纳入比赛范围,我们认为找到一些相对浅显的漏洞的机会很大,因为这些软件包可能没有经过太多安全审查。事实证明确实如此。

我们首先查看的是 Virtual Machine Manager(虚拟机管理器),我们直接在物理 DiskStation 上通过内置的 Package Center 安装了它。

利用空字节写入漏洞攻击 Synology DiskStation

然后,我们通过测试设备上的 SSH shell 使用 netstat 枚举了所有新的网络监听服务。结果显示除了一个绑定到所有接口的服务外,其他都是仅限本地的服务,该服务以 root 权限运行以下命令:

/var/packages/ReplicationService/target/sbin/synobtrfsreplicad --port 5566

这个监听器实际上是 Replication Service(复制服务)的一部分,这是一个独立的软件包,同时也是 Virtual Machine Manager(虚拟机管理器)和 Snapshot Replication(快照复制)的依赖项。考虑到该服务的高权限级别和易于通信的特点,我们对其产生了浓厚兴趣。

利用空字节写入漏洞攻击 Synology DiskStation

下一步是检查二进制文件。由于我们已经在真实设备上安装了该服务,因此能够通过 SSH 提取相关文件。

另外,也可以直接从 Synology 官网下载 DSM(核心操作系统)和软件包。然后可以使用一个提取工具来解析 Synology 自定义的归档文件,并提取出软件包、固件镜像或更新的内容。需要注意的是,这个特定工具是一个 FFI 封装器,围绕原生的 Synology 共享库构建,这些库可以从真实设备中提取,或者使用另一个工具从 DSM 归档中提取。

漏洞发现

在获得相关二进制文件后,我们就可以开始分析监听在 5566 端口的 TCP 服务代码。主二进制文件 synobtrfsreplicad 实际上只是一个驱动层,用于调用 libsynobtrfsreplicacore.so.7 中的功能,后者负责启动 TCP 监听器。

该服务是一个基于 Linux 的最小化 fork 服务器,主进程不断调用 accept() 并为每个新的远程客户端 fork 一个子进程来处理请求。相应地,子进程会运行一个基本的命令循环来解析发送到服务的传入消息。

每个命令都有一个简单的二进制格式,由一个操作码(opcode)和可选的可变长度数据负载组成:

unsigned cmd // command opcodeunsigned seq // sequence numberunsigned lenchar data[len]

定义了两个全局变量来解析这些命令消息。一个用于存储命令本身,另一个是类似环形缓冲区的结构,用于存储最多 3 个可变长度的命令负载。

struct{    unsigned char sector; // ring buffer index    char bufs[3][65536]; // ring buffer of 3 payloads    unsigned buf_lens[3]; // populated lengths of 3 payloads} g_recvbuf;struct{    ReplicaCmdHeader header; // opcode, seq, len    char *data; // will point into one of the 3 g_recvbuf bufs} g_cmd;

读取消息的命令循环如下所示:

voidrunCmdLoop() {    while(1) {        g_cmd.data = g_recvbuf.bufs[g_recvbuf.sector];        int err = recvCmd(&g_cmd);        if (err)            bail;        g_cmd.data[g_cmd.header.len] = 0;        // ... handle cmd ...    }}// function to read both the header and payload of a messageintrecvCmd(ReplicaCmd* cmd) {    int err = raw_tcp_recv(cmd->header, 12);    if (err)        return err;    if (cmd->header.len > 0x10000)        return err;    // read actual payload data    err = raw_tcp_recv(cmd->data, cmd->header.len);    // ...}

如果攻击者提供的长度过大,recvCmd 函数会在不读取任何有效载荷的情况下退出。然而,它的返回值为零,表示没有错误,这有点奇怪,因为头部长度是无效的...回到调用者那里,由于不知道任何错误,程序继续正常执行,并使用任意大的头部长度对命令有效载荷进行空终止。

这个漏洞非常简单,以至于在我们的初始 POC 中,我们可以使用 netcat 发送一个仅由 A 组成的信息(至少 12 个),以经典的 pwnable 方式:

利用空字节写入漏洞攻击 Synology DiskStation

除非你使用 gdb 附加到服务,否则设备上没有任何迹象表明出了问题。该故障似乎没有记录到 syslog 或任何其他 DSM 日志设施中,而且由于 forking server 的性质,不会立即导致功能丧失。

这个漏洞提供的原语允许我们在共享库的 BSS(数据段)的任意偏移处进行重复的空字节写入。非常像 CTF。虽然漏洞本身相当简单,但它的利用会更有趣一些。

无论如何,由于所有缓解措施都已启用,我们首先需要将其转化为信息泄露。

Forking Server

在继续之前,请记住我们正在处理一个 forking server,这对于打破 ASLR 非常有用。每个 fork 的子进程都将拥有与父进程完全相同的地址空间,崩溃它们不会有任何后果:我们只需重新连接到服务,就能获得一个干净的状态,即一个新的子进程。有点像时间循环,每个连接都是一个机会,可以以累积的方式获取有关地址空间的新信息。

在高层次上,每次迭代都有以下结构:

  1. 猜测某个值(例如一个地址)
  2. 让二进制文件使用猜测的值,这样如果它正确或不正确,行为会有所不同(例如错误的地址会导致崩溃)
  3. 观察二进制文件的行为以确定该值是否正确
  4. 如果正确,我们就找到了正确的值。否则,用下一个猜测重复

我们将在继续时看到如何将其应用于这个特定的二进制文件。

功能概述

由于相关漏洞发生在输入解析期间,我们还没有探索太多程序的功能,这些功能将在稍后构建漏洞利用时派上用场。

从网络读取命令后,命令循环会根据提供的操作码进行 switch-case。需要输入的操作码从可变长度的命令有效载荷中解析输入。我们查看了所有可用的操作码,以大致了解它们的功能:

  • CMD_DSM_VER : 无输入
    • 返回 DSM 版本号
  • CMD_SSL : 为连接初始化 SSL
  • CMD_TEST_CONNECT
  • CMD_NOP
  • CMD_VERSION : 输入整数
    • 设置连接的“版本”以处理兼容性差异
  • CMD_TOKEN : 输入字符串“token” 它必须作为键存在于磁盘上的 JSON 文件中
    • 执行初始化并设置全局变量 std::string g_token
  • CMD_NAME : 输入字符串“name”
    • 可能执行 btrfs 相关操作,和/或使用 g_token 修改 JSON 文件
  • CMD_SEND : 输入原始数据
    • 将输入代理到文件描述符,似乎在其他地方设置为到 btrfs 命令的管道
  • CMD_UPDATE
  • CMD_STOP : 输入 token 字符串
    • 从 JSON 中移除 token
  • CMD_COUNT
  • CMD_CLR_BKP
  • CMD_SYNCSIZE
  • CMD_END

很快我们就发现,许多代码路径都依赖于提供一个有效的“token”,它应该已经存在于 /usr/syno/etc/synobtrfsreplica/btrfs_snap_replica_recv_token 路径的 JSON 文件中。JSON 被用作一个简单的键值存储,其中 token 是键:

{    "<token>": {"<attribute>":value, ... other attributes ...},    ... other tokens ...}

推测是某个外部服务分发这些 token 并写入文件,但具体发生在哪里我们并不清楚。

然而,存在一个可能非预期的代码路径,允许向 JSON 文件添加 token。CMD_NAME 操作码使用当前的 g_token,并向文件写入一个属性,其中有两个重要的细节:

  • 它不会检查 g_token 是否被初始化过(例如通过 CMD_TOKEN
  • 如果 token 尚未作为键存在于 JSON 对象中,设置属性会将其添加

通常情况下,未初始化的 g_token 只是一个空字符串,但在内存损坏的情况下,一切皆有可能,我们稍后会看到这一点如何发挥作用。

ASLR Oracle #1: 释放伪造的堆块

我们的原语是一个空字节写操作,我们可以指定命令 payload 缓冲区中的任意偏移量。由于偏移量是无符号的,我们只能向 payload 缓冲区之后的内存写入空字节。

这就引出了一个问题:payload 缓冲区之后是什么?它将是共享库 BSS 段中 g_recvbuf 全局变量的三个 0x10000 大小缓冲区之一。除了少数std::string实例外,没有太多全局变量,这些实例具有以下结构:

struct std::string {    char* ptr; // for short strings, points into inline_buffer    unsigned long length;    char inline_buffer[16];}

默认构造函数将长度设置为 0,并将 char* 指向内联缓冲区。换句话说,我们将在 BSS 段中得到一堆 std::string 实例,它们的指针都指向自己的 BSS 地址加上 16 字节的偏移量。

现在,考虑如果我们使用空字节写操作将其中一个指针的最低两个字节置零。前面的 payload 缓冲区大小为 0x10000 字节,这足以保证部分置零的 BSS 指针指向该缓冲区内的某个位置,尽管我们不知道确切的偏移量。

利用空字节写入漏洞攻击 Synology DiskStation

由于 ASLR 具有页粒度(12 位),这个偏移量将有 4 位(一个半字节)的熵(即可以是 0, 0x1000, 0x2000, ... 0xf000)。

我们可以破坏的全局字符串之一是 _gSnapRecvPath,它可以通过 CMD_NAME 命令的操作被重新赋值。

当重新赋值 std::string 时,如果 char* 没有指向内联缓冲区,delete 将在赋值新值之前被调用在旧的(现在已损坏的)值上。这让我们能够在 payload 缓冲区中调用 free 来释放一个伪造的 chunk。我们自然可以通过命令 payload 控制这个缓冲区的内容。

free被调用时,如果伪造的 chunk 大小足够小,它将被放入 glibc 的 tcache 中。或者,如果大小无效(例如为零),free 将调用 abort,导致进程崩溃。这创建了我们的第一个 oracle,我们可以将其与 forking-server 行为结合,来确定伪造的 chunk 位于 16 个可能偏移量(0, 0x1000, ... 0xf000)中的哪一个。

对于每个可能的 16 个偏移量:

  1. 用填充数据填充 payload 缓冲区直到猜测的偏移量,后面跟着伪造 chunk 的元数据(即一个伪造的大小值)
  2. 触发漏洞两次,将 _gSnapRecvPath 的 char* 的最低两个字节置零
  3. 使用 CMD_NAME 释放损坏的 char*,它可能指向也可能不指向放置在猜测偏移量处的伪造 chunk
    • 如果 socket 保持连接并发送了响应,则猜测的偏移量是正确的
    • 如果 socket 关闭(即调用了 abort),则猜测错误;尝试下一个偏移量

我们现在已经解析了一个半字节的 ASLR 熵,并且可以可靠地释放 payload 缓冲区中的伪造 chunk,它将被放入 tcache 中。

ASLR Oracle #2: 泄露 Tokens

tcache 是一个单向链表,每个空闲的 chunk 都有一个 next 指针。由于 glibc 中的一些加固尝试,next 指针的填充方式如下:

chunk->next = (&chunk->next >> 12) ^ next

在我们的案例中,tcache 链表之前是空的(next = 0),因此写入的值将是 &chunk->next >> 12。换句话说,我们已经将一个偏移后的 BSS 指针放入了 payload 缓冲区。现在我们需要找到某种方法来泄露这个值。

当伪造的 chunk 被释放且偏移后的 BSS 指针被写入后,我们将把第二个全局 std::string 变量 g_token 的 char* 的低 2 字节置零。这种破坏将使 g_token 指向与 _gSnapRecvPath 完全相同的位置,即偏移后的 BSS 指针。

回想我们之前讨论的 CMD_NAME 功能,它可以将未初始化的 g_token 添加到磁盘上的 JSON 文件中。这个功能现在变得非常有用,因为"未初始化"的 g_token 不再持有空字符串,而是指向了偏移后的 BSS 指针。触发这个代码路径后,JSON 文件现在包含了我们想要泄露的值。

还需要注意的是,在将 g_token 写入磁盘之前,我们可以再次触发空字节写操作来截断偏移后的 BSS 指针。通过这种方式,我们可以逐个段地写出指针。例如,如果偏移后的指针是 0x766554433,我们可以从 333344,... 一直写到完整的 3344556607

利用空字节写入漏洞攻击 Synology DiskStation

一旦 JSON 文件包含了泄露的值,我们就可以按预期使用 CMD_TOKEN,它需要一个字符串参数来指定要使用的 token。这个 token 将在 JSON 文件中查找,并根据是否找到返回不同的错误代码。这创建了我们的第二个 oracle,我们可以用它来实现逐字节的暴力破解:

  • 对于偏移后的 BSS 指针的 5 个字节,循环 b 从 0 到 4:
    • 发送 CMD_TOKEN 并带上猜测的字节(在前面加上之前迭代中已知的字节,长度为 b
    • 返回的错误代码将指示提供的字节是否正确
    • 如果正确,我们就找到了偏移指针在索引 b 处的字节
    • 否则,继续尝试下一个可能的字节
    1. 将指针截断到长度 b+1,然后将截断的段写入 JSON 文件
    2. 遍历可能的字节 0 - 0xff

一旦这个逐字节的暴力破解完成,我们就泄露了偏移后的 BSS 指针,这给了我们共享库的基地址。由于 mmap 映射在虚拟内存中是连续的,这也给了我们所有共享库的地址,特别是 libc。

劫持控制流

有了泄露的信息,我们准备制作最终的 payload 来劫持控制流。

我们已经能够在 payload 缓冲区中释放一个伪造的 chunk,通过发送额外的命令,我们可以任意破坏这个空闲的 chunk。此时,我们可以以标准方式滥用 tcache 链表:

  1. 用任意地址破坏伪造 chunk 的 next 指针
  2. 分配与伪造 chunk 大小相同的东西
    • malloc 将返回伪造的 chunk,然后将 tcache 链表的新头设置为任意地址
  3. 再次分配相同大小,使 malloc 返回任意地址

我们只需要找到一些符合这种连续两次分配模式的代码。幸运的是,CMD_TOKEN 的处理程序符合这种模式,在两次分配完成后,一个临时包含我们输入参数的 std::string 被析构,调用 delete 来释放我们输入的 char*

这让我们得出了以下策略:

  1. 将伪造的 tcache chunk 的 next 指针破坏,使其指向共享库的 GOT 表中 delete 的入口附近
  2. 发送 CMD_TOKEN 命令
  3. 处理程序将从被破坏的 tcache 中分配两次,用 system 覆盖 delete 的 GOT 表入口
  4. 随后的析构函数调用 delete,但实际上会调用 system 并传入我们控制的输入字符串

从这里开始,游戏就结束了。我们可以简单地执行 /bin/sh 并将 stdio 重定向到已经连接的客户端 socket(避免了需要反向连接)。

我们提交的完整漏洞利用代码已经发布在这里。

修复

该漏洞被分配为 CVE-2024-10442。Synology 在 2024 年 11 月 5 日(Pwn2Own Ireland 于 10 月 22 日举行)相对较快地为 Replication Service 发布了补丁,你可以在这里找到公告。ZDI 的公告可以在这里找到。该补丁修改了 recvCmd 函数,如果提供的 header 长度过大,则返回错误而不是零。

if (cmd->header.len > 0x10000)    return 1; // instead of previous return 0

调用方随后检测到此错误并中止,而不是继续处理无效命令。

结论

尽管这个漏洞很容易发现,但利用过程却很有趣,因为 null byte write 作为一个原语相对较弱。这感觉就像你在 CTF 挑战中会遇到的那种漏洞,而 tcache 操作和暴力破解 oracle 也符合 CTF 的氛围。

从更严肃的角度来看,尽管这是一个非默认安装的包,但在一个远程可访问的服务(以 root 权限运行)中存在如此简单的漏洞,确实令人担忧,尤其是考虑到 Synology 是一个相当受欢迎的消费级和企业级 NAS,而且这些设备暴露在互联网上的情况并不少见。

原文始发于微信公众号(securitainment):利用空字节写入漏洞攻击 Synology DiskStation

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月24日15:41:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用空字节写入漏洞攻击 Synology DiskStationhttps://cn-sec.com/archives/3995059.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息