β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2367

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2367 Β· 20 files
FileTypeDescriptionSize
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=160 traces; K vs K-prime (key byte0 00->80) differ at EXACTLY position 0 in round 1 (00 vs 80), 136/160 positions after avalanche.
  • idxr1 matrix: every sample satisfies index[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).

VERDICT.md
↓ download raw

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)

  1. 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 are GETU32(pt) ^ rk[i] β€” in round 1 literally index = key_byte XOR plaintext_byte. The instrumented twin (validated byte-exact against the real rijndael_encrypt: 600 + 300 vectors, 0 mismatches, plus ct(real==twin) asserted per trace) dumping the exact index sequence through the REAL rijndaelKeySetupEnc schedule 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)

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

  2. 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).

  3. 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_encrypt calls 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_testable
per-fix-DF-2367

Confirmed kernel references

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