There are a few different bugs around but I took advantage of two in my exploit. A sprintf buffer overflow found in manual_jump and a minor out of bounds read present in quick_jump.
The buffer overread is present in most operations on the dial array. It is a bounds-checked buffer of 10 u32 numbers, however when indexed it is treated as an array of u64. The functions add, remove_year, and list all have this bug but it’s not particularly useful because things are otherwise interpreted as u32 (we can leak or set the lower four bytes for a bit of the stack frame, but the shadow stack means we can’t do anything interesting with the return pointer). There is one (that I found) useful leak with this bug — quick_jump will copy a year from the dial table to the year variable and print it as a u64.
The second major bug was present in manual_jump. A user provided length was used for scanf %s width and the bounds checking was insufficient. The width was limited to a maximum of 0x1e but there were no limits on the lower bound allowing for 0 or negative %s widths. There are two ways to take advantage of this (that i’m aware of) — a width of 0 (%0s) is equivalent to %s (causing a stack buffer overflow) and a width of -1 (%-1s) will not read bytes or place a terminating null byte.
I used %-1s to leak a stack pointer from the now uninitialized buffer like so:
I used %0s to build a write primitive. Due to the shadow stack I was unable to ROP and achieving code execution was nontrivial. With our previous leaks we can overflow past the saved RIP and canary — and then modify the saved RBP. Once we return to the caller function local variables are referenced relative to RBP — and we can easily build an arbitrary write primitive in many different ways. I chose to use manual_jump so that after the write I could use the buffer overflow to repair RBP to the original value.
leak of a .text pointer, a libc pointer, and a stack pointer
arbitrary write of a u32
although I did not need it, the list function can be used for an arbitrary u32
In most cases the challenge would be essentially over — arbitrary write is a powerful primitive, right? Just drop a one gadget over function pointers until something matches the constraints? In this case we’re still at the beginning of the challenge. As mentioned earlier, the binary is running inside QEMU with a plugin that does three things:
implement a shadow stack
block execve
add a backdoor syscall to print the flag
To get the flag we need to be able to make a syscall with control of the first two arguments — controlling rax, rdi, and rsi (or use the libc syscall function and control rdi, rsi, and rdx). However, the tools to do that are extremely scarce. I was stuck here for a long time with many failed approaches. After many failed approaches I came across a writeup on libc GOT hijacking. I’m familiar with the use of the libc GOT as a replacement for free_hook/malloc_hook to call a controlled function but I’d never considered using it for a code reuse attack.
The technique is roughly similar to GOP/JOP except it uses calls into the GOT as the method of directing control flow.
For example, this is the first gadget of my chain which I used to shift the stack upwards to the controlled area:
The address of the next gadget would be placed in the GOT entry for wcscmp, which would end in a call to another GOT entry and so on.
This technique has a rather significant fundamental limitation in that each GOT entry can only be used once or you’ll create a cycle — but in practice I found that wasn’t a huge issue and it was relatively straightforward to control the first three arguments (at which point you could instead call mprotect and run shellcode). The larger limitation is that you can’t clobber a GOT entry if your write primitive calls it or you’ll trigger your chain early and likely crash. I did this manually as I was writing my chain but it should be easy enough to automate in GDB (drop a tracepoint on each plt stub and check hitcounts? The GDB plugin API should be sufficient… I’ll probably write something up when I have time) and exclude those gadgets.
As mentioned earlier, my first gadget was intended to shift the stack to the controlled area. Although some registers were set, the data on stack was uncontrolled. Coincidentally, this gadget left rsp pointing directly to the start of the controlled stack data.
My second gadget was intended to populate rdx with 0x6942069420 — the magic value for the second argument to the backdoor function. My stack control used sprintf which limited the bytes which could be written to non-whitespace bytes. I used this gadget which read data from the stack and then added 1 to rdx.
The next gadget popped from the stack into a variety of registers — although none of these registers are directly used in the backdoor call I found a couple gadgets which moved from r registers.
The last two gadgets are fairly straightforward and just move from r13/r14 into rdi/rsi — at which pointer we have set up registers appropriately and can call syscall.
I used __stack_chk_fail to trigger my GOP chain because it is a relatively simple function with a slim stack frame which made it easier to access the controlled section of the stack from the gadgets. On the last write when the start of the chain was written I intentionally corrupted the canary to start the chain with a shorter stack frame.
It’s worth noting that this type of attack isn’t limited to just libc — it can be applied to any ELF with a writeable (non full RELRO) GOT (although in practice libc is big and consistently available). If a more complex chain were needed and more libraries were available then a chain could be built across multiple library GOT sections.
I had a lot of trouble with whitespace management. The author writeup used a double call gadget to call gets at the start — leaving a much more manageable constraint (no newlines)
Although during the event I found gadgets using a binary ninja script — after the fact I wrote a gadget finder which has no proprietary dependencies and can find gadgets including partial instructions. I make no promises about code quality, general maintained-ness, or really anything — i just thought it would be cool and slammed it together but im super busy :(
This was a challenge from the Battelle booth at Shmoocon 2024 — I like their recruiting CTF challenges and generally learn something cool when solving them. Also they gave out a really cool badge. If you’re interested you can find their cybersecurity careers page here. This is a no bias shill :) I do not work there.