vkexchange
umdctf
Task: a Vulkan-based exchange service on Mesa lavapipe lets the user quote positions through vkUpdateDescriptorSets, but the chosen array element is attacker-controlled despite a single-descriptor binding. Solution: align the resulting out-of-bounds descriptor write onto the settlement clearing buffer descriptor, retarget it to an attacker-owned account buffer, then settle rounds to copy out the flag and read it back with audit_account.
$ ls tags/ techniques/
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
vkexchange — UMDCTF
Description
No original organizer description was preserved in the local workspace files.
English summary: the service exposes a small trading interface backed by Vulkan compute resources. The intended bug is not in the shader itself, but in how the host program updates storage-buffer descriptors for market quotes, which lets us corrupt a neighboring descriptor set and redirect a later GPU copy into memory we control.
Analysis
The important host helper is update_storage_desc(), which wraps vkUpdateDescriptorSets() with:
.dstBinding = binding, .dstArrayElement = array_elem, .descriptorCount = 1,
For quote_position(), array_elem comes directly from the user-supplied price_index. The program also enforces:
MIN_PRICE_INDEX = 32768- quote descriptor binding 0 has
descriptorCount = 1
So every valid quote writes far beyond the single descriptor that should exist in quote_book binding 0. On lavapipe from Mesa 22.3.6 this becomes exploitable because:
- each descriptor is 32 bytes
- each descriptor set has an 88-byte header before its descriptor array
- descriptor sets are individually allocated, but in practice land contiguously enough for deterministic cross-object corruption
That means a very large dstArrayElement can walk out of the quote_book allocation and start overwriting descriptors in a later descriptor set.
Why the first layout does not work
With one market configured as:
outcome_slots = 32768memo_bytes = 0
the target settlement_book descriptors are misaligned relative to the oversized quote_book walk. Only partial overlaps at indices 32775 through 32777 are reachable, and those corruptions crash instead of producing a useful retargeted descriptor.
Fixing alignment with memo_bytes = 8
The trick is to create one market with memo_bytes = 8.
That adds a 40-byte shift in lavapipe's descriptor layout:
- 32 bytes for the inline uniform descriptor
- 8 bytes for the inline uniform storage itself
...
$ grep --similar
Similar writeups
- [pwn][free]bookmaker— umdctf
- [pwn][free]velvet-table— umdctf
- [reverse][Pro]locked-in— DiceCTF 2026 Quals
- [pwn][Pro]Scanwich Station— gpnctf
- [pwn][free]Stupidcontract— kitctf