← Back to Writeups
CUSTOMMediumHardware

Pokoy

XESXOR9/10/20264 min read
#hardware#custom#medium#aes_key_extraction#ecb_block_decryption#keypad_emulation#whitebox_aes_table_recovery

Pokoy

Platform: CUSTOM | Category: Hardware | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: aes_key_extraction, ecb_block_decryption, keypad_emulation, whitebox_aes_table_recovery

Summary

Walkthrough of the CUSTOM Hardware Pokoy. 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

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:

text
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.v was the synthesized Verilog design.
  • peace.py was 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 -> 2
  • 22 -> a
  • 222 -> b
  • 2222 -> c
  • # toggles an uppercase latch, so # then 22 -> A
  • cc -> _
  • 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 buffer
  • M_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 tables
  • M_00016..M_00159: 9 rounds × 16 32-bit tables

Those tables matched classic AES encryption components:

  • the 32-bit tables behaved like AES Te0..Te3 T-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:

text
3d2e73730ded60ec44750fe3b83b1572

The only remaining wrinkle was byte order. The exact per-block transform used by the display was:

text
disp_block = reverse_bytes(AES128_ECB_encrypt(reverse_bytes(plain_block), key))

So decryption is:

text
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:

python
#!/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.py prints the flag.
  • Re-encrypting the recovered plaintext with the same formula reproduces the exact 64-byte blob from the challenge.

Final Flag

text
alfa{REDACTED}

Key Takeaways / Lessons

  • Mastering aes_key_extraction, ecb_block_decryption, keypad_emulation, whitebox_aes_table_recovery was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-25 • CUSTOM • Pokoy • Difficulty: Medium