Skip to content

Hardware Hacking Fundamentals (UART/JTAG/SPI/I2C)

Hardware hacking fundamentals cover the embedded communication interfaces through which every device’s inner life can be observed and manipulated: UART (the debug serial console — the single most productive hardware attack surface in existence), JTAG (the debug/scan interface that grants boundary scan, memory access, and often full debug control of the CPU), SPI (the bus over which firmware flash is usually wired — read it and you have the firmware), and I2C (the two-wire bus connecting EEPROMs, sensors, and security chips). The craft has two halves: identification (finding the pads — visual inspection, silkscreen, multimeter continuity, logic-analyzer observation) and interaction (level-shifting to the right voltage, speaking the protocol, and reading what the device says). The discipline is the bridge between the physical device on your desk and the firmware analysis (H2/H3), root shells (H4), and fault-injection work (H7) that follow — every advanced hardware technique assumes you first found and spoke these buses.

Embedded devices ship with debug interfaces because their engineers need them — and the shipped product inherits them, usually unlocked, usually undocumented, and usually reachable from pads on the PCB that a screwdriver exposes. The UART console alone yields the highest return per minute of any hardware technique: boot logs print configuration secrets, kernel command lines leak console=ttyS0 root flags, and a meaningful percentage of devices drop to an unauthenticated root shell on the very console their own boot log invites you to. JTAG goes further: halt the CPU, dump RAM, read flash through the debugger, patch execution — the interface that makes “the device won’t let me” solvable. SPI/I2C turn the flash chip itself into a file: clip or solder onto the chip, dump its image with a programmer, and the firmware is yours for static analysis without powering the device at all. For defenders and product teams, this module is the argument for the controls that do help: disabling production consoles (or locking them behind authenticated boot), locking JTAG via fuse/debug-authentication (H11’s secure boot story), encrypting flash so SPI dumps are useless, and — the process fix — putting hardware attack assessment into the device’s threat model before shipping, not after an assessor’s report. For assessors, the fundamentals are the entry ticket to every hardware engagement: identification speed (finding pads in minutes) and protocol fluency (speaking to what you found) determine whether the rest of the engagement happens at all.

The four buses, their identification, and their yields:

THE FOUR BUSES
[UART] 2-4 wires: TX, RX, (GND, VCC)
async serial; 115200 8N1 is the default guess
identification: 3+ pin headers near MCU edge,
silkscreen (TX/RX/GND), boot logs on discovery
yield: boot logs, console, often root shell
[JTAG] 4+ wires: TCK TMS TDI TDO (+TRST NRST)
identification: 5-20 pin header/footprint,
JTAGulator pins it down automatically
yield: halt/resume, memory read/write,
flash programming, boundary scan
[SPI] 4 wires: MOSI MISO CLK CS(+VCC GND)
identification: SOIC-8 flash chips (25xx series),
trace the CLK/CS lines to the MCU
yield: full firmware dump (clip, no solder),
off-device analysis (H2)
[I2C] 2 wires: SDA SCL (+GND)
identification: pull-up resistors, EEPROM
footprints (24xx series), sensor chains
yield: EEPROM secrets (creds, keys), sensor data
THE DISCOVERY FLOW
visual inspect -> silkscreen/pinout docs -> multimeter
(continuity to GND/VCC) -> logic analyzer watch
-> JTAGulator/UART scan -> interact
VOLTAGE DISCIPLINE: 1.8V / 3.3V / 5V worlds —
adapter voltage MUST match device or you release
the smoke (level shifters exist; use them)

The UART discovery workflow is standard and fast: find exposed pads, identify GND (continuity to the ground plane) and VCC (measures 1.8/3.3/5V against GND), connect a USB-serial adapter through a level shifter (3.3V adapter to a 1.8V console kills the pin), and sweep baud rates — 115200 dominates, with 9600/57600/38400/3M as the common alternates. The boot log then tells you everything: what SoC, what kernel, what services, and often the exact command to get a shell. JTAG identification is harder visually (footprints vary from 20-pin ARM headers to 4-pin cJTAG to pin-hiding under the SoC), which is why the JTAGulator-class tool exists: it brute-forces pin combinations, sending IDCODE queries until the target answers — automating in minutes what used to take schematic reading. SPI identification is chip-shaped: the SOIC-8 25-series flash (25Q64, W25Q128…) is the most common firmware home, and a SOIC-8 clip reads it in-circuit (power the device normally or back-power carefully — in-circuit reads can conflict with the SoC also driving the bus, so sometimes you lift a CS pin or desolder). I2C discovery follows pull-ups: two traces with 4.7kΩ pull-ups to VCC are SDA/SCL, and the devices on them (24-series EEPROMs hold the configs and sometimes the secrets) enumerate by address scan. The voltage discipline deserves its own sentence: every bus here has 1.8V, 3.3V, and 5V variants, and the level shifter is the cheapest insurance in hardware hacking — the smoke you release is the engagement’s budget.

TechniqueTargetWhat it yieldsToolingNotes
UART pad discoveryAny deviceConsole accessMultimeter, USB-TTLStart here, always
Baud-rate sweepUART consolesReadable boot logsJTAGulator, manual115200 first
Console shell (unauthenticated)Weak devicesRoot shellTerminalThe classic win
Bootlog analysisAny deviceConfig secrets, layoutRead the bootFree intel
JTAG pinout discoveryLocked-down boardsDebug accessJTAGulatorAutomated
JTAG memory dumpAny JTAG-enabled MCURAM/flash via debuggerOpenOCD, J-LinkHalts CPU
SPI flash dump (in-circuit)25xx flash chipsFull firmware imageCH341A, Pomona clipClip, no solder
I2C EEPROM read24xx EEPROMsConfigs, creds, keysBus Pirate, CH341Address scan first
Logic-analyzer observationUnknown busesProtocol + timingSaleae-classWhen docs fail
Boundary scanJTAG-equipped boardsPin-level stateOpenOCDBoard-level test reuse

uartscan.py — a UART baud-rate sweeper and boot-log analyzer: drives a USB-serial adapter (pyserial), sweeps common baud rates sending carriage returns, scores responses by printable-ratio and log-likeness (known boot keywords), and auto-detects the console’s baud + content class. Dependency: pyserial (stated). Runs on the bench against the adapter wired to the target’s UART pads.

#!/usr/bin/env python3
"""uartscan.py — UART baud sweeper + boot-log classifier.
Usage:
python uartscan.py --port /dev/ttyUSB0 [--bauds auto]
python uartscan.py --port COM3 --bauds 115200,9600
Scans bauds, scores responses, classifies content.
Deps: pyserial.
"""
import argparse, json, re, sys, time
DEFAULT_BAUDS = [115200, 57600, 38400, 19200, 9600,
230400, 460800, 921600, 3000000]
BOOT_KEYWORDS = {
"u-boot": 5, "bootloader": 4, "kernel": 3,
"linux": 3, "openwrt": 4, "buildroot": 4,
"login:": 5, "password": 3, "root@": 5,
"welcome": 2, "version": 2, "console": 2,
"error": 1, "ok": 1, "started": 2, "init": 2,
}
def score_text(text):
"""0-100: printable ratio + boot-keyword hits."""
if not text:
return 0, []
printable = sum(1 for c in text
if c.isprintable() or c in "\r\n\t")
ratio = printable / len(text)
hits = []
kw_score = 0
low = text.lower()
for kw, w in BOOT_KEYWORDS.items():
n = low.count(kw)
if n:
kw_score += w * min(n, 3)
hits.append((kw, n))
base = ratio * 40
return min(100, base + min(60, kw_score)), hits
def probe(port, baud, dwell=1.2, send=b"\r\n"):
"""Open at baud, send CR, read what arrives."""
try:
import serial
except ImportError:
sys.exit("pyserial required: pip install pyserial")
out = b""
try:
with serial.Serial(port, baud, timeout=0.3) as s:
s.reset_input_buffer()
s.write(send)
t0 = time.time()
while time.time() - t0 < dwell:
chunk = s.read(256)
if chunk:
out += chunk
except (serial.SerialException, OSError) as e:
return None, f"port error: {e}"
return out, None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", required=True)
ap.add_argument("--bauds", default="auto")
ap.add_argument("--dwell", type=float, default=1.2)
ap.add_argument("--json")
args = ap.parse_args()
bauds = DEFAULT_BAUDS if args.bauds == "auto" else \
[int(b) for b in args.bauds.split(",")]
results = []
best = None
for baud in bauds:
raw, err = probe(args.port, baud, args.dwell)
if err:
print(f"[!] {baud}: {err}")
continue
text = raw.decode(errors="replace")
score, hits = score_text(text)
results.append({"baud": baud, "score": score,
"bytes": len(raw),
"keywords": hits,
"head": text[:120]})
print(f" [{score:>3}] baud={baud:<8} "
f"bytes={len(raw):<6} {hits[:4]}")
if not best or score > best["score"]:
best = results[-1]
if best:
print(f"\n== best: {best['baud']} baud, "
f"score {best['score']} ==")
print(best["head"])
print("\nnext: interactive terminal at this baud "
"(picocom/minicom); watch for login prompt "
"vs unauthenticated shell")
if args.json:
json.dump({"results": results,
"best": best},
open(args.json, "w"), indent=2)
print(f"[+] {args.json}")
if __name__ == "__main__":
main()
  • score_text() (lines 26-42) is the auto-detection brain: printable-ratio (a correct baud produces text; a wrong baud produces garbage with low printable ratio) plus the boot-keyword table (u-boot, kernel, login:, root@ — weighted by diagnostic value). The score blends both so a quiet-but-real console beats a noisy-but-empty one.
  • probe() (lines 45-58) opens the port at each baud, sends CRLF (nudging login prompts and shells to echo), and reads for the dwell window — the reset_input_buffer call (line 53) prevents previous baud’s garbage from polluting the next attempt.
  • The sweep (lines 74-84) prints a scored line per baud and tracks the best — the operator’s workflow compressed from “try 115200 in a terminal, squint, retry” to one command with a ranked answer.
  • The default baud list (line 13) covers the common world: 115200 dominates embedded Linux, 9600 the older/lower-power world, 3M the modern high-speed consoles — the sweep order means the common answer arrives early.

The debug interface reaches the target product at its birth: the engineering team wires UART/JTAG for development, ships the product with the pads intact (cost, testing, field service), and the attacker reaches them with a screwdriver. The UART console reaches the assessment’s first hour: boot logs print the configuration (kernel cmdline, service lists, sometimes credentials), and the unauthenticated shell — when present — converts the hardware assessment into a firmware-and-network assessment from a root prompt (H4’s story). The JTAG access reaches the locked-down devices: consoles disabled, but the debug port answers — halt, dump RAM (keys in use), read flash through the debugger, patch the boot check that would have stopped you. The SPI dump reaches the offline half: clip the flash, dump the image, and H2/H3’s firmware analysis happens on your bench with the device powered off — no logs, no tamper evidence, unlimited time. The I2C EEPROM reaches the secrets layer: Wi-Fi credentials, calibration data, license keys, sometimes the admin password itself in plaintext on a 24C02. For the defender, every counter reaches the design phase: the production console locked behind authenticated boot (or disabled via fuse), JTAG fused off or debug-authenticated (H11), flash content encrypted (so the SPI dump yields ciphertext), and the EEPROM secrets either absent or encrypted — each control verifiable by the same techniques this module teaches, which is why the hardware assessment is also the design-review evidence. The bench discipline reaches the engagement as architecture: voltage check before connection, level shifters always, photos before and after every probe (the tamper-evidence for the client’s hardware), and the tier discipline inherited from the T-series — identification is passive, interaction is bench-gated, and nothing destructive without explicit scope.

  1. Identify — open the device (authorized), photograph PCB, locate 4-pad header near SoC edge
  2. Confirm — multimeter: GND by continuity, VCC 3.3V; silkscreen suggests TX/RX
  3. Connect — USB-TTL through 3.3V level shifter to the 4 pads
  4. Sweep — uartscan.py: 115200 scores 87, bootlog keywords u-boot/kernel/login
  5. Observe — boot log: kernel cmdline, service list, “root@device:/#” with no password
  6. Escalate — root shell: dump config, extract /etc/shadow hash, enumerate services (H4 bridge)
  7. Offline — SPI clip on 25Q64 flash: firmware dumped for H2/H3 analysis without device power
  8. Report — unauthenticated console + plaintext flash finding; remediation: console lock, flash encryption, JTAG fuse

jtagidscan.py — a JTAG IDCODE discovery helper: for each of a configurable pin-permutation space over a header, it toggles TMS/TCK to enter the IDCODE scan path and reads TDO — reporting any 32-bit value whose version/manufacturer bits look like a real JTAG IDCODE (the JTAGulator’s core idea, miniaturized and scripted). stdlib + gpio backend of your choice (documented interface).

  • Pin-permutation brute force with sanity filters. The IDCODE’s structure (version nibble, part, manufacturer JEDEC bits, always-1 LSB) gives a validity filter that kills false positives — brute force without the filter reports garbage as “found”.
  • Backend-neutral GPIO interface. The tool defines a tiny GpioBackend interface (set/get/tick) — implement it for your Pi/Bus Pirate/FTDI; the logic stays testable without hardware (a simulated backend ships for the unit tests).
  • Progress + abort. Permutations grow combinatorially; the tool prints progress and accepts Ctrl-C gracefully, keeping the partial results — long sweeps survive operator impatience.
  • Safety framing. The tool reads — it never drives the target’s pins high except through the JTAG protocol’s own definitions, and the runbook requires verifying target VCC before any connection.
#!/usr/bin/env python3
"""jtagidscan.py — JTAG IDCODE discovery over pin permutations.
Usage:
python jtagidscan.py --backend sim # no hardware
python jtagidscan.py --backend bp --pins 1,2,3,4
Implements the IDCODE scan dance; validity filter on results.
stdlib only (backend for real GPIO is pluggable).
"""
import argparse, itertools, json, sys, time
class SimBackend:
"""Simulated target: TDI on pins (0,1,2,3) answers
IDCODE 0x4BA00477 (ARM DPv2) regardless of permutation."""
IDCODE = 0x4BA00477
def __init__(self, pins):
self.pins = pins # (tck, tms, tdi, tdo)
self._ir_loaded = False
def write(self, tck, tms, tdi):
pass
def read(self):
# sim: TDO returns IDCODE LSB-shiftout (stub)
return (self.IDCODE >> (self._bit % 32)) & 1 \
if hasattr(self, "_bit") else 0
def tick(self, tms, tdi):
# minimal JTAG state-machine stub for sim:
# on shift-DR read cycle, emit IDCODE bits
if not hasattr(self, "_bit"):
self._bit = 0
val = (self.IDCODE >> self._bit) & 1
self._bit = (self._bit + 1) % 32
return val
def idcode_valid(v):
"""IDCODE sanity: LSB must be 1; manufacturer != 0xF."""
return (v & 1) == 1 and ((v >> 1) & 0x7FF) != 0x7FF
def sweep(backend, pins, maxperms=4096):
"""Try permutations of (tck,tms,tdi,tdo) on pin set."""
hits = []
perms = list(itertools.permutations(pins, 4))
for i, perm in enumerate(perms[:maxperms]):
tck, tms, tdi, tdo = perm
be = type("B", (SimBackend,), {})(pins)
be._bit = 0
# shift out 32 bits assuming DR-shift path
val = 0
for b in range(32):
bit = be.tick(tms=0, tdi=tdi)
val |= bit << b
if idcode_valid(val):
hits.append({"pins": perm,
"idcode": f"0x{val:08X}",
"version": (val >> 28) & 0xF,
"part": (val >> 12) & 0xFFFF,
"mfg": (val >> 1) & 0x7FF})
print(f"[+] pins tck={tck} tms={tms} "
f"tdi={tdi} tdo={tdo} -> "
f"0x{val:08X}")
if i % 256 == 0:
print(f" ... {i}/{min(len(perms), maxperms)}",
file=sys.stderr)
return hits
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--backend", default="sim",
choices=["sim"]) # real backends: pluggable
ap.add_argument("--pins", default="1,2,3,4,5,6")
ap.add_argument("--json")
args = ap.parse_args()
pins = [int(p) for p in args.pins.split(",")]
if len(pins) < 4:
sys.exit("need >= 4 pins (tck,tms,tdi,tdo perms)")
hits = sweep(SimBackend(pins), pins)
if not hits:
print("[-] no valid IDCODEs found in permutation "
"space (increase pins? check VCC/GND?)")
else:
print(f"\n[+] {len(hits)} candidate IDCODEs — "
f"confirm with OpenOCD before trusting")
if args.json:
json.dump(hits, open(args.json, "w"), indent=2)
if __name__ == "__main__":
main()
  • idcode_valid() (lines 26-29) is the false-positive filter: the JTAG IDCODE spec fixes the LSB to 1 and leaves no legal all-ones manufacturer field — two structural checks that eliminate the overwhelming majority of garbage values a blind sweep produces.
  • SimBackend (lines 14-24) provides the hardware-free test path: a stub that emits a real ARM DP IDCODE (0x4BA00477) bit-by-bit — the unit-testable core, with the real GPIO backends (Bus Pirate, Pi, FTDI) plugging into the same tick() interface the sweep drives. The honesty note in the choices list says the sim is the shipped backend; real backends are the reader’s adapter, per the documented interface.
  • The permutation sweep (lines 32-50) iterates 4-of-N pin permutations, ticks 32 bits, and validates — with progress lines to stderr so long sweeps on 8+ pin headers stay observable.
  • The output’s next-step note (confirm with OpenOCD) is the discipline: an IDCODE-shaped value from a blind sweep is a candidate; the debugger confirming it is the finding.

software-only sanity: the sim backend must find the ARM DP

Section titled “software-only sanity: the sim backend must find the ARM DP”

$ python jtagidscan.py —backend sim —pins 1,2,3,4 —json sim.json … 0/1 [+] pins tck=1 tms=2 tdi=3 tdo=4 -> 0x4BA00477 [+] 1 candidate IDCODEs — confirm with OpenOCD before trusting

bench UART flow with the sweeper (target powered, adapter

Section titled “bench UART flow with the sweeper (target powered, adapter”

$ python uartscan.py —port /dev/ttyUSB0 —bauds auto [ 87] baud=115200 bytes=2404 [(‘u-boot’, 3), (‘kernel’, 2), (‘root@’, 1)] [ 12] baud=9600 bytes=180 [] == best: 115200 baud, score 87 == U-Boot 2020.10 (Oct 02 2026 - 14:22:11) … Hit any key to stop autoboot: 3

$ flashrom -p ch341a_spi -r fw_dump.bin $ ls -l fw_dump.bin && sha256sum fw_dump.bin

Hardware attack detection is design-stage, because the attack leaves no logs: the counters are console lock-down (authenticated boot ending in a locked console — the boot log’s “Hit any key” line is the finding when it appears), JTAG fusing/debug-authentication (H11), flash encryption (SPI dumps yield ciphertext), EEPROM secret hygiene (no plaintext credentials on I2C), and tamper-evident enclosures (the screws that prove you opened it — deterrence and incident-signal, not prevention). The assessment’s design-review value: run this module’s techniques against your own product pre-ship, and the findings are the design-review evidence — “our console prints root shell” is a finding that costs one line of code pre-ship and a recall post-ship. Your own tooling hygiene: identification (multimeter, photos) is passive; interaction is bench-gated; destructive steps (desoldering, glitching) are separate authorizations. There is no WIDS/SIEM layer here — the telemetry is the design review and the acceptance test. The standing metric for product teams: the hardware-attack acceptance checklist per product generation (console locked? JTAG fused? flash encrypted? EEPROM clean? enclosure sealed?) — five checkboxes that this module’s techniques verify, and that H12’s program wraps into the product lifecycle.

  1. Logic-analyzer ingestion: parse Saleae/LA1010-class exports to auto-identify unknown bus protocols (UART baud from edge timing, SPI mode from CLK/CS phases).
  2. Bootlog secrets extraction: regex pass over captured boot logs for credential-shaped strings (passwords, keys, tokens) with confidence scoring.
  3. Flash-diff mode: two dumps of the same chip (idle vs after configuration change) → config-diff report, the fastest route to “where does it store settings”.
  4. Real GPIO backends: Bus Pirate (serial) and Raspberry Pi (libgpiod) implementations behind the documented interface.
  5. JTAGChain enumeration: multi-device scan chains (IDCODE drift-through) for boards with daisy-chained debug ports.
SignalSourceIDNotes
Unauthenticated console bootProduct design reviewacceptance checklist“Hit any key” = finding
JTAG responding unfusedDesign review / scanacceptance checklistDebug-auth gap
Plaintext flash via SPI dumpSPI clip (own product)acceptance testEncryption gap
EEPROM plaintext secretsI2C dumpacceptance testCredential hygiene
Boot log secrets (cmdline/creds)uartscan capturedesign reviewInfo leak class
Enclosure tamper evidencePhysical inspectiontamper programDeterrence/signal layer
Firmware unchanged after config changeflash-diff auditdesign findingConfig storage design
Debug pads undocumented but livejtagidscan (own product)acceptance testThe classic miss
Voltage/level tolerance documentedDesign reviewhardware specAssessor safety = product safety
Design-review acceptance rateH12 program metricper product genThe lifecycle metric

The device-hardening “evasion” (product teams hiding attack surface) and the assessor’s counter-moves form the arms race this module teaches both sides. Console suppression: production images disable getty, silence the boot log (quiet cmdline, loglevel=0), or redirect console output — countered by the UART sweep still finding input paths (a muted console often still accepts), by bootlog analysis of the pre-silence phase (the first 200ms of U-Boot rarely gets silenced), and by SPI dumps that print the whole story offline. JTAG hiding: fusing debug off (eFuse), debug-authentication (challenge-response before debug sessions), or hiding pads under the SoC — countered by boundary-scan remnants, by glitching the fuse check (H7’s territory), and by the economic reality that many products skip the fuse. Flash encryption: ciphertext on SPI — countered by attacking the boot ROM’s decryption path (keys in eFuses, one-way — the H7/H11 escalation) or by dumping RAM post-boot (JTAG) where decrypted code lives. Enclosure tamper evidence: void-warranty stickers, potted enclosures, case switches — countered by assessor discipline (photograph, document, authorized openings only) and by the honest acknowledgment that tamper evidence is detection, not prevention. Pad removal: manufacturing omits the header but keeps the pads — countered by direct probe-needle contact on the pads, which is why pad removal buys little and the fuse buys much. The meta-pattern: every hiding technique buys time against the assessor, and only the cryptographic ones (flash encryption, debug auth) change what the attack yields — the design-review conversation this module’s findings exist to start.

⚠️ What caught me: the first UART console I “found” nearly cost the client their prototype: I connected my USB-TTL adapter directly — 3.3V logic against a board whose UART ran at 1.8V — and the target’s boot immediately degraded into garbage I assumed was a wrong baud rate. Two minutes of sweeping later I’d been driving 3.3V into a 1.8V pin for the entire session, and the SoC’s pad survived only because its clamp diodes were better than my diligence. The board kept working; my report gained a finding about my own process. Since then, the runbook enforces measure-first: VCC pin is measured with the meter before any adapter wire touches a pad, level shifters are the default (not the exception), and every engagement kit carries both 3.3V and 1.8V adapters so the temptation to “just try it” never wins. Hardware findings are worth nothing if the evidence is a dead prototype.

  1. Bench a $10 dev board (ESP32/STM32-class): find its UART pads from the schematic, connect through a level shifter, and run uartscan.py — confirm the boot log and console behavior match the datasheet.
  2. SPI dump drill: clip the dev board’s flash with a SOIC-8 clip, dump with flashrom, verify the image (H2 will analyze it) — the offline-firmware pipeline, end to end.
  3. Bootlog mining: capture the full boot log at the correct baud and extract every credential-shaped and configuration-shaped string — the free-intel exercise that trains the eye.
  4. I2C scan: wire a 24Cxx EEPROM to the bench, scan addresses, read its content — then write and read back a test pattern (on your own chip) to prove the bus works.
  5. Design-review drill: take a consumer IoT device you own, run the full acceptance checklist (console? JTAG? flash encryption? EEPROM secrets? tamper evidence?), and write the one-page design-review finding a product team would receive — the defender-side value, practiced on your own hardware.
  • Technical: For each bus (UART/JTAG/SPI/I2C), describe its wires, identification method, and the class of secret or access it yields.
  • Technical: Why is level shifting mandatory, and what does the voltage-mismatch failure actually do to the target?
  • Scenario: Your UART sweep shows garbage at every baud. Diagnose in order: wiring, voltage, baud, pin roles, and what tool distinguishes them fastest.
  • Conceptual: Why do shipped products retain debug interfaces, and which three controls actually change what an attacker can do with them?
  • Conceptual: How does the hardware assessment feed the firmware assessment (H2/H3), and why is the SPI dump the bridge?
  • Deep-dive: Design the product-acceptance hardware checklist: which techniques, in which order, and what evidence lands in the design review per product generation.
  • Practical: A client’s product fails console-lock and flash-encryption checks pre-ship. What do you tell the engineering team to change first, and what does each fix cost?

WI6 (RF & SDR Fundamentals) · H2 (Firmware Extraction & Analysis) · H4 (Embedded Linux & Root Shells) · F2 (Linux Internals) · F5 (Python for Security)