{"id":489,"date":"2026-11-10T06:52:00","date_gmt":"2026-11-10T14:52:00","guid":{"rendered":"https:\/\/blog.canutethegreat.com\/?p=489"},"modified":"2026-11-09T20:12:24","modified_gmt":"2026-11-10T04:12:24","slug":"hardware-hacking-part-4-uart-console-router","status":"publish","type":"post","link":"https:\/\/blog.canutethegreat.com\/index.php\/2026\/11\/10\/hardware-hacking-part-4-uart-console-router\/","title":{"rendered":"Hardware hacking, part 4: finding the UART console on a router I own"},"content":{"rendered":"<p><em>Part 4 of 7 in the Hardware hacking series. Part 1: <a href=\"https:\/\/blog.canutethegreat.com\/?p=435\">updating the firmware on a Pinecil V2<\/a>. Part 2: <a href=\"https:\/\/blog.canutethegreat.com\/?p=477\">the bench I work on<\/a>. Part 3: <a href=\"https:\/\/blog.canutethegreat.com\/?p=482\">using my 5V FTDI cable on 3.3V UARTs<\/a>.<\/em><\/p>\n<p>The first thing I check on a router I own is whether it has a serial console, and whether that console hands a root shell to anyone who clips a cable on. Plenty of consumer gear does. It takes physical access, so it is not a remote bug, but routers get stolen, resold and left in shared closets, and a free root prompt turns a lost box into an open one. This post covers finding the console and listening to it.<\/p>\n<p>Everything here is for hardware I own. If a router is ISP supplied or rented, I check my terms first. I only open routers that run from a low-voltage external DC adapter, and the board stays unplugged while I open it and do the continuity checks. If a device has mains-voltage circuitry inside, or I can&#8217;t tell what powers it, I don&#8217;t open it, and I never connect anything to a board that has mains on it. A cheap transformerless adapter is not isolated, so I use the one that shipped with the router. After unplugging I wait a minute, then touch an earthed metal object (or wear a wrist strap) before handling the board.<\/p>\n<h2>Finding the pads<\/h2>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/hh4-console-flow.png\" alt=\"Flowchart I drew: unpowered board, find pads, measure idle voltage, listen with the logic analyzer, find baud rate, then wire the cable per part 3, with warnings never to connect the 5 V wire or a mains-powered board\" \/><\/figure>\n<p>The flowchart above is one I drew. It is the order I work in, and the two red warnings apply at every step.<\/p>\n<p>A router UART is usually four pads or an unpopulated header near the SoC: ground, TX, RX and a supply pin, often 3.3V. Some are labelled and many aren&#8217;t. With the board unpowered I use the multimeter&#8217;s continuity mode. Ground beeps to the metal shield cans and to the ground of the power jack. The pad that beeps to the 3.3V regulator output is the supply pin, and I never connect that one to anything.<\/p>\n<p>Then I power the board from its own adapter and measure the other pads against ground, with insulated probe tips and a hand that stays still, because a slipped probe shorting two fine pads is how boards die. TX usually idles high and flickers during boot. RX usually idles high and doesn&#8217;t move, though a floating pad can read anything. I measure at idle after boot, since a meter averages a toggling line into a meaningless number.<\/p>\n<p>About 3.3V is what I expect. If a pad reads 1.8V, 2.5V or anything well above 3.6V, I stop. The divider from part 3 is wrong for that board and I need a proper level shifter.<\/p>\n<h2>Finding the baud rate<\/h2>\n<p>A UART has no clock, so the receiver has to know the bit time in advance. Consoles almost always use 8N1: eight data bits, no parity, one stop bit. The line idles high and a frame starts with a low start bit, which is how the sigrok UART decoder documentation puts it. So the shortest pulse on the line is one bit, and one over its width is the baud rate. 115200 baud is 8.68 \u00b5s per bit, 57600 is 17.36 \u00b5s and 9600 is 104.17 \u00b5s.<\/p>\n<p>With the board unpowered, analyzer ground goes to board ground first. Then one channel goes on the router&#8217;s TX pad and I power the board. I only listen, though I still touch only pads I have measured, because some SoC pins double as boot straps. I haven&#8217;t found a datasheet for the clone analyzer&#8217;s input limits, so I only clip it onto a line I have measured at 3.3V or lower.<\/p>\n<p>Sample rate matters. At 1 MHz a 115200 baud bit is 8.7 samples wide, which is fine. At 921600 baud it is 1.09 samples and you can&#8217;t measure anything. When hunting for an unknown rate I use the highest rate the analyzer gives me for the channels I enable, power cycle the router and capture the boot.<\/p>\n<p>In PulseView I put two cursors across the narrowest pulse. The same estimate in code is a few lines, and I snap the result to the nearest standard rate because one sample of error moves the raw number by several percent. The capture below is synthetic: I generated a 57600 baud signal with Python so I could show the decoding without publishing a real device&#8217;s boot log.<\/p>\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/hh4-uart-baud-synthetic.png\" alt=\"Terminal output from a synthetic 57600 baud capture generated with Python: a pulse-width baud estimate, sigrok-cli decoding garbage at the wrong rate 115200, and readable text at 57600\" \/><\/figure>\n<p>The screenshot is real output from sigrok-cli 0.7.2 and my small Python script, run on the synthetic file in a throwaway container.<\/p>\n<p>The first command measures the pulse: 17 samples at 1 MHz, 58824 raw, snapped to 57600. The second decodes the same file with a wrong guess of 115200 and gives bytes like 0x80 and 0x98 instead of text. At 57600 the text appears. The pulse method needs a bit that sits alone between two opposite levels, which boot messages almost always have. If the estimate is ambiguous I try the usual rates in turn, starting with 115200, the most common and U-Boot&#8217;s CONFIG_BAUDRATE default (boards override it). A capture of a real boot log is sensitive, since it can hold Wi-Fi keys, tokens and serial numbers.<\/p>\n<h2>Talking to it<\/h2>\n<p>Once I can read the boot text I know the rate, and I haven&#8217;t connected a transmitter yet. That is deliberate. I add the second wire only after I can see the whole boot log.<\/p>\n<p>The wiring is part 3: cable ground to board ground first, board TX straight to the cable&#8217;s RX, and the cable&#8217;s TX through the 1 k\u03a9 and 2 k\u03a9 divider to the board&#8217;s RX. The cable&#8217;s red 5V wire stays insulated and unconnected. The board&#8217;s only power source is its own adapter. I plug the cable into USB last, and I never power cycle the board with the cable still plugged in, because the cable&#8217;s TX idling high into an unpowered board can feed it through the pin&#8217;s protection diodes. To finish I unplug USB, power the board off, remove the signal wires and take ground off last. Then I open the port:<\/p>\n<pre tabindex=\"0\" class=\"wp-block-code\"><code>picocom -b 57600 \/dev\/ttyUSB0<\/code><\/pre>\n<p>Use the rate you measured in place of 57600. picocom defaults to 8N1 with no flow control, and Ctrl-A then Ctrl-X exits. On Debian-family systems the account needs to be in the dialout group, and without it you get a permission error, not garbage. Garbage usually means the wrong rate, but I also check ground, voltage level and framing. Boot text with no response to typing can be a wiring fault, but it can also be the vendor&#8217;s doing: input disabled in the bootloader, or no login process on the port. I go back to the meter before I guess.<\/p>\n<h2>What I check as the owner<\/h2>\n<p>Does the bootloader stop at a prompt if I press a key during the countdown? Does the kernel boot into a shell with no login? Does the boot log print secrets? Does root have a password? Any yes goes in my notes for that device, and if the problem is the design and not just my unit I tell the vendor. I don&#8217;t change a bootloader environment on a router I depend on until I have a verified flash backup. That is part 5.<\/p>\n<h2>Closing the console<\/h2>\n<p>The U-Boot documentation lists the knobs. The <code>bootdelay<\/code> variable set to -2 boots with no delay and doesn&#8217;t check for a keypress. Set to -1 it disables autoboot and leaves you at a prompt, the worst choice for hardening. CONFIG_AUTOBOOT_KEYED requires a specific stop string, so a stray character doesn&#8217;t halt the boot, though a string compiled into the image is not a password. <code>silent_linux<\/code> boots the kernel with <code>console=<\/code> empty, which hides output and removes no shell. These are environment settings, and the environment is often writable from the prompt or from flash, so they are only as strong as access to those. A build-time CONFIG_BOOTDELAY is stronger.<\/p>\n<p>On the OS side the fix is boring: no auto-login root shell on the serial port, a real login prompt, and a root password that isn&#8217;t shared across a product line. Leaving the header unpopulated costs a vendor nothing and stops casual attempts. None of it stops someone who desolders the flash chip, which is what the next parts are about.<\/p>\n<p><em>Other parts of the Hardware hacking series: <a href=\"https:\/\/blog.canutethegreat.com\/?p=435\">1, updating the firmware on a Pinecil V2<\/a>; <a href=\"https:\/\/blog.canutethegreat.com\/?p=477\">2, the bench I work on<\/a>; <a href=\"https:\/\/blog.canutethegreat.com\/?p=482\">3, using my 5V FTDI cable on 3.3V UARTs<\/a>; <a href=\"https:\/\/blog.canutethegreat.com\/?p=489\">4, finding the UART console on a router I own<\/a>; <span class=\"ctg-soon\">5, reading SPI flash without wrecking it <small>(publishes Nov 11)<\/small><\/span>; <span class=\"ctg-soon\">6, decoding I2C and SPI with sigrok <small>(publishes Nov 12)<\/small><\/span>; <span class=\"ctg-soon\">7, JTAG and SWD basics <small>(publishes Nov 13)<\/small><\/span>.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Part 4 of 7 in the Hardware hacking series. Part 1: updating the firmware on a Pinecil V2. Part 2: the bench I work on. Part 3: using my 5V FTDI cable on 3.3V UARTs. The first thing I check on a router I own is whether it has a serial console, and whether that [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":492,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[318,21],"tags":[155,323,328,153,329,331],"class_list":["post-489","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-hardware","category-security","tag-embedded","tag-hardware-hacking-series","tag-logic-analyzer","tag-serial-console","tag-uart","tag-u-boot"],"_links":{"self":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/comments?post=489"}],"version-history":[{"count":3,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/489\/revisions"}],"predecessor-version":[{"id":684,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/489\/revisions\/684"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media\/492"}],"wp:attachment":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media?parent=489"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/categories?post=489"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/tags?post=489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}