Introduction
This is part one of the two-part walkthrough series we’re writing for the wonderful challenges on ROP Emporium. As always, we’re offering spoiler-free hints and tips for those of you who just want some help without the entire challenge being spoiled!
There are eight challenges in total. This part covers the first four.
General advice
- If you’re just starting out, I recommend you read the Beginner’s Guide at least twice before proceeding to the challenges.
- Fully read each challenge’s page first. It’s full of essential and helpful tips (I needed to hear this too).
- If you need a good workflow to enumerate the binaries, I recommend the following:
# this extracts strings cleaner than `strings` does
# comes with radare2
rabin2 -z <challenge>
# in gdb
checksec # to see what protections it has
info function # to see what functions we can use to our advantage
ret2win
This challenge covers the most basic form of ROP. Return to ret2win
to win. If you’re feeling lost, I recommend reviewing the calling conventions on all the platforms that you’re doing the challenge on.
Remember that a basic ROP is just an overflow in which we gain control of the EIP and we simulate the stack frame for functions that we want to call.
x86
Hint (how to find overflow offset)
With gdb-peda
:
pattern create 200 200.txt
creates a pattern file of 200 characters.
r < 200.txt
pipes the pattern into the program.
You’ll see a SIGSEGV
along with the address that it segfaults on.
pattern offset <segfault address>
determines the offset!
Hint (skeleton of a payload)
<offset> <function to return to>
x64
Hint (how to find overflow offset)
Using the same workflow that we use for x86, you’ll find that gdb
doesn’t tell us which address it segfaults on anymore on x64. Instead, we can examine $rbp
with x $rbp
, find the offset for it with pattern offset <address>
, and add 8 bytes to this offset to get our true EIP overwriting offset. This is because $rbp+8
is typically where the return address resides (review stack frames for more details).
Hint (skeleton of a payload)
It’s the same as on x86.
split
This challenge covers calling a function with one argument!
So the author tells us that system
and /bin/cat flag.txt
are already present in the binary. All we need to do is to find them and stitch them together.
Hint (how to find /bin/cat flag.txt
)
While running the program in gdb, you can find a string with the command find <string>
.
x86
Hint (skeleton of a payload)
<offset> <function #1> <function #2> <arguments to function #1>
The offset is the usual "A" * 44
. Function #1 gets executed after the overflow (because we overwrote the original return address at this position) with its arguments, then function #2 gets executed. Function #2 can just be "AAAA"
if you don’t care about exiting gracefully.
x64
Hint (skeleton of a payload)
It’s mostly the same with x86, but x64 passes arguments in rdi
, rsi
, rdx
, rcx
registers and so on, which means you need to pop
or mov
your arguments into the right registers first, then you can call the function.
So for calling a function with one argument: <offset> <address of pop rdi> <argument #1> <function #1>
. Calling a function with multiple arguments is covered later.
Hint (how to find useful gadgets)
With gdb-peda
:
ropsearch "pop rdi"
callme
x86
The challenge in this one is to chain function calls. In a single chain we’d do
"A" * 44 + first_function_address + second_function_address + func_1_arguments
But if we want to chain two functions, after the first one executes and returns, the stack will look like this
+ func_1_arguments second_return_address
Ok, the program pops second_return_address
and executes it, now the stack is
func_1_arguments
Which obviously will stop the chain because it’s not a valid function.
So the solution is to use pop
gadgets to clean up the stack frame. For example, take a look at this two-function chain.
# pop_2 can point to a gadget like pop ecx ; pop edx ; ret
"A" * 40 + first_function + pop_2 + first_func_arg_1 + first_func_arg_2
+ second_function + junk_return_address + second_func_arg
After first_function
executes, the stack becomes
+ first_func_arg_1 + first_func_arg_2
pop_2 + second_function + junk_return_address + second_func_arg
The pop_2
pops the next two arguments which have already been used by first_function
. Now the stack is
+ pop_1 + second_func_arg second_function
Which executes second_function
just fine! And with this technique, we can construct a ROP chain however long we’d like (as long as there is enough space on the stack for it).
x64
The solution for x64 is surprisingly easier than the x86 one, because on x64 we don’t pass arguments on the stack, we do it via the registers. This means we don’t actually have to clean anything up and we can just chain on forever!
Hint (how to pass arguments on x64)
We pass them in registers rdi
rsi
rdx
rcx
r8
r9
, in this order.
Hint (how we can find gadgets to help us)
Thankfully, the author put some useful gadgets in the aptly named function usefulGadgets
. In gdb, dissassemble the function with disas usefulGadgets
.
write4
In the previous challenges, the arguments (“flag.txt”, etc.) have always been conveniently in the executable, ready for us to reuse to our benefit. This time, it’s different. This challenge covers manually writing arbitrary data to memory.
How might we execute this arbitrary write? Say we’d like to write the string “bird” to the start of the data segment. One of the simplest chains could be:
pop eax ; "bird"
pop ebx ; address of the start of the data segment
mov [ebx], eax ; move "bird" into the address
x86
Hint (how do I write a bigger string?)
You append the same chain to the payload again, but this time the address that you’re writing to should be increased by 4 bytes. Repeat this until you’ve written the whole string 4 bytes at a time.
Hint (where should I write the string?)
Check for sections with write privileges using readelf -S ./write4
. For example, you could write it to the start of the GOT section in this one.
Hint (skeleton of a payload) (slight spoiler)
<offset> <arbitrary write for "flag"> <arbitrary write for ".txt">
<address of print_file> <junk return address> <address of "flag.txt" that you wrote>
x64
Tip: you can write an 8-byte string in one go because we’re on x64.
If you like my approach to these ROP challenges, maybe you’d like my fun & interactive cybersec courses too :)