Merge pull request #511 from Tunas1337/uz801

Add UZ801 support
This commit is contained in:
Markus Unterwaditzer
2025-08-05 21:23:27 +02:00
committed by GitHub
11 changed files with 457 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ pub mod tmobile;
pub mod tplink;
pub mod tplink_framebuffer;
pub mod tplink_onebit;
pub mod uz801;
pub mod wingtech;
#[derive(Clone, Copy, PartialEq)]

View File

@@ -0,0 +1,89 @@
/// Display module for Uz801, light LEDs on the front of the device.
/// DisplayState::Recording => Green LED is solid.
/// DisplayState::Paused => Signal LED is solid blue (wifi LED).
/// DisplayState::WarningDetected => Signal LED is solid red.
use log::{error, info};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use std::time::Duration;
use crate::config;
use crate::display::DisplayState;
macro_rules! led {
($l:expr) => {{ format!("/sys/class/leds/{}/brightness", $l) }};
}
async fn led_on(path: String) {
tokio::fs::write(&path, "1").await.ok();
}
async fn led_off(path: String) {
tokio::fs::write(&path, "0").await.ok();
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>,
mut ui_update_rx: mpsc::Receiver<DisplayState>,
) {
let mut invisible: bool = false;
if config.ui_level == 0 {
info!("Invisible mode, not spawning UI.");
invisible = true;
}
task_tracker.spawn(async move {
let mut state = DisplayState::Recording;
let mut last_state = DisplayState::Paused;
let mut last_update = std::time::Instant::now();
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
}
Err(oneshot::error::TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
}
match ui_update_rx.try_recv() {
Ok(new_state) => state = new_state,
Err(mpsc::error::TryRecvError::Empty) => {}
Err(e) => error!("error receiving ui update message: {e}"),
};
// Update LEDs if state changed or if 5 seconds have passed since last update
let now = std::time::Instant::now();
let should_update = !invisible
&& (state != last_state
|| now.duration_since(last_update) >= Duration::from_secs(5));
if should_update {
match state {
DisplayState::Paused => {
led_off(led!("red")).await;
led_off(led!("green")).await;
led_on(led!("wifi")).await;
}
DisplayState::Recording => {
led_off(led!("red")).await;
led_off(led!("wifi")).await;
led_on(led!("green")).await;
}
DisplayState::WarningDetected => {
led_off(led!("green")).await;
led_off(led!("wifi")).await;
led_on(led!("red")).await;
}
}
last_state = state;
last_update = now;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
}

View File

@@ -243,6 +243,7 @@ async fn run_with_config(
Device::Tmobile => display::tmobile::update_ui,
Device::Wingtech => display::wingtech::update_ui,
Device::Pinephone => display::headless::update_ui,
Device::Uz801 => display::uz801::update_ui,
};
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);

View File

@@ -7,7 +7,7 @@ use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use log::error;
use rayhunter::util::RuntimeMetadata;
use rayhunter::{Device, util::RuntimeMetadata};
use serde::Serialize;
use tokio::process::Command;
@@ -19,10 +19,10 @@ pub struct SystemStats {
}
impl SystemStats {
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
Ok(Self {
disk_stats: DiskStats::new(qmdl_path).await?,
memory_stats: MemoryStats::new().await?,
disk_stats: DiskStats::new(qmdl_path, device).await?,
memory_stats: MemoryStats::new(device).await?,
runtime_metadata: RuntimeMetadata::new(),
})
}
@@ -40,13 +40,22 @@ pub struct DiskStats {
impl DiskStats {
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
// the QMDL file
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
let mut df_cmd = Command::new("df");
// the QMDL file.
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
// Uz801 needs to be told to use the busybox df specifically
let mut df_cmd: Command;
if matches!(device, Device::Uz801) {
df_cmd = Command::new("busybox");
df_cmd.arg("df");
} else {
df_cmd = Command::new("df");
}
df_cmd.arg("-h");
df_cmd.arg(qmdl_path);
let stdout = get_cmd_output(df_cmd).await?;
let mut parts = stdout.split_whitespace().skip(7).to_owned();
// Handle standard df -h format
let mut parts = stdout.split_whitespace().skip(7);
Ok(Self {
partition: parts.next().ok_or("error parsing df output")?.to_string(),
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
@@ -83,9 +92,16 @@ async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
}
impl MemoryStats {
// runs "free -k" and parses the output to retrieve memory stats
pub async fn new() -> Result<Self, String> {
let mut free_cmd = Command::new("free");
// runs "free -k" and parses the output to retrieve memory stats for most devices,
pub async fn new(device: &Device) -> Result<Self, String> {
// Use busybox for Uz801
let mut free_cmd: Command;
if matches!(device, Device::Uz801) {
free_cmd = Command::new("busybox");
free_cmd.arg("free");
} else {
free_cmd = Command::new("free");
}
free_cmd.arg("-k");
let stdout = get_cmd_output(free_cmd).await?;
let mut numbers = stdout
@@ -111,7 +127,7 @@ pub async fn get_system_stats(
State(state): State<Arc<ServerState>>,
) -> Result<Json<SystemStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
match SystemStats::new(qmdl_store.path.to_str().unwrap(), &state.config.device).await {
Ok(stats) => Ok(Json(stats)),
Err(err) => {
error!("error getting system stats: {err}");

View File

@@ -16,6 +16,7 @@
- [TP-Link M7350](./tplink-m7350.md)
- [TP-Link M7310](./tplink-m7310.md)
- [Tmobile TMOHS1](./tmobile-tmohs1.md)
- [UZ801](./uz801.md)
- [Wingtech CT2MHS01](./wingtech-ct2mhs01.md)
- [PinePhone and PinePhone Pro](./pinephone.md)
- [Support, feedback, and community](./support-feedback-community.md)

View File

@@ -24,6 +24,7 @@ Rayhunter is confirmed to work on these devices.
| [Tmobile TMOHS1](./tmobile-tmohs1.md) | Americas |
| [TP-Link M7310](./tplink-m7310.md) | Africa, Europe, Middle East |
| [PinePhone and PinePhone Pro](./pinephone.md) | Global |
| [FY UZ801](./uz801.md) | Asia, Europe |
## Adding new devices
Rayhunter was built and tested primarily on the Orbic RC400L mobile hotspot, but the community has been working hard at adding support for other devices. Theoretically, if a device runs a Qualcomm modem and exposes a `/dev/diag` interface, Rayhunter may work on it.

View File

@@ -22,3 +22,18 @@ Your device is now Rayhunter-free, and should no longer be in a rooted ADB-enabl
4. `update-rc.d rayhunter_daemon remove`
5. (hardware revision v4.0+ only) In `Settings > NAT Settings > Port Triggers` in TP-Link's admin UI, remove any leftover port triggers.
## UZ801
0. (Optional): Back up the qmdl folder with all of the captures:
`adb pull /data/rayhunter/qmdl .`
1. Run `adb shell` to get a root shell on the device
2. Delete the /data/rayhunter folder: `rm -rf /data/rayhunter`
3. Modify the initmifiservice.sh script to remove the rayhunter
startup line:
```sh
mount -o remount,rw /system
busybox vi /system/bin/initmifiservice.sh
```
Then type 999G (shift+g), then type dd. Then press the colon key (:) and type wq. Finally, press Enter.
4. Lastly, run `setprop persist.sys.usb.config rndis`.
5. Type `reboot` to reboot the device.

67
doc/uz801.md Normal file
View File

@@ -0,0 +1,67 @@
# UZ801
The UZ801 is a 4G/LTE USB modem which is built on top of a Qualcomm Snapdragon 410 (MSM8916, with MDM8916 modem.) It does not have a screen, but it does have LEDs which can be used to signal the same status as the green/red bar on the Orbic hotspot. It uses a custom Android-based firmware with limited coreutils. More information about this device can be found [here](https://github.com/AlienWolfX/UZ801-USB_MODEM/wiki/Overview)
It is worth noting that even though the Snapdragon 410 is a quad-core SoC, the CPU has only 2 of the cores enabled on the stock Android-based firmware, probably to avoid overheating as they did not exactly engineer any cooling solution. Regardless, even with 2 disabled cores there is plenty of compute overhead. There are 384MB of RAM on the SoC, and 4GB of eMMC in the form of an SK Hynix NAND flash chip located external to the SoC.
Rayhunter has been tested on UZ801 devices with firmware supporting USB debugging backdoor access. It is not certain whether all of the sticks that use this board will be compatible with the automated installer, or even with any alternative manual installation method. Please consider sharing your device's firmware version and hardware information [here](https://github.com/EFForg/rayhunter/discussions/479) to help improve compatibility.
## Where to purchase
There are several option to purchase this device:
1. AliExpress:
- [1](https://www.aliexpress.us/item/3256808999940005.html)
- [2](https://www.aliexpress.us/item/3256809191207903.html)
- [3](https://www.aliexpress.us/item/3256809191207903.html)
2. eBay:
- [1](https://www.ebay.com/itm/394512588226)
- [2](https://www.ebay.com/itm/195655408253)
- [3](https://www.ebay.com/itm/116678550086)
3. Amazon:
- [1](https://www.amazon.com/150Mbps-Adapter-Network-Lightweight-Portable/dp/B0DQC64ZFS)
- [2](https://www.amazon.com/Heayzoki-Network-Adapter-Wireless-Connection/dp/B0CG4W31M4)
## Supported bands
The UZ801 supports various LTE bands depending on the specific hardware revision and carrier customization. Check your device specifications for the exact band support.
The most frequent bands found on these devices are LTE bands 1/3/5/8/20. In the US, this means that Verizon's band 5 towers are the only towers that this device could communicate with in its normal usage as an LTE modem. Research on whether Qualcomm diagnostic tools can be used to write new band support into the NVRAM is pending.
## Installing
With the device fully booted (i.e. beaming a wifi network, blue LED, etc.) and plugged into the computer that is performing the installation, run:
```sh
./installer uz801
```
Note: The default IP for UZ801 is typically `192.168.100.1`; if yours differs, use the `--admin-ip` argument to specify it.
## LED modes
| Rayhunter state | LED indicator |
| ---------------- | ------------------- |
| Recording | Green LED solid on |
| Paused | WiFi (blue) LED solid on |
| Warning Detected | Red LED solid on |
Note: Unlike the TMOHS1, the UZ801 uses solid LED indicators instead of blinking patterns.
## Obtaining a shell
The UZ801 supports ADB access after the USB debugging backdoor is activated.
```sh
adb shell
```
## Device-specific notes
The UZ801 uses a unique installation process that activates a hidden USB debugging backdoor.
The installation process works as follows:
1. Activates the USB debugging backdoor via HTTP AJAX request
2. Waits for device reboot and ADB availability
3. Uses ADB to install rayhunter files and modify the startup script
4. Launches rayhunter daemon automatically
- The UZ801 does not symlink busybox for some core system utils, for some reason. Please use `busybox <utility_name>`, e.g. `busybox df -h`.
- USB debugging must be activated via the web backdoor before ADB access is possible (this is required only once.) The installer does this already.
- The device uses `/system/bin/initmifiservice.sh` as the main startup script.

View File

@@ -7,6 +7,7 @@ mod pinephone;
mod tmobile;
mod tplink;
mod util;
mod uz801;
mod wingtech;
pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.in");
@@ -27,6 +28,8 @@ enum Command {
Orbic(InstallOrbic),
/// Install rayhunter on the TMobile TMOHS1.
Tmobile(TmobileArgs),
/// Install rayhunter on the Uz801.
Uz801(Uz801Args),
/// Install rayhunter on a PinePhone's Quectel modem.
Pinephone(InstallPinephone),
/// Install rayhunter on the TP-Link M7350.
@@ -82,6 +85,8 @@ enum UtilSubCommand {
TmobileStartAdb(TmobileArgs),
/// Root the Tmobile and launch telnetd.
TmobileStartTelnet(TmobileArgs),
/// Root the Uz801 and launch adb.
Uz801StartAdb(Uz801Args),
/// Root the tplink and launch telnetd.
TplinkStartTelnet(TplinkStartTelnet),
/// Root the Wingtech and launch telnetd.
@@ -115,6 +120,13 @@ struct TmobileArgs {
admin_password: String,
}
#[derive(Parser, Debug)]
struct Uz801Args {
/// IP address for Uz801 admin interface, if custom.
#[arg(long, default_value = "192.168.100.1")]
admin_ip: String,
}
#[derive(Parser, Debug)]
struct TplinkStartTelnet {
/// IP address for TP-Link admin interface, if custom.
@@ -168,6 +180,7 @@ async fn run() -> Result<(), Error> {
match command {
Command::Tmobile(args) => tmobile::install(args).await.context("Failed to install rayhunter on the Tmobile TMOHS1. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
Command::Uz801(args) => uz801::install(args).await.context("Failed to install rayhunter on the Uz801. Make sure your computer is connected to the hotspot using USB.")?,
Command::Tplink(tplink) => tplink::main_tplink(tplink).await.context("Failed to install rayhunter on the TP-Link M7350. Make sure your computer is connected to the hotspot using USB tethering or WiFi.")?,
Command::Pinephone(_) => pinephone::install().await
.context("Failed to install rayhunter on the Pinephone's Quectel modem")?,
@@ -195,6 +208,7 @@ async fn run() -> Result<(), Error> {
UtilSubCommand::Shell => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?,
UtilSubCommand::TmobileStartTelnet(args) => wingtech::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?,
UtilSubCommand::TmobileStartAdb(args) => wingtech::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?,
UtilSubCommand::Uz801StartAdb(args) => uz801::activate_usb_debug(&args.admin_ip).await.context("\nFailed to activate USB debug on the Uz801")?,
UtilSubCommand::TplinkStartTelnet(options) => {
tplink::start_telnet(&options.admin_ip).await?;
}

239
installer/src/uz801.rs Normal file
View File

@@ -0,0 +1,239 @@
use std::io::Write;
use std::path::Path;
/// Installer for the Uz801 hotspot.
///
/// Installation process:
/// 1. Use curl to activate USB debugging backdoor
/// 2. Wait for device reboot and ADB availability
/// 3. Use ADB to install rayhunter files
/// 4. Modify startup script to launch rayhunter on boot
use std::time::Duration;
use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError};
use anyhow::{Result, anyhow};
use md5::compute as md5_compute;
use tokio::time::sleep;
use crate::Uz801Args as Args;
use crate::util::echo;
pub async fn install(Args { admin_ip }: Args) -> Result<()> {
run_install(admin_ip).await
}
async fn run_install(admin_ip: String) -> Result<()> {
echo!("Activating USB debugging backdoor... ");
activate_usb_debug(&admin_ip).await?;
println!("ok");
echo!("Waiting for device reboot and ADB connection... ");
let mut adb_device = wait_for_adb().await?;
println!("ok");
echo!("Installing rayhunter files... ");
install_rayhunter_files(&mut adb_device).await?;
println!("ok");
echo!("Modifying startup script... ");
modify_startup_script(&mut adb_device).await?;
println!("ok");
echo!("Rebooting the device... ");
let _ = adb_device.reboot(adb_client::RebootType::System);
println!("ok");
println!("Installation complete!");
println!("Please wait for the device to reboot (light will turn green)");
println!("Then access rayhunter at: http://{admin_ip}:8080");
Ok(())
}
pub async fn activate_usb_debug(admin_ip: &str) -> Result<()> {
let url = format!("http://{admin_ip}/ajax");
let referer = format!("http://{admin_ip}/usbdebug.html");
let origin = format!("http://{admin_ip}");
let _handle = tokio::spawn(async move {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let _response = client
.post(&url)
.header("Accept", "application/json, text/javascript, */*; q=0.01")
.header("Accept-Encoding", "gzip, deflate")
.header("Referer", &referer)
.header(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8",
)
.header("X-Requested-With", "XMLHttpRequest")
.header("Origin", &origin)
.body(r#"{"funcNo":2001}"#)
.send()
.await;
// Ignore any errors - the device will reboot and connection will be lost
});
Ok(())
}
async fn wait_for_adb() -> Result<ADBUSBDevice> {
const MAX_ATTEMPTS: u32 = 30; // 30 seconds
let mut attempts = 0;
// Wait a bit for the reboot to start
sleep(Duration::from_secs(10)).await;
loop {
if attempts >= MAX_ATTEMPTS {
anyhow::bail!("Timeout waiting for ADB connection after USB debug activation");
}
// UZ801 USB vendor and product IDs.
// TODO: Research if other variants use different IDs.
match ADBUSBDevice::new(0x05c6, 0x90b6) {
Ok(mut device) => {
// Test ADB connection
if test_adb_connection(&mut device).await.is_ok() {
return Ok(device);
}
}
Err(RustADBError::DeviceNotFound(_)) => {
// Device not ready yet, continue waiting
}
Err(e) => {
anyhow::bail!("ADB connection error: {}", e);
}
}
sleep(Duration::from_secs(1)).await;
attempts += 1;
}
}
async fn test_adb_connection(adb_device: &mut ADBUSBDevice) -> Result<()> {
let mut buf = Vec::<u8>::new();
adb_device.shell_command(&["echo", "test"], &mut buf)?;
let output = String::from_utf8_lossy(&buf);
if output.contains("test") {
Ok(())
} else {
anyhow::bail!("ADB connection test failed")
}
}
async fn install_rayhunter_files(adb_device: &mut ADBUSBDevice) -> Result<()> {
// Create rayhunter directory
let mut buf = Vec::<u8>::new();
adb_device.shell_command(&["mkdir", "-p", "/data/rayhunter"], &mut buf)?;
// Remount system as writable
adb_device.shell_command(&["mount", "-o", "remount,rw", "/system"], &mut buf)?;
// Install rayhunter daemon binary with verification
let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON"));
install_file(
adb_device,
"/data/rayhunter/rayhunter-daemon",
rayhunter_daemon_bin,
)?;
// Install config file
let config_content = crate::CONFIG_TOML.replace("#device = \"orbic\"", "device = \"uz801\"");
let mut config_data = config_content.as_bytes();
adb_device.push(&mut config_data, &"/data/rayhunter/config.toml")?;
// Make daemon executable
let mut buf = Vec::<u8>::new();
adb_device.shell_command(
&["chmod", "755", "/data/rayhunter/rayhunter-daemon"],
&mut buf,
)?;
Ok(())
}
/// Transfer a file to the device's filesystem with adb push.
/// Validates the file sends successfully to /data/local/tmp
/// before overwriting the destination.
fn install_file(adb_device: &mut ADBUSBDevice, dest: &str, payload: &[u8]) -> Result<()> {
const MAX_RETRIES: u32 = 3;
let file_name = Path::new(dest)
.file_name()
.ok_or_else(|| anyhow!("{dest} does not have a file name"))?
.to_str()
.ok_or_else(|| anyhow!("{dest}'s file name is not UTF8"))?
.to_owned();
let push_tmp_path = format!("/data/local/tmp/{file_name}");
let file_hash = md5_compute(payload);
for attempt in 1..=MAX_RETRIES {
// Push the file
let mut payload_copy = payload;
if let Err(e) = adb_device.push(&mut payload_copy, &push_tmp_path) {
if attempt == MAX_RETRIES {
return Err(e.into());
}
continue;
}
// Verify with md5sum
let mut buf = Vec::<u8>::new();
if adb_device
.shell_command(&["busybox", "md5sum", &push_tmp_path], &mut buf)
.is_ok()
{
let output = String::from_utf8_lossy(&buf);
if output.contains(&format!("{file_hash:x}")) {
// Verification successful, move to final destination
let mut buf = Vec::<u8>::new();
adb_device.shell_command(&["mv", &push_tmp_path, dest], &mut buf)?;
println!("ok");
return Ok(());
}
}
// Verification failed, clean up and retry
if attempt < MAX_RETRIES {
println!("MD5 verification failed on attempt {attempt}, retrying...");
let mut buf = Vec::<u8>::new();
adb_device
.shell_command(&["rm", "-f", &push_tmp_path], &mut buf)
.ok();
}
}
anyhow::bail!("MD5 verification failed for {dest} after {MAX_RETRIES} attempts")
}
async fn modify_startup_script(adb_device: &mut ADBUSBDevice) -> Result<()> {
// Pull the existing startup script
let mut script_content = Vec::<u8>::new();
adb_device.pull(&"/system/bin/initmifiservice.sh", &mut script_content)?;
// Convert to string and add our line
let mut script_str = String::from_utf8_lossy(&script_content).into_owned();
// Add rayhunter startup line if not already present
let rayhunter_line = "/data/rayhunter/rayhunter-daemon /data/rayhunter/config.toml &\n";
if !script_str.contains("/data/rayhunter/rayhunter-daemon") {
script_str.push_str(rayhunter_line);
}
// Push the modified script back
let mut modified_script = script_str.as_bytes();
adb_device.push(&mut modified_script, &"/system/bin/initmifiservice.sh")?;
// Make sure it's executable
let mut buf = Vec::<u8>::new();
adb_device.shell_command(
&["chmod", "755", "/system/bin/initmifiservice.sh"],
&mut buf,
)?;
Ok(())
}

View File

@@ -25,4 +25,5 @@ pub enum Device {
Tmobile,
Wingtech,
Pinephone,
Uz801,
}