diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 09e7a2b..3eec793 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [features] # These feature flags are mutually exclusive, and exactly one must be enabled. orbic = ["rayhunter/orbic"] +tmobile = ["rayhunter/tmobile"] tplink = ["rayhunter/tplink"] wingtech = ["rayhunter/wingtech"] diff --git a/daemon/src/display/mod.rs b/daemon/src/display/mod.rs index cf7a3d2..1b2fd48 100644 --- a/daemon/src/display/mod.rs +++ b/daemon/src/display/mod.rs @@ -1,5 +1,11 @@ +#[cfg(any(feature = "orbic", feature = "tplink", feature = "wingtech"))] mod generic_framebuffer; +#[cfg(feature = "tmobile")] +mod tmobile; +#[cfg(feature = "tmobile")] +pub use tmobile::update_ui; + #[cfg(feature = "tplink")] mod tplink; #[cfg(feature = "tplink")] @@ -20,6 +26,7 @@ mod wingtech; #[cfg(feature = "wingtech")] pub use wingtech::update_ui; +#[derive(Clone, Copy, PartialEq)] pub enum DisplayState { Recording, Paused, diff --git a/daemon/src/display/tmobile.rs b/daemon/src/display/tmobile.rs new file mode 100644 index 0000000..0d48634 --- /dev/null +++ b/daemon/src/display/tmobile.rs @@ -0,0 +1,85 @@ +/// Display module for Tmobile TMOHS1, blink LEDs on the front of the device. +/// DisplayState::Recording => Signal LED slowly blinks blue. +/// DisplayState::Paused => WiFi LED blinks white. +/// DisplayState::WarningDetected => Signal LED slowly blinks red. +use log::{error, info}; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tokio_util::task::TaskTracker; + +use std::fs::write; +use std::thread::sleep; +use std::time::Duration; + +use crate::config; +use crate::display::DisplayState; + +macro_rules! led { + ($l:expr) => {{ + format!("/sys/class/leds/led:{}/blink", $l) + }}; +} + +fn start_blinking(path: String) { + write(&path, "1").ok(); +} + +fn stop_blinking(path: String) { + write(&path, "0").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_blocking(move || { + let mut state = DisplayState::Recording; + let mut last_state = DisplayState::Paused; + + 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}"), + }; + if invisible || state == last_state { + sleep(Duration::from_secs(1)); + continue; + } + match state { + DisplayState::Paused => { + stop_blinking(led!("signal_blue")); + stop_blinking(led!("signal_red")); + start_blinking(led!("wlan_white")); + } + DisplayState::Recording => { + stop_blinking(led!("wlan_white")); + stop_blinking(led!("signal_red")); + start_blinking(led!("signal_blue")); + } + DisplayState::WarningDetected => { + stop_blinking(led!("wlan_white")); + stop_blinking(led!("signal_blue")); + start_blinking(led!("signal_red")); + } + } + last_state = state; + sleep(Duration::from_secs(1)); + } + }); +} diff --git a/installer/build.rs b/installer/build.rs index 0da4a53..a83f3b0 100644 --- a/installer/build.rs +++ b/installer/build.rs @@ -14,6 +14,11 @@ fn main() { "FILE_RAYHUNTER_DAEMON_ORBIC", "rayhunter-daemon", ); + set_binary_var( + include_dir, + "FILE_RAYHUNTER_DAEMON_TMOBILE", + "rayhunter-daemon", + ); set_binary_var( include_dir, "FILE_RAYHUNTER_DAEMON_TPLINK", diff --git a/installer/src/main.rs b/installer/src/main.rs index 44a6b55..39debf9 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use env_logger::Env; mod orbic; +mod tmobile; mod tplink; mod util; mod wingtech; @@ -21,6 +22,8 @@ struct Args { enum Command { /// Install rayhunter on the Orbic Orbic RC400L. Orbic(InstallOrbic), + /// Install rayhunter on the TMobile TMOHS1. + Tmobile(TmobileArgs), /// Install rayhunter on the TP-Link M7350. Tplink(InstallTpLink), /// Install rayhunter on the Wingtech CT2MHS01. @@ -67,6 +70,10 @@ enum UtilSubCommand { Serial(Serial), /// Start an ADB shell Shell(Shell), + /// Root the Tmobile and launch adb. + TmobileStartAdb(TmobileArgs), + /// Root the Tmobile and launch telnetd. + TmobileStartTelnet(TmobileArgs), /// Root the tplink and launch telnetd. TplinkStartTelnet(TplinkStartTelnet), /// Root the Wingtech and launch telnetd. @@ -85,6 +92,17 @@ enum UtilSubCommand { WingtechSendFile(WingtechSendFile), } +#[derive(Parser, Debug)] +struct TmobileArgs { + /// IP address for Tmobile admin interface, if custom. + #[arg(long, default_value = "192.168.0.1")] + admin_ip: String, + + /// Web portal admin password. + #[arg(long)] + admin_password: String, +} + #[derive(Parser, Debug)] struct TplinkStartTelnet { /// IP address for TP-Link admin interface, if custom. @@ -140,6 +158,7 @@ async fn run() -> Result<(), Error> { let Args { command } = Args::parse(); 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::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::Orbic(_) => orbic::install().await.context("\nFailed to install rayhunter on the Orbic RC400L")?, Command::Wingtech(args) => wingtech::install(args).await.context("\nFailed to install rayhunter on the Wingtech CT2MHS01")?, @@ -163,6 +182,8 @@ async fn run() -> Result<(), Error> { } } UtilSubCommand::Shell(_) => orbic::shell().await.context("\nFailed to open shell on Orbic RC400L")?, + UtilSubCommand::TmobileStartTelnet(args) => tmobile::start_telnet(&args.admin_ip, &args.admin_password).await.context("\nFailed to start telnet on the Tmobile TMOHS1")?, + UtilSubCommand::TmobileStartAdb(args) => tmobile::start_adb(&args.admin_ip, &args.admin_password).await.context("\nFailed to start adb on the Tmobile TMOHS1")?, UtilSubCommand::TplinkStartTelnet(options) => { tplink::start_telnet(&options.admin_ip).await?; } diff --git a/installer/src/tmobile.rs b/installer/src/tmobile.rs new file mode 100644 index 0000000..ee8bcae --- /dev/null +++ b/installer/src/tmobile.rs @@ -0,0 +1,187 @@ +/// Installer for the TMobile TMOHS1 hotspot. +/// +/// Tested on (from `/etc/wt_version`): +/// WT_INNER_VERSION=SW_Q89527AA1_V045_M11_TMO_USR_MP +/// WT_PRODUCTION_VERSION=TMOHS1_00.05.20 +/// WT_HARDWARE_VERSION=89527_1_11 +use std::io::Write; +use std::net::SocketAddr; +use std::str::FromStr; +use std::time::Duration; + +use aes::Aes128; +use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray}; +use anyhow::{Result, bail}; +use base64_light::base64_encode_bytes; +use block_padding::{Padding, Pkcs7}; +use reqwest::Client; +use tokio::time::sleep; + +use crate::TmobileArgs as Args; +use crate::util::{echo, telnet_send_command, telnet_send_file}; + +pub async fn install( + Args { + admin_ip, + admin_password, + }: Args, +) -> Result<()> { + run_install(admin_ip, admin_password).await +} + +const KEY: &[u8] = b"abcdefghijklmn12"; + +/// Returns password encrypted in AES128 ECB mode with the key b"abcdefghijklmn12", +/// with Pkcs7 padding, encoded in base64. +fn encrypt_password(password: &[u8]) -> Result { + let c = Aes128::new_from_slice(KEY)?; + let mut b = GenericArray::from([0u8; 16]); + b[..password.len()].copy_from_slice(password); + Pkcs7::pad(&mut b, password.len()); + c.encrypt_block(&mut b); + Ok(base64_encode_bytes(&b)) +} + +pub async fn start_telnet(admin_ip: &str, admin_password: &str) -> Result<()> { + run_command(admin_ip, admin_password, "busybox telnetd -l /bin/sh").await +} + +pub async fn start_adb(admin_ip: &str, admin_password: &str) -> Result<()> { + run_command(admin_ip, admin_password, "/sbin/usb/compositions/9025").await +} + +async fn run_command(admin_ip: &str, admin_password: &str, cmd: &str) -> Result<()> { + let qcmap_auth_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_auth"); + let qcmap_web_cgi_endpoint = format!("http://{admin_ip}/cgi-bin/qcmap_web_cgi"); + + let encrypted_pw = encrypt_password(admin_password.as_bytes()).ok().unwrap(); + + let client = Client::new(); + let login = client + .post(&qcmap_auth_endpoint) + .body(format!( + "type=login&pwd={encrypted_pw}&timeout=60000&user=admin" + )) + .send() + .await? + .text() + .await?; + let token = match login.find("token") { + Some(n) => &login[n + 8..n + 8 + 16], + None => bail!("login did not return a token in response: {}", login), + }; + + let telnet = client.post(&qcmap_web_cgi_endpoint) + .body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05||{cmd}&key=50:5A:CA:B5:05:AC&token={token}")) + .send() + .await?; + if telnet.status() != 200 { + bail!( + "starting telnet failed with status code: {:?}", + telnet.status() + ); + } + + Ok(()) +} + +async fn run_install(admin_ip: String, admin_password: String) -> Result<()> { + echo!("Starting telnet ... "); + start_telnet(&admin_ip, &admin_password).await?; + println!("ok"); + + echo!("Connecting via telnet to {admin_ip} ... "); + let addr = SocketAddr::from_str(&format!("{admin_ip}:23")).unwrap(); + telnet_send_command(addr, "mkdir -p /data/rayhunter", "exit code 0").await?; + println!("ok"); + + telnet_send_command(addr, "mount -o remount,rw /", "exit code 0").await?; + + telnet_send_file( + addr, + "/data/rayhunter/config.toml", + crate::CONFIG_TOML.as_bytes(), + ) + .await?; + + let rayhunter_daemon_bin = include_bytes!(env!("FILE_RAYHUNTER_DAEMON_TMOBILE")); + telnet_send_file( + addr, + "/data/rayhunter/rayhunter-daemon", + rayhunter_daemon_bin, + ) + .await?; + telnet_send_command( + addr, + "chmod 755 /data/rayhunter/rayhunter-daemon", + "exit code 0", + ) + .await?; + telnet_send_file( + addr, + "/etc/init.d/misc-daemon", + include_bytes!("../../dist/scripts/misc-daemon"), + ) + .await?; + telnet_send_command(addr, "chmod 755 /etc/init.d/misc-daemon", "exit code 0").await?; + telnet_send_file( + addr, + "/etc/init.d/rayhunter_daemon", + crate::RAYHUNTER_DAEMON_INIT.as_bytes(), + ) + .await?; + telnet_send_command( + addr, + "chmod 755 /etc/init.d/rayhunter_daemon", + "exit code 0", + ) + .await?; + + println!("Rebooting device and waiting 30 seconds for it to start up."); + telnet_send_command(addr, "reboot", "exit code 0").await?; + sleep(Duration::from_secs(30)).await; + + echo!("Testing rayhunter ... "); + let max_failures = 10; + http_ok_every( + format!("http://{admin_ip}:8080/index.html"), + Duration::from_secs(3), + max_failures, + ) + .await?; + println!("ok"); + println!("rayhunter is running at http://{admin_ip}:8080"); + + Ok(()) +} + +async fn http_ok_every(rayhunter_url: String, interval: Duration, max_failures: u32) -> Result<()> { + let client = Client::new(); + let mut failures = 0; + loop { + match client.get(&rayhunter_url).send().await { + Ok(test) => match test.status().is_success() { + true => break, + false => bail!( + "request for url ({rayhunter_url}) failed with status code: {:?}", + test.status() + ), + }, + Err(e) => match failures > max_failures { + true => return Err(e.into()), + false => failures += 1, + }, + } + sleep(interval).await; + } + + Ok(()) +} + +#[test] +fn test_encrypt_password() { + let p = b"80536913"; + let s = encrypt_password(p).ok(); + let expected = Some("5brvd8xl732cSoFTAy67ig==".to_string()); + assert_eq!(s, expected); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 496cdb7..8861136 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,6 +12,7 @@ path = "src/lib.rs" [features] default = [] orbic = [] +tmobile = [] tplink = [] wingtech = []