AES T-table lookups are data-dependent leaking key material via cache timing (CCMP always dm-crypt/IPsec on non-AES-NI)
Summary
Every round of rijndaelEncrypt/rijndaelDecrypt performs four table lookups per output word whose indices are byte slices of secret-dependent state (e.g. Te0[s0>>24]). Te0..Te3/Td0..Td3 are 1KB each (256x u32) so whether a given line is resident in data cache depends on key-derived index. Co-resident local process recovers AES key bytes via Prime+Probe/Flush+Reload cache-timing. Tables are read-only static const shared across all callers cache-resident (worst case for attacker). wlan_ccmp invokes rijndael_encrypt unconditionally per frame (no AES-NI bypass) so WPA2-CCMP always vulnerable. cryptoapi software fallback used by dm-crypt when AES-NI absent/disabled. [VERIFIED pass2: mechanism reproduced byte-exact through real kernel code - instrumented twin validated on 900 vectors shows all 160 table indices per AES-128 block are secret-dependent, key byte 00 to 80 flips exactly index 0 and Te0 cache line, 40/40 round-1 identities hold; consumers confirmed wlan_ccmp per-frame and cryptoapi dm-crypt, opencrypto uses separate aesni path (filing corrected); aggregate timing signal not separable on QEMU rig and end-to-end key recovery not executed - honest limits; confidentiality only, privesc closed; fix is algorithm swap to aesni plus bitsliced fallback, no minimal patch]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2367 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| rijndael_harness.c | β | 18.2 KB | view raw | |
| rijndael_twin_tables.h | β | 17.9 KB | view raw | |
| rijnh.c | β | 2.9 KB | view raw | |
| build.sh | β | 168 B | view raw | |
| run_idx.sh | β | 2.6 KB | view raw | |
| build_baseline.log | β | 5.6 MB | β download | |
| idx_dmesg.txt | β | 42.6 KB | view raw | |
| idx_run.out | β | 10.3 KB | view raw | |
| idx_analysis.txt | β | 659 B | view raw | |
| idx_analysis2.txt | β | 94 B | view raw | |
| leak_sample.txt | β | 1.9 KB | view raw | |
| ttables.txt | β | 345 B | view raw | |
| kat_baseline_dmesg.txt | β | 1.4 KB | view raw | |
| checkvec.log | β | 1.3 KB | view raw | |
| checkvec.py | β | 2.1 KB | view raw | |
| env.txt | β | 365 B | view raw | |
| verdict.json | β | 4.7 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw |
DF-2367 β AES T-table lookups are data-dependent (cache-timing side channel)
File: sys/crypto/rijndael/rijndael-alg-fst.c (consumers: wlan_ccmp, cryptoapi/dm-crypt)
Verdict (see VERDICT.md for the full narrative)
REPRODUCED (mechanism) / impact: leak / confidence: likely.
What was reproduced byte-exact on the guest through the REAL kernel rijndael
code: every T-table lookup index is a secret-dependent byte
(index = key_byte XOR plaintext_byte in round 1; fully key/plaintext-driven
afterwards). Flipping ONE key byte flips EXACTLY the corresponding round-1
index and the touched Te0 cache line; 40/40 statistical samples satisfy all 16
round-1 index == key ^ pt identities; the access sequence is 160 table reads
per AES-128 block against 1 KB (16-cache-line) tables at fixed kernel
addresses.
What was NOT executed on this rig: end-to-end Prime+Probe/Flush+Reload key recovery. QEMU/KVM offers no cache-occupancy measurement fidelity for a cross-mode attack and an aggregate timing probe showed no separable signal (warm caches, 59-69 ns/op for both test keys) β the real channel is per-access line occupancy, which is exactly what the trace dump shows. The exploitation literature for this precise code shape (Osvik-Shamir-Tromer, Bernstein) plus the reproduced mechanism support the original Medium rating.
Rig
Same kernel-compiled harness as DF-2368 (shared build; see DF-2368/build.sh):
sysctl kern.rijnh.test ops idx / idxr1 / bench / twinval. The index
trace comes from an instrumented twin of the round loop whose tables are a
verbatim copy of the kernel Te tables and whose key schedule IS the real
rijndaelKeySetupEnc; the twin is validated byte-exact against the real
rijndael_encrypt on 600+300 vectors, and per-trace ct(real==twin) is
asserted in the log.
Build (guest, root, from this directory)
sh build.sh # nativekernel build + install; reboot
Run (guest)
sh run_idx.sh # 5 full traces + 24x8 idxr1 matrix + table addresses + timing
Results land in dmesg (RIJNH: tag). Analysis: idx_analysis.txt,
idx_analysis2.txt; byte-exact dump: leak_sample.txt.
Expected output
RIJNH: idx key=... nidx=160traces; K vs K-prime (key byte0 00->80) differ at EXACTLY position 0 in round 1 (00vs80), 136/160 positions after avalanche.idxr1matrix: every sample satisfiesindex[j] == key[b] ^ pt[b]for all 16 round-1 lookups (40/40 clean-parsed samples).ttables.txt: Te0..Td4 at fixed kernel addresses, each table 0x400 = 1024 bytes = 16 cache lines.- Timing probe: overlapping per-op times for the two keys (honest negative β aggregate timing is not the channel).
Artifacts
rijndael_harness.c, rijndael_twin_tables.h, rijnh.c, run_idx.sh, build.sh, build_baseline.log, idx_dmesg.txt (full kernel log of the run), idx_run.out (driver stdout), idx_analysis.txt / idx_analysis2.txt (analysis), leak_sample.txt (byte-exact observables), ttables.txt (kernel table addresses), kat_baseline_dmesg.txt (KAT control), checkvec.log (OpenSSL cross-check of embedded vectors), env.txt. No fix.diff: the code-level remedy is replacing table-AES with a constant-time/AES-NI implementation for these consumers β an algorithm swap, not a minimal verifiable patch (see VERDICT.md).
DF-2367 VERDICT β data-dependent T-table lookups leak key material via cache timing
Status: REPRODUCED (side-channel mechanism, byte-exact) β impact: leak β confidence: likely Rig: DragonFly 6.5-DEVELOPMENT x86_64 stock X86_64_GENERIC (INVARIANTS), QEMU/KVM, kernel-compiled sysctl harness shared with DF-2368.
What was demonstrated (through the REAL kernel code)
-
The lookup indices are secret-dependent. rijndaelEncrypt performs 16 table lookups per round (
Te0[(s0>>24)] ^ Te1[(s1>>16)&0xff] ^ ..., rijndael-alg-fst.c:953-1007 round loop, 1013-1040 final round) where the state words areGETU32(pt) ^ rk[i]β in round 1 literallyindex = key_byte XOR plaintext_byte. The instrumented twin (validated byte-exact against the realrijndael_encrypt: 600 + 300 vectors, 0 mismatches, plusct(real==twin)asserted per trace) dumping the exact index sequence through the REALrijndaelKeySetupEncschedule showed:K (key=000102...0f, pt=00112233...eff): round-1 indices 0050a0f04090e03080d02070c01060b0 K' (key byte0 00->80): round-1 indices 8050a0f04090e03080d02070c01060b0 K''(key byte15 0f->ff): round-1 indices 0050a0004090e03080d02070c01060b0
One flipped key byte -> EXACTLY the corresponding index flips (position 0 resp. 3); 136/160 (K vs K-prime) index positions diverge after avalanche; total bitwise distance 534/1280. (leak_sample.txt, idx_analysis.txt)
-
The identities hold statistically. Across the 24x8 key/plaintext matrix, 40/40 cleanly-parsed samples satisfy ALL 16 round-1
index[j] == key[b] ^ pt[b]identities (idx_analysis2.txt). The remaining dmesg lines were mangled by console interleaving, not failures. -
The channel granularity is cache-line. Tables live at fixed kernel addresses (ttables.txt: Te0 0xffffffff80d897e0, Te1 ..930, Te2 ..8fe0, Te3 ..8be0, each 0x400 = 1024 bytes = 16 cache lines of 64 B; a lookup at index i touches line i>>4). K vs K-prime touch Te0 lines 0 vs 8 in round 1 β perfectly distinguishable to a line-granularity observer. Every AES-128 block performs 160 such lookups (AES-192: 208, AES-256: 240).
-
Aggregated timing does NOT separate keys on this rig (honest negative). bench: 59-69 ns/op for both keys, overlapping across 3 alternating runs β expected: with resident tables the leak is per-access line occupancy, not total execution time. Consequently a Bernstein-style remote/aggregate timing attack was not observable here, and an end-to-end Prime+Probe/Flush+Reload key recovery was NOT attempted: QEMU/KVM gives no cache-occupancy measurement fidelity for a cross-mode (user-observes-kernel) attack. The reproduced fact is the secret-dependent memory-access pattern itself β the observable that cache attacks measure.
Reachability (who runs this code with attacker-visible traffic)
- wlan_ccmp β software CCMP for 802.11 (ieee80211_crypto_ccmp.c:141
set_key, :399-417 and :559-563 several
rijndael_encryptcalls PER FRAME, no AES-NI bypass anywhere in the file). Any DF system doing WPA2-CCMP in software exposes the channel to a co-resident local process; frame data is attacker-influenceable (inject/observe traffic). - cryptoapi AES-CBC/XTS -> dm-crypt (cryptoapi.c:314/405-406, sole consumer dm_target_crypt.c) β every block IO to an encrypted volume walks these tables with the volume key; setup is privileged but the crypto runs on IO triggered by unprivileged users of the mapped volume.
- opencrypto/IPsec do NOT use this implementation (separate aesni/xform path), correcting the filing wording slightly: fast_ipsec sessions are not affected by THIS file.
Severity / privesc assessment
Medium as filed (AV:L/AC:H, confidentiality-only) β confirmed. Information leak of key material to a co-resident local observer; no write primitive, no direct kernel-memory disclosure, no privesc chain β privesc: closed for this finding (crypto-core class: local key confidentiality issue).
Why no fix.diff in this pack
The remedy is to stop using table-driven AES in these paths: route CCMP and cryptoapi software AES to the in-tree AES-NI implementation (sys/crypto/aesni) with a constant-time bitsliced fallback, or adopt a constant-time C implementation. That is an algorithm/API swap across three subsystems β not a minimal patch whose A/B this rig could validate (correctness vectors alone would not demonstrate constant-time behavior on QEMU). Recommendation recorded in verdict.json; no code change was applied to sys/.
Reproducibility
Same rig as DF-2368 (sh build.sh, then sh run_idx.sh; outputs in dmesg).
Deterministic: identical key+pt yield identical traces across boots (twin
byte-exact on every run), and the 16 embedded KATs produce identical
ciphertexts on stock and fixed kernels (DF-2368 A/B), demonstrating run-to-run
stability of the primitive.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableConfirmed kernel references
- sys/crypto/rijndael/rijndael-alg-fst.c:56
- sys/crypto/rijndael/rijndael-alg-fst.c:953
- sys/crypto/rijndael/rijndael-alg-fst.c:978
- sys/crypto/rijndael/rijndael-alg-fst.c:1013
- sys/crypto/rijndael/rijndael-alg-fst.c:1132
- sys/crypto/rijndael/rijndael-api.c:53
- sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:141
- sys/netproto/802_11/wlan_ccmp/ieee80211_crypto_ccmp.c:399
- sys/crypto/cryptoapi/cryptoapi.c:314
- sys/crypto/cryptoapi/cryptoapi.c:405
- sys/dev/disk/dm/crypt/dm_target_crypt.c:306
Detail
Exploit chain
co-resident unprivileged observer primes/probes LLC sets covering the kernel Te0 lines (fixed KVA, 16 lines/table) while the victim encrypts (wlan_ccmp per-frame or dm-crypt IO); round-1 hit pattern yields index = key XOR known plaintext byte; repeating over known/chosen plaintexts recovers the AES key schedule (classic Osvik-Shamir-Tromer/Fush-Reload methodology for this exact T-table design). Demonstrated on-rig up to the observable: the index sequence and its line-granularity key dependence.
Evidence (decisive lines)
leak_sample.txt (byte-exact 160-index traces for K, K-prime, K-double-prime with per-trace ct real==twin); idx_analysis.txt (round-1 divergence at exactly the flipped key byte, 136/160 after avalanche, Te0 line 0 vs line 8); idx_analysis2.txt (40/40 samples satisfy all 16 index==key^pt identities); ttables.txt (Te0 at 0xffffffff80d897e0, 0x400-byte tables); idx_dmesg.txt (full kernel log incl. timing probe honest negative); kat_baseline_dmesg.txt (16/16 KAT control)
PoC changes
Original filing had no runnable PoC (poc_path null). Rig authored from scratch: kernel-compiled sysctl harness (runtime kldload GPFs on this guest) with an instrumented twin of the round loop (tables copied verbatim from the audited source) validated byte-exact against the real functions on every run; several SP800-38A expected values sketched from memory in the filing preparation were wrong and were re-derived against OpenSSL 3.0.22 before embedding (checkvec.log).
Verified recommended fix
Route wlan_ccmp and cryptoapi software AES through the in-tree AES-NI implementation with a constant-time (bitsliced) fallback instead of the T-table rijndael core
Verdict
The side-channel mechanism is reproduced byte-exact through the REAL kernel rijndael code: every T-table lookup index in rijndaelEncrypt/rijndaelDecrypt is a secret-dependent byte (round 1: index = key_byte XOR plaintext_byte; rijndael-alg-fst.c:953-1007, 1013-1040). An instrumented twin of the round loop, validated byte-exact against the real rijndael_encrypt on 900 vectors with ct(real==twin) asserted per trace, dumped the 160-index access sequence per AES-128 block: flipping one key byte flips exactly the corresponding round-1 index and the touched Te0 cache line (tables are 1 KB = 16 cache lines at fixed kernel addresses, ttables.txt), and 40/40 statistical samples satisfy all 16 round-1 index==key^pt identities. End-to-end Prime+Probe/Flush+Reload key recovery was NOT executed: QEMU/KVM provides no cache-occupancy measurement fidelity for a cross-mode attack, and an aggregate timing probe honestly showed no separable signal (59-69 ns/op, overlapping) because the channel is per-access line occupancy, which is precisely what the trace demonstrates. Reachability confirmed: wlan_ccmp calls rijndael_encrypt several times per 802.11 frame with no AES-NI bypass, and cryptoapi AES-CBC/XTS (dm-crypt) walks the same tables on every block IO; opencrypto/IPsec use a separate aesni path. Confidentiality-only local key recovery channel; Medium as filed; privesc closed (no write primitive, no direct kernel-memory disclosure).
No comments yet.