hardwarefreemedium

Nishcheta

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.

$ ls tags/ techniques/
verilog_instrumentationsignal_forcingmulti_tap_decodingframebuffer_tracing

Nishcheta — AlfaCTF

Short Challenge Summary

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.

Recon / File Analysis

The useful files were:

  • peace.py — a cocotb harness for interacting with the device in simulation
  • peace.v — a large Yosys-generated Verilog netlist
  • Dockerfile and docker-compose.yml — environment helpers

This 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.

Input Model: Multi-Tap Keypad

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 -> y
  • 4 -> 4
  • 22 -> a
  • 66 -> m

So 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.

Reverse-Engineering Findings in the Verilog

The heavy lifting came from instrumenting peace.v rather than treating it as a black box.

Important findings:

  • the archive combined a user-facing cocotb harness with a much larger synthesized netlist
  • the interesting state was not just the visible keypad behavior, but the internal display/output pages
  • tracing the writes into the display buffer pages associated with _M_00324 exposed readable ASCII chunks after the correct committed input

This 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.

How the Winning Input Was Identified

The successful workflow was:

  1. Use the cocotb setup to drive the keypad through controlled grouped taps.
  2. Force the internal key-detect path for one cycle so each group is committed cleanly.
  3. Validate the resulting plaintext bytes in the internal input buffer.
  4. Trace the Verilog writes into the _M_00324 display pages.
  5. Reassemble the visible ASCII chunks.

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.

Solution Script

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{niwhta_or_pokoy_decided_in_silicon_6fb9b7249ff149931f8b959}"

Final Flag

alfa{niwhta_or_pokoy_decided_in_silicon_6fb9b7249ff149931f8b959}

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups