From e82ba39c5501cefe4da0850e6d2ca40df1c5944f Mon Sep 17 00:00:00 2001 From: LIAUD Corentin Date: Fri, 25 Oct 2024 18:01:25 +0200 Subject: [PATCH] feat: add adb push over USB --- Cargo.toml | 4 +- README.md | 19 ++- adb_cli/README.md | 25 ++++ adb_cli/src/commands/usb.rs | 2 + adb_cli/src/main.rs | 15 ++- adb_client/README.md | 55 ++++++-- adb_client/src/adb_device_ext.rs | 3 + .../src/server/adb_server_device_commands.rs | 4 + adb_client/src/server/device_commands/send.rs | 2 +- adb_client/src/usb/adb_usb_device.rs | 127 +++++++++++++++--- adb_client/src/usb/adb_usb_device_commands.rs | 41 +++++- 11 files changed, 249 insertions(+), 48 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6818113..6031fe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,12 @@ resolver = "2" [workspace.package] edition = "2021" +keywords = ["adb", "android", "tcp", "usb"] license = "MIT" repository = "https://github.com/cocool97/adb_client" version = "1.0.7" -keywords = ["adb", "android"] -# To build locally when working on a new version +# To build locally when working on a new release [patch.crates-io] adb_client = { path = "./adb_client" } diff --git a/README.md b/README.md index e06f65e..a35cf55 100644 --- a/README.md +++ b/README.md @@ -14,31 +14,30 @@

-Main features : +Main features of this library: -- Full Rust, no need to use `adb *` shell commands -- Currently only support server TCP/IP protocol +- Full Rust, don't use `adb *` shell commands to interact with devices +- Supports: + - **TCP/IP** protocol, using ADB server as a proxy (standard behavior when using `adb` CLI) + - **USB** protocol, interacting directly with end devices +- Implements hidden `adb` features, like `framebuffer` - Highly configurable - Easy to use ! ## adb_client -Rust library implementing ADB protocol and providing high-level abstraction over commands. +Rust library implementing both ADB protocols and providing a high-level abstraction over many supported commands. Improved documentation [here](./adb_client/README.md). ## adb_cli -Rust binary providing an improved version of `adb` CLI, using `adb_client` library. Can be used as an usage example of the library. +Rust binary providing an improved version of official `adb` CLI, wrapping `adb_client` library. Can act as an usage example of the library. Improved documentation [here](./adb_cli/README.md). -## Missing features - -- USB protocol (Work in progress) - ## Related publications - [Diving into ADB protocol internals (1/2)](https://www.synacktiv.com/publications/diving-into-adb-protocol-internals-12) -All pull requests are welcome ! +Some features may still be missing, all pull requests are welcome ! diff --git a/adb_cli/README.md b/adb_cli/README.md index c30201c..29ba48f 100644 --- a/adb_cli/README.md +++ b/adb_cli/README.md @@ -15,6 +15,8 @@ cargo install adb_cli Usage is quite simple, and tends to look like `adb`: +- To use ADB server as a proxy: + ```bash user@laptop ~/adb_client (main)> adb_cli --help Rust ADB (Android Debug Bridge) CLI @@ -49,3 +51,26 @@ Options: -h, --help Print help -V, --version Print version ``` + +- To interact directly with end devices + +```bash +user@laptop ~/adb_client (main)> adb_cli usb --help +Device commands via USB, no server needed + +Usage: adb_cli usb [OPTIONS] --vendor-id --product-id + +Commands: + shell Spawn an interactive shell or run a list of commands on the device + pull Pull a file from device + push Push a file on device + stat Stat a file on device + reboot Reboot the device + help Print this message or the help of the given subcommand(s) + +Options: + -v, --vendor-id Hexadecimal vendor id of this USB device + -p, --product-id Hexadecimal product id of this USB device + -k, --private-key Path to a custom private key to use for authentication + -h, --help Print help +``` \ No newline at end of file diff --git a/adb_cli/src/commands/usb.rs b/adb_cli/src/commands/usb.rs index 3c43365..9aff77d 100644 --- a/adb_cli/src/commands/usb.rs +++ b/adb_cli/src/commands/usb.rs @@ -30,6 +30,8 @@ pub enum UsbCommands { Shell { commands: Vec }, /// Pull a file from device Pull { source: String, destination: String }, + /// Push a file on device + Push { filename: String, path: String }, /// Stat a file on device Stat { path: String }, /// Reboot the device diff --git a/adb_cli/src/main.rs b/adb_cli/src/main.rs index a317767..0f21b92 100644 --- a/adb_cli/src/main.rs +++ b/adb_cli/src/main.rs @@ -36,7 +36,7 @@ fn main() -> Result<()> { } LocalCommand::Push { filename, path } => { let mut input = File::open(Path::new(&filename))?; - device.send(&mut input, &path)?; + device.push(&mut input, &path)?; log::info!("Uploaded {filename} to {path}"); } LocalCommand::List { path } => { @@ -158,8 +158,12 @@ fn main() -> Result<()> { } } Command::Usb(usb) => { - let mut device = - ADBUSBDevice::new(usb.vendor_id, usb.product_id, usb.path_to_private_key)?; + let mut device = match usb.path_to_private_key { + Some(pk) => { + ADBUSBDevice::new_with_custom_private_key(usb.vendor_id, usb.product_id, pk)? + } + None => ADBUSBDevice::new(usb.vendor_id, usb.product_id)?, + }; match usb.commands { UsbCommands::Shell { commands } => { @@ -197,6 +201,11 @@ fn main() -> Result<()> { log::info!("Reboots device in mode {:?}", reboot_type); device.reboot(reboot_type.into())? } + UsbCommands::Push { filename, path } => { + let mut input = File::open(Path::new(&filename))?; + device.push(&mut input, &path)?; + log::info!("Uploaded {filename} to {path}"); + } } } } diff --git a/adb_client/README.md b/adb_client/README.md index a44ed8d..f29bca5 100644 --- a/adb_client/README.md +++ b/adb_client/README.md @@ -17,16 +17,6 @@ adb_client = "*" ## Examples -### Launch a command on device via ADB server - -```rust no_run -use adb_client::{ADBServer, ADBDeviceExt}; - -let mut server = ADBServer::default(); -let mut device = server.get_device().expect("cannot get device"); -device.shell_command(["df", "-h"],std::io::stdout()); -``` - ### Get available ADB devices ```rust no_run @@ -41,7 +31,19 @@ let mut server = ADBServer::new(SocketAddrV4::new(server_ip, server_port)); server.devices(); ``` -### Push a file to the device +### Using ADB server as proxy + +#### [TCP] Launch a command on device + +```rust no_run +use adb_client::{ADBServer, ADBDeviceExt}; + +let mut server = ADBServer::default(); +let mut device = server.get_device().expect("cannot get device"); +device.shell_command(["df", "-h"],std::io::stdout()); +``` + +#### [TCP] Push a file to the device ```rust no_run use adb_client::ADBServer; @@ -51,6 +53,33 @@ use std::path::Path; let mut server = ADBServer::default(); let mut device = server.get_device().expect("cannot get device"); -let mut input = File::open(Path::new("/tmp")).expect("Cannot open file"); -device.send(&mut input, "/data/local/tmp"); +let mut input = File::open(Path::new("/tmp/f")).expect("Cannot open file"); +device.push(&mut input, "/data/local/tmp"); +``` + +### Interacting directly with device + +#### [USB] Launch a command on device + +```rust no_run +use adb_client::{ADBUSBDevice, ADBDeviceExt}; + +let vendor_id = 0x04e8; +let product_id = 0x6860; +let mut device = ADBUSBDevice::new(vendor_id, product_id).expect("cannot find device"); +device.shell_command(["df", "-h"],std::io::stdout()); +``` + +#### [USB] Push a file to the device + +```rust no_run +use adb_client::{ADBUSBDevice, ADBDeviceExt}; +use std::fs::File; +use std::path::Path; + +let vendor_id = 0x04e8; +let product_id = 0x6860; +let mut device = ADBUSBDevice::new(vendor_id, product_id).expect("cannot find device"); +let mut input = File::open(Path::new("/tmp/f")).expect("Cannot open file"); +device.push(&mut input, "/data/local/tmp"); ``` diff --git a/adb_client/src/adb_device_ext.rs b/adb_client/src/adb_device_ext.rs index 93d32bf..b392ced 100644 --- a/adb_client/src/adb_device_ext.rs +++ b/adb_client/src/adb_device_ext.rs @@ -23,6 +23,9 @@ pub trait ADBDeviceExt { /// Pull the remote file pointed to by [source] and write its contents into [`output`] fn pull, W: Write>(&mut self, source: A, output: W) -> Result<()>; + /// Push [stream] to [path] on the device. + fn push>(&mut self, stream: R, path: A) -> Result<()>; + /// Reboots the device using given reboot type fn reboot(&mut self, reboot_type: RebootType) -> Result<()>; } diff --git a/adb_client/src/server/adb_server_device_commands.rs b/adb_client/src/server/adb_server_device_commands.rs index de636e1..16e7796 100644 --- a/adb_client/src/server/adb_server_device_commands.rs +++ b/adb_client/src/server/adb_server_device_commands.rs @@ -117,4 +117,8 @@ impl ADBDeviceExt for ADBServerDevice { fn reboot(&mut self, reboot_type: crate::RebootType) -> Result<()> { self.reboot(reboot_type) } + + fn push>(&mut self, stream: R, path: A) -> Result<()> { + self.push(stream, path) + } } diff --git a/adb_client/src/server/device_commands/send.rs b/adb_client/src/server/device_commands/send.rs index a631897..2b7eff7 100644 --- a/adb_client/src/server/device_commands/send.rs +++ b/adb_client/src/server/device_commands/send.rs @@ -43,7 +43,7 @@ impl Write for ADBSendCommandWriter { impl ADBServerDevice { /// Send [stream] to [path] on the device. - pub fn send>(&mut self, stream: R, path: A) -> Result<()> { + pub fn push>(&mut self, stream: R, path: A) -> Result<()> { log::info!("Sending data to {}", path.as_ref()); let serial = self.identifier.clone(); self.connect()? diff --git a/adb_client/src/usb/adb_usb_device.rs b/adb_client/src/usb/adb_usb_device.rs index 6fd4cc8..49b5aeb 100644 --- a/adb_client/src/usb/adb_usb_device.rs +++ b/adb_client/src/usb/adb_usb_device.rs @@ -4,12 +4,14 @@ use std::fs::read_to_string; use std::io::Cursor; use std::io::Read; use std::io::Seek; +use std::path::Path; use std::path::PathBuf; use std::time::Duration; use byteorder::LittleEndian; use super::{ADBRsaKey, ADBUsbMessage}; +use crate::constants::BUFFER_SIZE; use crate::models::AdbStatResponse; use crate::usb::adb_usb_message::{AUTH_RSAPUBLICKEY, AUTH_SIGNATURE, AUTH_TOKEN}; use crate::{ @@ -24,16 +26,8 @@ pub struct ADBUSBDevice { pub(crate) transport: USBTransport, } -fn read_adb_private_key(private_key_path: Option) -> Result> { - let private_key = private_key_path - .or_else(|| { - homedir::my_home() - .ok()? - .map(|home| home.join(".android").join("adbkey")) - }) - .ok_or(RustADBError::NoHomeDirectory)?; - - read_to_string(&private_key) +fn read_adb_private_key>(private_key_path: P) -> Result> { + read_to_string(private_key_path.as_ref()) .map_err(RustADBError::from) .map(|pk| match ADBRsaKey::from_pkcs8(&pk) { Ok(pk) => Some(pk), @@ -46,7 +40,34 @@ fn read_adb_private_key(private_key_path: Option) -> Result) -> Result { + pub fn new(vendor_id: u16, product_id: u16) -> Result { + let private_key_path = homedir::my_home() + .ok() + .flatten() + .map(|home| home.join(".android").join("adbkey")) + .ok_or(RustADBError::NoHomeDirectory)?; + + let private_key = match read_adb_private_key(private_key_path)? { + Some(pk) => pk, + None => ADBRsaKey::random_with_size(2048)?, + }; + + let mut s = Self { + private_key, + transport: USBTransport::new(vendor_id, product_id), + }; + + s.connect()?; + + Ok(s) + } + + /// Instantiate a new [ADBUSBDevice] using a custom private key path + pub fn new_with_custom_private_key( + vendor_id: u16, + product_id: u16, + private_key_path: PathBuf, + ) -> Result { let private_key = match read_adb_private_key(private_key_path)? { Some(pk) => pk, None => ADBRsaKey::random_with_size(2048)?, @@ -156,14 +177,14 @@ impl ADBUSBDevice { } /// Expect a message with an `OKAY` command after sending a message. - /// Return the value if it conforms to the constraint or error out. pub(crate) fn send_and_expect_okay(&mut self, message: ADBUsbMessage) -> Result { self.transport.write_message(message)?; let message = self.transport.read_message()?; - if message.header().command() != USBCommand::Okay { + let received_command = message.header().command(); + if received_command != USBCommand::Okay { return Err(RustADBError::ADBRequestFailed(format!( "expected command OKAY after message, got {}", - message.header().command() + received_command ))); } Ok(message) @@ -220,8 +241,82 @@ impl ADBUSBDevice { Ok(()) } - pub(crate) fn begin_transaction(&mut self) -> Result<(u32, u32)> { - let sync_directive = "sync:.\0"; + pub(crate) fn push_file( + &mut self, + local_id: u32, + remote_id: u32, + mut reader: R, + ) -> std::result::Result<(), RustADBError> { + let mut buffer = [0; BUFFER_SIZE]; + let amount_read = reader.read(&mut buffer)?; + let subcommand_data = USBSubcommand::Data.with_arg(amount_read as u32); + + let mut serialized_message = + bincode::serialize(&subcommand_data).map_err(|_e| RustADBError::ConversionError)?; + serialized_message.append(&mut buffer[..amount_read].to_vec()); + + let message = + ADBUsbMessage::new(USBCommand::Write, local_id, remote_id, serialized_message); + + self.send_and_expect_okay(message)?; + + loop { + let mut buffer = [0; BUFFER_SIZE]; + + match reader.read(&mut buffer) { + Ok(0) => { + // Currently file mtime is not forwarded + let subcommand_data = USBSubcommand::Done.with_arg(0); + + let serialized_message = bincode::serialize(&subcommand_data) + .map_err(|_e| RustADBError::ConversionError)?; + + let message = ADBUsbMessage::new( + USBCommand::Write, + local_id, + remote_id, + serialized_message, + ); + + self.send_and_expect_okay(message)?; + + // Command should end with a Write => Okay + let received = self.transport.read_message()?; + match received.header().command() { + USBCommand::Write => return Ok(()), + c => { + return Err(RustADBError::ADBRequestFailed(format!( + "Wrong command received {}", + c + ))) + } + } + } + Ok(size) => { + let subcommand_data = USBSubcommand::Data.with_arg(size as u32); + + let mut serialized_message = bincode::serialize(&subcommand_data) + .map_err(|_e| RustADBError::ConversionError)?; + serialized_message.append(&mut buffer[..size].to_vec()); + + let message = ADBUsbMessage::new( + USBCommand::Write, + local_id, + remote_id, + serialized_message, + ); + + self.send_and_expect_okay(message)?; + } + Err(e) => { + return Err(RustADBError::IOError(e)); + } + } + } + } + + pub(crate) fn begin_synchronization(&mut self) -> Result<(u32, u32)> { + let sync_directive = "sync:\0"; let mut rng = rand::thread_rng(); let message = ADBUsbMessage::new( diff --git a/adb_client/src/usb/adb_usb_device_commands.rs b/adb_client/src/usb/adb_usb_device_commands.rs index e7a8cb7..025e9a9 100644 --- a/adb_client/src/usb/adb_usb_device_commands.rs +++ b/adb_client/src/usb/adb_usb_device_commands.rs @@ -56,7 +56,18 @@ impl ADBDeviceExt for ADBUSBDevice { mut reader: R, mut writer: W, ) -> Result<()> { - let (local_id, remote_id) = self.begin_transaction()?; + let sync_directive = "shell:\0"; + + let mut rng = rand::thread_rng(); + let message = ADBUsbMessage::new( + USBCommand::Open, + rng.gen(), /* Our 'local-id' */ + 0, + sync_directive.into(), + ); + let message = self.send_and_expect_okay(message)?; + let local_id = message.header().arg1(); + let remote_id = message.header().arg0(); let mut transport = self.transport.clone(); @@ -94,14 +105,14 @@ impl ADBDeviceExt for ADBUSBDevice { } fn stat(&mut self, remote_path: &str) -> Result { - let (local_id, remote_id) = self.begin_transaction()?; + let (local_id, remote_id) = self.begin_synchronization()?; let adb_stat_response = self.stat_with_explicit_ids(remote_path, local_id, remote_id)?; self.end_transaction(local_id, remote_id)?; Ok(adb_stat_response) } fn pull, W: Write>(&mut self, source: A, output: W) -> Result<()> { - let (local_id, remote_id) = self.begin_transaction()?; + let (local_id, remote_id) = self.begin_synchronization()?; let source = source.as_ref(); let adb_stat_response = self.stat_with_explicit_ids(source, local_id, remote_id)?; @@ -140,6 +151,30 @@ impl ADBDeviceExt for ADBUSBDevice { Ok(()) } + fn push>(&mut self, stream: R, path: A) -> Result<()> { + let (local_id, remote_id) = self.begin_synchronization()?; + + let path_header = format!("{},0777", path.as_ref()); + + let send_buffer = USBSubcommand::Send.with_arg(path_header.len() as u32); + let mut send_buffer = + bincode::serialize(&send_buffer).map_err(|_e| RustADBError::ConversionError)?; + send_buffer.append(&mut path_header.as_bytes().to_vec()); + + self.send_and_expect_okay(ADBUsbMessage::new( + USBCommand::Write, + local_id, + remote_id, + send_buffer, + ))?; + + self.push_file(local_id, remote_id, stream)?; + + self.end_transaction(local_id, remote_id)?; + + Ok(()) + } + fn reboot(&mut self, reboot_type: RebootType) -> Result<()> { let mut rng = rand::thread_rng();