Skip to content

GamingNJncos/ResearchNotes_RC400L

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MDM9607 Down the Rabbit Hole: RC400L, Rayhunter, and Cross-Vendor Firmware Forensics

Audience: This is a living research document, not a polished tutorial. It's the unfiltered record of my process — wrong turns included.

RayTrap — browser-based control UI surfacing all capabilities built during this research (iptables, tcpdump, tinyproxy, wpa_supplicant, policy routing) behind a single web interface over ADB tunnel. → RayTrap.md


Table of Contents

Phase 1 — Reconnaissance & Initial Access

Phase 2 — Mapping the Attack Surface

Phase 3 — Cross-Vendor Firmware Forensics

Phase 4 — Exploitation & Tool Development



Phase 1 — Reconnaissance & Initial Access

Why This Device?

After seeing the EFF's Rayhunter project I noticed a few things that immediately piqued my interest.

The hardware:

  • Cheap. Currently under $15 on eBay — I found one for $11.
  • Qualcomm SoC (MDM9207/MDM9607). I have background with esoteric Qualcomm cellular modems and protocols.
  • ARM Cortex-A7
  • Dual-band WiFi (2.4 + 5 GHz)
  • 4G LTE functionality
  • Physical SIM slot
  • USB-C

Rayhunter's angle:

  • Specifically mentions /dev/diag — a Qualcomm diagnostic interface I have prior familiarity with
  • Root is technically accessible via Rayhunter, but the project keeps it narrowly scoped to its IMSI catcher detection use case
  • Plenty of surface area to expand — good foundation to build on or pivot toward unrelated cellular/wireless research

What I wanted out of it:

  • Rayhunter is written mostly in Rust. I have zero formal experience reading or writing Rust. Good excuse.
  • This MDM9207 SoC differs from prior Qualcomm research I've done but should have meaningful overlap
  • Genuine curiosity: what's the difference between a hotspot and a phone when it comes to the LTE stack? Can you MITM both directions simultaneously — endpoint-to-WiFi AND WiFi-to-LTE?
  • Rayhunter is hunting fake cell sites. What else is visible if you drop that specific focus entirely?

This document started as scattered notes and facts-and-findings, then I decided to structure it into something that shows my process for reverse engineering both the physical device and the Rayhunter installer. After a conversation with an interviewer who asked "can you share your process," I realized I'd rather demonstrate it than describe it. This is that demonstration.

Fair warning: because "showing the process" is the goal, the flow can seem backwards in places. This is a black-box device I happened to have the luxury of root on. Instead of working toward root as the objective, the starting point was (a) how does existing root access work, and (b) how do we expand functionality completely independent of Rayhunter.


Capability Achievements

Each capability listed here was executed on live hardware during this research — not described hypothetically. The kill chain table maps demonstrated techniques to the specific steps where they appear in this document.

Kill Chain Phase Framework Technique Demonstrated
Privilege Escalation MITRE T1548 · T1037.004 Linux capability ceiling bypass Escaped CapBnd=0x00c0 ADB ceiling via once entry injection into writable /etc/inittab; PID 1 spawns with CapEff=0x3fffffffff outside ADB process tree — Step 20
Persistence MITRE T1037.004 RC script implant All capabilities persist via inittab respawn entries — survive full device reboot without firmware modification — Steps 20–21
Collection MITRE T1040 Network sniffing tcpdump on wlan0/bridge0 captures 802.11 management frames, probe requests, and client data to valid pcap — Step 20
Collection MITRE T1557 Passive traffic duplication (TEE) iptables TEE mirrors every WiFi client packet to a designated capture host — passive, no ARP poisoning, zero client visibility — Step 21
Collection MITRE T1557 Transparent intercept (REDIRECT/TPROXY) REDIRECT silently forwards client port 80 to tinyproxy; TPROXY rule configured for TLS interception path — Step 21
Initial Access MITRE T1599 Network boundary bridging Concurrent AP+STA on single radio: device serves WiFi clients while upstream-connected to a second network; full traffic visibility on both sides — Step 21
Credential Access MITRE T1552.001 · OWASP WSTG-CRYPST-04 Embedded credential recovery RSA private key and IMEI-derived AES PSK extracted from stripped ARM ELF (atfwd_daemon, 164 KB) via static analysis — Step 8
Defense Evasion MITRE T1562.004 Firewall bypass via chain insertion Custom ORBIC_* chains inserted at position 1 in nat/mangle tables — before all QCMAP chains, which remain unmodified and functional — Step 21
Discovery MITRE T1082 · T1005 Firmware & platform enumeration Full firmware extracted via EDL; cross-vendor binary inventory (RC400L vs JMR540, 667 vs 569 binaries) with ABI portability assessment — Steps 9–16
Command & Control MITRE T1219 Operator interface (RayTrap) Browser UI over ADB tunnel surfacing all capabilities point-and-click; boot-persistent, no shell required — Step 22
Credential Access MITRE T1552.001 · OWASP WSTG-CRYPST-04 Hardcoded credential recovery (TR-069) Shared ODM RSA keypair (cert.pem + key.pem, 1024-bit, common to all units) extracted in cleartext from RC400L rootfs; JMR540 primary ACS password fully reconstructable from device WiFi MAC — Step 18 · SideQuest
Discovery MITRE T1592.002 · T1082 Build artifact disclosure Yocto BitBake recipe left in production firmware exposes ODM identity (meigsmart, Xi'an), developer email, multi-carrier build scope (Orbic + Verizon OMA-DM from single codebase), and full source tree layout — SideQuest
Collection MITRE T1119 · T1005 IMSI exfiltration via management protocol JMR540 IMSI_NOTIFY=1 reports active SIM IMSI to carrier ACS on every 24-hour CWMP Inform; combined with MAC-derived auth credential, enables fleet-wide SIM-to-hardware correlation — SideQuest
Impact MITRE T1495 Unsigned remote firmware update channel UpgradesManaged=1 on JMR540 grants ACS unconditional Download RPC authority; no signature verification parameters present in CPE config — firmware replacement is a single ACS RPC — Step 18 · SideQuest

Achieving tcpdump — Escaping the Capability Jail

Rayhunter gives you uid=0 but CapBnd=0x00c0 — only CAP_SETUID and CAP_SETGID. The entire ADB process tree inherits this ceiling. tcpdump needs CAP_NET_RAW (bit 13). Not present. Every socket call from rootshell returns EPERM due to a Qualcomm LSM hook that blocks AF_PACKET — and AF_INET, and AF_UNIX — from the ADB subtree entirely.

The escape: init (PID 1) has CapBnd=0x3fffffffff. /etc/inittab is writable from rootshell. A once entry injected and signalled with kill -HUP 1 causes init to spawn directly with full capabilities — outside the ADB process tree entirely.

Result: tcpdump running with CapEff=0x3fffffffff, PPid=1, capturing live 802.11 management frames and client data on wlan0/bridge0 to a valid pcap.

What this opens:

  • Full layer-2 visibility of every device connecting to the Orbic hotspot — 802.11 management frames, probe requests with device SSIDs, association sequences
  • Passive identification of client devices and their preferred network lists before they even associate
  • Capture of unencrypted DNS queries, HTTP, and any plaintext protocol from connected clients
  • Baseline for correlating IMSI catcher activity (Rayhunter's purpose) against concurrent WiFi client behavior
  • The inittab escape itself is the general primitive — it unlocks the full capability set for any subsequent tool, not just tcpdump

Integrating iptables — Mangle, NAT, and the FIFO Daemon

The device already had xtables-multi and all xtables plugins compiled in (TEE, REDIRECT, DNAT, TPROXY, MARK, CLASSIFY, CONNMARK, 90+ others). The kernel tables were live. rootshell just couldn't touch them — CAP_NET_ADMIN is absent from 0x00c0.

The challenge beyond caps: QCMAP (Qualcomm's mobile AP manager) already owns iptables. It sets INPUT/FORWARD default DROP, manages port-forwarding chains, and handles NAT via QMI hardware offload — no MASQUERADE rule anywhere. Flushing everything to start fresh would instantly kill WiFi client internet access.

The solution: a persistent respawn daemon (ipdm:5:respawn) running with full capabilities, listening on a named pipe at /cache/ipt/cmd.fifo. rootshell writes commands; the daemon executes them and writes output back. Custom chains (ORBIC_PREROUTING, ORBIC_MANGLE, ORBIC_FILTER) are inserted at position 1 in each table — before all QCMAP chains — and end with implicit RETURN. QCMAP never knows they exist.

What this opens:

Traffic redirection:

  • Any port from any WiFi client can be silently redirected to any local service. Port 777 → rayhunter's web UI. Port 80 → a custom tinyproxy instance for transparent HTTP interception. Port 443 → TPROXY for TLS MITM with a certificate proxy.
  • DNAT rules can forward client traffic destined for specific IPs to a different host entirely — redirect a client's DNS to a custom resolver, or forward a specific app's traffic to a capture server on the LAN.

Traffic duplication (TEE):

  • The TEE module duplicates every packet to a configurable gateway IP. A laptop connected to the Orbic hotspot receives a bitwise copy of every packet from every WiFi client simultaneously — fully passive, no ARP poisoning, no TCP resets, zero visibility to clients. This is a hardware-accelerated version of what would normally require a managed switch with port mirroring.
  • Scope can be narrowed to a single client IP: capture one device while others are unaffected.

Traffic marking and QoS:

  • MARK and DSCP rules in the mangle table let you tag specific flows for downstream policy routing — prioritize or deprioritize specific clients or protocols at the LTE uplink (rmnet0).
  • CONNMARK persists marks across a connection's lifetime, enabling stateful per-flow policies without per-packet matching overhead.

The broader implication: Any device connecting to this hotspot is subject to arbitrary traffic manipulation — redirection, duplication, injection, or blocking — with no indication to the client. The Orbic presents itself as a normal LTE hotspot. There is no banner, no certificate warning, no behavioral change. The attack surface is every HTTP session, every DNS query, every protocol that doesn't do its own endpoint verification.


Establishing Formal Shadow Authentication

Orbic ships with root credentials stored as an MD5 crypt hash inline in /etc/passwd — no /etc/shadow, no shadow password suite, no useradd/usermod, no account management tooling of any kind. The busybox su and login applets work but have no shadow support.

From the JMR540 firmware — same glibc, same platform — the full shadow-utils suite was extracted and confirmed zero-dependency portable. On deployment: /etc/shadow is created by migrating the existing hash from /etc/passwd, the passwd file is updated to x, and /etc/login.defs is installed. su.shadow, passwd.shadow, login.shadow, useradd, usermod, groupadd, and the full shadow_extras suite (pwck, grpck, gpasswd, newusers, lastlog, faillog, chage, etc.) all install to /cache/bin/ and function correctly.

What this opens:

  • Proper multi-user account management on a device that previously had one hardcoded root account
  • passwd.shadow can rotate the root credential without touching /etc/passwd directly — the credential lives in /etc/shadow with correct permissions (640, root-owned)
  • useradd/usermod enables creating unprivileged service accounts — running daemons as non-root where QCMAP or other system services don't require it
  • faillog and lastlog provide primitive audit capability — tracking login attempts on a device that previously had none
  • chage enables password aging policies, logoutd enables time-based login restrictions — neither meaningful in isolation on an embedded device, but both available to scripts that automate access control
  • The formal credential separation is a prerequisite for anything that wants to call pam_unix or link against libshadow — opening the path to PAM-aware services

Integrating wpa_supplicant — Concurrent AP and STA Mode

The assumption going in: single-radio device, iw list shows #{ managed, AP } <= 1 in valid interface combinations — AP and managed can't coexist. This turned out to be wrong in practice. Using the iptables daemon as a CAP_NET_ADMIN proxy, iw phy phy0 interface add wlan1 type managed succeeds while wlan0 is actively serving AP clients. The driver allows it; the nl80211 combination advertisement was either conservative or misread.

wpa_supplicant v2.3 (extracted from JMR540, zero extra dependencies) launches on wlan1 via inittab escape with full capabilities. wpa_cli communicates through its control socket: status, scan, add_network, set_network, enable_network, list_networks all work correctly. One constraint: passive channel scanning returns empty while the AP is active — the radio cannot go off-channel to scan without disrupting AP clients. Networks must be configured directly by SSID and PSK; the supplicant will associate when it hears the target beacon on the current channel.

What this opens:

  • The RC400L can simultaneously serve as an 802.11 AP for clients and connect as a STA to an upstream WiFi network — turning it into a transparent WiFi bridge or repeater without any upstream router involvement
  • Combined with the iptables mangle/NAT stack: traffic from clients on bridge0/wlan0 can be routed through the wlan1 STA uplink rather than rmnet0 (LTE) — WiFi uplink with LTE fallback, or LTE uplink for selected clients and WiFi for others, all policy-routed via MARK rules
  • wpa_supplicant in STA mode authenticates with WPA2-Enterprise (PEAP, EAP-TLS) in addition to PSK — the device can join corporate or research lab networks that require certificate-based auth
  • A device that looks like a normal Orbic hotspot from the client side is actually upstream-connected to whatever network the operator chooses, with full traffic visibility and manipulation capability on both sides
  • The P2P-GO mode (also supported by the driver alongside AP) opens Wi-Fi Direct — the device can act as a P2P group owner for direct device-to-device transfers outside the normal AP/STA model
  • The operational picture: a $15 device on a SIM card, connected wirelessly to an upstream network it can monitor, serving a local hotspot it can manipulate, with packet capture and arbitrary iptables rules on both sides — deployed in a laptop bag or left on a table

RayTrap — Unified Web Control Interface

All prior capabilities — iptables manipulation, wpa_supplicant, tcpdump, tinyproxy, policy routing — shared one problem: they required shell access and command-line literacy to use. The FIFO daemon removed the capability constraint but not the friction. RayTrap packages all of it behind a browser UI accessible over ADB tunnel.

The web server selection problem was itself illustrative. The device ships with a customized thttpd that validates Host headers (breaks ADB tunnel), remaps -h to --help (exits), and is already serving the admin panel on port 80. The correct answer was busybox httpd — a six-line invocation, no extra binary, no host header restriction, standard CGI. But it can't be launched from rootshell because the Qualcomm LSM blocks socket() from the ADB process subtree. The inittab escape that unlocked tcpdump in Step 20 unlocks httpd identically: a once entry via init, spawned with CapEff=0x3fffffffff, outside the ADB subtree entirely.

The CGI layer is six shell scripts. The constraint: shell CGI has no standard URL-decoding library. read -r drops the final byte of a POST body when there's no trailing newline, silently discarding form fields. The fix (printf '%s\n' "$1" | sed) is a one-liner but the failure mode — missing data, no error — is exactly the kind of thing that wastes hours if you don't know to look for it.

What this opens:

  • Everything the FIFO daemon, wpa_cli, tcpdump, tinyproxy, and ip rule could do is now accessible without knowing any syntax. A Mirror rule for Wireshark is three fields and a button. A transparent HTTP proxy is a toggle. A PCAP download is a link.
  • The access model (adb forward tcp:8889 tcp:8888) works from any OS without installing anything beyond adb. Browser as the only client requirement.
  • Boot persistence via misc-daemon means the UI is available within ~30 seconds of device power-on, before a rootshell session is even established. The device can be deployed, powered on remotely, and accessed with no further shell interaction.
  • Adding new capabilities — new CGI endpoints, new tools — requires only dropping a shell script in cgi-bin/ and reloading. No recompilation, no firmware flash, no dependency chain.
  • The Capture tab, combined with the Firewall TEE mirror rule, creates a complete passive interception workflow from a browser: add a TEE rule targeting your laptop, start a tcpdump on bridge0, download the PCAP. The entire sequence is point-and-click with no command line.
  • The net result: the accumulated research from Steps 1–21 is now accessible to anyone with ADB access to the device — no embedded Linux expertise required, no memorized incantations, no rootshell one-liners. The device is operationally complete as a research platform.

Step 1 — Pre-Purchase: FCC Docs and Internal Photos

Step 1 — Pre-Purchase: FCC Docs and Internal Photos

Before I bought anything I pulled the FCC filings. This is a habit I'd strongly recommend for any IoT/embedded research target — you can learn a lot about a device without ever touching it.

FCC ID: 2ABGH-RC400L

Notes from the internal photos:

The device has plenty of options for external antennas but ships with compression-style antennas by default. Out of the box the range is fairly limited for anything that needs strong cellular signal — but crucially, there appears to be room to expand without hardware modification (maybe). Worth revisiting.

There's also what appears to be an unused RGB LED header. No idea if this is wired up in firmware or just a floating PCB pad. Filed away as "interesting, return to later."


Step 2 — Digging Through Rayhunter's Codebase

Step 2 — Digging Through Rayhunter's Codebase

I'll skip the installation walkthrough — the Rayhunter README covers it well. A few quick notes post-install instead.

Accessing root after Rayhunter installs:

Rayhunter does not replace the existing su binary or modify the root password. Instead it pushes its own binary called rootshell:

# On local PC
adb shell

# In the ADB user shell
rootshell

This is actually a quality-of-life win. The rootshell binary is a proper bash shell with color support — a real shell as god intended, not some stripped busybox sh. This distinction matters when you're doing complex one-liners later.

The high-level of what the Rayhunter installer does:

  • Changes USB mode from stock using a special USB control query
  • Enables ADB
  • Sends AT commands to the system shell
  • Pushes the rootshell binary, sets permissions, pushes its web server binary

That last bullet point is where things get interesting. How does it send AT commands before ADB is even available?


Step 3 — How Does the Installer Actually Work?

Step 3 — How Does the Installer Actually Work?

Let's actually trace through the install process instead of treating it as a black box.

install-linux.sh calls a binary called serial from the downloaded package.

install-common.sh is where the actual orchestration happens. The sequence is roughly:

  1. Call serial --root to get elevated access to the device shell
  2. Use adb push to copy rootshell and set its permissions
  3. Rayhunter's web UI is served via a simple adb forward

The wait_for_atfwd_daemon call at the end of the install script is notable — it implies the installer is waiting for a specific daemon to come up before proceeding. That daemon name is a clue. More on that in a moment.


Step 4 — Serial Sorcery and AT+SYSCMD

Step 4 — Serial Sorcery and AT+SYSCMD

serial/src/main.rs

The serial binary accepts either a command string or --root. There's a key line in there: it sends an AT command.

That's what we want. But initially there's no indication of the exact format or what AT commands are supported.

Working backwards from install-common.sh, the --root flag resolves to something like:

serial "AT+SYSCMD=<shell command here>"

It's just wrapping AT+SYSCMD and stuffing a shell command into it. The "serial" mystery was a function wrapper around a single AT command. That's it.

Sidebar on Mac: There's a frustrating platform-specific note buried in the Rust source — something that will bite Mac users. I'm on Windows/Linux, so I dodged this, but it's worth flagging for anyone following along on macOS. The serial port enumeration behaves differently and the install script has workarounds that aren't always obvious.

Manual AT+SYSCMD demo:

Once you understand what serial is doing, you can replicate it manually:

# Using the serial binary directly from the rayhunter package
./serial "AT+SYSCMD=id"
# Returns: OK (the command ran but output isn't echoed back over USB)

This leads to the first real frustration: AT+SYSCMD executes commands but you don't get stdout back in the AT response. You only get OK or an error. Output is written elsewhere — specifically to /data/logs/atfwd.log.

What atfwd.log gives you:

Nov 19 22:48:44 mdm9607 local3.info ATFWD[1069]: Registered AT Commands event handler
Nov 19 22:48:44 mdm9607 local3.info ATFWD[1069]: Waiting for ctrCond

Grepping that log for +SYSCMD reveals the exact commands the Rayhunter installer ran — which is both useful for understanding what happened and useful as a debugging channel when you're running your own commands and can't trust the terminal output.

Summary of what AT+SYSCMD gives us:

  • A way to send shell commands with elevated permissions via USB serial (before ADB is up)
  • A binary (serial) and AT mode to do it
  • atfwd.log as the only reliable debug channel for those commands


Phase 2 — Mapping the Attack Surface

Step 5 — Hunting the Basics: Script Crawling

Step 5 — Hunting the Basics: Script Crawling

When I'm trying to understand "how does this device actually work at a system level," my first move on any embedded Linux target is to crawl for shell scripts. Even without root, scripts often reveal execution flow, elevated calls, or things that survive across reboots.

Basic search pattern:

find / -name "*.sh" 2>/dev/null

One result that jumped out immediately: DEBUG.sh

That sounds interesting. I noted it and moved on to chase the AT command angle first — then came back to it, because hunting scripts paid off in a different way than expected.


Step 6 — Chasing AT Commands

Step 6 — Chasing AT Commands

While searching for RC400L rooting info I found an XDA thread that didn't initially match what Rayhunter was doing — but a specific comment flagged two AT commands: AT+SYSCMD (which Rayhunter clearly uses) and something called AT+SER.

The thread mentioned AT+SER as a USB mode switcher but didn't explain how that conclusion was reached.

Going back to the shell scripts found earlier — the answer was in there:

The scripts contain explicit references to modes 1 and 9, with echo commands containing the strings "serial" and "adb". The mode switch mechanism was documented in the device's own init scripts. The AT command research and the script crawling converged on the same information from two different directions.

Lesson: When you're researching a device, the device often documents itself. Shell scripts in /etc/init.d/, /etc/, or scattered across /data/ frequently contain exactly the information you're looking for — you just have to look.


Step 7 — USB Modes, VID/PID, and a Mismatch

Step 7 — USB Modes, VID/PID, and a Mismatch

From prior Qualcomm research I know the VID for 9008 EDL (Emergency Download) mode is 05C6:

USB\VID_05C6&PID_9008

This VID shows up in the device's USB configuration files too, which confirms the device supports 9008 mode (useful for firmware flashing and full partition dumps).

A notable mismatch:

Rayhunter's serial binary uses 0xF626 as the USB composition value. But /etc/debug.sh sets 0xF622. These are different USB compositions, meaning Rayhunter is deliberately picking a different mode than what debug.sh establishes.

cat /data/usb/boot_hsusb_composition

This file defines ~20 USB state modes. The discrepancy between F622 and F626 is worth noting — it suggests Rayhunter made a deliberate choice about which USB interface profile to expose. Whether this matters for anything beyond driver compatibility on the host side has not been tested.

The boot_hsusb_composition file also provides kernel notes on /dev/diag which is relevant if you want to use QCSuper (covered later).


Step 8 — ATFWD Daemon Deep Dive

Step 8 — ATFWD Daemon Deep Dive

The wait_for_atfwd_daemon call in the installer, combined with +SYSCMD in atfwd.log, pointed me toward the ATFWD daemon as a target worth understanding.

Running strings on the atfwd binary reveals:

  • AT baud rate settings and config file references
  • A binary in /sbin/ that can reboot to EDL mode (more on that shortly)
  • Echo commands, daemon calls, and TTY definitions
  • Loads of internal state management

The log file at /data/logs/atfwd.log is the reliable output channel for everything. Grepping it for AT commands beyond +SYSCMD gives you the full registered command list that the daemon handles — and one particular grep is a BINGO moment that reveals the full set of registered AT commands the device responds to.

grep -i "registered AT" /data/logs/atfwd.log


Step 9 — EDL and Fastboot Modes

Step 9 — EDL and Fastboot Modes

This is where the research diverges meaningfully from "just use Rayhunter."

The ATFWD strings mentioned a binary that can reboot the device into EDL mode. Exciting — but when you try to call it directly from rootshell, it fails. Permissions, likely, or it requires a specific invocation context.

Physical alternative: you have to pry up the screen to access the physical EDL pads, which are under the LCD after removing the case. This isn't covered in Rayhunter at all, and it's a perfect example of the difference between "using a convenient installer" and actually learning the device.

The XDA thread at https://xdaforums.com/t/resetting-verizon-orbic-speed-rc400l-firmware-flash.4334899/#post-86616269 is where I first found details on boot modes. That thread also contains pre-root research from other people that has genuinely useful detail — worth reading in full.

USB ports exposed during firmware flashing:

If you unplug the device mid-update (as described in that thread), Windows exposes a different set of COM ports. This is cleaner to parse on Windows than via lsusb on Linux because Windows enumerates them with VID/PID labels. The VID 05C6 appears for the 9008 debug mode interface, and QPST works with that driver.


Step 10 — Firmware Backup via EDL

Step 10 — Firmware Backup via EDL

Once you understand EDL mode, backing up the full firmware is straightforward using bkerler/edl with the rl (read-all) flag:

edl rl [OUTPUT_DIR]

This dumps every partition the EDL loader exposes. Keep a backup. Seriously. I've bricked enough devices to make this a reflex.

The partitions of interest for system-level research:

  • system — main rootfs
  • recovery — recovery image
  • cache — overlay/cache partition
  • modem — baseband firmware (separate from application processor)
  • userdata / usrfs — persistent user data

The modem partition is worth a separate look if you're interested in the baseband — it's a completely separate RTOS running on the modem DSP, with its own filesystem.


Step 11 — QCSuper, QPST, and EFS Explorer

Step 11 — QCSuper, QPST, and EFS Explorer

Two tools that are useful at this layer:

QCSuper (https://github.com/P1sec/QCSuper) — captures live cellular traffic via the Qualcomm DIAG interface (/dev/diag). Useful for getting pcaps of the modem's radio-level communications.

# Example pcap capture via DIAG
qcsuper --usb-modem <VID:PID> --wireshark-live

The boot_hsusb_composition settings matter here — you need the DIAG interface exposed over USB for QCSuper to work.

QPST (Qualcomm Product Support Tools) is the official Qualcomm toolkit. With the right driver and 9008/DIAG mode:

  • Phone Properties — reports IMEI, software version, hardware info
  • Service Programming — allows NV item reads and writes
  • EFS Explorer — filesystem explorer for the modem's Embedded File System

The EFS contains NV (non-volatile) items that control modem behavior. A generic NV items list is documented at https://xdaforums.com/t/qualcomm-complete-list-of-nv-items.1954029/ — the raw list has 8000+ items. Filtering for debug/diag-relevant ones:

Notable debug-related NV items:

  • NV 370 — DIAG Default SIO Baud Rate
  • NV 388 — DIAG Boot Port Selection
  • NV 403 — DIAG Restart Configuration
  • NV 1830/1833 — Diag Debug Control / Detail
  • NV 4144 — Crash Debug Disallowed
  • NV 4860 — DIAG FTM Mode Switch

Step 12 — AT Command Surface Area (AT+CLAC)

Step 12 — AT Command Surface Area (AT+CLAC)

The command AT+CLAC lists all supported AT commands on the device. Some commands use $, +, or ^ prefixes:

AT$QCDGEN
AT$QCCLAC    -- note: has a slightly different list than AT+CLAC, or ordering change
AT$QCDMR

There's an interesting overlap between AT+CLAC and AT$QCCLAC — the command sets aren't identical. Whether this is a firmware version artifact or an intentional separation of AT command domains I haven't fully resolved.

For AT terminal work: if the terminal session freezes, sending a BREAK signal (in PuTTY: Special Commands > Break) usually clears it.

Reference for Qualcomm modem AT commands that are hard to find elsewhere: https://manualsdump.com/en/download/manuals/maxon_telecom-mm-6280ind/143553

A separate angle: the SIM7600 module documentation. The SIM7600 ships with a Qualcomm MDM9607 chipset and its AT command manual (V1.07) covers a command set that overlaps meaningfully with what the RC400L responds to. If you're reverse-engineering AT command behavior and hitting gaps in the RC400L docs, cross-referencing the SIM7600 manual is a productive shortcut.

TR-069 reference docs also came up during this research. The RC400L has a tr069 binary in its rootfs — flagging it here as something to return to.


Phase 3 — Cross-Vendor Firmware Forensics

Pivoting: Why Look at the JMR540?

After a few weeks of prodding the RC400L, a natural question emerged: what does this device look like from the outside, through the lens of a similar device from a different vendor?

The JioFi JMR540 is a Jio (India) mobile hotspot made by Foxconn. It runs on the same Qualcomm MDM9607 SoC. Same ARM Cortex-A7. Same generation of LTE Cat-4 hardware. But different vendor, different carrier market, different software stack built on top of the same Qualcomm BSP.

The research question: What did Foxconn ship on JMR540 that Orbic/Verizon didn't ship on the RC400L, and can any of it be ported?

This is a common technique in embedded research — when you have limited capability on a target device, look at platform siblings. The ABI compatibility between devices using the same SoC, OS version, and C library version is often high enough that binaries are directly portable.


Step 13 — Getting the JMR540 Firmware

Step 13 — Getting the JMR540 Firmware

The JMR540 firmware is publicly available. Several community dumps exist covering the main partitions of interest:

  • system dump — most complete, main rootfs
  • recoveryfs dump — recovery partition
  • root dump — root with cachefs overlay applied
  • modem image — baseband firmware partition

Extraction tooling note (Windows pain):

Getting UBI filesystem images to extract on Windows is not pleasant. ubireader exists but the Windows path for the installed script is not what you'd expect:

C:\Users\<user>\AppData\Roaming\Python\Python314\Scripts\ubireader_extract_files.exe

Use the full path. Don't try to rely on it being in PATH on Windows Git Bash. I wasted time on this.


Step 14 — Platform Fingerprinting

Step 14 — Platform Fingerprinting

Before doing any binary analysis, establish the ground truth on both platforms. This determines what's actually portable.

Property RC400L (Orbic) JMR540 (Foxconn/Jio)
SoC Qualcomm MDM9607 Qualcomm MDM9607
CPU ARM Cortex-A7 (armv7, 32-bit) ARM Cortex-A7 (armv7, 32-bit)
C Library glibc 2.22 glibc 2.22
OpenSSL 1.0.0 1.0.0
Init system SysV (/etc/init.d/) SysV (/etc/init.d/)
IPC stack QMI/QCMAP QMI/QCMAP
Busybox size 1.26 MB (~183 applets) 979 KB (~152 applets)
Busybox extras chattr, lsattr, su, login fatattr, sha3sum
Root password Inline in /etc/passwd (MD5 $1$) In /etc/shadow (DES crypt)

glibc 2.22 on both = ABI compatibility. Binaries compiled for one will generally run on the other, as long as their library dependencies are satisfied. This is the key finding that makes the entire PortableApps effort viable.

The busybox difference is interesting in both directions:

  • Orbic's busybox is larger and has su and login as busybox applets
  • JMR540's busybox is smaller but has fatattr and sha3sum not compiled into Orbic's build
  • JMR540 ships standalone shadow suite binaries (su.shadow, login.shadow) instead of relying on busybox

Password storage difference:

The Orbic stores root password directly in /etc/passwd as an MD5 hash — old-school, no shadow file. The JMR540 has a proper /etc/shadow setup with DES crypt hashes. This matters if you're trying to port the shadow suite tools: the Orbic doesn't have /etc/shadow at all, so you'd need to create it.


Step 15 — The Binary Audit (667 vs 569)

Step 15 — The Binary Audit (667 vs 569)

Methodology:

With both firmware sets extracted, the process was:

  1. Enumerate every file in bin/ and sbin/ on both devices (recursively, following symlinks)
  2. Record names, sizes, and whether each entry is a binary or symlink
  3. Diff the two lists to find what's unique to each
  4. For each unique-to-JMR540 binary, extract its ELF dynamic dependency list

Dependency extraction without readelf:

Here's where Windows tooling limitations bite again. No readelf, no strings in Git Bash. Workaround:

tr '\0' '\n' < binary_file | grep -E '^lib.*\.so'

This converts the null-separated ELF string table into newlines and greps for shared library names. Not elegant, but it works. Cross-referencing against Orbic's rootfs with a recursive find tells you which deps are already satisfied.

Results:

  • RC400L (Orbic): 569 bin/sbin entries
  • JMR540 (Foxconn): 667 bin/sbin entries
  • 131 unique-to-JMR540 binaries analyzed

Step 16 — Key Findings by Category

Step 16 — Key Findings by Category

Tier 1 — Immediate Value (Auth & Privilege)

The JMR540 ships a complete shadow-utils suite that the Orbic simply doesn't have:

Binary Size Notes
su.shadow 36 KB Full su with shadow support. Orbic has NO su binary at all.
login.shadow 68 KB Full login with PAM/shadow.
passwd.shadow 42 KB Standalone password changer.
vipw.shadow 43 KB Safe passwd/shadow editor.
nologin 6 KB Account lockout utility.

The Orbic's busybox has su as an applet, but it's the stripped-down busybox version. The shadow suite versions are proper implementations.

Tier 2 — User/Group Management

The full shadow-utils package: useradd, userdel, usermod, groupadd, groupdel, groupmod, groupmems, newusers, chage, chpasswd, pwck, grpck, lastlog, faillog, and more. The Orbic has none of these as standalone tools.

Tier 3 — Network Tools

Binary Notes
xtables-multi iptables/ip6tables unified binary. Orbic has NO iptables binary. All the iptables libs (libip4tc, libip6tc, libxtables) already exist on the Orbic — this binary is a drop-in.
wpa_supplicant WiFi client mode. Not on Orbic. Needs libwpa_client.so.
wpa_cli / wpa_passphrase WPA supplicant control tools.
pppd PPP daemon. Serial/modem/VPN connections.
chat Modem chat scripts (used with pppd).
tinyproxy Lightweight HTTP proxy.
thttpd Lightweight HTTP server.
conntrackd Connection tracking daemon.
ddclient Dynamic DNS client (Perl).
nfnl_osf OS fingerprinting via netfilter.

The iptables finding is significant. The Orbic's entire netfilter/iptables infrastructure exists in shared libraries already — Orbic just ships zero iptables binaries. xtables-multi from JMR540 satisfies all dependencies against what's already on the Orbic. Drop it in and you have full firewall control.

Tier 4 — D-Bus (IPC Framework)

The JMR540 ships a full D-Bus stack: dbus-daemon, dbus-send, dbus-monitor, dbus-launch, dbus-run-session. The Orbic has no D-Bus at all.

Catch: These require libdbus-1.so.3 which is also absent on Orbic. Bringing the binaries means bringing the library — but it's a self-contained dependency (no further chain required).

Tier 5 — MCM Framework (Additional Modem Control)

Binary Notes
MCM_MOBILEAP_ConnectionManager MCM mobile AP manager
MCM_ATCOP_CLI MCM AT command CLI
mcm_ril_service MCM RIL (Radio Interface Layer)
MCM_atcop_svc MCM AT command service

Requires libmcm.so.0, libmcmipc.so.0, libmcm_log_util.so.0 — all on JMR540 only. Portable as a bundle.

Tier 6 — Foxconn Device Management

Binary Notes
cfg Foxconn configuration management CLI (452 KB)
cwmpCPE TR-069 CPE client — remote device management
simlock SIM lock/unlock
freset Factory reset
thttpd.sh Init script for the HTTP server

This tier is where Foxconn-specific binaries live. Some of these will have Foxconn-specific library dependencies or assume Foxconn partition layout — not all of them are portable despite ABI compatibility.

Tiers 7-8 — Audio and GPS (Dead Ends for Porting)

The JMR540 has a full ALSA audio stack (aplay, arec, amix, alsaucm_test) and GPS/location tools (garden_app, location_hal_test).

These are not portable:

  • Audio needs 7+ missing libraries: libalsa_intf.so.1, libaudioalsa.so.1, libaudcal.so.1, libacdbloader.so.1, and more
  • GPS needs 18+ location libraries: libloc_*.so, libgps_*.so, libgeofence, libizat_core, etc.

None of these exist on the Orbic, and pulling them all in would be a significant undertaking for uncertain payoff on a device that wasn't designed with location or audio hardware in mind.

Notable Things Orbic Has That JMR540 Doesn't

Binary Notes
LKCore Orbic's main application (LittleKernel-based UI)
goahead GoAhead web server (JMR540 uses thttpd)
mbimd MBIM daemon (JMR540 is QMI-only)
iperf / iperf3 Network performance testing
sqlite3 SQLite CLI
i2cdetect/dump/get/set I2C bus tools
oma_dm / dmclient OMA-DM device management
tr069 Orbic's own TR-069 client
ethtool Ethernet tool
nanddump/nandwrite NAND flash tools
sigma_dut WiFi certification test tool
perl5.22.0 Perl 5.22 (JMR540 has 5.20)

The iperf/iperf3 presence on Orbic is genuinely useful and unexpected. The mbimd difference tells you something about the QMI vs MBIM interface choice — Foxconn went pure QMI, Orbic supports MBIM (which is what Windows prefers for USB modem interfaces).


Step 17 — Staging PortableApps for the RC400L

Step 17 — Staging PortableApps for the RC400L

With the binary audit complete, I staged the most useful candidates into a PortableApps/ directory organized into 26 numbered packages. Total size: ~8.3 MB. All ARM 32-bit EABI5, GNU/Linux 2.6.32.

Portability breakdown:

  • 22 of 26 packages need zero additional libraries — every dependency is already present on the Orbic
  • 4 packages include their required libraries:
    • 09_dbus/ — includes libdbus-1.so.3
    • 19_traf_monitor/ — includes libbroker.so
    • 20_mcm_framework/ — includes 3 MCM libs
    • 06_pppd/ — optional libpcap.so.1 (in package 08)

Package index highlights:

Package Content Size Notes
00_audit/ Capability audit script 10 KB Run this first
01_xtables/ iptables/ip6tables 71 KB Drop-in ready
02_shadow_suite/ su, login, passwd, nologin 663 KB Needs /etc/shadow created
03_wpa_supplicant/ wpa_supplicant + wpa_cli 907 KB WiFi client mode
04_thttpd/ Lightweight HTTP server 118 KB Web shell / file transfer
05_tinyproxy/ HTTP proxy 60 KB Traffic pivoting
06_pppd/ PPP daemon + chat 278 KB Serial/VPN
07_simlock/ SIM lock control 22 KB Foxconn-specific, YMMV
08_libpcap_tcpdump/ Static tcpdump 2.2 MB Self-contained, no libpcap needed
09_dbus/ Full D-Bus stack 625 KB libdbus included
10_reg/ Register access tool 6 KB Hardware register read/write
15_ubi_tools/ UBI filesystem tools 229 KB For filesystem manipulation
20_mcm_framework/ MCM modem control 671 KB MCM libs included

Deployment strategy:

The RC400L's writable space:

  • /tmp — tmpfs (RAM), lost on reboot, ~4-8 MB available
  • /cache — persistent, limited space
  • /data — persistent, limited space
  • /usrfs — persistent overlay
# Add to PATH for persistence
export PATH=/cache/bin:$PATH

Minimum deployment for maximum value (~2.4 MB):

  • 00_audit/check_caps.sh — know what you're working with
  • 01_xtables/xtables-multi — firewall control
  • 02_shadow_suite/su.shadow + passwd.shadow — proper auth
  • 03_wpa_supplicant/wpa_cli — WiFi client control
  • 05_tinyproxy/tinyproxy — HTTP proxy
  • 08_libpcap_tcpdump/tcpdump — packet capture
  • 10_reg/reg — register access

Phase 4 — Exploitation & Tool Development

Step 18 — The TR-069 Rabbit Hole (cwmpCPE)

Step 18 — The TR-069 Rabbit Hole (cwmpCPE)

The JMR540's /sbin/cwmpCPE is a TR-069 CPE (Customer Premises Equipment) client — the protocol ISPs use to remotely manage devices.

What TR-069 lets a carrier do:

  • Remotely configure any parameter (APN, WiFi SSID/password, firewall rules, etc.)
  • Push firmware updates over-the-air
  • Monitor device health, signal strength, connection quality
  • Provision new devices automatically on first boot
  • Run remote diagnostics (ping, traceroute, speed test)

The CPE connects back to an ACS (Auto Configuration Server) operated by the carrier.

Discovery: cwmpCPE showed up in the binary audit as a 435 KB Foxconn binary that stood out from the noise. Its size suggested real functionality. The JMR540 has a full config directory at /etc/cwmp/ and an init script (cwmpcfg) to manage it.

Dependency analysis:

libbroker.so       — Foxconn message broker IPC (staged in 19_traf_monitor, already present)
libc.so.6          — present on Orbic
libcrypto.so.1.0.0 — present on Orbic
libfwupgrade.so    — Foxconn firmware upgrade lib (JMR540 only, NOT on Orbic)
libpthread.so.0    — present on Orbic
libssl.so.1.0.0    — present on Orbic

Status: Partially portable. Two libs needed:

  • libbroker.so — already staged (202 KB, in 19_traf_monitor)
  • libfwupgrade.so — on JMR540's /usr/lib/, deps TBD

Why this matters:

The Orbic already has its own tr069 binary in its rootfs. That's worth investigating separately — but cwmpCPE running on the Orbic creates an interesting scenario: pointing it at a local ACS (e.g., GenieACS, OpenACS) for full remote management via a protocol the carrier themselves trust.

Security implications:

A CWMP client running on the Orbic that dials home to an ISP ACS is a significant attack surface in both directions — the carrier has full remote control, and a malicious or compromised ACS could push arbitrary configuration changes or firmware. Understanding this attack surface is valuable for both offensive research and device hardening.


Step 19 — The SMB Dead End [dead end]

Step 19 — The SMB Dead End

One early hypothesis was that the JMR540 might ship SMB file sharing capability, which could be interesting for exposing the Orbic's filesystem over the network.

The modify_smbuser and modify_workgroup binaries on JMR540 looked promising. After analysis: they are configuration helpers only. They modify SMB-related config files but do not implement SMB.

Neither device ships smbd or nmbd. Neither device has Samba. Both devices have SMB config tooling that assumes Samba is installed by an integration that never made it into the shipping firmware.

This is a dead end for SMB without bringing a static smbd binary compiled for ARM/glibc-2.22. That's a possible future project but outside the current scope.

Lesson from this: Just because a binary is named modify_smbuser doesn't mean SMB is implemented. Check the actual binary behavior before assuming functionality.


Step 20 — Getting tcpdump Working: Escaping the Capability Jail

Step 20 — Getting tcpdump Working: Escaping the Capability Jail

Confirmed: TCPDUMP WORKING — live packet capture running on the RC400L with full kernel capabilities, producing valid pcap output.


The Problem with rootshell

Rayhunter's rootshell binary gives you uid=0, which looks like full root. It isn't.

CapInh: 0000000000000000
CapPrm: 00000000000000c0
CapEff: 00000000000000c0
CapBnd: 00000000000000c0

0x00c0 is two bits: CAP_SETUID (bit 7) and CAP_SETGID (bit 6). That's it. The entire ADB process tree — adbd and every shell it spawns including rootshell — is capped at this bounding set. The bounding set is a hard ceiling that no child process can exceed, regardless of setuid binaries or file capabilities.

Consequences that hit immediately:

  • tcpdump requires CAP_NET_RAW (bit 13) to open an AF_PACKET socket. Not in 0x00c0. Socket returns EPERM.
  • chmod on any file not owned by rootshell fails — CAP_FOWNER (bit 3) is missing. Files pushed via adb push are owned by uid 2000 (shell), and rootshell can't chmod them even as uid=0.
  • socket() for any protocol — TCP, UDP, raw — is blocked by a Qualcomm LSM hook in the kernel. rootshell cannot make any network connections at all.

This is not accidental. It's a deliberate design choice in the Rayhunter installer. The rootshell gives you filesystem access but deliberately withholds network and device capabilities.


Finding the Way Out

Every process not spawned from the ADB tree has a full bounding set:

PID=1    NAME=init        BND=0000003fffffffff
PID=1513 NAME=atfwd_daemon BND=0000003fffffffff
PID=1738 NAME=rayhunter-daemon BND=0000003fffffffff

init (PID 1) is the obvious target. On this device, init uses standard SysV inittab. Because /etc is writable from rootshell (it's a ubifs mount, root-owned, and rootshell is uid=0), you can add entries to /etc/inittab directly. Sending kill -HUP 1 causes busybox init to re-read the file and execute new once entries — spawning them as direct children of PID 1, with the full 0x3fffffffff bounding set.

That's the escape.


What Had to Be Solved Along the Way

1. Getting a root-owned executable binary

adb push creates files owned by uid=2000. rootshell can't chmod them (no CAP_FOWNER). Solution: cp the pushed binary to a new path. cp creates a new file owned by the calling process — uid=0 — which rootshell can chmod.

cp /data/tmp/tcpdump /data/tmp/tcpdump_r
chmod +x /data/tmp/tcpdump_r

2. Choosing the right writable persistent path

/tmp is a symlink to /var/tmp (tmpfs). Files there get wiped by cleanup processes while long-running commands are in flight — learned the hard way when a live tcpdump had its binary and output pcap deleted mid-capture while the process still had them open. /data/tmp/ (ubifs, persistent) is the right staging area, but it's root-owned 755 so adb push can't write there directly. rootshell must pre-create it with chmod 777.

3. The inittab tag length limit

Busybox init's inittab id field has a 4-character maximum. A tag like tc022550 is silently mishandled. Tags must be ≤4 characters. Also: busybox tracks once entries by their tag — reusing the same tag in the same session means init won't re-run it. The tag must change each run.

4. Restoring inittab

The deploy script backs up /etc/inittab before injection and restores it after capture, followed by another kill -HUP 1. This leaves the system clean with no persistent inittab changes.


The Result

tcpdump PID=23197  PPid=1
CapEff: 0000003fffffffff

tcpdump_r: listening on wlan0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

Valid pcap (D4C3B2A1 magic, 420 bytes from wlan0 management traffic).


How to Use It

Files are in PortableApps/08_libpcap_tcpdump/:

# Push from PC (once):
adb push tcpdump /data/tmp/tcpdump
adb push deploy_tcpdump.sh /data/tmp/deploy_tcpdump.sh

# On device:
adb shell
rootshell
sh /data/tmp/deploy_tcpdump.sh wlan0 100 /data/tmp/cap.pcap

# Pull result:
adb pull /data/tmp/cap.pcap cap.pcap
# Open in Wireshark

Interface guide:

  • wlan0 — WiFi clients connected to the Orbic hotspot (recommended)
  • bridge0 — LAN bridge (includes wlan0 + USB RNDIS)
  • rmnet0 — LTE uplink (requires active data session)

The script handles the full flow: binary copy, inittab injection, init signal, process detection, wait loop, inittab restoration, and pull instructions.


Step 21 — Live iptables Control: QCMAP-Safe Daemon Architecture

Step 21 — Live iptables Control: QCMAP-Safe Daemon Architecture

With tcpdump confirmed working via the inittab escape, the next problem was iptables. The RC400L ships with xtables-multi (the combined iptables/ip6tables binary) at /usr/sbin/xtables-multi and a full set of xtables extension plugins in /usr/lib/xtables/ — including TEE, REDIRECT, DNAT, MARK, CLASSIFY, TPROXY, and 90+ others. All the kernel modules are loaded. But rootshell has the same capability ceiling problem as tcpdump: CAP_NET_ADMIN is required to modify netfilter rules, and it isn't in 0x00c0.

The deeper complication: QCMAP is already running iptables rules. QCMAP (Qualcomm Mobile Access Point Manager) manages the device's NAT and forwarding using QMI hardware offload, and it uses iptables extensively. The wrong approach — flushing all rules and starting fresh — would break WiFi client internet access. Any iptables solution has to coexist safely with whatever QCMAP has already configured.


QCMAP Baseline State

Before touching anything, the full iptables state was captured:

filter table:
  INPUT   — default DROP
  FORWARD — default DROP, but: -A FORWARD -i bridge0 -j ACCEPT  ← WiFi forwarding
  OUTPUT  — default ACCEPT

nat table:
  POSTROUTING — QMI hardware NAT handles masquerade; no iptables MASQUERADE rule

mangle, raw — empty

The critical rules to never touch:

  • -A FORWARD -i bridge0 -j ACCEPT — this is what allows WiFi clients to route packets
  • -A INPUT -i bridge0 -j ACCEPT — this is what makes the device reachable from LAN
  • Default policies — QCMAP sets INPUT/FORWARD to DROP; changing them risks open-forwarding the LTE interface

The Design: Custom Chains + FIFO Daemon

Rather than competing with QCMAP rules, the solution uses custom chains that hook before QCMAP rules at position 1. QCMAP chains are never modified. Custom chains end with an implicit RETURN, so unmatched packets fall through to QCMAP rules unchanged.

Three custom chains:

Chain Table Purpose
ORBIC_PREROUTING nat REDIRECT / DNAT (port forwarding, port 777)
ORBIC_MANGLE mangle MARK, DSCP, TEE mirroring, CONNMARK
ORBIC_FILTER filter rate limiting, selective DROP/ACCEPT (off by default)

Hook insertion is idempotent — -C checks existence before -I to avoid duplicates on daemon restart:

$IPT -t nat -C PREROUTING -j ORBIC_PREROUTING 2>/dev/null || \
    $IPT -t nat -I PREROUTING 1 -j ORBIC_PREROUTING

The persistent daemon is installed via inittab as a respawn entry — it restarts automatically if it crashes:

ipdm:5:respawn:/bin/sh /cache/ipt/ipt_daemon.sh

ipt_daemon.sh starts with CapEff=0x3fffffffff (full caps from init), creates a named pipe at /cache/ipt/cmd.fifo, applies the saved ruleset from /cache/ipt/rules.sh on startup, then enters a command loop:

while true; do
    if read -r CMD < "$FIFO"; then
        [ -z "$CMD" ] && continue
        eval "$CMD" >> "$OUT" 2>&1
        echo "##DONE##" >> "$OUT"
    fi
done

rootshell writes to the FIFO. The daemon executes with full caps. Output lands in /cache/ipt/last_out. The ##DONE## sentinel lets ipt_ctl.sh know when the response is complete.


The Control Client

ipt_ctl.sh is the user-facing tool, run directly from rootshell:

# From rootshell:
sh /cache/ipt/ipt_ctl.sh status           # dump all iptables tables
sh /cache/ipt/ipt_ctl.sh reload           # reapply /cache/ipt/rules.sh
sh /cache/ipt/ipt_ctl.sh flush            # clear ORBIC_* chains only (QCMAP untouched)
sh /cache/ipt/ipt_ctl.sh log              # daemon log with timestamps and CapEff

# Pass through any iptables command:
sh /cache/ipt/ipt_ctl.sh iptables -t nat -L -n -v
sh /cache/ipt/ipt_ctl.sh iptables -t nat -A ORBIC_PREROUTING \
    -i bridge0 -p tcp --dport 777 -j REDIRECT --to-ports 8080

Live rules take effect immediately — no reload needed for pass-through commands. The reload command re-runs /cache/ipt/rules.sh which is the persistent on-disk configuration. The save command reads back live ORBIC rules and writes a new rules.sh. Together this gives a full edit-reload-save workflow from rootshell.


Example: Port 777 Redirect

WiFi clients connecting to the Orbic hotspot can be transparently redirected from any port to any local service. With rayhunter running on port 8080, enabling port 777 as an alias:

# Inline (live, not persisted):
sh /cache/ipt/ipt_ctl.sh iptables -t nat -A ORBIC_PREROUTING \
    -i bridge0 -p tcp --dport 777 -j REDIRECT --to-ports 8080

# Or edit /cache/ipt/rules.sh and uncomment section [1], then:
sh /cache/ipt/ipt_ctl.sh reload

Any WiFi client connecting to 192.168.1.1:777 gets silently redirected to the rayhunter UI on port 8080.


Example: Traffic Mirroring via TEE

The TEE xtables module duplicates packets to a gateway address on the LAN. Combined with Wireshark on a laptop connected to the Orbic hotspot, this gives a passive capture of all WiFi client traffic without any changes visible to the clients:

# Mirror all WiFi client traffic to 192.168.1.50:
sh /cache/ipt/ipt_ctl.sh iptables -t mangle -A ORBIC_MANGLE \
    -i bridge0 -j TEE --gateway 192.168.1.50

# Or mirror a single client:
sh /cache/ipt/ipt_ctl.sh iptables -t mangle -A ORBIC_MANGLE \
    -i bridge0 -s 192.168.1.152 -j TEE --gateway 192.168.1.50

TEE duplicates at the mangle/PREROUTING stage — the mirror host receives a copy of every packet regardless of where it's destined.


Files

All files in PortableApps/01_xtables/:

File Role
deploy_xtables.sh One-time installer: pushes files, patches inittab, starts daemon, smoke tests
ipt_daemon.sh Persistent full-caps daemon, FIFO command loop, ruleset-on-startup
ipt_ctl.sh rootshell control client: start/stop/reload/flush/save/status/log + pass-through
ipt_rules.sh Editable ruleset: ORBIC_* chain setup + commented examples for all use cases

To deploy from PC:

MSYS_NO_PATHCONV=1 adb push PortableApps/01_xtables /data/tmp/xtables
MSYS_NO_PATHCONV=1 adb shell
# then in adb shell:
rootshell
sh /data/tmp/xtables/deploy_xtables.sh

The installer verifies xtables-multi, creates /cache/ipt/, installs and chmod's all scripts, patches inittab with the respawn entry, signals init, waits for the FIFO to appear, confirms full CapEff, and runs a smoke test showing the filter table. On any subsequent reboot the daemon comes up automatically — no re-deploy needed.


Step 22 — RayTrap: Unified Web Control Interface

Step 22 — RayTrap: Unified Web Control Interface

By Step 21, the device had a fully operational iptables daemon, wpa_supplicant in concurrent AP+STA mode, tcpdump, tinyproxy, and a full shadow-utils suite — but every operation required either rootshell one-liners through the FIFO or direct file edits. The capability was there; the friction was not. RayTrap is the answer to that friction: a single-page web app running on the device itself that surfaces all of those primitives through a browser UI.

Why Build This?

The access model from a laptop is:

adb forward tcp:8889 tcp:8888
# Then open http://127.0.0.1:8889/ in browser

That's it. No rootshell, no FIFO commands, no manual wpa_cli sequences. Everything that previously required knowing the exact syntax of ipt_ctl.sh, wpa_cli, ip rule, tcpdump, and tinyproxy is now behind a web form.

The practical motivation: once the device is deployed — in a bag, on a bench, plugged into a car — ADB is the only reliable channel back to it without connecting a WiFi client. A web UI over ADB tunnel removes the need to remember every command incantation and makes the research workflow sustainable.

Choosing the Web Server

The Orbic device has two web server binaries available:

thttpd — the existing Orbic web server, running on port 80 to serve the device admin panel. Initial instinct was to repurpose it. This failed for three reasons:

  1. The Orbic build is customized: -h exits instead of showing help, -d specifies a docroot that points to cgi-bin/index.html, not index.html.
  2. It validates the Host header — requests must include 192.168.1.1 as the host or they're rejected. Fine for LAN access, broken over ADB tunnel.
  3. It's already in use. Running a second instance would need a different port and still hit the host-header problem.

busybox httpd — the httpd applet built into the device's busybox binary. Smaller, no host header validation, standard CGI execution, configurable docroot and port on the command line. No separate binary to deploy. The catch: it can't be launched from rootshell because the Qualcomm LSM blocks socket() for the ADB process subtree (CapBnd=0x00c0). The inittab escape handles this — a once entry spawns httpd with full capabilities via init.

# Injected by deploy.sh:
rt1:5:once:busybox httpd -p 8888 -h /cache/raytrap/www

The deploy script automates this, waits up to 20 seconds for port 8888 to appear in /proc/net/tcp6, then cleans up the once entry and signals init again.

CGI Architecture

Every tab in the UI is backed by a shell CGI script in /cache/raytrap/www/cgi-bin/. The six scripts are:

Script Purpose
status.cgi System overview: service PIDs, uptime, disk free, rule count, wlan1 state
firewall.cgi Add/delete/flush ORBIC_* iptables rules (TEE, REDIRECT, DNAT, DROP, MARK)
proxy.cgi Start/stop tinyproxy, enable/disable transparent HTTP, edit config, tail log
wifi.cgi wpa_supplicant status, add/remove/connect networks via wpa_cli
routing.cgi ip rule list, setup policy tables, per-client LTE vs wlan1 routing
capture.cgi tcpdump start/stop with BPF filter, interface selection, PCAP download

All CGI scripts output JSON (Content-Type: application/json). The frontend is a single index.html with ~875 lines of vanilla JS that fetches from those endpoints and renders results client-side. No framework, no build step, no external dependencies.

One CGI gotcha worth documenting: URL-decoding POST bodies in busybox sh. The obvious approach — read -r BODY — silently drops the final line if it doesn't end with a newline, which POST bodies don't. The fix is:

BODY=$(printf '%s\n' "$QUERY_STRING")   # force newline
# or for POST:
BODY=$(dd bs=$CONTENT_LENGTH count=1 2>/dev/null | printf '%s\n' "$(cat)")

Then URL-decode with sed: printf '%s\n' "$val" | sed 's/+/ /g; s/%/\\x/g' | xargs -0 printf '%b'. Getting this wrong results in silently truncated form data — no error, just missing fields.

The Six Tabs

Dashboard — live status poll every 15 seconds. Shows green/red indicators for the iptables daemon, tinyproxy, wpa_supplicant, and the active capture. System panel shows uptime, /cache and /data free space, kernel version.

RayTrap Dashboard

Firewall — add rules to the ORBIC_PREROUTING (nat) and ORBIC_MANGLE chains without touching any QCMAP chain. Five rule types exposed as form presets:

  • Mirror (TEE): duplicate all WiFi client traffic (or a single source IP) to a Wireshark host on the LAN — passive capture with no ARP poisoning
  • Redirect Port: transparent port redirect (REDIRECT target, nat PREROUTING) — e.g., port 80 → 8118 for tinyproxy
  • Forward to Host (DNAT): forward a port to a different IP:port — redirect DNS, forward specific app traffic to a capture server
  • Block Source: DROP in filter FORWARD for a source IP or subnet
  • Mark Traffic: MARK in mangle for use with policy routing on the Routing tab

The active rules table shows all entries in ORBIC_* chains with type badges (TEE / REDIRECT / DNAT / DROP / MARK) and a per-rule delete button.

RayTrap Firewall

Proxy — tinyproxy lifecycle control. Start/stop buttons, PID display, toggle for transparent HTTP mode (which adds/removes the port 80 REDIRECT rule automatically), editable config (port, log level, allow subnet, max clients, timeout), and a live log tail showing the last 30 lines of the tinyproxy access log.

RayTrap Proxy

WiFi — wpa_supplicant STA management for wlan1. Shows connection state, current SSID, IP address, and wpa_supplicant PID. Add Network form takes SSID + passphrase (blank for open) and calls wpa_cli add_network / set_network / enable_network / select_network. The saved networks table shows all configured networks with current/saved status badges and connect/remove buttons per row. Raw wpa_cli status output shown in a log panel.

Note the wlan1 scanning limitation: entering a note in the form UI explains that scanning returns empty while wlan0 AP is active, so SSID must be entered directly.

RayTrap WiFi

Routing — policy routing control. Separate ip rule tables for LTE (rmnet0, table 100) and wlan1 STA (table 200). An "Initialize Routing Tables" button runs the one-time ip route add setup to populate both tables. Per-client routing rules let you assign a specific WiFi client's traffic to either uplink — run two clients simultaneously on different exits.

RayTrap Routing

Capture — tcpdump control. Interface picker (bridge0 / wlan0 / rmnet0 / wlan1 / any), BPF filter text field, duration selector (30s to unlimited), and optional filename prefix. Start/Stop/Refresh buttons. Active capture shows PID, interface, filename, and elapsed time. Saved captures list with file sizes and a Download link that serves the PCAP directly from the CGI (Content-Disposition: attachment).

RayTrap Capture

Deployment

# From PC (repo root):
export MSYS_NO_PATHCONV=1
adb push PortableApps/26_raytrap /data/tmp/raytrap
adb shell
# In adb shell:
rootshell
sh /data/tmp/raytrap/deploy.sh

# Clean up staging (uid=2000 — must be done from adb shell, not rootshell):
exit   # back to adb shell (non-root)
adb shell rm -rf /data/tmp/raytrap

The deploy script:

  1. Verifies busybox httpd is available and the package is complete
  2. Stops any existing httpd on port 8888
  3. Installs tinyproxy, tcpdump, libpcap.so.1 to /cache/bin/ and /cache/lib/
  4. Creates /cache/raytrap/www/cgi-bin/ and /cache/raytrap/captures/
  5. Installs all CGI scripts (chmod 755) and index.html
  6. Patches /etc/init.d/misc-daemon to call raytrap_daemon start at boot (after modem ONLINE)
  7. Injects a once inittab entry, signals init, waits for port 8888
  8. Cleans up the once entry

After deploy, access:

adb forward tcp:8889 tcp:8888
# Open: http://127.0.0.1:8889/

Boot persistence is via the misc-daemon patch — on every subsequent reboot, RayTrap starts automatically without any intervention. The inittab escape handles the capability requirement: httpd runs with CapEff=0x3fffffffff.

Files

All files in PortableApps/26_raytrap/:

File Role
deploy.sh One-step installer from rootshell
raytrap/start.sh Manual start script (used by raytrap_daemon)
raytrap/raytrap_daemon /etc/init.d/ service script (start/stop/status)
raytrap/tinyproxy HTTP proxy binary (ARM, glibc 2.22)
raytrap/tcpdump Packet capture binary (ARM, glibc 2.22)
raytrap/libpcap.so.1 libpcap shared library
raytrap/tinyproxy.conf Default tinyproxy configuration
raytrap/www/index.html Single-page web UI (~875 lines, vanilla JS)
raytrap/www/cgi-bin/*.cgi Six shell CGI scripts (status, firewall, proxy, wifi, routing, capture)

Retrospective: What I'd Do Differently

Start with firmware extraction, not the software installer.

Having root handed to you by Rayhunter is convenient, but it can create a false sense that you understand the device. I spent time reverse-engineering the Rayhunter installer to understand how root worked before I had a full picture of the filesystem. In retrospect, dumping the firmware via EDL first gives you a static snapshot to analyze offline, lets you understand the full partition layout, and you can then approach the live device with much better context.

The Mac friction was real.

The AT command research via the serial binary had consistent problems on macOS that didn't exist on Linux/Windows. The workaround (atfwd.log as the debug channel) was functional but added friction. If you're replicating this: start on Linux, verify behavior there first.

Dependency analysis without readelf is painful.

The tr '\0' '\n' < binary | grep '^lib.*\.so' trick works but is fragile — it catches NEEDED library names from the ELF string table but can miss things or catch false positives. If I were doing this on Linux from the start, readelf -d on every binary would have been faster and more reliable. The Windows Git Bash environment forced a workaround that added uncertainty to every portability assessment.

The "try it and see" instinct vs. the "understand it first" discipline.

There were a few moments where I started running commands without fully understanding what they'd do — particularly with ATFWD commands and USB mode switching. Nothing broke, but it could have. On a device you can't reflash easily (before you know how EDL works), running unknown AT commands is a real risk. Understand first, execute second.

Not everything that looks Foxconn-specific is Foxconn-specific.

Some of the JMR540 binaries I initially flagged as "Foxconn-only, probably not portable" turned out to be straightforwardly portable because they only depend on standard system libs. Conversely, some that looked generic (simlock, for example) turned out to have Foxconn-specific internal assumptions. The dep analysis tells you about library requirements but not about internal assumptions about filesystem layout or IPC topology.


Side Quests

Deeper dives that don't fit the main narrative flow. Collapsed by default — each is a standalone document.

QMI Capability Comparison — RC400L vs JMR540

Full comparison of the QMI infrastructure on both devices: binary sizes, library versions, MCM framework (JMR540 only), QCMAP stack differences, AT command surface, QMI IP config, SoC variants, modem transport, and diagnostic test coverage.

SideQuests/QMI_Comparison.md

atfwd_daemon Reverse Engineering — RC400L AT Command Surface & Security Analysis

Static analysis of the Orbic atfwd_daemon (164 KB, ARM stripped ELF). Covers all 44 registered AT commands, QMI services accessed, QCMAP C++ interface, Meige module origin, and a full security assessment including: RSA private key embedded in binary, IMEI-derived WiFi PSK via AES, unauthenticated AT command socket (/tmp/at-interface.srv.sock → AT+SYSCMD = local root without inittab), +GETSIB/+PCISCAN as an AT-based alternative to Rayhunter's DIAG path, unconstrained GPIO control, active voice QMI on a data-only device, and shared-firmware implications across the Meige module customer base.

SideQuests/atfwd_re.md

SideQuest: PinePhone Modem SDK — Cross-Pollination with RC400L

The PinePhone's Quectel EG25-G modem uses the same Qualcomm MDM9607 SoC as the RC400L. The Modem Distro project (open-source replacement firmware for EG25-G) exposes cellular capabilities the stock firmware hides — signal tracking, cell broadcast relay, GPS, engineering mode AT commands, and cpufreq tuning — all directly portable to the RC400L application processor without kernel changes.

SideQuests/PinePhone_Modem_SDK.md

SideQuest: LCD Framebuffer — 128×128 RGB565 Display Control

128×128 RGB565 LCD panel via FBTFT (/dev/fb0). Covers the Qt Embedded 4.8.7 stock display stack, rayhunter's framebuffer takeover (ui_level modes, 1s refresh loop), raw RGB565 pixel format, and host-side tools (PortableApps/27_lcd/) for pushing arbitrary images to the screen over ADB.

SideQuests/LCD.md

SideQuest: Rayhunter Fork — Boot Mask & DIAG Log Categories

Documents the rayhunter fork deployed on this RC400L: a reference table of all 14 DIAG log mask categories (mapped to actual Qualcomm log code IDs), what each captures on the MDM9607, and which are no-ops on this hardware. Covers the boot-default problem in stock rayhunter — masks were never applied in debug_mode=true — and the Option<DiagDevice> fix that seeds the modem mask at every startup regardless of operating mode.

SideQuests/Rayhunter_Fork.md


Navigation

Phase 1 — Reconnaissance & Initial Access

Phase 2 — Mapping the Attack Surface

Phase 3 — Cross-Vendor Firmware Forensics

Phase 4 — Exploitation & Tool Development



References


Research ongoing. This document is updated as findings develop.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Contributors