前言
问题处在 ggml 组件中,影响的最后一个 tag 是 b4651。在 llama.cpp 修复了 GHSA-5vm9-p64x-gqw9和GHSA-wcr5-566p-9cwj 之后,rpc_server::copy_tensor 中仍然存在堆溢出漏洞。
参考链接:
- https://github.com/ggml-org/llama.cpp/commit/1d20e53c40c3cc848ba2b95f5bf7c075eeec8b19
- https://github.com/ggml-org/ggml/pull/1103
Llama’s Paradox 文章并没有涉及到对内存行为的分析,笔者在研究了分配过程之后实现了更简单的利用方式
漏洞分析
COPY_TENSOR
1 | bool rpc_server::copy_tensor(const rpc_msg_copy_tensor_req & request, rpc_msg_copy_tensor_rsp & response) { |
调用链
- rpc_server::copy_tensor
- ggml_backend_buffer_copy_tensor
- ggml_backend_cpu_buffer_cpy_tensor
- ggml_backend_buffer_copy_tensor
rpc_server::copy_tensor 在 deserialize_tensor 之后没有计算 ggml_nbytes(src) 和 ggml_nbytes(dst) 的大小。deserialize_tensor 也会把 ne 和 nb 两个数组复制到 result 里,我们能完全控制,所以可以构造 ggml_nbytes(src)大于 dst->data 的 buffer 来实现溢出
内存分配过程
llama rpc 有一些难以解释的行为,我们这里随便 alloc 几个 buffer,可以看到 chunk 全部被打散了,并且还伴随着一些 tcache bins 的释放

笔者仔细研究了一下内存的分配过程
1 | void rpc_server::alloc_buffer(const rpc_msg_alloc_buffer_req & request, rpc_msg_alloc_buffer_rsp & response) { |
alloc_buffer 到实际的分配函数的调用链如下:
- rpc_server::alloc_buffer
- ggml_backend_buft_alloc_buffer
- ggml_backend_cpu_buffer_type_alloc_buffer
- ggml_aligned_malloc
- posix_memalign
- _int_memalign
- posix_memalign
- ggml_aligned_malloc
- ggml_backend_cpu_buffer_type_alloc_buffer
- ggml_backend_buft_alloc_buffer
在 ggml_aligned_malloc 函数中,设置了一个固定的 alignment
1 | void * ggml_aligned_malloc(size_t size) { |
所以在尝试分配 0x50 大小 buffer 的时候,posix_memalign 传入的参数如下
一直到_int_memalign发现 malloc 的 size 变成了 0xc0
检查一下 glibc 的代码,malloc 的 size 实际上是0x10+0x50(size)+0x40(alignment)+0x20(MINSIZE)=0xc0(我们最后拿到的是0x50这部分,0x10 是提前预留出来的 header)
在 malloc 之后,_int_memalign会两次判断
-
if ((((unsigned long) (m)) % alignment) != 0)-
_int_memalign先判断 m 地址能够被 alignment 整除,不可以才会进入 if 逻辑,然后从 chunk 头部开始返还 0x20
-
-
if (!chunk_is_mmapped (p))- chunk 不是 mmap 出来的则会从尾部开始返还 0x50,如果没有返还开头,则会直接返还 0x70
此时堆分布如下,所以大部分情况下,我们拿到的 tensor->data 都是空的,并且这也是为什么出现了“难以解释的行为”。
最完美的情况就是这种,笔者发现0x40(alignment)+0x20(MINSIZE)刚好等于 0x60,这和ggml_backend_buffer_t buffer的大小相同。
由于0x555555580e80 % 0x40 == 0,_int_memalign不会进入第一个 if 逻辑,我们拿到的 chunk 不会被分割,进入第二个 if 后会返还尾部的 0x70 到 tcache bins,
ggml_aligned_malloc后,就会被ggml_backend_buffer_init 立即分配出来
1 | static ggml_backend_buffer_t ggml_backend_cpu_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) { |
当这两个 chunk 是从 unsorted bin 中分配出来的时候,幸运的是 llama 并没有清空 chunk(CTF 选手的 DNS 又动了),我们可以通过 get_tensor 直接泄露出 libc 和 heap 地址。
漏洞利用
经过对内存分配过程的研究,只要经过一定程度的堆风水,我们就可以轻松的泄露出 libc 和 heap 地址,因此利用会变得非常简单。
1 | #!/usr/bin/env python3 |