mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-05-30 10:19:26 -07:00
feat: support Tmobile TMOHS1 hotspot
Add support for the Tmobile TMOHS1, a Wingtech CT2MHS01-based hotspot with a Qualcomm mdm9607. The TMOHS1 has no screen, only 5 LEDs, two of which are RGB.
This commit is contained in:
committed by
Cooper Quintin
parent
f23cc07652
commit
b7636386fc
@@ -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"]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
85
daemon/src/display/tmobile.rs
Normal file
85
daemon/src/display/tmobile.rs
Normal file
@@ -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<DisplayState>,
|
||||
) {
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
187
installer/src/tmobile.rs
Normal file
187
installer/src/tmobile.rs
Normal file
@@ -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<String> {
|
||||
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);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
orbic = []
|
||||
tmobile = []
|
||||
tplink = []
|
||||
wingtech = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user