Pokoy
alfactf
Task: a Verilog keypad/display module stored typed text in one 64-byte buffer but exposed only a transformed 64-byte display blob. Solution: simulate the keypad, recognize a white-box AES-128 ECB core in the Verilog tables, recover the key, and decrypt each reversed 16-byte block.
$ ls tags/ techniques/
Pokoy — alfactf
Challenge Overview
В модном коворкинге есть две зоны — «Нищета» и «Покой». Доступ решает аппаратный модуль. Один человек ввёл флаг задачи на клавиатуре, а после завершения на дисплее остался 64-байтный hex-blob.
The archive contained a Verilog design and a Python/cocotb interface for a keypad-driven display module. We were given the 64-byte blob shown after someone typed the real flag and had to recover the original plaintext.
Given output:
71 80 dd 08 6c 43 37 6b 3c a8 e2 68 6d 0a 97 9b f8 23 b9 66 6a b9 92 d3 cd ad 80 cf e8 5a ed 1f c5 c4 6c 60 c0 a3 a5 c9 11 a1 d1 98 f1 83 53 95 53 e4 b9 7c 16 5f c4 3b a3 f5 1e 12 53 1f ea ca
Recon and Initial Observations
The supplied files immediately suggested a hardware-reversing task rather than a normal software crackme:
peace.vwas the synthesized Verilog design.peace.pywas a cocotb TUI that let us press keys and view the display.- Replaying the circuit in simulation made it possible to inspect internal memories directly.
The first key observation was that the display buffer was not a plain echo of the typed text. Even with empty input, the internal output area already contained nonzero bytes. That ruled out a direct framebuffer copy and strongly suggested some fixed transform or whitening stage.
Keypad and Multi-Tap Behavior Discovery
Before reversing the crypto-like core, it was useful to understand how text entered the device.
By driving the keypad in simulation, the text entry logic turned out to be old-phone multi-tap input:
2->222->a222->b2222->c#toggles an uppercase latch, so#then22->Acc->_ddd->{dddd->}
This was important for two reasons. First, it confirmed the flag could be entered entirely through the keypad. Second, it let us generate controlled plaintexts and compare them with the transformed display bytes.
Internal Buffers and Block Structure
Simulation exposed two especially important memories:
M_00160: the 64-byte plaintext input bufferM_00161: the 64-byte display/output buffer
The output was processed as four independent 16-byte blocks. Looking at block boundaries made the structure much clearer: changes in one 16-byte region of M_00160 only affected the corresponding 16-byte region of M_00161.
That behavior matched ECB-style per-block processing rather than a stream cipher or chained mode.
Why the Transform Is White-Box AES / T-Table AES
The Verilog was huge, but the memory layout gave the game away.
Important layout facts:
M_00000..M_00015: 16 final 8-bit tablesM_00016..M_00159: 9 rounds × 16 32-bit tables
Those tables matched classic AES encryption components:
- the 32-bit tables behaved like AES
Te0..Te3T-tables with round-key material already folded in, - the final 8-bit tables matched
SBOX[x ^ k] ^ c, - after reordering indices by output-byte position, the wiring matched AES ShiftRows and normal T-table round structure.
In other words, the design did not implement a custom cipher at all. It implemented white-boxed AES-128 encryption, with key material hidden inside lookup tables instead of appearing as a normal round-key array.
Key Recovery and Decryption Formula
After identifying the AES structure, the embedded AES-128 key could be reconstructed from the tables:
3d2e73730ded60ec44750fe3b83b1572
The only remaining wrinkle was byte order. The exact per-block transform used by the display was:
disp_block = reverse_bytes(AES128_ECB_encrypt(reverse_bytes(plain_block), key))
So decryption is:
plain_block = reverse_bytes(AES128_ECB_decrypt(reverse_bytes(disp_block), key))
Applying that to each of the four 16-byte blocks recovered the full plaintext.
Final Solve Script and Verification
The final recovery script is short once the AES key and byte-order convention are known:
#!/usr/bin/env python3 from Crypto.Cipher import AES KEY = bytes.fromhex("3d2e73730ded60ec44750fe3b83b1572") TARGET = bytes.fromhex( "7180dd086c43376b3ca8e2686d0a979b" "f823b9666ab992d3cdad80cfe85aed1f" "c5c46c60c0a3a5c911a1d198f1835395" "53e4b97c165fc43ba3f51e12531feaca" ) def decrypt_display_blob(blob: bytes) -> bytes: aes = AES.new(KEY, AES.MODE_ECB) out = bytearray() for i in range(0, len(blob), 16): block = blob[i:i + 16] out.extend(aes.decrypt(block[::-1])[::-1]) return bytes(out) def encrypt_plaintext_blob(blob: bytes) -> bytes: aes = AES.new(KEY, AES.MODE_ECB) out = bytearray() for i in range(0, len(blob), 16): block = blob[i:i + 16] out.extend(aes.encrypt(block[::-1])[::-1]) return bytes(out) if __name__ == "__main__": pt = decrypt_display_blob(TARGET) print(pt.decode()) assert encrypt_plaintext_blob(pt) == TARGET
Verification steps:
python3 recover_flag.pyprints the flag.- Re-encrypting the recovered plaintext with the same formula reproduces the exact 64-byte blob from the challenge.
Final Flag
alfa{whitebox_means_open_floor_plan_3f16ce7b86f56a86f645c24e4b1}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [hardware][free]Nishcheta— alfactf
- [crypto][Pro]Одноразовый блокнот (One-Time Pad)— hackerlab
- [reverse][Pro]Not reverse 100% - TaipanByte CTF— taipanbyte
- [crypto][Pro]Обратный шифр (Reverse Cipher)— duckerz
- [reverse][Pro]Matryoshka— duckerz