This is a write-up on the DEF CON 34 badge, focused on recovering dc34/k0, the shared AES-256 key used by the badge's QR-code light-exchange protocol. Along the way, I also recovered my badge's device-specific PDDB master key and used it to decrypt the .System basis of its plausibly deniable database.
I did not figure all of this out alone, nor was I the first to capture the flag. The work grew out of parallel research, Anthony Mattas's crucial write-up, and discussion in the Baochip Discord. My goal here is to assemble the complete path—from the boot-header flaw to verified PDDB extraction—in one place for others facing similar embedded-security problems.
AI disclosure: this article was written by a human, but AI tools helped with portions of the reverse engineering and implementation. I discuss that process, and my mixed feelings about it, at the end. AI also helped copy-edit and cite this article (keeping a well structured bibliography of source code citations is hard, sorry)
Introduction
The DEF CON 34 badge features a removable core module built around the Baochip 1x system-on-chip. The core sits in a clear case and, after the conference, can be detached from the larger art board and used as a FIDO2 security token. It has a camera for scanning QR codes, a 128-by-128 OLED display, and USB-C. The backing board carries the conference artwork and a ring of multicolor LEDs. Bunnie Huang led the technical effort, with contributions from Team Cheeso — Yafsec, Delia, and Wietsman — Kingpin, and others.
The conference game revolved around exchanging QR codes with other badges. A genetics-inspired mixing algorithm combined parts of their LED colors and timing patterns with yours, producing a pattern that evolved as you met more people.
The Baochip 1x is described by its designer as a mostly open, full-custom SoC fabricated on TSMC's 22 nm process. Its main CPU implements RISC-V, and the badge runs Xous, a Rust-based microkernel intended for high-security applications.
The badge stores secrets in the Plausibly Deniable DataBase, or PDDB. The PDDB combines encrypted storage with a layout designed to conceal not only the contents of secret bases, but whether those bases exist at all.
The badge firmware can be updated, but loading unsigned developer firmware takes a deliberately destructive path: it sets a one-way developer-mode state and erases part of the material needed to reproduce the production device key. Existing production secrets then become unrecoverable through the normal key hierarchy. The official badge page warns:
NOTE: Loading your own code onto the badge causes it to enter developer mode. This is a one-way door which causes provisioned secrets to be erased. As a result, you will be unable to exchange lights with others (at least until the light encryption key is extracted...I expect that won't take too long)
"Not too long" was right: I heard of a successful extraction only hours after badges were distributed on Thursday.
The main public repositories are:
dc34-core-hw: KiCad hardware design files.dc34-api: shared IPC APIs and data structures.dc34-console: the console, power management, and LED services.dc34-vault: the conference game, QR-code protocol, token mode, and cryptographic implementation.defcon-scheme.md: the original light-exchange threat model and protocol proposal.
This article explains how we recovered dc34/k0, which allows us to generate valid light-exchange responses and arbitrary badge patterns. We also derive the device-specific PDDB master key and decrypt the .System basis containing that record. That does not give us a firmware-signing key, bypass passwords for other secret bases, or authorize arbitrary signed updates. The firmware compromise and the database recovery are related parts of the chain, but they are not the same capability.
The Game

The badge software includes a game for DEF CON attendees. The badge has three face buttons, plus a side reset button that is not used during normal play. The left and right buttons display a QR code, while the center button opens the camera. During normal operation, the display alternates between the DEF CON logo and an image uploaded over USB. Meanwhile, the LEDs around the edge show the badge's evolving color and timing pattern.
You can mix light colors and patterns with another badge by completing a two-QR-code handshake. The genetic algorithm incorporates parts of the donor's color and timing pattern into your own, with a small chance of mutation.
Like many badge games, it is designed to encourage social interaction: you have to meet people and scan their badges. Goons—DEF CON staff—speakers, vendors, and other badge classes begin with distinctive patterns, making them desirable donors. Under normal operation, this is how you acquire rare colors and behaviors. The handshake is:
- Press the left or right button to display a QR code containing a nonce—a number used once, for the UK readers, not the other thing.
- Ask the donor to open their camera with the center button and scan your code. Their badge responds by displaying another QR code.
- Scan the donor's response with your badge, then accept or reject the resulting light pattern.
The response is encrypted and authenticated using a key shared by the entire badge population. As long as that key remains secret, production badges accept only responses generated by another badge—or by someone who has recovered the key. Once the key is known, a researcher can generate valid exchanges for arbitrary patterns.
Token Mode
When detached from the backing board, the core module can operate as a FIDO2 authenticator and password/TOTP store. That mode is not central to the light-exchange challenge, but it matters when discussing disclosure: a badge's device-specific PDDB key can protect personal data beyond the conference game.
The Scheme
The original defcon-scheme.md describes the game's threat model. The 20-byte light genome is exchanged over QR codes and mixed nondeterministically. Developer firmware may display any pattern, but entering developer mode erases the production secret required to participate in authenticated exchanges. The design also envisioned brute force as a legitimate late-conference solution, with the remaining secret strength reduced over time.
The proposed construction used AES-256-GCM-SIV:
(C, tag) = AES256E(K, N, M, AD)
(M, valid) = AES256D(K, N, C || tag, AD)
Here, K is the key, N the nonce, M the plaintext, and AD any associated data. The proposal divided the 256-bit key into a secret portion and a disclosed portion:
K = Ko || Kp
Ko was envisioned as an initially 96-bit shared secret, while Kp would be public and grow over the course of the conference to reduce the remaining brute-force work.
There is an important implementation detail: the shipped application does not assemble Ko || Kp at the point of use. It reads a 32-byte record named dc34/k0 from the PDDB and passes those 32 bytes directly to Aes256GcmSiv. In the rest of this article, k0 refers to that complete stored AES-256 key; Ko refers to the secret portion described by the earlier design document.
Light Exchange Protocol
Phase 1
Bob wants Alice to share her lights. Pressing [Left] or [Right] enters VaultMode::ShowKey on Bob's badge and displays header || Nonce1 as a QR code. The header is a fixed preamble of roughly 16 bytes. Nonce1 is a randomly generated 12-byte value, rejected and regenerated if it matches the prior nonce or the preamble. Alice presses [Center] to enter VaultMode::ResponseGene and open her camera.
Phase 2
Alice consents by scanning Bob's nonce. Her badge returns a QR code containing:
AES256E(K, Nonce1, donor-light-pattern || pad || badge_type, null) || tag
The light pattern is nine bytes, followed by currently unused padding. Bob scans this response and performs:
AES256D(K, Nonce1, C || tag, null)
The badge accepts the pattern only if the authentication tag validates, then presents VaultMode::ConfirmGene so Bob can keep or discard it.
The protocol does not aim to keep light patterns secret forever. QR codes can be photographed and retained, and the design anticipated gradual disclosure of key material. Its primary goal is to make exchanges fresh and interactive: production firmware accepts a response only for the nonce currently displayed, rotates that nonce between rounds, and times out the exchange after about one minute. Recovering the shared secret therefore acts as the intended flag: with k0, a researcher can synthesize a valid response instead of obtaining one from another production badge.
The Vulnerability
The Baochip bootloader accepts firmware updates in Microsoft's UF2 format. Holding a face button while pressing the side reset button enters update mode, where the badge appears as a USB mass-storage device. Copying UF2 files to that volume programs the corresponding firmware records. Outside update mode, Boot1 also exposes a serial console over USB.
The chip is factory-programmed with two early boot stages:
boot0is the root of trust. It resides in hardware-protected RRAM and validatesboot1.boot1implements USB mass storage, USB serial, the firmware-update path, and a small device-management terminal. It validates the loader before transferring control.
The proper order is:
Boot0 → verifies Boot1
Boot1 → verifies Loader
Loader → verifies/loads Kernel (xous)
Each executable partition begins with a 768-byte SignatureInFlash block. The first word is a RISC-V JAL x0, ... instruction that skips the header and enters the stage's code. It is followed by the Ed25519 signature, optional WebAuthn associated-data fields, and the sealed metadata:
| Offset | Size | Field | Verification coverage |
|---|---|---|---|
0x000 |
4 | RISC-V JAL x0, ... |
Excluded |
0x004 |
64 | Ed25519 signature | Excluded; this is the signature value |
0x044 |
4 | aad_len |
Excluded; selects the verification protocol |
0x048 |
60 | AAD storage | Excluded from the body hash; optionally supplied to WebAuthn verification |
0x084 |
4 | Format version | Signed body begins here |
0x088 |
8 | Magic value | Signed |
0x090 |
4 | Signed-body length | Signed |
0x094 |
4 | Function/partition code | Signed |
0x098 |
4 | Anti-rollback version | Signed |
0x09c |
16 | Minimum compatible version | Signed |
0x0ac |
16 | Image version | Signed |
0x0bc |
144 | Four Pubkey records |
Signed |
0x14c |
20 | Toolchain commit | Signed |
0x160–0x2ff |
416 | Signature-block padding | Signed |
0x300 |
— | Executable stage code | Signed |
The source makes the boundary explicit: UNSIGNED_LEN is 132 bytes. For the badge's normal Ed25519ph images, aad_len is zero, so verification hashes from offset 0x084 through the declared signed length. The first 132 bytes are outside that body. Most of this area is metadata, but the CPU begins execution at offset zero—on the unsigned jump instruction.
That four-byte instruction is enough. The normal loader header starts with:
ram:60060000 6f 00 00 30 j +0x300
We replaced it with:
ram:60060000 6f c0 02 00 j +0x2c000
The new target, 0x6008c000, is well within the ±1 MiB reach of a RISC-V J-type jump. The loader's signature and every byte it covers remain unchanged; only the unsigned entry instruction changes.
Finding somewhere useful to land required examining the actual update artifacts. The loader partition starts at 0x60060000. Its authenticated extent ended at 0x6008a2a0, while loader.uf2 ended at 0x6008b200. The next page-aligned address was 0x6008c000. The kernel signature block did not begin until 0x6009fd00, with kernel code at 0x600a0000. That left 84,736 bytes between the end of the loader update and the kernel header, or 81,152 bytes after aligning our payload start to a page. I call this unused interval the slack space.
0x60060000 → 0x6008b200
Authenticated through 0x6008a2a0
→ 0x6008c000
stage2 landing zone
0x6008c000 → 0x6009fd00
81,152 page-aligned bytes
0x6009fd00 → 0x600a0000
768 bytes
0x600a0000 →
Redirecting execution there was only half the problem: we also had to populate it. This is where Boot1's UF2 handler supplied the second primitive. Boot1 treats each valid UF2 record as an independently addressed RRAM write rather than requiring the upload to describe one complete authenticated partition.
A UF2 block contains fields equivalent to:
pub target_addr: u32,
pub payload_size: u32,
pub block_no: u32,
pub num_blocks: u32,
pub file_size_family_id: u32,
pub data: [u8; 476],
Because we control target_addr, we can generate records aimed at the slack space:
UF2 block 0 → 0x6008c000
UF2 block 1 → 0x6008c100
UF2 block 2 → 0x6008c200
...
Boot1 permits RRAM updates beginning at BAREMETAL_START, 0x60060000, through the end of the storage region. The relevant UF2-handler check is:
const START_RANGE: usize = bao1x_api::BAREMETAL_START;
//...
if matches!(record.address() as usize, START_RANGE..=STORAGE_END_ADDR)
&& record.family() == bao1x_api::BAOCHIP_1X_UF2_FAMILY
{
let mut rram = bao1x_hal::rram::Reram::new();
let offset = record.address() as usize - utralib::HW_RERAM_MEM;
rram.write_slice(offset, record.data());
}
The parser checks the UF2 framing, payload constraints, Baochip family ID, and destination range. It does not require all declared blocks to arrive, require blocks to be contiguous, bind the write to a known partition manifest, or prohibit writes to unused addresses. We could therefore build a UF2 file containing only our payload records.
To summarize, 0x6008c000 was:
- Beyond every byte supplied by loader.uf2.
- Before every byte supplied by xous.uf2.
- Outside the loader’s authenticated length.
- Outside the kernel’s authenticated image.
- Still inside
boot1’s permitted RRAM update range.
We named the code placed there stage2. We flashed the payload UF2 first, while the original loader still booted normally, and then flashed a second, tiny activation UF2 that changed the loader's entry jump. Our first stage2 payload drew to the OLED, giving us unmistakable proof of execution.
At this point we had achieved arbitrary code execution in RISC-V Machine mode without modifying the loader's signed body. But code execution was only the beginning; the flag was still inside an encrypted database, and its key hierarchy was protected by additional hardware access controls.


The Extraction
Code execution alone was not enough. The dc34/k0 record was inside the PDDB on the badge's external SPI-NOR flash. We could read that flash, but its contents were encrypted under a hierarchy rooted in device-specific material stored in hardware-protected internal slots. During a normal boot, Boot1 seals the Coreuser configuration before it transfers control to the loader.
The PDDB
The PDDB uses layered key protection rather than one key encrypting the entire flash. At the top is a 256-bit device master key, derived at boot with HKDF-SHA-256. Its inputs include:
- A device-unique root seed
- Two large regions of nuisance keys, intended to complicate direct silicon readout
- Chaff keys XORed together to frustrate repeated power-side-channel measurements
- A salt, composed of the device UUID and chip production ID
- A context string such as
sec,dev,oem, ortampered
The master key is not stored in the PDDB image. Instead, its secret inputs live in slots assigned to the trusted Fw0 hardware identity. The slot definitions explicitly describe the nuisance, chaff, and root-seed roles.
When the PDDB is formatted, it generates two independent random AES-256 keys for the .System basis:
- A page-table key
- A data-page key
The StaticCryptoData page stores those keys after wrapping them under the device master key using the NIST AES Key Wrap with Padding construction. Each 32-byte key therefore appears as a 40-byte wrapped blob. An incorrect master key fails the AES-KWP integrity check; even a superficially plausible result would later fail page authentication.
The page-table key encrypts each 16-byte page-table entry as an independent AES block. A page-table entry contains:
- A virtual-page address
- Flags
- A random 32-bit nonce
- A weak Murmur3 checksum used for fast plausibility testing, not cryptographic authentication
Data pages are protected by AES-256-GCM-SIV and contain:
- A 12-byte nonce
- Encrypted journal revision + data
- A 16-byte authentication tag
The associated authenticated data is:
basis name || PDDB format version || device DNA
This binds a valid page to:
- Its basis, such as
.System - The PDDB format version
- The individual badge
The root page of each basis carries an additional key commitment, added to address the AES-GCM-SIV multi-key ambiguity known as the salamander problem. A candidate basis key is accepted only if both the AEAD authentication and key-commitment comparison succeed.
The .System basis containing dc34/k0 uses these wrapped, device-derived System keys. Other PDDB bases may be protected by separately password-derived keys, so recovering the System keys does not automatically unlock every possible secret basis.
We now needed two artifacts: a complete PDDB image and the inputs required to reproduce its device master key.
Our first stage2 payloads wrote to the OLED, which was ideal for proving execution and inspecting a few bytes. It was a miserable channel for megabytes of data, although other researchers built clever visual encodings using colored blocks rather than hexadecimal text. We instead implemented a small, read-only USB probe.
Because the payload runs before Xous and its drivers, the probe initializes the badge's USB and SPI peripherals directly. An INFO command reports the flash JEDEC identifier and the PDDB's raw four-megabyte region, 0x00400000 through 0x007fffff. A bounded READ command returns at most a small block from that region, with CRC-32 over every response. We deliberately implemented no flash erase, program, or write command.
On the host, a resumable Python dumper checked every response's address, length, and CRC, maintained a running SHA-256 digest, and wrote periodic checkpoints. We acquired the complete four-megabyte region twice and required the resulting files to have identical SHA-256 hashes. Overkill? Maybe, but it ruled out a loose USB connection or transient SPI error before we trusted the evidence.
We now possessed the encrypted PDDB: page-table ciphertext, authenticated data pages, and the two wrapped .System keys. The next task was not to crack the master key, but to reproduce its legitimate derivation.
The Master Key
Baochip's Coreuser hardware maps RISC-V address-space identifiers to four hardware identities. ASID 3—the PID reserved for the keystore—maps to trusted identity Fw0; the kernel and ordinary processes map to the less-trusted Fw1. Boot1 then sets INVERT_PRIV, making the trusted mapping effective only for paged User-mode access. The source comments are unusually direct: arbitrary execution in the bootloader or kernel is game over for this access-control scheme.
Consequently, simply executing in Machine mode did not let us read the protected slots. We had to lower the CPU's architectural privilege while choosing the trusted identity. stage2 constructed an Sv32 page table, selected ASID 3, identity-mapped the required RRAM, SRAM, and peripheral windows as user-accessible, and entered User mode with mret. From that context, our capture stub could read the root seed, nuisance keys, chaff keys, UUID, chip ID, and security-state counters into a buffer. The USB recovery protocol returned that KDF bundle to the host in bounded, CRC-checked chunks.
With both artifacts—the verified PDDB image and its matching KDF bundle—we reproduced the normal keystore path offline:
- Derive the PDDB master key with HKDF-SHA-256
- Use the master key to unwrap the
.Systempage table and data keys - Scan and decrypt the encrypted page table
- Validate candidate mappings by authenticating their AES-GCM-SIV data pages
- Authenticate the
.Systembasis root including key commitment - Reconstruct the PDDB dictionary and key structures
- Extract the value stored under
dc34/k0
No cryptographic primitive was broken. The vulnerability let us cross the hardware boundary protecting the KDF inputs; after that, we followed the database's intended decryption and authentication path.
The Result

The exploit and recovery chain produced my badge's device-specific PDDB master key. Its SHA-256 fingerprint, sufficient to identify this result without disclosing the key, is:
b8013559817282c1d682d1ee628c18bd643a424b95461ca40a6c8df131efe199
The recovered conference-wide dc34/k0 value—the complete key used by the shipped light-exchange implementation—is:
7ad84ed0e00aec0499ede65615e1da517c0150230d2abc6ec7b566e621e740b3
(this key has been published online by others with Bunnie's permission)
<3 kthxbye
Acknowledgements and References
I got here because other researchers got here first. Most notably, Anthony Mattas's Only 132 Bytes put me on the right path. I had been exploring a completely different rabbit hole involving BIO programs and the DMA memory firewall. His observation that the entry jump was not covered by the image signature caused an immediate headdesk moment; from there, I could build the rest of the extraction chain.
There was also healthy discussion in the Baochip Discord's DEF CON channel, where people exchanged hints and progress (without spoiling it!). The lesson for me was not to become attached to my initial assumptions. I had treated the firmware as an indivisible signed object and was afraid that experimenting with updates would destroy the production secrets.
Primary sources:
- DEF CON 34 badge information and developer-mode warning
- DEF CON light-exchange design document
- Shipped
k0loading and AES-256-GCM-SIV initialization - Baochip signature-block definition
- Baochip signature-verification implementation
- Boot1 UF2 write handler
- Coreuser identity and ASID mapping
- BaoSec keystore master-key derivation
- PDDB wrapped-key storage and authenticated page decryption
- UF2 format overview
- PDDB design overview
- Xous documentation
Postscript: AI
GPT-5.6 Sol helped me get the OLED and USB code working, and it consolidated my collection of shaky Python host scripts into one coherent tool. I was unfamiliar with this corner of hardware hacking when I began; this was my first encounter with both UF2 and RISC-V. I'm a small baby and prefer to have system calls and an operating system holding my hand. For parts of the implementation, I acted more like a project manager and reviewer than the person typing every line, and I think that is fine.
I think of AI here as a code calculator. If you are learning multiplication, using a calculator defeats the exercise. By Calculus II, nobody cares whether you use a graphing calculator; the point is understanding the concepts and the problem you are trying to solve. An inexperienced researcher could still have stared at this challenge with every available model and missed the essential insight: patching the unsigned entry instruction and placing executable code in the inter-image gap. There were plenty of rabbit holes and false starts, particularly my attempt to enumerate memory from an uploaded BIO program.
Everyone I spoke with who worked on the badge challenge used AI somewhere in the process, in various interactive or agentic setups. I also tried Kimi and a local Qwen model. I have mixed feelings about what this means for CTFs. The ability to spend heavily on tokens can become an advantage unrelated to a team's underlying skill. Watching agentic systems in a competition can feel like watching slot machines: pull the handle and wait to see whether the model produces lemons or cherries.
That said, I think AI use in a CTF is fine when the rules and expectations are clear. I would also enjoy "Faraday cage" events with no outside assistance. Automation has shaped competitions for a long time: Metasploit, sqlmap, and private libraries of reusable solvers already rewarded teams that had seen a challenge pattern before. AI accelerates that trend and makes the access and disclosure rules more important.
There was also the issue of model refusals. I repeatedly hit refusals because the work looked like offensive firmware exploitation without its surrounding CTF context. Access to trusted cybersecurity programs, including their identity-verification requirements, made the workflow much smoother.
In conclusion: I learned something. I understand secure boot, UF2, RISC-V privilege transitions, and encrypted embedded storage better than I did before, and I am excited to get my grubby mitts on more firmware and more chips. In that sense, I call it a win. Thanks for reading.