diff --git a/Cargo.lock b/Cargo.lock index 21bff5f..f8ca5db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -350,6 +361,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64_light" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6aca08f76b8485947a20a1b3096e5a8cd6edbcecc6d2a8932df9b41d36aadf" + [[package]] name = "base64ct" version = "1.7.3" @@ -411,6 +428,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "built" version = "0.7.7" @@ -504,6 +530,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.38" @@ -1431,13 +1467,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "installer" version = "0.3.4" dependencies = [ "adb_client", + "aes", "anyhow", "axum", + "base64_light", + "block-padding", "bytes", "clap", "env_logger 0.11.8", diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 7f2758c..41109e7 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # These feature flags are mutually exclusive, and exactly one must be enabled. orbic = ["rayhunter/orbic"] tplink = ["rayhunter/tplink"] +wingtech = ["rayhunter/wingtech"] default = ["orbic"] diff --git a/bin/src/display/mod.rs b/bin/src/display/mod.rs index 124ef71..3708305 100644 --- a/bin/src/display/mod.rs +++ b/bin/src/display/mod.rs @@ -15,6 +15,11 @@ mod orbic; #[cfg(feature = "orbic")] pub use orbic::update_ui; +#[cfg(feature = "wingtech")] +mod wingtech; +#[cfg(feature = "wingtech")] +pub use wingtech::update_ui; + pub enum DisplayState { Recording, Paused, @@ -24,5 +29,8 @@ pub enum DisplayState { #[cfg(all(feature = "orbic", feature = "tplink"))] compile_error!("cannot compile for many devices at once"); -#[cfg(not(any(feature = "orbic", feature = "tplink")))] +#[cfg(all(feature = "orbic", feature = "wingtech"))] +compile_error!("cannot compile for many devices at once"); + +#[cfg(not(any(feature = "orbic", feature = "tplink", feature = "wingtech",)))] compile_error!("cannot compile for no device at all"); diff --git a/bin/src/display/wingtech.rs b/bin/src/display/wingtech.rs new file mode 100644 index 0000000..be228cb --- /dev/null +++ b/bin/src/display/wingtech.rs @@ -0,0 +1,54 @@ +/// Display support for the Wingtech CT2MHS01 hotspot. +/// +/// Tested on (from `/etc/wt_version`): +/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP +/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55 +/// WT_HARDWARE_VERSION=89323_1_20 +use crate::config; +use crate::display::generic_framebuffer::{self, Dimensions, GenericFramebuffer}; +use crate::display::DisplayState; + +use tokio::sync::mpsc::Receiver; +use tokio::sync::oneshot; +use tokio_util::task::TaskTracker; + +const FB_PATH: &str = "/dev/fb0"; + +#[derive(Copy, Clone, Default)] +struct Framebuffer; + +impl GenericFramebuffer for Framebuffer { + fn dimensions(&self) -> Dimensions { + Dimensions { + height: 128, + width: 160, + } + } + + fn write_buffer(&mut self, buffer: &[(u8, u8, u8)]) { + let mut raw_buffer = Vec::new(); + for (r, g, b) in buffer { + let mut rgb565: u16 = (*r as u16 & 0b11111000) << 8; + rgb565 |= (*g as u16 & 0b11111100) << 3; + rgb565 |= (*b as u16) >> 3; + raw_buffer.extend(rgb565.to_le_bytes()); + } + + std::fs::write(FB_PATH, &raw_buffer).unwrap(); + } +} + +pub fn update_ui( + task_tracker: &TaskTracker, + config: &config::Config, + ui_shutdown_rx: oneshot::Receiver<()>, + ui_update_rx: Receiver, +) { + generic_framebuffer::update_ui( + task_tracker, + config, + Framebuffer, + ui_shutdown_rx, + ui_update_rx, + ) +} diff --git a/installer/Cargo.toml b/installer/Cargo.toml index cfe747b..8b6ee3e 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -4,8 +4,11 @@ version = "0.3.4" edition = "2024" [dependencies] +aes = "0.8.4" anyhow = "1.0.98" axum = "0.8.3" +base64_light = "0.1.5" +block-padding = "0.3.3" bytes = "1.10.1" clap = { version = "4.5.37", features = ["derive"] } env_logger = "0.11.8" diff --git a/installer/build.rs b/installer/build.rs index 55b271b..9bb50e8 100644 --- a/installer/build.rs +++ b/installer/build.rs @@ -19,6 +19,11 @@ fn main() { "FILE_RAYHUNTER_DAEMON_TPLINK", "rayhunter-daemon", ); + set_binary_var( + &include_dir, + "FILE_RAYHUNTER_DAEMON_WINGTECH", + "rayhunter-daemon", + ); } fn set_binary_var(include_dir: &Path, var: &str, file: &str) { diff --git a/installer/src/main.rs b/installer/src/main.rs index 9de31e1..9cbbf86 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -4,6 +4,7 @@ use env_logger::Env; mod orbic; mod tplink; +mod wingtech; pub static CONFIG_TOML: &str = include_str!("../../dist/config.toml.example"); pub static RAYHUNTER_DAEMON_INIT: &str = include_str!("../../dist/scripts/rayhunter_daemon"); @@ -21,6 +22,8 @@ enum Command { Orbic(InstallOrbic), /// Install rayhunter on the TP-Link M7350. Tplink(InstallTpLink), + /// Install rayhunter on the Wingtech CT2MHS01. + Wingtech(InstallWingtech), /// Developer utilities. Util(Util), } @@ -51,6 +54,17 @@ struct InstallTpLink { #[derive(Parser, Debug)] struct InstallOrbic {} +#[derive(Parser, Debug)] +struct InstallWingtech { + /// IP address for Wingtech admin interface, if custom. + #[arg(long, default_value = "192.168.1.1")] + admin_ip: String, + + /// Web portal admin password. + #[arg(long)] + admin_password: String, +} + #[derive(Parser, Debug)] struct Util { #[command(subcommand)] @@ -91,6 +105,7 @@ async fn run() -> Result<(), Error> { match command { 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")?, Command::Util(subcommand) => match subcommand.command { UtilSubCommand::Serial(serial_cmd) => { if serial_cmd.root { diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index 466fcb9..566d909 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -46,6 +46,7 @@ macro_rules! echo { let _ = std::io::stdout().flush(); }; } +pub(crate) use echo; pub async fn install() -> Result<()> { let mut adb_device = force_debug_mode().await?; diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index ac6c684..60c844a 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -236,7 +236,7 @@ async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> R Ok(()) } -async fn telnet_send_command( +pub async fn telnet_send_command( addr: SocketAddr, command: &str, expected_output: &str, diff --git a/installer/src/wingtech.rs b/installer/src/wingtech.rs new file mode 100644 index 0000000..35aa2b1 --- /dev/null +++ b/installer/src/wingtech.rs @@ -0,0 +1,207 @@ +/// Installer for the Wingtech CT2MHS01 hotspot. +/// +/// Tested on (from `/etc/wt_version`): +/// WT_INNER_VERSION=SW_Q89323AA1_V057_M10_CRICKET_USR_MP +/// WT_PRODUCTION_VERSION=CT2MHS01_0.04.55 +/// WT_HARDWARE_VERSION=89323_1_20 +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::io::AsyncWriteExt; +use tokio::net::TcpStream; +use tokio::time::sleep; + +use crate::InstallWingtech as Args; +use crate::orbic::echo; +use crate::tplink::telnet_send_command; + +pub async fn install( + Args { + admin_ip, + admin_password, + }: Args, +) -> Result<()> { + wingtech_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 { + 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 cmd = "busybox telnetd -l /bin/sh"; + let telnet = client.post(&qcmap_web_cgi_endpoint) + .body(format!("page=setFWMacFilter&cmd=add&mode=0&mac=50:5A:CA:B5:05:AC||{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(true) +} + +async fn wingtech_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(); + println!("ok"); + + telnet_send_command(addr, "mkdir -p /data/rayhunter", "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_WINGTECH")); + 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/rayhunter_daemon", + crate::RAYHUNTER_DAEMON_INIT.as_bytes(), + ) + .await?; + telnet_send_command( + addr, + "chmod 755 /etc/init.d/rayhunter_daemon", + "exit code 0", + ) + .await?; + telnet_send_command(addr, "update-rc.d rayhunter_daemon defaults", "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... "); + const MAX_FAILURES: u32 = 10; + let mut failures = 0; + let rayhunter_url = format!("http://{admin_ip}:8080/index.html"); + let client = Client::new(); + loop { + match client.get(&rayhunter_url).send().await { + Ok(test) => { + if test.status() == 200 { + println!("rayhunter is running at http://{admin_ip}:8080"); + return Ok(()); + } else { + bail!( + "request for url ({rayhunter_url}) failed with status code: {:?}", + test.status() + ); + } + } + Err(e) => { + if failures > MAX_FAILURES { + return Err(e.into()); + } else { + failures += 1; + sleep(Duration::from_secs(3)).await; + } + } + } + } +} + +async fn telnet_send_file(addr: SocketAddr, filename: &str, payload: &[u8]) -> Result<()> { + println!("Sending file {filename}"); + + { + let filename = filename.to_owned(); + let handle = tokio::spawn(async move { + telnet_send_command(addr, &format!("nc -l -p 8081 >{filename}.tmp"), "").await + }); + + sleep(Duration::from_millis(100)).await; + + let mut addr = addr; + addr.set_port(8081); + let mut stream = TcpStream::connect(addr).await?; + stream.write_all(payload).await?; + + handle.await??; + } + + let checksum = md5::compute(payload); + + telnet_send_command( + addr, + &format!("md5sum {filename}.tmp"), + &format!("{checksum:x} {filename}.tmp"), + ) + .await?; + + telnet_send_command( + addr, + &format!("mv {filename}.tmp {filename}"), + "exit code 0", + ) + .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 6d18d96..24cbdc3 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -13,6 +13,7 @@ path = "src/lib.rs" default = [] orbic = [] tplink = [] +wingtech = [] [dependencies] bytes = "1.5.0"