<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Paradoxblog</title>
    <subtitle>Notes from the bench on hardware security, side-channel analysis, and fault injection.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://blog.paradoxical.net/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://blog.paradoxical.net/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-17T00:00:00+00:00</updated>
    <id>https://blog.paradoxical.net/atom.xml</id>
    <entry xml:lang="en">
        <title>EM Side-Channel Analysis of the EFR32MG12 Hardware AES Accelerator</title>
        <published>2026-06-17T00:00:00+00:00</published>
        <updated>2026-06-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Paradoxical Networking Group
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://blog.paradoxical.net/efr32mg12-hw-aes-em/"/>
        <id>https://blog.paradoxical.net/efr32mg12-hw-aes-em/</id>
        
        <content type="html" xml:base="https://blog.paradoxical.net/efr32mg12-hw-aes-em/">&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;&#x2F;h2&gt;
&lt;p&gt;We used a near-field EM probe to recover an AES-128 key while the
Silicon Labs EFR32MG12&#x27;s &lt;code&gt;CRYPTO0&lt;&#x2F;code&gt; hardware accelerator performed decryption.
The setup uses a Rigol MHO98 oscilloscope, a NewAE probe and amplifier, and a
hobby CNC for positioning.&lt;&#x2F;p&gt;
&lt;p&gt;We couldn&#x27;t quite get all 16 bytes reliably from correlation power analysis
(CPA) alone. Even after tuning the capture and analysis, there was always one
byte left to brute-force. In the run below, CPA recovered 15 bytes of the
round-10 key. Searching the remaining byte and checking against a captured
plaintext&#x2F;ciphertext pair finished the job.&lt;&#x2F;p&gt;
&lt;p&gt;Most of the work went into getting usable traces. We started with software
AES, spent about a month trying deep-learning approaches, then came back to
CPA with better probe positioning and much more averaging. Raelize&#x27;s
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;raelize.com&#x2F;blog&#x2F;espressif-systems-esp32-breaking-hw-aes-with-electromagnetic-analysis&#x2F;&quot;&gt;Breaking HW-AES with Electromagnetic Analysis&lt;&#x2F;a&gt;
was the main reference. Their ESP32 work provided a useful starting point for
both the position scan and the averaging strategy.&lt;&#x2F;p&gt;
&lt;p&gt;For these tests, we used a development board and firmware that lets us choose
inputs, repeat operations, and trigger the scope with a GPIO. The package is
intact. We used a known key to find a probe position and useful sample points,
then tested recovery on a separate capture with a randomly selected key.
That&#x27;s a convenient setup to work with, and those conditions matter when
considering whether the result applies to a real device.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;starting-with-software-aes&quot;&gt;Starting with software AES&lt;&#x2F;h2&gt;
&lt;p&gt;The target firmware is a fork of ChipWhisperer&#x27;s &lt;code&gt;simpleserial&lt;&#x2F;code&gt; AES example.
A Makefile switch, &lt;code&gt;CRYPTO=SW|HW&lt;&#x2F;code&gt;, selects TinyAES or the on-chip &lt;code&gt;CRYPTO0&lt;&#x2F;code&gt;
engine, so we could use the same command protocol for both. We started capturing
traces with a
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chipwhisperer.readthedocs.io&#x2F;en&#x2F;latest&#x2F;Capture&#x2F;ChipWhisperer-Husky.html&quot;&gt;ChipWhisperer Husky&lt;&#x2F;a&gt;.
The target runs from its own 38.4 MHz crystal; the Husky&#x27;s HS2 clock is not
connected.&lt;&#x2F;p&gt;
&lt;p&gt;CPA on TinyAES worked with a few thousand traces. For encryption, the model
&lt;code&gt;HW(SBox(pt ⊕ guess))&lt;&#x2F;code&gt; recovers the master-key bytes. For decryption,
&lt;code&gt;HW(InvSBox(ct ⊕ guess))&lt;&#x2F;code&gt; recovers the round-10 key, which can be inverted
through the AES key schedule to obtain the master key. All 16 bytes ranked
first in the software tests. The tools call that rank 0.&lt;&#x2F;p&gt;
&lt;p&gt;We moved the capture code out of the H-Field Probe Demo notebook into a
headless &lt;code&gt;capture.py&lt;&#x2F;code&gt; CLI, with Lascar handling CPA. We started with
ChipWhisperer&#x27;s &lt;code&gt;CWContainer&lt;&#x2F;code&gt; glue and extracted it to avoid pulling IPython
and pandas into the script. Having both capture and analysis on the command
line made the later scans and long acquisitions much easier to automate.&lt;&#x2F;p&gt;
&lt;p&gt;Then we switched to &lt;code&gt;CRYPTO=HW&lt;&#x2F;code&gt;. With 15,000 traces, none of the models we tried
recovered a byte, and the best correlations were around 0.04. The setup that
worked for software AES wasn&#x27;t giving us a useful hardware-AES signal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trying-deep-learning&quot;&gt;Trying deep learning&lt;&#x2F;h2&gt;
&lt;p&gt;During these experiments, we moved trace capture from the ChipWhisperer to
the Rigol because we thought deep learning would need the higher sampling
rate. The ChipWhisperer may have been enough, but we never went back to check.&lt;&#x2F;p&gt;
&lt;p&gt;We tried Google&#x27;s SCAAML first, with a converter (&lt;code&gt;cwp_to_scaaml.py&lt;&#x2F;code&gt;) to turn
ChipWhisperer projects into training shards. Its &lt;code&gt;scaaml_intro&lt;&#x2F;code&gt; ResNet1D stayed
near the random-guess baseline: validation loss around 5.545, or &lt;code&gt;ln(256)&lt;&#x2F;code&gt;.
Changing normalization and training for longer didn&#x27;t get it past that.&lt;&#x2F;p&gt;
&lt;p&gt;Next we tried Ledger Donjon&#x27;s SCADL approach, which uses CPA to
locate points of interest, trains a small CNN on a window around each one,
and combines candidate scores across attack traces.&lt;&#x2F;p&gt;
&lt;p&gt;That worked on software AES. The CNN ranked 12 of 16 bytes first, and a search
of roughly 36 million candidates completed the remaining four in about two
minutes. On hardware AES, though, validation loss stayed around 5.55 and none
of the 16 bytes ranked first.&lt;&#x2F;p&gt;
&lt;p&gt;The software result gave us some confidence in the pipeline, but we weren&#x27;t
getting anywhere with the hardware traces. At that point, we put model
training aside and worked on the measurements. Deep learning may still work
on this target; the approaches we tried didn&#x27;t recover the key from the
traces we had.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;averaging-and-the-test-setup&quot;&gt;Averaging and the test setup&lt;&#x2F;h2&gt;
&lt;p&gt;Raelize averaged repeated operations with identical inputs to reduce noise.
For independent noise, averaging &lt;em&gt;K&lt;&#x2F;em&gt; aligned measurements reduces its standard
deviation by a factor of √&lt;em&gt;K&lt;&#x2F;em&gt;. At 1000 repetitions, that&#x27;s about a 32-fold reduction
under those assumptions. It also means collecting a lot of raw acquisitions:
200,000 averaged traces at 1000× represent 200 million AES operations.&lt;&#x2F;p&gt;
&lt;p&gt;Here&#x27;s the setup we used:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Target:&lt;&#x2F;strong&gt; an EFR32MG12 development board (SLWRB4161A + WSTK), running from
the 38.4 MHz HFXO. We bypassed the integrated DC-DC converter with
&lt;code&gt;emuDcdcMode_Bypass&lt;&#x2F;code&gt; because its switching dominated the EM measurement.
PA2 supplies the trigger around the AES operation; input and RNG preparation
happen outside that window.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Probe and amplifier:&lt;&#x2F;strong&gt; a NewAE
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chipwhisperer.readthedocs.io&#x2F;en&#x2F;latest&#x2F;Tools&#x2F;CW505%20Planar%20H-Field%20Probe.html&quot;&gt;planar H-field probe&lt;&#x2F;a&gt;
and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chipwhisperer.readthedocs.io&#x2F;en&#x2F;latest&#x2F;Tools&#x2F;CW502%20Low%20Noise%20Amplifier.html&quot;&gt;low-noise amplifier&lt;&#x2F;a&gt;,
connected to scope channel 1.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Positioning:&lt;&#x2F;strong&gt; a Genmitsu CNC 3018-PRO carrying the probe over the package.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Capture:&lt;&#x2F;strong&gt; a Rigol MHO98. The Husky remains connected as the UART bridge
to the target.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;probe_and_dev_board.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;probe_and_dev_board.jpeg&quot; alt=&quot;NewAE planar H-field probe held over the EFR32MG12 on the Mighty Gecko development board.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The probe over the target. The holder fits the CNC; the
&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;h-field_prob_jig.png&quot;&gt;jig model&lt;&#x2F;a&gt; shows its mounting arrangement.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;newae_lna.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;newae_lna.jpeg&quot; alt=&quot;NewAE amplifier connected beside the probe holder, with the ChipWhisperer Husky in the background.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The amplifier in the measurement path. A separate
&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;newae_probe_psu.jpeg&quot;&gt;NewAE probe power supply&lt;&#x2F;a&gt; powers it.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;capturing-and-averaging-on-the-rigol&quot;&gt;Capturing and averaging on the Rigol&lt;&#x2F;h2&gt;
&lt;p&gt;Arming and reading one trace at a time over SCPI gave us about 7 traces&#x2F;s.
That wasn&#x27;t going to be practical at this averaging depth. The MHO&#x27;s segmented
acquisition mode, WaveRecord, can capture many frames in one session, but we
couldn&#x27;t find a stock interface for automating their bulk readback. The
playback controls were in the UI, with CSV export available for saving a session.&lt;&#x2F;p&gt;
&lt;p&gt;After some time with the scope firmware in Ghidra, we used Frida to access
the frame buffers from the scope process. It took a few iterations to settle
on a native Frida CModule that reads the buffers and streams them to the host.
We eventually pulled this into a standalone package,
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;uberjay&#x2F;rigol-fastrec&quot;&gt;&lt;code&gt;rigol-fastrec&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Internal DMA readback reached about 470 MB&#x2F;s, while the built-in 100 MbE port
delivered about 11.7 MB&#x2F;s to the host. A Realtek RTL8153 USB Gigabit adapter
raised that to about 27 MB&#x2F;s. Network transfer was still the limiting step for
raw readback.&lt;&#x2F;p&gt;
&lt;p&gt;So we moved the averaging onto the scope to send less data over that link. A NEON
accumulator in the Frida agent combines repeated frames before returning them
to the host, exposed as &lt;code&gt;read(average=1000)&lt;&#x2F;code&gt;. The scope still has to acquire
every repetition, but only the averaged trace crosses the network.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;scope.jpeg&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;scope.jpeg&quot; alt=&quot;Rigol MHO98 displaying the EM measurement on channel 1 and the GPIO trigger on channel 2.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The scope during setup, displaying the EM signal and trigger. The screen
settings in this photo are not the final capture settings below.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Along the way, we switched the capture and analysis tools to sedpack, the
LZ4&#x2F;FlatBuffer storage format used by SCAAML. That let us use the same analysis
tools with traces from either the Husky or the Rigol.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trigger-timing-and-shorter-records&quot;&gt;Trigger timing and shorter records&lt;&#x2F;h2&gt;
&lt;p&gt;Only about half the frames in our first bulk captures were usable. The triggered
operation took about 7.8 µs, while each 10,000-sample record at 1 GSa&#x2F;s covered
10 µs. The firmware was issuing triggers faster than the scope could finish
and re-arm.&lt;&#x2F;p&gt;
&lt;p&gt;We added a &lt;code&gt;delay-iters&lt;&#x2F;code&gt; parameter to wait after each AES operation. For that
capture configuration, 60 iterations gave the scope enough time to keep up.
The delay sits outside the trigger window and needs to be tuned with the
record length.&lt;&#x2F;p&gt;
&lt;p&gt;With the timing sorted out, we captured 100,000 full-length traces and used
known-key correlation to see which part of the record was useful:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;cpa_plot_full.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;cpa_plot_full.png&quot; alt=&quot;Known-key correlation for 100,000 traces with 10,000 samples each; many of the strongest peaks are near sample 5,200.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Full-window correlation from &lt;code&gt;datasets&#x2F;100K&lt;&#x2F;code&gt;; the
&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;cpa_plot_full.log&quot;&gt;saved log&lt;&#x2F;a&gt; lists the per-byte peaks.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;!-- The command pasted at the top of cpa_plot_full.log names 200K_UNKNOWN,
     but its loader output and the plot identify the dataset as 100K. --&gt;
&lt;p&gt;Selected lines from that output:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Loading train split from datasets&#x2F;100K&#x2F;&#x2F; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  100000 traces × 10000 samples each&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  mode=decrypt  model=invsbox_hw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  preprocessing: ac-couple&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  1: peak corr=−0.1607 @ sample 5222&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  4: peak corr=−0.0930 @ sample 5115&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  9: peak corr=−0.1417 @ sample 5169&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte 12: peak corr=+0.2027 @ sample 5194&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Once we knew where to look, we could shorten the captures. For &lt;code&gt;200K_NARROW&lt;&#x2F;code&gt;,
we reduced the record to 1000 samples, set a 5 µs trigger offset, and kept
samples &lt;code&gt;0:300&lt;&#x2F;code&gt; using the scope-side &lt;code&gt;analysis_window&lt;&#x2F;code&gt;. The agent averages and
crops before sending the result. That leaves 300 samples per stored trace
instead of 10,000, a roughly 33-fold reduction in data size on top of averaging.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;scanning-the-probe-position&quot;&gt;Scanning the probe position&lt;&#x2F;h2&gt;
&lt;p&gt;We used the CNC to scan an 8 mm square over the package, scoring each position
with known-key correlation using &lt;code&gt;invsbox_hw&lt;&#x2F;code&gt; on bytes 0, 4, 8, and 12. The
&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;cnc_jog.png&quot;&gt;CNC jog interface&lt;&#x2F;a&gt; handles manual positioning before a
scan.&lt;&#x2F;p&gt;
&lt;p&gt;The scan used a 30×30 grid, about 0.28 mm between points. At each of the 900
positions it collected 1500 averaged traces, with 1000 repetitions per trace
and 1000-sample records at 1 GSa&#x2F;s. That&#x27;s 1.35 million stored traces from
1.35 billion raw acquisitions. At roughly 13,300 raw acquisitions&#x2F;s, the scan
took about 28 hours.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;scan_dashboard.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;scan_dashboard.png&quot; alt=&quot;Completed EM scan dashboard showing 900 positions and stronger correlation on the right side of the rotated package map.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The scan dashboard, with the map rotated 90° counterclockwise. The selected
peak is about 0.195 at CNC coordinates (−1.52, −3.45) mm, displayed as
(+3.45, −1.52) mm on the rotated map.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The stronger correlation was concentrated toward one side of the package,
so we parked the probe at the selected position for the next captures. We
borrowed the grid and averaging settings from Raelize&#x27;s experiment, which
also took roughly a day to scan. The similar timing gives some context for
the capture speed, though the chips and measurement equipment are different.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;inspecting-the-captures&quot;&gt;Inspecting the captures&lt;&#x2F;h2&gt;
&lt;p&gt;We used the capture dashboard to keep an eye on progress and run CPA checks
on saved datasets:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;capture_dashboard.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;capture_dashboard.png&quot; alt=&quot;Capture dashboard for 200K_NARROW showing 200,000 training traces, 5,000 holdout traces, and an initial CPA result of 12 out of 16 bytes.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;This is the earlier known-key &lt;code&gt;200K_NARROW&lt;&#x2F;code&gt; capture, with 1000× averaging.
CPA over all 300 samples ranked 12 of 16 bytes first. It is separate from the
later 15&#x2F;16-byte recovery run below.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Known-key correlation on this dataset shows where each byte leaks:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cpa_plot.py&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --leakage-model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; invsbox_hw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --dataset-path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; datasets&#x2F;200K_NARROW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --ac-couple \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; config&#x2F;efr32_hwaes_200k.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;cpa_plot_narrow.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;images&#x2F;cpa_plot_narrow.png&quot; alt=&quot;Known-key correlation over the 300 stored samples in 200K_NARROW, with peaks at several distinct sample positions.&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The narrower capture. Sample indices are relative to the 5 µs trigger offset;
for example, sample 194 corresponds to sample 5194 in the full capture.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Selected lines from the &lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;cpa_plot_narrow.log&quot;&gt;narrow-window log&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  200000 traces × 300 samples each&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  1: peak corr=−0.1590 @ sample 222&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  4: peak corr=−0.0960 @ sample 115&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte  9: peak corr=−0.1378 @ sample 169&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte 10: peak corr=+0.0634 @ sample 88&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  byte 12: peak corr=+0.1993 @ sample 194&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Subtracting each trace&#x27;s mean (&lt;code&gt;--ac-couple&lt;&#x2F;code&gt;) helped with baseline drift.
We also got better key rankings by restricting CPA to samples
&lt;code&gt;115,169,192,194,220,222&lt;&#x2F;code&gt;. In &lt;code&gt;lascar_check.py&lt;&#x2F;code&gt;, sample selection happens before
mean subtraction, so changing the window changes both the samples being scored
and the baseline used for centering.&lt;&#x2F;p&gt;
&lt;p&gt;One detail that took some care: a correlation peak for the known key doesn&#x27;t
guarantee that it beats all 255 wrong guesses. For example, near sample 88,
byte 10&#x27;s true value had a correlation of about 0.063, while a wrong guess
reached about 0.083. We chose the six sample points using the known-key
captures, then kept them fixed for the separate random-key capture.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;recovering-15-bytes-and-searching-the-last-one&quot;&gt;Recovering 15 bytes and searching the last one&lt;&#x2F;h2&gt;
&lt;p&gt;For the next run, we increased averaging to 2000 repetitions per trace and
captured 200,000 traces under a randomly selected key. We kept the 300-sample
records and the same six points for CPA. Here&#x27;s the command from the
&lt;a href=&quot;https:&#x2F;&#x2F;blog.paradoxical.net&#x2F;efr32mg12-hw-aes-em&#x2F;lascar_check_blind.log&quot;&gt;recovery log&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; lascar_check.py&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; config&#x2F;efr32_hwaes_2kavg_unknown.json&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --leakage-model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; invsbox_hw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --ac-couple \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --dataset-path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; datasets&#x2F;200K_2KAVG_UNKNOWN&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --analysis-window&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 115,169,192,194,220,222&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    --blind-verify --brute-complete 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lascar took about seven seconds for the CPA pass. This is the result before
brute-force completion:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;byte  PGE   top1 |corr|   top2 |corr|   true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;----  ---   ----  ----    ----  ----    ----&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  0    23   80  0.0201    9F  0.0199    D9&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  1     0   AD  0.0834    8E  0.0263    AD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  2     0   92  0.0238    9F  0.0203    92&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  3     0   69  0.0843    E4  0.0262    69&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  4     0   54  0.0378    36  0.0342    54&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  5     0   B6  0.0902    01  0.0473    B6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  6     0   41  0.0493    85  0.0362    41&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  7     0   9B  0.0750    5B  0.0589    9B&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  8     0   00  0.0266    64  0.0132    00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  9     0   9A  0.1405    2D  0.0570    9A&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 10     0   EF  0.0232    BD  0.0116    EF&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 11     0   99  0.0286    61  0.0168    99&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 12     0   BA  0.0329    1D  0.0279    BA&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 13     0   CC  0.1156    7B  0.0470    CC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 14     0   3A  0.0306    74  0.0139    3A&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 15     0   10  0.0462    9D  0.0176    10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;15&#x2F;16 bytes correct (round-10 key)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;recovered: 80ad926954b6419b009aef99bacc3a10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;true:      d9ad926954b6419b009aef99bacc3a10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Byte 0 was still wrong. Its true value, &lt;code&gt;D9&lt;&#x2F;code&gt;, was at zero-based rank 23, and
the top two guesses had nearly identical scores. Inverting the top guesses
through the key schedule produced a master key that failed the captured
plaintext&#x2F;ciphertext checks.&lt;&#x2F;p&gt;
&lt;p&gt;The completion step orders bytes by the gap between their top two correlation
scores and searches the least confident byte first. Each candidate round-10
key is inverted to a master key and checked against a captured pair.
&lt;code&gt;--brute-complete 3&lt;&#x2F;code&gt; permits escalation to three bytes if needed; this run
finished after searching only byte 0, a space of at most 256 candidates.&lt;&#x2F;p&gt;
&lt;p&gt;That was enough to finish the recovery:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  escalate k=1: brute-force weakest 1 byte(s) [0] (≤256 combos, PT&#x2F;CT-validated)...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;BLIND KEY RECOVERY COMPLETED ✓  1 byte(s) actually wrong: [0]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  round-10 key = d9ad926954b6419b009aef99bacc3a10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  master       = 5f82c2d9cfeb0fa321d7d982f8bd1045&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The table&#x27;s &lt;code&gt;true&lt;&#x2F;code&gt; and PGE columns use the stored key for diagnostics. Candidate
ranking, selection of bytes to search, and plaintext&#x2F;ciphertext verification
don&#x27;t use it. That&#x27;s what we mean by blind recovery here. We had already used
a known key to work out the probe position and sample selection.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-we-d-try-next&quot;&gt;What we&#x27;d try next&lt;&#x2F;h2&gt;
&lt;p&gt;We got the key, but never quite got the repeatable 16&#x2F;16-byte CPA result we
wanted. Even with 2000× averaging, there was still one byte to brute-force.
It&#x27;s a small search, but it shows how little margin we have on the weaker
byte measurements.&lt;&#x2F;p&gt;
&lt;p&gt;We expect a better probing setup would help a lot: a more sensitive probe,
a better LNA, and more repeatable control of probe height and orientation.
We&#x27;re already using a NewAE amplifier, so the next thing we&#x27;d try is upgrading
the probe and amplifier together. We haven&#x27;t tested that yet, so we can&#x27;t say
whether it would get us the last byte directly or how much less averaging
we&#x27;d need.&lt;&#x2F;p&gt;
&lt;p&gt;For us, automating probe placement and averaging on the scope made the biggest
practical difference. Once we had usable traces, CPA and a small candidate
search were enough to recover the key.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;applying-this-to-the-original-device&quot;&gt;Applying this to the original device&lt;&#x2F;h2&gt;
&lt;p&gt;The development board was a stand-in for a commercial IoT device with
encrypted firmware updates. We haven&#x27;t recovered that device&#x27;s firmware
encryption key yet.&lt;&#x2F;p&gt;
&lt;p&gt;We don&#x27;t have its source code. Based on its black-box behavior, our working
assumption is that it stages an encrypted update in flash, checks a signature,
and only then decrypts and installs it. If that&#x27;s right, it limits the
chosen-input access we relied on in these tests. Getting repeated captures
with a useful trigger may also be much harder during an actual update.&lt;&#x2F;p&gt;
&lt;p&gt;A limited set of signed updates doesn&#x27;t by itself rule out CPA: one image
contains many AES blocks, and known inputs can be enough. We still need to
work out whether we can identify those inputs, align the corresponding
operations, and repeat the captures enough times for this setup.&lt;&#x2F;p&gt;
&lt;p&gt;The AES mode matters too. If the device uses CTR, the AES input is the counter
block, and decryption uses the AES encryption operation. We&#x27;d need to work
out that input and adapt the leakage model. We don&#x27;t yet know how the device
handles its IV&#x2F;counter; that&#x27;s a gap in our understanding of the device, not
evidence that CTR prevents side-channel analysis.&lt;&#x2F;p&gt;
&lt;p&gt;So far, we&#x27;ve recovered a development-board key with CPA and a one-byte
search. To apply that to the original device, we still need a workable way to
capture traces and a better understanding of its update path.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
