diff --git a/daemon/src/display/mod.rs b/daemon/src/display/mod.rs index 6f19ca5..ce06324 100644 --- a/daemon/src/display/mod.rs +++ b/daemon/src/display/mod.rs @@ -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)] diff --git a/daemon/src/display/uz801.rs b/daemon/src/display/uz801.rs new file mode 100644 index 0000000..ea0fd7e --- /dev/null +++ b/daemon/src/display/uz801.rs @@ -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, +) { + 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; + } + }); +} diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 31c4ec5..e53cd46 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -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); diff --git a/daemon/src/stats.rs b/daemon/src/stats.rs index 6fdcfb4..0b5e90d 100644 --- a/daemon/src/stats.rs +++ b/daemon/src/stats.rs @@ -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 { + pub async fn new(qmdl_path: &str, device: &Device) -> Result { 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 " to get storage statistics for the partition containing - // the QMDL file - pub async fn new(qmdl_path: &str) -> Result { - let mut df_cmd = Command::new("df"); + // the QMDL file. + pub async fn new(qmdl_path: &str, device: &Device) -> Result { + // 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 { } impl MemoryStats { - // runs "free -k" and parses the output to retrieve memory stats - pub async fn new() -> Result { - 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 { + // 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>, ) -> Result, (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}"); diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md index ddd5114..b62d88d 100644 --- a/doc/SUMMARY.md +++ b/doc/SUMMARY.md @@ -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) diff --git a/doc/supported-devices.md b/doc/supported-devices.md index db6751e..b41f034 100644 --- a/doc/supported-devices.md +++ b/doc/supported-devices.md @@ -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. diff --git a/doc/uninstalling.md b/doc/uninstalling.md index 4649999..6b83ece 100644 --- a/doc/uninstalling.md +++ b/doc/uninstalling.md @@ -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. \ No newline at end of file diff --git a/doc/uz801.md b/doc/uz801.md new file mode 100644 index 0000000..ed1d5ed --- /dev/null +++ b/doc/uz801.md @@ -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 `, 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. diff --git a/installer/src/main.rs b/installer/src/main.rs index 620c2ee..5095d02 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -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?; } diff --git a/installer/src/uz801.rs b/installer/src/uz801.rs new file mode 100644 index 0000000..2d66375 --- /dev/null +++ b/installer/src/uz801.rs @@ -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 { + 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::::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::::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::::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::::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::::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::::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::::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::::new(); + adb_device.shell_command( + &["chmod", "755", "/system/bin/initmifiservice.sh"], + &mut buf, + )?; + + Ok(()) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c53f741..a1afa73 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -25,4 +25,5 @@ pub enum Device { Tmobile, Wingtech, Pinephone, + Uz801, }