DEF CON CTF 2021 Finals Retrospective
Contents
Translation
This post is also available in Simplified Chinese.
Hangzhou was rainy again today, making it difficult to feel energetic. I swore long ago that I would stop procrastinating, but nothing has changed after several years. After both the DEF CON CTF 2020 Finals and the 2021 Qualifiers, I confidently planned to publish something, only to abandon it each time. I could not postpone it again, so here is an account of my two days in Shanghai.
Before
As in the qualifiers, the competition took place in a meeting room at Tencent’s Shanghai offices. Keen Security Lab’s logistics were excellent, with an uninterrupted supply of snacks and drinks. Unfortunately, we could not stay at the Renaissance this time, so AAA booked a nearby Yitel hotel. I originally planned to return there for some sleep during the break, but never did.
Day 1
At the start, the organizers, OOO, accidentally leaked every challenge. One series, ooows, dealt with virtual devices. Looking for an easy target, I first tried zero is you, a game based on Baba Is You. Its basic idea was to assemble CPU is run and execute shellcode while minimizing the number of moves. Algorithms are clearly not my strength, so I contributed little there.
barb-metal
The challenge supplied mruby bytecode and an mrubyc interpreter. The interpreter registered several device classes, including Thermometer and Alarm, to emulate a bare-metal sensor platform. The mruby bytecode mainly validated arguments before passing them to mrubyc objects.
A distinctive feature of mruby bytecode is that instructions operate on adjacent registers. For example, OP_SEND performs a function call; if R1 is the this pointer, its arguments must occupy R2, R3, R4, and so on. It is quite an unusual design.

At first I did not reverse the bytecode carefully enough and missed the out-of-bounds access. I only noticed that both time and date could overflow after failing to find a bug in the interpreter and being asked once more whether mruby’s checks were correct. Looking at the organizers’ source after the event, the vulnerability was almost handed to us, which made the miss especially regrettable.

Another vulnerability existed in Speaker, but it used an array-based heap with a structure too complicated for me to reverse. We consequently remained vulnerable to attacks. My reverse-engineering skills still had plenty of room for improvement.
ooows-p92021
After barb-metal went offline, I moved to p92021. Fortunately, the experts had already solved ooows-flag-baby, establishing that the ooows series concerned vulnerabilities in virtual devices and that we had to upload BIOS firmware to attack them.
p92021 implemented a file server using the 9P protocol. The 9P documentation was fragmented and structures differed slightly among versions, so we had to infer them from the binary. Gui found a use-after-free, and we immediately patched our service, but we did not know how to write code that communicated over 9P.

Debug information in another challenge, ogx, saved us. By copying its structures into p92021, we understood the VMM’s MMIO implementation. This challenge used a virtio driver for 9P communication, but virtio requires a collection of vring structures that would have been extremely difficult to write in assembly. I found the virtio driver from U-Boot, adapted it, and used it as firmware.
Kira and I spent the entire afternoon and evening debugging the modified driver. When I could no longer stay awake, I slept on an air mattress in the next room. Less than two hours later, TTX woke me and whispered that we were solving a challenge. Half asleep, I thought I had seen a ghost and stood frozen for half a minute before understanding what was happening. CTF is truly an unhealthy sport.
After more debugging, Kira finally resolved the compilation and linking problems. We placed the prepared 9P payload into virtio scatter-gather structures, sent it through ring 0, read the flag from ring 1, and wrote it to the serial port.
Day 2
hyper-o
The organizers had said that hyper-o would not be released, but later announced a modified challenge using the old hyper-o.ko. It implemented a virtual machine with VT-x instructions to run shellcode. Kira and I examined the EPT implementation but found no obvious problem. Page-table analysis caused many difficulties: once structures involved conversions between virtual and physical addresses, IDA handled their members poorly. The program used the following pattern to obtain physical addresses:

This code obtains the physical address of vmxon_virtual[v0]. Its last line can actually be rewritten as:
| |
The actual conversion is:
| |
IDA interpreted 0x800000 as an offset into the preceding structure, creating considerable confusion. If that was still understandable, constants such as 0xCA000 and 0xFFFFFFFFFFD7C000LL in the following output were nearly incomprehensible:


That single 0x800000 complicated the reverse engineering and wasted substantial time. At critical moments, reading the assembly directly is still more reliable. As a free IDA user, I probably should not complain too much.
Unable to find the bug, we speculated about a multi-CPU race or an incorrectly initialized register during vmlaunch. Xshj and I even inspected .altinstr_replacement for tricks, then looked through KVM, VirtualBox, and VMware. VirtualBox saves RFLAGS with pushf when entering a guest while KVM does not, but debugging showed that the guest did not affect RFLAGS. Eventually someone found an ordinary off-by-one in ept_map_memory, which we had simply overlooked.

The off-by-one expanded the intended 0x200000-byte memory region by 4 KB, enough to overwrite the EPTP. Once the vulnerability was identified, the experts began developing an exploit, but StarBugs had already obtained first blood, so we searched the traffic and recovered their solution. The organizers’ operations then failed us: first our patch was reverted, and later we lost network access entirely and could only endure attacks. Fortunately, only four and a half hours remained, and the competition soon ended.
While watching wzh debug shellcode, I saw him append a
vmcallinstruction. If the shellcode finishes normally without a segmentation fault,vmcalltriggers a VM exit with the distinctive reason codeVMX_REASON_VMCALL(18). The log therefore immediately reveals whether execution reached the end. I had encounteredvmcallwhile studying vmtools, but never imagined using it this way. Perhaps tricks like this are what separate experts from me.
Other Challenges
I did not examine the other challenges closely. According to dydxh, ogx used Intel MPX rather than Intel SGX to emulate an enclave. My computer’s CPU was too old to support either instruction set, so I could not experiment with it. broadcooom appeared to be firmware for another architecture. Many people were already studying it, and I badly wanted to sleep, so I left it alone.
After
The competition ended at 5:30 a.m. I stayed at Tencent to watch the closing ceremony, only to find that CTF was scheduled last. Before it came speeches from DEF CON operations, organizers, and assorted staff; someone even mentioned that two attendees had been removed for not wearing masks. I could not stay awake. When noise woke me again, everyone in the room was already cheering. I had waited for nothing, then hurried back to the hotel to sleep.
Compared with the previous year, this DEF CON was less of a spectacle: there were no airplane or card-game challenges, but the technical depth increased considerably. Instead of participating only in reverse engineering, I felt more involved this year. I patched mruby bytecode and attempted to write a virtio driver, learning a great deal. I hope I can join such a strong team again next year and return to DEF CON CTF.