Unlike many pwn challenges, this had no binary to be downloaded. The reason why quickly became clear when we connected to the provided service and it dropped an ELF binary.
The binary wasn’t in a format which could easily be made into a file first thing I did was construct a brief script to connect and write the binary to a file for further analysis.
The main function is pretty minimal; only calling two functions.
The first disables buffering on stdout, common in CTF challenges over a TCP socket like this because buffering can make exploits behave strangely.
The second reads from stdin, compares it to a static string, and then branches based on the equality. Each branch calls another function which does essentially the same thing, forming a tree of functions, and then finally there are “leaf” functions which do not call other functions at all.
While looking through the stripped functions I saw a buffer overflow in one of the leaf functions. At this point the rough shape of the challenge is becoming clear. Subsequent connections to the service provide binaries which follow the same archetype but are subtly different — strings, buffer sizes, etc differ. We need to write code to generate an exploit in a generic enough manner that it will work for any binary the server provides.
I used angr to generate exploiting payloads for these binaries. I would love to have created this myself, but I was able to pretty much wholesale snag a script from a blog post on discovering buffer overflows with angr
This script took a binary as an argument, attempted to derive a corrupting payload, and then wrote it to a file for my usage elsewhere.
So, we have a decent (but variable) sized buffer overflow. Where do we go from here?
Mitigations are pretty light — NX is enabled but it lacks a canary and PIE is disabled. Usually in this scenario I would reach for ret2libc, but ASLR makes that troublesome in this situation because we have no easy method of leaking a libc pointer. On the bright side, no PIE means we can easily pivot the stack into BSS.
We have access to a pretty slim set of libc functions, none of which write to stdout. Interestingly enough there is an stdout symbol, from buffering being disabled, but I do not believe that to be exploitable.
I was stuck at this point for ages trying to think up a viable exploit method when I noticed something interesting about the disassembly of read.
At read+48 there is an instruction which performs a syscall and more importantly this is quite close to the beginning of the function so there is a pretty decent chance the difference between read+0 and read+48 is only the last byte. This is vitally important because ASLR does not randomize the lowest 12 bytes which means the value of this byte on the server is constant. If we can figure out what this byte is on the server, we can transmute read into a syscall gadget which can be leveraged for information leakage.
As it turns out, constructing a ROP chain which can utilize a syscall gadget to leak information in this binary is nontrivial. My original plan was to use sigreturn to set the argument registers, but that turned out to not work because it clobbered the segment registers (genuinely unsure why because I don’t think it should; let me know if you’re reading this and you know why). I managed to make a rop chain capable of leaking a single byte by leveraging leftover register values from read, an INC ECX gadget, and a “mov al, 4; or byte ptr [ecx], al; leave; ret” gadget (I later improved on this rop chain to be able to leak larger ranges, but this worked to brute force this byte).
I wrote up a quick script to brute force overwrite the last byte and apply this ROP chain. The theory was that if the overwrite was correct, read would become a syscall gadget and I would see an extra byte in the response.
(this was pretty much my earliest functional script; see later ones if you want comments :) )
This turned out to work fine (although it was quite close to the end, I was getting a little scared) and I discovered that 244 (0xf4) was the correct byte to overwrite with to construct a syscall gadget.
Once I discovered how to leak a single byte, I leaked individual bytes of the GOT by varying the number of INC ECX gadgets to determine the lower 12 bits of __libc_start_main (0xa50) and setvbuf (0x700). I then used libc.rip to determine that the libc version on remote was libc6-i386_2.28-10_amd64.
At this point I had all the building blocks necessary to get a shell, I just needed to refine and put it together. The key difference between my earlier rop chain and the chain I used to get a shell was the size of my information leak. I lacked any gadgets to control edx directly, so I was left to rely on whatever had previously set it — in this case the read call I used to turn read into a syscall gadget. Naively, this left 1 in edx because I was overwriting a single byte. I realized I could take advantage of the fact that read length is a maximum and it will happily read fewer bytes if that is all that is present. I adjusted it to read 4 bytes (a full word leak) and then dropped a time.sleep after sending only a single byte to force read to return early.
Putting this into action got me a total ASLR leak, allowing me to return to libc with 100% success (more or less, I got the odd connection error)
As some careful enumeration of the box showed (very careful because I got about 10 seconds before the timeout kicked me) it was missing several important binaries (/bin/sh, cat, etc). It did, luckily, have ls which showed me the contents of the working directory.
The most interesting one is hint.txt — I had assumed that getting a shell wouldn’t be sufficient because there are mentions of having to exploit multiple binaries and calling the symbol “holy_grail” which did not exist in the binary. The text of this hint tells us that it is linked with “libgrail.so” using LD_PRELOAD.
This likely contains the aforementioned “holy_grail” symbol which needs to be called to win. I exfiltrated it with base64 (love that binary, dropping static binaries on boxes you really aren’t supposed to have them with base64 is a quality strat) and found the following function.
I learned from an admin that the intended solution was ret2dlresolve — the binary had no plt stub for this function so it couldn’t be called directly, but if you forged one the linker would happily fetch a pointer to this function for you.
I, being incredibly lazy, chose to just fake this function by writing DONE to the log file in my rop chain. sorry ;)
As I had an 100% accurate ret2libc chain it didn’t take much to finish up the challenge. I modified the end of my chain to mprotect and write shellcode which mimicked the holy grail function, wrapped it in a loop, and let it run.