diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2c031cf..647fa11 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -109,7 +109,7 @@ jobs: - run: cargo build --bin installer --release --target ${{ matrix.platform.target }} --features vendor - uses: actions/upload-artifact@v4 with: - name: tplink-installer-${{ matrix.platform.name }} + name: installer-${{ matrix.platform.name }} path: target/${{ matrix.platform.target }}/release/installer${{ matrix.platform.os == 'windows-latest' && '.exe' || '' }} if-no-files-found: error @@ -118,14 +118,15 @@ jobs: - build_rayhunter_check - build_rootshell - build_rayhunter + - build_rust_installer runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 - name: Fix executable permissions on binaries - run: chmod +x serial-*/serial rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon + run: chmod +x installer-*/installer rayhunter-check-*/rayhunter-check rayhunter-daemon-*/rayhunter-daemon - name: Setup release directory - run: mv rayhunter-daemon-* rootshell/rootshell serial-* dist + run: mv rayhunter-daemon-* rootshell/rootshell installer-* dist - name: Archive release directory run: tar -cvf release.tar -C dist . # TODO: have this create a release directly diff --git a/.github/workflows/check-and-test.yml b/.github/workflows/check-and-test.yml index c79f706..d7ea876 100644 --- a/.github/workflows/check-and-test.yml +++ b/.github/workflows/check-and-test.yml @@ -37,3 +37,17 @@ jobs: - name: Run clippy run: cargo clippy --verbose --no-default-features --features=${{ matrix.device.name }} + windows_installer_check_and_test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: cargo check + shell: bash + run: | + cd installer + cargo check --verbose + - name: cargo test + shell: bash + run: | + cd installer + cargo test --verbose --no-default-features --features=${{ matrix.device.name }} diff --git a/Cargo.toml b/Cargo.toml index 776a869..204db37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ members = [ "lib", "bin", - "serial", "rootshell", - "telcom-parser", "installer", + "telcom-parser", + "installer", ] resolver = "2" diff --git a/installer/src/main.rs b/installer/src/main.rs index 5e93001..effa13b 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Error}; use clap::{Parser, Subcommand}; +mod orbic; mod tplink; pub static CONFIG_TOML: &[u8] = include_bytes!("../../dist/config.toml.example"); @@ -15,6 +16,8 @@ struct Args { #[derive(Subcommand, Debug)] enum Command { + /// Install rayhunter on the Orbic Orbic RC400L. + InstallOrbic(InstallOrbic), /// Install rayhunter on the TP-Link M7350. InstallTplink(InstallTpLink), } @@ -39,13 +42,24 @@ struct InstallTpLink { admin_ip: String, } -#[tokio::main] -async fn main() -> Result<(), Error> { +#[derive(Parser, Debug)] +struct InstallOrbic {} + +async fn run_function() -> Result<(), Error> { let Args { command } = Args::parse(); match command { Command::InstallTplink(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. Currently only Hardware Revision v3 is supported.")?, + Command::InstallOrbic(_) => orbic::install().await.context("Failed to install rayhunter on the Orbic RC400L")?, } Ok(()) } + +#[tokio::main] +async fn main() { + if let Err(e) = run_function().await { + eprintln!("{e:?}"); + std::process::exit(1); + } +} diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index 812a499..b2c9103 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -1,40 +1,304 @@ -//! Serial communication with the orbic device -//! -//! This binary has two main functions, putting the orbic device in update mode which enables ADB -//! and running AT commands on the serial modem interface which can be used to upload a shell and chown it to root -//! -//! # Errors -//! -//! No device found - make sure your device is plugged in and turned on. If it is, it's possible you have a device with a different -//! usb id, file a bug with the output of `lsusb` attached. -use std::str; +use std::io::{ErrorKind, Write}; +use std::path::Path; use std::time::Duration; -use anyhow::{bail, Context, Result}; +use adb_client::{ADBDeviceExt, ADBUSBDevice, RustADBError}; +use anyhow::{Context, Result, anyhow, bail}; +use nusb::hotplug::HotplugEvent; use nusb::transfer::{Control, ControlType, Recipient, RequestBuffer}; use nusb::{Device, Interface}; +use sha2::{Digest, Sha256}; +use tokio::time::sleep; +use tokio_stream::StreamExt; + +use crate::{CONFIG_TOML, RAYHUNTER_DAEMON_INIT}; const ORBIC_NOT_FOUND: &str = r#"No Orbic device found. Make sure your device is plugged in and turned on. -If it's possible you have a device with a different usb id: -please file a bug with the output of `lsusb` attached."#; +If you're sure you've plugged in an Orbic device via USB, there may be a bug in +our installer. Please file a bug with the output of `lsusb` attached."#; -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<()> { - let args: Vec = std::env::args().collect(); +const ORBIC_BUSY: &str = r#"The Orbic is plugged in but is being used by another program. - if args.len() != 2 || args[1] == "-h" || args[1] == "--help" { - println!("usage: {0} [ | --root]", args[0]); - std::process::exit(1); +Please close any program that might be using your USB devices. +If you have adb installed you may need to kill the adb daemon"#; + +const VENDOR_ID: u16 = 0x05c6; +const PRODUCT_ID: u16 = 0xf601; + +macro_rules! echo { + ($($arg:tt)*) => { + print!($($arg)*); + let _ = std::io::stdout().flush(); + }; +} + +pub async fn install() -> Result<()> { + let mut adb_device = force_debug_mode().await?; + let serial_interface = open_orbic()?.ok_or_else(|| anyhow!(ORBIC_NOT_FOUND))?; + echo!("Installing rootshell... "); + setup_rootshell(&serial_interface, &mut adb_device).await?; + println!("done"); + echo!("Installing rayhunter... "); + let mut adb_device = setup_rayhunter(&serial_interface, adb_device).await?; + println!("done"); + echo!("Testing rayhunter... "); + test_rayhunter(&mut adb_device).await?; + println!("done"); + Ok(()) +} + +async fn force_debug_mode() -> Result { + println!("Forcing a switch into the debug mode to enable ADB"); + enable_command_mode()?; + echo!("ADB enabled, waiting for reboot... "); + let mut adb_device = wait_for_adb_shell().await?; + println!("it's alive!"); + echo!("Waiting for atfwd_daemon to startup... "); + adb_command(&mut adb_device, &["pgrep", "atfwd_daemon"])?; + println!("done"); + Ok(adb_device) +} + +async fn setup_rootshell( + serial_interface: &Interface, + adb_device: &mut ADBUSBDevice, +) -> Result<()> { + #[cfg(feature = "vendor")] + let rootshell_bin = include_bytes!("../../rootshell/rootshell"); + + #[cfg(not(feature = "vendor"))] + let rootshell_bin = &tokio::fs::read("target/armv7-unknown-linux-musleabihf/release/rootshell") + .await + .context("Error reading rootshell from local file system")?; + + install_file( + serial_interface, + adb_device, + "/bin/rootshell", + rootshell_bin, + ) + .await?; + tokio::time::sleep(Duration::from_secs(1)).await; + at_syscmd(serial_interface, "chown root /bin/rootshell").await?; + tokio::time::sleep(Duration::from_secs(1)).await; + at_syscmd(serial_interface, "chmod 4755 /bin/rootshell").await?; + let output = adb_command(adb_device, &["/bin/rootshell", "-c", "id"])?; + if !output.contains("uid=0") { + bail!("rootshell is not giving us root."); } + Ok(()) +} - if args[1] == "--root" { - enable_command_mode() - } else { - match open_orbic()? { - Some(interface) => send_command(interface, &args[1]).await, - None => bail!(ORBIC_NOT_FOUND), +async fn setup_rayhunter( + serial_interface: &Interface, + mut adb_device: ADBUSBDevice, +) -> Result { + #[cfg(feature = "vendor")] + let rayhunter_daemon_bin = include_bytes!("../../rayhunter-daemon-orbic/rayhunter-daemon"); + + #[cfg(not(feature = "vendor"))] + let rayhunter_daemon_bin = + &tokio::fs::read("target/armv7-unknown-linux-musleabihf/release/rayhunter-daemon") + .await + .context("Error reading rayhunter-daemon from local file system")?; + + at_syscmd(serial_interface, "mkdir -p /data/rayhunter").await?; + install_file( + serial_interface, + &mut adb_device, + "/data/rayhunter/rayhunter-daemon", + rayhunter_daemon_bin, + ) + .await?; + install_file( + serial_interface, + &mut adb_device, + "/data/rayhunter/config.toml", + CONFIG_TOML, + ) + .await?; + install_file( + serial_interface, + &mut adb_device, + "/etc/init.d/rayhunter_daemon", + RAYHUNTER_DAEMON_INIT, + ) + .await?; + install_file( + serial_interface, + &mut adb_device, + "/etc/init.d/misc-daemon", + include_bytes!("../../dist/scripts/misc-daemon"), + ) + .await?; + at_syscmd(serial_interface, "chmod 755 /etc/init.d/rayhunter_daemon").await?; + at_syscmd(serial_interface, "chmod 755 /etc/init.d/misc-daemon").await?; + println!("done"); + echo!("Waiting for reboot... "); + at_syscmd(serial_interface, "shutdown -r -t 1 now").await?; + // first wait for shutdown (it can take ~10s) + tokio::time::timeout(Duration::from_secs(30), async { + while let Ok(dev) = adb_echo_test(adb_device).await { + adb_device = dev; + sleep(Duration::from_secs(1)).await; + } + }) + .await + .context("Orbic took too long to shutdown")?; + // now wait for boot to finish + let adb_device = wait_for_adb_shell().await?; + Ok(adb_device) +} + +async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { + const MAX_FAILURES: u32 = 10; + let mut failures = 0; + while failures < MAX_FAILURES { + if let Ok(output) = adb_command( + adb_device, + &["wget", "-O", "-", "http://localhost:8080/index.html"], + ) { + if output.contains("html") { + return Ok(()); + } + } + failures += 1; + sleep(Duration::from_secs(3)).await; + } + bail!("timeout reached! failed to reach rayhunter, something went wrong :(") +} + +async fn install_file( + serial_interface: &Interface, + adb_device: &mut ADBUSBDevice, + dest: &str, + payload: &[u8], +) -> Result<()> { + const MAX_FAILURES: u32 = 5; + let mut failures = 0; + loop { + match install_file_impl(serial_interface, adb_device, dest, payload).await { + Ok(()) => return Ok(()), + Err(e) => { + if failures > MAX_FAILURES { + return Err(e); + } else { + sleep(Duration::from_secs(1)).await; + failures += 1; + } + } + } + } +} + +async fn install_file_impl( + serial_interface: &Interface, + adb_device: &mut ADBUSBDevice, + dest: &str, + payload: &[u8], +) -> Result<()> { + 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!("/tmp/{file_name}"); + let mut hasher = Sha256::new(); + hasher.update(payload); + let file_hash_bytes = hasher.finalize(); + let file_hash = format!("{file_hash_bytes:x}"); + #[allow(clippy::useless_asref)] + adb_device.push(&mut payload.as_ref(), &push_tmp_path)?; + at_syscmd(serial_interface, &format!("mv {push_tmp_path} {dest}")).await?; + let file_info = adb_device + .stat(dest) + .context("Failed to stat transfered file")?; + if file_info.file_size == 0 { + bail!("File transfer unseccessful\nFile is empty"); + } + let ouput = adb_command(adb_device, &["sha256sum", dest])?; + if !ouput.contains(&file_hash) { + bail!("File transfer unseccessful\nBad hash expected {file_hash} got {ouput}"); + } + Ok(()) +} + +fn adb_command(adb_device: &mut ADBUSBDevice, command: &[&str]) -> Result { + let mut buf = Vec::::new(); + adb_device.shell_command(command, &mut buf)?; + Ok(String::from_utf8_lossy(&buf).into_owned()) +} + +async fn wait_for_adb_shell() -> Result { + const MAX_FAILURES: u32 = 10; + let mut failures = 0; + loop { + match ADBUSBDevice::new(VENDOR_ID, PRODUCT_ID) { + Ok(dev) => match adb_echo_test(dev).await { + Ok(dev) => return Ok(dev), + Err(e) => { + if failures > MAX_FAILURES { + return Err(e); + } else { + sleep(Duration::from_secs(1)).await; + failures += 1; + } + } + }, + Err(RustADBError::IOError(e)) if e.kind() == ErrorKind::ResourceBusy => { + bail!(ORBIC_BUSY); + } + Err(RustADBError::DeviceNotFound(_)) => { + wait_for_usb_device(VENDOR_ID, PRODUCT_ID).await?; + } + Err(e) => { + if failures > MAX_FAILURES { + return Err(e.into()); + } else { + sleep(Duration::from_secs(1)).await; + failures += 1; + } + } + } + } +} + +async fn adb_echo_test(mut adb_device: ADBUSBDevice) -> Result { + let mut buf = Vec::::new(); + // Random string to echo + let test_echo = "qwertyzxcvbnm"; + let thread = std::thread::spawn(move || { + // This call to run a shell command is run on a separate thread because it can block + // indefinitely until the command runs, which is undesirable. + adb_device.shell_command(&["echo", test_echo], &mut buf)?; + Ok::<(ADBUSBDevice, Vec), RustADBError>((adb_device, buf)) + }); + sleep(Duration::from_secs(1)).await; + if thread.is_finished() { + if let Ok(Ok((dev, buf))) = thread.join() { + if let Ok(s) = std::str::from_utf8(&buf) { + if s.contains(test_echo) { + return Ok(dev); + } + } + } + } + // I'd like to kill the background thread here if that was possible. + bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting."); +} + +async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> { + loop { + let mut watcher = nusb::watch_devices()?; + while let Some(event) = watcher.next().await { + if let HotplugEvent::Connected(dev) = event { + if dev.vendor_id() == vendor_id && dev.product_id() == product_id { + return Ok(()); + } + } } } } @@ -42,13 +306,13 @@ async fn main() -> Result<()> { /// Sends an AT command to the usb device over the serial port /// /// First establish a USB handle and context by calling `open_orbic() -async fn send_command(interface: Interface, command: &str) -> Result<()> { +async fn at_syscmd(interface: &Interface, command: &str) -> Result<()> { let mut data = String::new(); data.push_str("\r\n"); - data.push_str(command); + data.push_str(&format!("AT+SYSCMD={command}")); data.push_str("\r\n"); - let timeout = Duration::from_secs(1); + let timeout = Duration::from_secs(2); let enable_serial_port = Control { control_type: ControlType::Class, @@ -89,8 +353,7 @@ async fn send_command(interface: Interface, command: &str) -> Result<()> { // the garbage with `from_utf8_lossy` and look for our expected success string. let responsestr = String::from_utf8_lossy(&response); if !responsestr.contains("\r\nOK\r\n") { - println!("Received unexpected response: {0}", responsestr); - std::process::exit(1); + bail!("Received unexpected response: {0}", responsestr); } Ok(()) @@ -107,7 +370,7 @@ fn enable_command_mode() -> Result<()> { let timeout = Duration::from_secs(1); - if let Some(device) = open_device(0x05c6, 0xf626)? { + if let Some(device) = open_usb_device(VENDOR_ID, 0xf626)? { let enable_command_mode = Control { control_type: ControlType::Vendor, recipient: Recipient::Device, @@ -135,7 +398,7 @@ fn enable_command_mode() -> Result<()> { /// Get an Interface for the orbic device fn open_orbic() -> Result> { // Device after initial mode switch - if let Some(device) = open_device(0x05c6, 0xf601)? { + if let Some(device) = open_usb_device(VENDOR_ID, PRODUCT_ID)? { let interface = device .detach_and_claim_interface(1) // will reattach drivers on release .context("detach_and_claim_interface(1) failed")?; @@ -143,7 +406,7 @@ fn open_orbic() -> Result> { } // Device with rndis enabled as well - if let Some(device) = open_device(0x05c6, 0xf622)? { + if let Some(device) = open_usb_device(VENDOR_ID, 0xf622)? { let interface = device .detach_and_claim_interface(1) // will reattach drivers on release .context("detach_and_claim_interface(1) failed")?; @@ -154,7 +417,7 @@ fn open_orbic() -> Result> { } /// General function to open a USB device -fn open_device(vid: u16, pid: u16) -> Result> { +fn open_usb_device(vid: u16, pid: u16) -> Result> { let devices = match nusb::list_devices() { Ok(d) => d, Err(_) => return Ok(None),