$ cat writeup.md…
$ cat writeup.md…
alfactf
Task: a keypad-and-display hardware simulation hid the flag behind synthesized Verilog logic and SMS-style input. Solution: instrument the netlist, force one-cycle key commits, decode the winning multi-tap groups into y4am, and read the final ASCII chunks from the written display pages.
We were given peace_6ce0fda.tar.gz, containing a hardware-style keypad/display simulation. The goal was to understand how the 4x4 phone keypad fed the synthesized Verilog design and then recover the flag that occasionally appeared on the 128x64 display.
The useful files were:
peace.py — a cocotb harness for interacting with the device in simulationpeace.v — a large Yosys-generated Verilog netlistDockerfile and docker-compose.yml — environment helpersThis immediately suggested that the intended path was not manual clicking alone. peace.py made simulation easy, while peace.v exposed the real behavior of the device, including the internal buffers and display writes.
The most important early observation was that several false leads existed around hidden selector logic. Instead of spending too much time guessing what hidden branch might unlock the flag, the reliable approach was to inspect what the Verilog actually wrote into the display/output pages after a committed input.
The keypad logic uses old-phone multi-tap input. Repeated presses on one key select a specific symbol for that key's group, and the character is only useful once the tap is cleanly committed.
The character mapping needed for the solve was established from the simulation:
9999 -> y4 -> 422 -> a66 -> mSo the winning input groups were exactly:
9999 4 22 66
These four committed groups produced plaintext bytes corresponding to:
y4am
A key optimization during solving was to force the internal key-detect signals for a single cycle. That emulated one clean committed tap group, avoided timing noise from manual interaction, and made it much easier to test candidate inputs reproducibly.
The heavy lifting came from instrumenting peace.v rather than treating it as a black box.
Important findings:
_M_00324 exposed readable ASCII chunks after the correct committed inputThis was the decisive pivot. Once the correct groups were entered, the relevant page data clearly contained pieces of the flag. At that point, the solve became an extraction and reconstruction problem rather than a search for more hidden branching logic.
The successful workflow was:
_M_00324 display pages.The winning groups were:
9999, 4, 22, 66
These decode to y4am in the internal buffer. After that committed input, the display/output pages revealed the following visible chunks:
alfa{niw hta_or_p okoy_dec ided_in_ silicon_ 6fb9b724 9ff14993 1f8b959}
Concatenating the rows reconstructs the full flag.
The final verification script below records the solved tap groups and reconstructs the flag from the traced display rows:
#!/usr/bin/env python3 TAP_GROUPS = ["9999", "4", "22", "66"] TOKEN_TO_CHAR = { "9999": "y", "4": "4", "22": "a", "66": "m", } DISPLAY_ROWS = [ "alfa{niw", "hta_or_p", "okoy_dec", "ided_in_", "silicon_", "6fb9b724", "9ff14993", "1f8b959}", ] def decode_groups(groups): return "".join(TOKEN_TO_CHAR[g] for g in groups) def reconstruct_flag(rows): return "".join(rows) if __name__ == "__main__": plaintext = decode_groups(TAP_GROUPS) flag = reconstruct_flag(DISPLAY_ROWS) print(f"winning groups: {TAP_GROUPS}") print(f"decoded plaintext: {plaintext}") print(f"flag: {flag}") assert plaintext == "y4am" assert flag == "alfa{REDACTED}"
alfa{REDACTED}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar