Rayhunter on a HiMI Dongle

Field build · working solution

Rayhunter on a HiMI MSM8916 Dongle

A complete, tested walkthrough for turning a cheap HiMI-firmware 4G USB dongle (Qualcomm MSM8916) into a self-contained EFF Rayhunter IMSI-catcher detector that boots, auto-starts and records on its own from any power bank. Every command, script and recovery step is the one that actually worked on the device.

HiMI · MSM8916 ✓ /dev/diag ✓ adb root ✓ busybox + curl ✓ auto-start ✓ config: uz801
The 4G LTE USB modem (HiMI / MSM8916 family) this build targets
The cheap 4G LTE USB modem this build targets - a HiMI / MSM8916 family dongle.

What this build is, and isn't

Rayhunter is EFF's open-source IMSI-catcher detector. It runs a small daemon on a Qualcomm hotspot, reads the modem's diagnostic stream (/dev/diag), and watches the LTE control plane for the tricks fake base stations use - odd identity requests, forced downgrades to 2G, null-cipher. It is passive and defensive: it only observes the cell your own SIM is camped on. It does not transmit, does not see other people's phones, and is not an IMSI catcher.

Rayhunter officially supports the UZ801 (also MSM8916). This guide adapts that to a HiMI-firmware dongle - a different vendor on the same chip - where the official installer's backdoor doesn't apply and the autostart path is different. The daemon binary and device = "uz801" config are reused as-is; everything around them is device-specific.

Know your device first This worked because the stick is MSM8916, has /dev/diag, allows ADB root, and runs HiMI's Android 4.4. A dongle on a different SoC (UNISOC, ASR, etc.) has no Qualcomm diag interface and cannot run Rayhunter. Phase 0 is how you confirm you have the right hardware before investing time.
0

Identify the hardware

Confirm ADB is reachable, the SoC is MSM8916, and the diag device exists. If any of these fail, stop - this device isn't a Rayhunter target.

0.1 Install adb/fastboot on the host & see the stick

$ sudo apt install -y android-tools-adb fastboot
$ adb devices
# 234a3430    device   ← your serial, "device" state

If it shows unauthorized or nothing, the stick's firmware may need USB debugging enabled first (varies by HiMI build). On this unit ADB was already open.

0.2 Confirm chip, diag, firmware

$ adb shell "cat /proc/cpuinfo | grep Hardware"
# Hardware : Qualcomm Technologies, Inc MSM8916
$ adb shell "ls -l /dev/diag"
# crw-rw---- system qcom_diag 243, 0 ... diag   ← must exist
$ adb shell "getprop ro.product.manufacturer; getprop ro.build.version.release"
# HiMI
# 4.4.4
Go / no-go MSM8916 + /dev/diag present = viable. No diag node, or a non-Qualcomm chip, means Rayhunter can't read the modem - stop here.
1

Get root

Rayhunter and the backup both need root. On a userdebug build this is one command - but it resets on every reboot, so you'll re-run it often.

1.1 Elevate the ADB shell to root

$ adb shell "setprop service.adb.root 1; busybox killall adbd"
$ adb wait-for-device
$ adb shell id
# uid=0(root) gid=0(root) ...   ← you're root
Remember this Root resets on every reboot. Any time adb reconnects after a reboot and a command fails with permission denied, run line 1.1 again. The auto-started daemon (Phase 5) does not need this - it launches as root from a boot script.
2

Back up every partition

Before touching anything, dump the flash. The modem/calibration partitions hold your IMEI and radio tuning and cannot be recovered any other way. This backup is also what saves you if a later step goes wrong.

2.1 See the partition map

$ adb shell "ls -l /dev/block/platform/*/by-name/"
# note the mmcblk0pN number for each name

On this device the important ones were: modem=p1, modemst1=p13, modemst2=p14, fsg=p20, persist=p24, boot=p22, system=p23. Yours may differ - use your own listing.

2.2 Dump each partition to the host

This loops over the partitions (skipping userdata/cache), dumping each to the stick, pulling it, then deleting the temp copy. Adjust the name:number pairs to match your map.

$ mkdir -p ~/stick-backup && cd ~/stick-backup
$ for pair in modem:1 sbl1:2 sbl1bak:3 aboot:4 abootbak:5 \
      rpm:6 rpmbak:7 tz:8 tzbak:9 hyp:10 hypbak:11 pad:12 \
      modemst1:13 modemst2:14 misc:15 fsc:16 ssd:17 splash:18 \
      DDR:19 fsg:20 sec:21 boot:22 system:23 persist:24 \
      config:26 recovery:27; do
    name=${pair%%:*}; num=${pair##*:}
    echo "== $name (mmcblk0p$num)"
    adb shell "dd if=/dev/block/mmcblk0p$num of=/data/local/tmp/$name.img bs=4096" && \
    adb pull /data/local/tmp/$name.img . && \
    adb shell "rm /data/local/tmp/$name.img"
  done
$ ls -lh

2.3 Verify the irreplaceable partitions

$ adb shell "busybox md5sum /dev/block/mmcblk0p1 \
      /dev/block/mmcblk0p13 /dev/block/mmcblk0p14 \
      /dev/block/mmcblk0p20 /dev/block/mmcblk0p24"
$ md5sum modem.img modemst1.img modemst2.img fsg.img persist.img
# the two lists must match line-for-line

A mismatch on modemst1/2 usually just means the modem wrote to it mid-dump - re-pull that one partition and it'll match. modem, fsg and persist must match.

Copy it off the host Move ~/stick-backup to a second location - USB drive, laptop, cloud. A backup that lives only on the Pi's SD card is not a backup. Do this before Phase 5.
3

Install Rayhunter (manual)

The official ./installer uz801 fails on HiMI because it expects a web backdoor the firmware doesn't have. ADB is already open, so we push the daemon by hand instead - which is simpler and more reliable here.

3.1 Download a release & find the device binary

Grab any host build from the releases page (the daemon inside is the same ARM device binary regardless of which host zip you pick). On the Pi (aarch64) we used the aarch64 zip:

$ wget https://github.com/EFForg/rayhunter/releases/download/v0.13.0/rayhunter-v0.13.0-linux-aarch64.zip
$ unzip rayhunter-v0.13.0-linux-aarch64.zip
$ cd rayhunter-v0.13.0-linux-aarch64
$ file rayhunter-daemon/rayhunter-daemon
# ELF 32-bit LSB executable, ARM, EABI5 ... statically linked, stripped

"ELF 32-bit ARM, statically linked" is exactly right for MSM8916 - static means no library-dependency problems on the old Android userspace.

3.2 Write the config

$ cat > config.toml <<'EOF'
device = "uz801"
qmdl_store_path = "/data/rayhunter/qmdl"
port = 8080
EOF

The uz801 profile matches the MSM8916 diag layout. If the config isn't read for any reason the daemon falls back to the Orbic profile, which still works but isn't ideal - so confirm the log says device: Uz801 later.

3.3 Push to the stick

$ adb shell "setprop service.adb.root 1; busybox killall adbd"
$ adb wait-for-device
$ adb shell "mkdir -p /data/rayhunter/qmdl"
$ adb push rayhunter-daemon/rayhunter-daemon /data/rayhunter/
$ adb push config.toml /data/rayhunter/
$ adb shell "chmod 755 /data/rayhunter/rayhunter-daemon"
4

Test it reads the modem

Prove the daemon runs and opens diag before automating anything. Run it in the foreground once - that's the most reliable way to see the log and confirm the web UI.

4.1 Run it in the foreground

$ adb shell
# now inside the device shell:
# cd /data/rayhunter
# RUST_LOG=info ./rayhunter-daemon config.toml

You want to see, in order:

R A Y H U N T E R 🐳
Using configuration for device: Uz801
Diag device initialization succeeded after 0 retries
enabled logging for log type 1 / 4 / 5 / 7 / 10 / 11 / 13
Starting recording with 1502MB disk space available
The orca is hunting for stingrays...

That "Diag device initialization succeeded" line is the whole game - it means Rayhunter is pulling live LTE signaling from the modem. The leftover bytes when parsing Message warnings are harmless parser noise.

4.2 Open the web UI

While it runs, connect a phone to the stick's Wi-Fi and open:

http://192.168.100.1:8080

The dashboard should load and show an active recording. (The Pi talks to the stick over USB/ADB, so 192.168.100.1 isn't reachable from the Pi itself - use the phone, or adb forward tcp:8080 tcp:8080 then localhost:8080.)

Checkpoint Web UI loads + "orca is hunting" in the log = Rayhunter works on your stick. Stop the foreground run (close the adb window, or from another terminal adb shell "busybox killall rayhunter-daemon") and move to auto-start.
5

Auto-start on boot

Make it launch itself on power-on, with no PC. The key insight: edits to the init ramdisk don't persist (it's rebuilt from boot.img each boot), and repacking boot.img risks a no-boot. The safe path is a /system script that init already runs late in boot - HiMI even uses it for its own launcher.

Backup gate This edits /system. Make sure Phase 2 is done and copied off the host before continuing. (Unlike a boot.img repack, this can't stop the device booting - but verify anyway.)

5.1 Why not the init.rc / boot.img route

Two approaches fail or are risky on this firmware:

  • Editing /init.target.rc live - reverts on reboot. The root filesystem is a ramdisk rebuilt from boot.img every boot, so the edit vanishes.
  • Repacking boot.img (unpack, add the service, reflash) - this bricked the boot in testing. The Qualcomm bootloader is strict about ramdisk packing and the repacked image dropped to fastboot. Recoverable via the backup, but not worth it.

Instead, hook a script on the writable /system partition that init runs anyway.

5.2 Find the late-boot /system script

Qualcomm Android runs /system/etc/init.qcom.post_boot.sh near the end of boot. On HiMI it already ends with the vendor's own launcher (/data/SelfHost/startRIDL.sh &) - proof this hook works for backgrounded /data scripts. Confirm it exists:

$ adb shell "ls -l /system/etc/init.qcom.post_boot.sh"
$ adb shell "busybox tail -4 /system/etc/init.qcom.post_boot.sh"
# ... ends with:  /data/SelfHost/startRIDL.sh &

5.3 Create the launcher on /data

A small wrapper that waits for the modem to come up, then starts the daemon in the background so it never blocks boot.

~/stick-backup/start.sh

#!/system/bin/sh
(
sleep 30
RUST_LOG=info /data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml \
  > /data/rayhunter/rayhunter.log 2>&1
) &
$ printf '#!/system/bin/sh\n(\nsleep 30\nRUST_LOG=info /data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml > /data/rayhunter/rayhunter.log 2>&1\n) &\n' > ~/stick-backup/start.sh
$ adb push ~/stick-backup/start.sh /data/rayhunter/start.sh
$ adb shell "chmod 755 /data/rayhunter/start.sh"

5.4 Add one line to post_boot.sh

Pull it, edit on the host, push it back - never edit in place.

$ cd ~/stick-backup
$ adb shell "mount -o remount,rw /system"
$ adb pull /system/etc/init.qcom.post_boot.sh .
$ cp init.qcom.post_boot.sh init.qcom.post_boot.sh.bak
$ nano init.qcom.post_boot.sh

At the very end, after the startRIDL.sh & line, add:

# Start Rayhunter IMSI-catcher detector
/system/bin/sh /data/rayhunter/start.sh &

Verify, then push back (keep the original 644 permissions):

$ tail -6 init.qcom.post_boot.sh    # confirm your line is there
$ adb push init.qcom.post_boot.sh /system/etc/init.qcom.post_boot.sh
$ adb shell "chmod 644 /system/etc/init.qcom.post_boot.sh"

5.5 Reboot & verify it self-starts

$ adb reboot
# wait ~75s (boot + the 30s sleep), then:
$ adb shell "setprop service.adb.root 1; busybox killall adbd"
$ adb wait-for-device
$ adb shell "ps | busybox grep rayhunter"
# root 1387 1207 ... /data/rayhunter/rayhunter-daemon  ← PPID 1207 = launched by boot
$ adb shell "busybox cat /data/rayhunter/rayhunter.log"
# ... Using configuration for device: Uz801 ... The orca is hunting ...
Success looks like this A rayhunter-daemon process with a boot PPID, a log showing device: Uz801 and "orca is hunting", and the web UI loading on your phone with no PC attached. That's a standalone detector.
6

Run it standalone

The payoff, plus the one thing that makes it actually detect.

Two cosmetic notes The daemon logs update check failed when the stick has no internet - harmless. The device clock may be wrong (captures are timestamped with it); set it if timestamps matter to you.
7

LED warning indicator (optional)

A standalone unit benefits from an at-a-glance status light. This dongle makes it awkward, so treat it as polish - the web UI already shows everything.

What we found on this board:

FactDetail
LEDs4g_1, 4g_type, wifistatus, bat1 under /sys/class/leds/ - plain on/off brightness, no timer trigger (so no hardware blink)
No screen/dev/graphics/fb0 exists but writes return ENODEV - the display controller is present but no panel is wired, so the framebuffer is a dead end
The catchhimiwebserver (init service himiwebsvr, toggled by setprop sys.himiweb_start 0|1) continuously repaints the LEDs by network state and overwrites any value you set

So a single echo > brightness won't hold. Three options, in order of effort:

/data/rayhunter/rayled.sh - adjust the API path after capturing the real endpoint from the web UI's DevTools → Network

#!/system/bin/sh
LED=/sys/class/leds/wifistatus/brightness
API=http://127.0.0.1:8080
PREV=0
led(){ echo "$1" > "$LED" 2>/dev/null; }
blink(){ n=0; while [ $n -lt 6 ]; do led 255; usleep 150000; led 0; usleep 150000; n=$((n+1)); done; }
while true; do
  J=$(curl -s --max-time 3 "$API/api/analysis-report" 2>/dev/null)
  W=$(echo "$J" | busybox grep -o warning | busybox wc -l)
  if   [ -z "$J" ];          then led 0
  elif [ "$W" -gt "$PREV" ]; then blink; PREV=$W
  else led 255; fi
  sleep 2
done

To auto-run it too, add /system/bin/sh /data/rayhunter/rayled.sh & to start.sh after the daemon line.

Recovery - if it won't boot

You have full backups, so a bad boot image is a 2-minute fix, not a brick.

If an experiment (e.g. a boot.img repack) leaves the stick not booting - no Wi-Fi, no adb - check USB:

$ lsusb
# 18d1:d00d ... (fastboot)   → bootloader/fastboot mode: easy recovery
# 05c6:9008                 → EDL mode: use an EDL/QDL tool

From fastboot, restore the original boot partition:

$ cd ~/stick-backup
$ sudo fastboot devices
$ sudo fastboot flash boot boot.img
$ sudo fastboot reboot
# wait ~60s - Wi-Fi and adb should return

Because the /system/etc/init.qcom.post_boot.sh method in Phase 5 never touches boot.img, it can't cause a no-boot - the worst case is the daemon not starting, fixed by re-editing that one line.

Capabilities & limits