前言
24 年的时候,llama.cpp 出了两个漏洞 GHSA-5vm9-p64x-gqw9和GHSA-wcr5-566p-9cwj(也就是 CVE-2024-42478 和 CVE-2024-42479)。影响版本是<=b3560,并在 b3561 中进行了修复。
根据 Github 中的描述,我们能控制 rpc_tensor 结构体中的 data 指针,可以实现任意地址读写,并且给出了调用链和 poc。
环境搭建
编译命令
1 | cmake -B build -DGGML_RPC=ON -DCMAKE_CXX_FLAGS_RELEASE="-g" |
漏洞分析
Diff 分析
根据给出的版本号,笔者对 b3560 和 b3561 两个 tag 进行了 diff。结果如下:
1 | diff --git a/examples/rpc/README.md b/examples/rpc/README.md |
rpc_server::deserialize_tensor、rpc_server::set_tensor、rpc_server::get_tensor 这几个方法添加了对 tensor->data、offset、size 的边界检查。根据 patch 来看,在添加检查之前,tensor->data+offset+size 是有可能越界的。
调用链分析
作者直接给出了两个漏洞的调用链。
任意地址读调用链:
- start_rpc_server
- rpc_serve_client
- rpc_server::get_tensor
- ggml_backend_tensor_get
- ggml_backend_cpu_buffer_get_tensor
- ggml_backend_tensor_get
- rpc_server::get_tensor
- rpc_serve_client
任意地址写调用链:
- start_rpc_server
- rpc_serve_client
- rpc_server::set_tensor
- ggml_backend_tensor_set
- ggml_backend_cpu_buffer_set_tensor
- ggml_backend_tensor_set
- rpc_server::set_tensor
- rpc_serve_client
这两个漏洞的调用链差不多,我们先来看任意读
任意地址读漏洞
start_rpc_server
start_rpc_server 是 RPC 服务的开始,初始化 socket 服务之后,进入循环
socket_accept()阻塞等待客户端连接- 调用
rpc_serve_client()处理单个客户端的 RPC 请求
rpc_serve_client
rpc_serve_client 为每一个客户端连接创建一个 rpc_server 实例,然后读取 1 字节 cmd,8 字节 input_size,以及 input.data()。因此每次通信的数据包结构如下:
| 数据包 | cmd | input_size | input.data() |
|---|---|---|---|
| bytes | 1 | 8 | input_size |
继续往下看是一个 switch 结构,add、edit、show、delete 都有了(CTF 选手的 DNA 动了)

rpc_server::get_tensor
1 | bool rpc_server::get_tensor(const std::vector<uint8_t> & input, std::vector<uint8_t> & output) { |
get_tensor 会先验证输入的数据大小,并且解析出:rpc_tensor 结构体、offset(8 字节)、size(8 字节),这几个字段。也就是说我们能完全控制 tensor 结构体的内容。
然后创建一个临时的 ctx,并且对输入的 tensor 反序列化。deserialize_tensor 会把 tensor 中的一些字段拷贝到 result 中,这里需要注意的是 deserialize_tensor 会检测 result->buffer 是否在 buffers 中,不在则会返回 nullptr,所以 buffer 必须是一个合法的
1 | ggml_tensor * rpc_server::deserialize_tensor(struct ggml_context * ctx, const rpc_tensor * tensor) { |
那么 buffers 中的元素是怎么来的?在 rpc_server::alloc_buffer 中会根据 size 申请一个 buffer 插入到 buffers 集合里。
1 | bool rpc_server::alloc_buffer(const std::vector<uint8_t> & input, std::vector<uint8_t> & output) { |
ggml_backend_tensor_get
1 | GGML_CALL void ggml_backend_tensor_get(const struct ggml_tensor * tensor, void * data, size_t offset, size_t size) { |
只对 offset + size <= ggml_nbytes(tensor) 进行了检测,然后就调用了 buf->iface.get_tensor 拷贝数据。完全没有考虑 data 和 buffer 字段是否合法性。所以我们只要构造一个能通过检查的 buffer 字段,修改 data 就能实现任意地址读写了。
作者在这里直接告诉了我们 buf->iface.set_tensor 执行的是 ggml_backend_cpu_buffer_get_tensor 函数,那 iface 又是怎么分配的?让我们回到 rpc_server::alloc_buffer,可以看到调用了 ggml_backend_get_default_buffer_type 获取默认的 buft,然后后执行 ggml_backend_buft_alloc_buffer 分配 buffer。
1 | ggml_backend_buffer_type_t buft = ggml_backend_get_default_buffer_type(backend); |
ggml_backend_get_default_buffer_type 会返回一个静态的 ggml_backend_buffer_type 结构体
1 | GGML_CALL ggml_backend_buffer_type_t ggml_backend_cpu_buffer_type(void) { |
因此 ggml_backend_buft_alloc_buffer 调用的是 ggml_backend_cpu_buffer_type_alloc_buffer。
1 | GGML_CALL ggml_backend_buffer_t ggml_backend_buft_alloc_buffer(ggml_backend_buffer_type_t buft, size_t size) { |
分配 heap 到 data 指针后,通过 ggml_backend_buffer_init 初始化成 ggml_backend_buffer 后返回。
1 | (*buffer) = (struct ggml_backend_buffer) { |
最终的 buffer 结构是这个样子
1 | pwndbg> p *buffer |
任意地址写漏洞
调用的是 rpc_server::set_tensor,成因和任意地址写基本一样,不再赘述。
漏洞利用
根据前面的分析,我们修改 data 就能实现任意地址读写,并且 alloc_buffer 还贴心给了 buffer 的地址,所以先考虑读 heap 上的内容。看了一圈 heap 中的内容,只能找到 ggml 的地址可以泄露。

然后利用 ggml.so 中已经链接到真实地址的 got 表,我们还可以泄露出 libc 地址,然后修改堆上的 buft->iface,最后通过 BUFFER_CLEAR 触发 system
1 | #!/usr/bin/env python3 |
