[博客翻译]堆溢出Llama.cpp到RCE


原文地址:https://retr0.blog/blog/llama-rpc-rce


介绍:Llama.cpp 的堆溢出问题

Llama.cpp简介

Llama.cpp是一个用于处理大型语言模型(LLM)推理的开源项目,支持远程过程调用(RPC)功能,使用户能够通过网络进行分布式计算。然而,由于其实现复杂性以及内存管理的独特性,该项目也成为了研究人员关注的安全目标。

最近,研究者Patrick Peng在探索Llama.cpp的RPC组件时发现了一个堆溢出漏洞,并成功将其转化为远程代码执行(RCE)。这篇文章将详细介绍这一漏洞的发现过程、分析方法以及最终的利用方式。


堆溢出的起因

Llama.cpp使用了自己的内存管理机制,基于glibc的基本malloc和经典的ptmalloc管理方法,并在此基础上添加了一些优化措施以适配张量(Tensor)相关操作的需求。

核心问题

漏洞的核心在于ggml_backend_cpu_buffer_cpy_tensor函数中对张量维度大小计算的不当处理。具体来说,ggml_nbytes函数用于计算张量的数据大小,但当输入的张量维度被精心构造时,可能导致计算结果超出实际分配的缓冲区范围,从而引发堆溢出。

size_t ggml_nbytes(const struct ggml_tensor * tensor) {
    size_t nbytes;
    const size_t blck_size = ggml_blck_size(tensor->type);
    if (blck_size == 1) {
        nbytes = ggml_type_size(tensor->type);
        for (int i = 0; i < GGML_MAX_DIMS; ++i) {
            nbytes += (tensor->ne[i] - 1) * tensor->nb[i];
        }
    } else {
        nbytes = tensor->ne[0] * tensor->nb[0] / blck_size;
        for (int i = 1; i < GGML_MAX_DIMS; ++i) {
            nbytes += (tensor->ne[i] - 1) * tensor->nb[i];
        }
    }
    return nbytes;
}

通过对ne[]nb[]数组的控制,攻击者可以调整返回值nbytes,进而导致memcpy操作溢出目标缓冲区。


利用过程解析

第一步:控制溢出位置

在发现堆溢出后,关键是如何利用这一漏洞。研究者注意到,buffer结构体与dst->data缓冲区在内存中相邻。通过精确计算溢出偏移量,可以覆盖buffer结构体中的成员,例如iface指针。

第二步:绕过边界检查

为了成功触发后续的执行流重定向,必须绕过Llama.cpp引入的严格边界检查。这些检查确保了所有对buffer->data的操作都在合法范围内。然而,通过部分写入技术(partial-write),研究者成功修改了iface->get_base指针,指向了一个可控的地址。

第三步:泄露基址

为了进一步利用,需要泄露动态链接库(如libggml-base.solibc.so.6)的加载基址。通过构造虚假的buffer结构体,并结合ggml_backend_cpu_buffer_get_tensor函数的行为,研究者成功泄露了memcpy的GOT表项地址,从而推导出libc的基址。

第四步:远程代码执行

最后,研究者采用了一种称为“结构导向编程”(Structure-Oriented Programming, SOP)的技术,将原本要求为buffer结构体的参数重新解释为其他类型的结构体。这种方法使得攻击者能够在不违反现有安全检查的情况下,调用任意函数指针。

以下是利用的关键步骤:

  1. 构造虚假的backend结构体,并设置其device->iface->get_buffer_type指针为目标函数(如system())。
  2. 使用ggml_backend_get_alignment作为入口点,触发深层调用链,最终执行目标命令。

实际效果演示

最终,研究者编写了一个名为exp.py的脚本,实现了完整的利用过程。该脚本通过以下步骤完成远程代码执行:

  1. 分配并初始化必要的缓冲区。
  2. 通过部分写入技术泄露libggml-base.so的基址。
  3. 利用GOT表泄露libc.so.6的基址。
  4. 构造虚假的backend结构体,调用system()执行反向Shell命令。

运行结果如下:

$ python3 exp.py
[*] Leaked libggml-base.so base address: 0x7ffff7e7b000
[*] Leaked libc.so.6 base address: 0x7ffff7a00000
[*] Exploitation complete. Waiting for reverse shell...
reverse_shell@target:~$

总结

本次研究展示了如何通过深入分析和创造性思维,突破看似无法逾越的安全防护机制。尽管Llama.cpp实现了多种内存保护措施,但在复杂系统的实现中,仍然可能存在可被利用的漏洞。这次成功的利用不仅证明了二进制安全的重要性,也为未来的系统设计提供了宝贵的经验。

希望本文能够帮助读者更好地理解现代软件安全的挑战,并激发大家对逆向工程和漏洞挖掘的兴趣!