Skip to content

Instantly share code, notes, and snippets.

@QINGCHARLES
Last active April 10, 2026 20:09
Show Gist options
  • Select an option

  • Save QINGCHARLES/cb5c4fd48e8d67b17261daf278e7a75c to your computer and use it in GitHub Desktop.

Select an option

Save QINGCHARLES/cb5c4fd48e8d67b17261daf278e7a75c to your computer and use it in GitHub Desktop.

Smiirl Counter Research Notes

Reverse-engineering and hardware exploration notes for the Smiirl Counter — a wifi-connected physical number display sold for showing metrics like Facebook likes, Stripe revenue, etc. Smiirl does not provide repair services, spare parts, or repair documentation, so keeping these devices alive is left to owners.

This document is a working reference, not a finished guide.

Platform

  • SoC: Atheros AR9331 (or close AR933x/AR934x relative). MIPS 24K core, integrated 2.4GHz wifi. Extremely well-documented in the OpenWrt community.
  • OS: OpenWrt Chaos Calmer 1.0.4 (r43781). Released late 2015, EOL in 2017. Frozen base — Smiirl patches their own Lua endpoints on top, does not update the OpenWrt base.
  • BusyBox: v1.22.1, build dated 2017-11-14. Stripped of non-essential utilities (e.g. no whoami, no nc in some builds).
  • Hostname: SmiirlCounter
  • Web stack: uhttpd serving /www, CGI under /cgi-bin, LuCI Lua dispatcher for the API.
  • Boot: standard OpenWrt — U-Boot → kernel → procd → services. askfirst is configured on the serial console, meaning the UART is wired up in software even on boards with unpopulated headers.

Running processes (from a unit with shell access)

Notable entries from ps:

  • /usr/sbin/uhttpd -f -h /www -r SmiirlCounter -x /cgi-bin ...
  • /sbin/rpcd — OpenWrt RPC daemon, exposes ubus over HTTP
  • /sbin/ubusd — ubus broker
  • /usr/sbin/dnsmasq, /usr/sbin/ntpd, /usr/sbin/crond
  • /sbin/askfirst /bin/ash --login on the console
  • /usr/bin/lua /usr/bin/smiirl-count... — Lua daemon
  • smiirlcounter — compiled binary, presumably handles the display protocol (SPI or serial to chained digit modules)
  • No dropbear running by default, but the binary is almost certainly still in the firmware image — it just isn't started.

Discovered HTTP endpoints

All under /cgi-bin/luci/smiirl/. All GET, all return JSON with at least a result field.

api/calibrate/* — wizard state machine

  • api/calibrate/is/ready Polled while the device boots its hardware enumeration. Returns {result: "OK", digits: N} once the digit modules are detected. The digit count is stored in a browser cookie for the next page.

  • api/calibrate/calibrated Called when stripe-test calibration passes with no defective digits. No params. On OK, the wizard navigates to /calibrate/finished.html.

api/factory/* — hardware ops

  • api/factory/number?number=<string> Sets the persisted displayed number. The string contains one character per digit cell: 09, a (blank), or b (special glyph / dash, rendered with a tiny base64 PNG marker in the UI). Used during step 1 of calibration to sync software to the physical display.

  • api/factory/pushit?mask=<string> Called when at least one digit is flagged as showing stripes during the QA test. The mask string has x for good digits and o for bad. Presumably re-runs the test pattern on flagged modules. Does not navigate; the wizard loops until clean.

api/test/* — runtime test ops

  • api/test/number?number=<string> Sets the displayed number temporarily (does not persist). Returns {result: "OK"}. Same string format as factory/number. Implies a test/ namespace with likely siblings; not yet enumerated.

api/activate/* — historical, now removed

  • api/activate/ssh (REMOVED in current firmware) Old developer/factory backdoor. Returned {"ssh": "OK", "password": "<random>"} and enabled dropbear with that root password. Removed by Smiirl in a firmware update, but the underlying capability (dropbear binary, password setting, service start) is still present in the firmware — only the dispatcher route was removed.

wifi/* — wifi setup wizard (separate namespace, NOT under api/)

  • wifi/scan?r=<random> Returns scanned BSSIDs as JSON. Polled until results arrive. Used by the wifi setup wizard between calibrate steps. May return {has_error: true} to trigger redirect to error.html.

  • Other wifi/* endpoints (connect, status, list, etc.) not yet enumerated. The wifi-connect endpoint specifically is the most promising injection target on the device, since by design it takes user input (SSID, PSK) and turns it into UCI config writes and a service restart.

Hardware

Board generations

Three observed generations of the Smiirl counter mainboard, in chronological order:

  1. Earliest: secret USB socket soldered inside the unit.
  2. Middle: unpopulated 4-pin UART header on the PCB.
  3. Current: 4-pin header pads still present on the board, no header soldered. USB footprint also present but unpopulated. Some friend/dev units exist with USB still soldered in.

The trajectory is clear: Smiirl has progressively removed visible debug access without removing the underlying support in software.

PCB landmarks

  • Atheros SoC — center of the board, identifiable by Atheros logo.
  • 8-pin SOIC SPI flash — to the right of the SoC. Almost certainly a Winbond W25Q64 (8MB) or W25Q128 (16MB) or Macronix equivalent. Holds U-Boot, kernel, rootfs, and ART (wifi calibration) data.
  • 4-pin header pads — top of the board, near the USB-C area. Unpopulated through-holes. Confirmed UART based on AR9331 conventions and the active askfirst console process.
  • USB-C connector — power input on current units. Replaces earlier barrel-jack designs. Note: applying the wrong PSU to a barrel-jack unit is a common cause of bricking.

UART pinout discovery procedure

The 4 pins are GND, VCC (3.3V), TX, and RX in some order. To identify without guessing:

  1. Power off, multimeter in continuity mode. Find which pin connects to a known ground (USB shield, electrolytic cap negative leg). That's GND.
  2. Power on, multimeter in DC voltage mode, GND probe on the identified GND pin. Measure each remaining pin:
    • Steady 3.3V → VCC. Leave disconnected from your serial adapter.
    • 3.3V but flickering/noisy → TX (SoC transmitting). Wire to your USB-serial adapter's RX.
    • Steady 3.3V with no activity → RX (SoC listening). Wire to your USB-serial adapter's TX.
  3. Settings: 115200 baud, 8N1, no flow control.
  4. Use a 3.3V USB-to-TTL adapter, not 5V. 5V on the AR9331's UART pins will damage the SoC.

What to do once you have UART

Best case: askfirst drops you into an unauthenticated root shell on the console. Hit enter at the prompt and you're in.

Fallback: interrupt U-Boot during the boot countdown (mash a key when you see "Hit any key to stop autoboot"). At the U-Boot prompt:

printenv
setenv bootargs '<original bootargs> init=/bin/sh'
boot

The kernel will boot directly into a shell as PID 1 with no auth. Then:

mount -o remount,rw /
passwd root
uci set dropbear.@dropbear[0].enable=1
uci commit dropbear
sync
reboot -f

After reboot, dropbear is running on port 22 with your password.

SPI flash dump (alternative path)

For a bricked unit or as a firmware backup before any modification:

  • Programmer: CH341A USB stick (~$10 kit including SOIC-8 clip, ZIF socket, and 1.8V level-shifter adapter board).
  • Caveat: the bare CH341A outputs 5V on the clip's VCC line even with the "3.3V" jumper set, which can damage 3.3V flash chips over time. Mitigations:
    • Route through the included 1.8V adapter board (it's a level shifter and works for 3.3V too).
    • Power the chip from a separate 3.3V source and leave the clip's VCC pin disconnected.
    • Mod the CH341A board to be properly 3.3V (well-documented online).
    • Use a Raspberry Pi with flashrom instead (native 3.3V SPI).
  • Software (Windows 11): AsProgrammer (GUI, easy) or flashrom (CLI, more capable). Driver via Zadig (install WinUSB driver) or the official CH341PAR driver from wch-ic.com.
  • Procedure:
    1. Power the Smiirl off.
    2. Clip onto the SOIC-8 flash chip, aligning pin 1 carefully.
    3. Read with AsProgrammer's "Read IC" or flashrom -p ch341a_spi -r dump.bin.
    4. Save as dump.bin.original, never modify the original.
    5. Verify by reading a second time and comparing hashes.
  • Partition layout (typical AR9331 OpenWrt): u-boot → u-boot-env → kernel → rootfs (squashfs) → rootfs_data (jffs2) → ART. Never overwrite ART (per-device wifi calibration, factory-unique).

Firmware modification workflow

  1. binwalk -Me dump.bin — auto-extract embedded filesystems.
  2. Locate the squashfs (binwalk identifies it). Note offset and compression options with unsquashfs -s rootfs.squashfs.
  3. unsquashfs rootfs.squashfssquashfs-root/ directory.
  4. Modify files: etc/shadow, etc/config/dropbear, usr/lib/lua/luci/controller/smiirl/*, etc.
  5. Repack with mksquashfsmust use OpenWrt's patched squashfs-tools that supports squashfs4 + LZMA. Stock Debian/Ubuntu squashfs-tools will produce a subtly broken image that won't boot.
  6. Splice the new squashfs into a copy of the dump with dd conv=notrunc seek=<offset>, leaving U-Boot, kernel, and ART regions untouched.
  7. Write back to the chip via CH341A. Verify after write.
  8. Reinstall chip if removed, power on with UART connected, watch boot.

Easier alternative: edit the live filesystem

Once you have any shell access (UART or otherwise), changes made to /etc/shadow, /etc/config/dropbear, etc., persist across reboots via the JFFS2 overlay (/overlay). No reflash needed for the SSH-enable goal. The full dump-modify-reflash workflow is only necessary if you need to change something in the squashfs base itself, or if you want a clean modified "stock" image.

Recommended sequencing

For a unit you want to gain access to, in order of effort and risk:

  1. Walk the wifi setup wizard with browser devtools open. Capture every endpoint, especially whatever submits SSID/PSK. Free, immediate, may reveal injection targets.
  2. Enumerate the test/, wifi/, and activate/ namespaces by trying obvious sibling endpoint names with curl. Compare 404 vs 403 vs 500 responses to infer route existence.
  3. Try ubus over HTTP via rpcd. rpcd is running. Hit /cgi-bin/luci/rpc/* and /ubus. Anonymous sessions sometimes have surprising ACLs and may permit service dropbear start.
  4. Open the bricked unit, find the UART pads. Cheapest reliable path to a root shell. ~$5 for a 3.3V USB-to-TTL adapter.
  5. Dump the SPI flash from the bricked unit if UART doesn't yield results. Read offline, find the new SSH-enable mechanism in the Lua source, apply the lesson to the working unit.
  6. Apply learnings to the working unit. Either soft path (endpoint) or hardware path (UART), whichever proved viable on the brick.

Open questions

  • What replaced the activate/ssh endpoint? Renamed, gated, or removed entirely? Firmware diff vs an older image would answer this in minutes.
  • Is the current U-Boot locked against key-interrupt? Some vendors patch this. UART access on the bricked unit will reveal it.
  • Does rpcd accept anonymous ubus calls, and if so what ACLs apply?
  • What protocol does smiirlcounter use to talk to the digit modules? (Relevant for any "drive the device with my own data" follow-up project.)
  • Where does the unit phone home, and with what credentials? (/etc/config/ after shell access will answer this.)

Useful references

  • OpenWrt Chaos Calmer documentation (archived): the base system is standard OpenWrt 15.05.
  • AR9331 datasheet and the OpenWrt device pages for any AR9331 board (Carambola, MR3020, Black Swift, etc.) — UART pinouts, U-Boot quirks, and flashing procedures all transfer.
  • binwalk, flashrom, squashfs-tools-ng, AsProgrammer — all standard tools, all open source, all documented.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment