Static analysis: cryptography (key generation, ciphers, RSA wrap)

Address: 0x140036740 (per-file keys and RSA wrap), 0x14007b1f0 / 0x1400822c0 (Yarrow) Callers / callees: FUN_1400ae840 → FUN_140036740 → Yarrow ×4 → cipher inits → RSA encrypt

Purpose

Each file gets four fresh random values from a weakly seeded PRNG. They drive two stream ciphers and are sealed with RSA-4096. Every primitive below was re-implemented in Python (scripts/akira_crypto.py, scripts/akira_ciphers.py, now folded into 06-decryptor/akira_decrypt.py) and matched against data captured live from the running sample.

Walkthrough

Diagram: artifacts/diagrams/encryption-flow.png (decryption counterpart: artifacts/diagrams/decryption-flow.png).

Component Implementation Where Validation
RNG Nettle Yarrow-256: two SHA-256 pools, an AES-256 counter and 1,500 reseed iterations. A new instance per value, seeded only with QPC × 100 ns as a decimal ASCII string, e.g. "867151895800" FUN_14007b1f0, FUN_1400822c0 Python regenerated all 80 key bytes of a captured RSA blob from its four seeds (run 2), and again for all three run-3 files
Per-file secrets key1 32 B, iv1 16 B, key2 16 B, iv2 16 B, generated in that order FUN_140036740 Seen at both cipher inits
Cipher A ChaCha with key setup called as kbits = 0x40, which takes the 16-byte branch: constant “expand 16-byte k”, key = key1[0:16] used twice, nonce = iv1[0:8], 64-bit counter from 0. 8 rounds (4 double-rounds). Partial 64-byte blocks are discarded at each 0xFFFF chunk FUN_14007b0b0 (init), FUN_14007c570 (key setup), FUN_14007beb0 (block) 128 B of live keystream matched only the 128-bit-key, 8-round variant; 12/20 rounds and 256-bit failed. Live state showed counter 0x400 at chunk 3
Cipher B KCipher-2 (RFC 7008), key2 / iv2, byte-continuous keystream FUN_14007b4e0 (init), FUN_14007c690 (key schedule), FUN_14007c9c0 (update), FUN_14007c8a0 (XOR) The model built from the binary’s tables reproduces RFC 7008 vector C.1 (f871ebef945b7272…) and matched 64 B of live keystream
Key wrap Nettle rsa_encrypt, PKCS#1 v1.5, embedded RSA-4096 key (e = 65537). Input is a 91-byte blob, output 512 B with each 32-bit word byte-swapped before storage FUN_14007b8d0, FUN_140081ac0 (DER import), FUN_14007ba90 91-byte input and 512-byte footer write both captured

Key constants / structures

91-byte RSA plaintext

Offset Size Content
0 1 0x01
1 1 mode (0 Full, 1 Spot, 2 Part)
2 1 percent (0 in Full mode)
3 8 original file size, little-endian
11 48 key1 and iv1 as 32-bit words in the order k0 k1 v0 k2 k3 v1 k4 k5 v2 k6 k7 v3, each word byte-swapped
59 32 key2 and iv2 interleaved k0 v0 k1 v1 k2 v2 k3 v3, each word byte-swapped

KCipher-2 tables. The α-multiplication tables do not all use one polynomial. amul0 fits 0x1C3. All four tables were rebuilt by GF(2)-linearity from the eight basis entries of each table read from the binary. The first attempt, which assumed one polynomial, failed the RFC vector.

The RSA context has its own Yarrow instance for PKCS#1 padding, seeded the same QPC-only way (FUN_14007bbe0). The private key never touches the victim, so recovery cannot come from the RSA side.

Findings: weaknesses

| Weakness | Verified? | Recovery impact | |—|—|—| | Each of key1, iv1, key2 and iv2 comes from a fresh Yarrow-256 seeded only with QPC×100 as decimal text (100 ns steps on a 10 MHz QPC) | Yes: captured seeds regenerated all key bytes | Search the seed instead of the key: about 10⁷ candidates per second of uptime uncertainty | | The four seeds are taken back-to-back per file (key1 → iv1 → key2 → iv2) and increase from file to file | Yes. Native gap measured in run 4: ~0.4 ms (median 4,121 ticks, floor ~3,890, max 27,109) | Gap search span of ~4,200 ticks covers 20 of 21 cases; one solved file brackets its neighbours to a few ms | | The first chunk of every region is KCipher-2 under key2/iv2, over predictable headers (PK\x03\x04, %PDF, D0 CF 11 E0, SQLite format 3) | Yes | A cheap known-plaintext test for each (key2-seed, iv2-seed) pair | | ChaCha uses only key1[0:16] and 8 rounds | Yes | Doesn’t shrink the search, but makes checking a candidate simpler | | Spot and Part leave data unencrypted | Yes, by read offsets | Partial carving without keys | | Original size stored; footer fixed at 512 B | Yes | Footer can be stripped exactly |

Open question: the QPC frequency differs on some hardware. The code handles any frequency, but on hosts that are not 10 MHz the seeds are not multiples of 100, which changes the search space.

Cross-references