From 482c9805c45aebe53df239f33e4743fd50400e58 Mon Sep 17 00:00:00 2001 From: ValorBao <139973913+ValorBao@users.noreply.github.com> Date: Wed, 17 Sep 2025 03:44:13 +0800 Subject: [PATCH] feat: add screenshotr for iOS versions below iOS17 (#27) * feat: add screenshot for iOS versions below iOS17 * "cargo fmt" * Style cleanup --------- Co-authored-by: Jackson Coxson --- idevice/Cargo.toml | 1 + idevice/src/services/mod.rs | 2 + idevice/src/services/screenshotr.rs | 123 ++++++++++++++++++++++++++++ tools/src/screenshot.rs | 58 ++++++++----- 4 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 idevice/src/services/screenshotr.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index b5dbf79..87165fe 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -90,6 +90,7 @@ preboard_service = [] obfuscate = ["dep:obfstr"] restore_service = [] rsd = ["xpc"] +screenshotr = [] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] tunnel_tcp_stack = [ diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 24afbd6..e5e3c91 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -41,6 +41,8 @@ pub mod preboard_service; pub mod restore_service; #[cfg(feature = "rsd")] pub mod rsd; +#[cfg(feature = "screenshotr")] +pub mod screenshotr; #[cfg(feature = "springboardservices")] pub mod springboardservices; #[cfg(feature = "syslog_relay")] diff --git a/idevice/src/services/screenshotr.rs b/idevice/src/services/screenshotr.rs new file mode 100644 index 0000000..4105242 --- /dev/null +++ b/idevice/src/services/screenshotr.rs @@ -0,0 +1,123 @@ +//! iOS screenshotr service client +//! +//! Provides functionality for interacting with the screenshot service on iOS devices below iOS 17, +//! which allows taking screenshots. + +use crate::{Idevice, IdeviceError, IdeviceService, obf}; +use log::{debug, warn}; +use std::borrow::Cow; +use tokio::io::AsyncReadExt; +pub struct ScreenshotService { + /// Underlying device connection + pub idevice: Idevice, +} +impl IdeviceService for ScreenshotService { + fn service_name() -> Cow<'static, str> { + obf!("com.apple.mobile.screenshotr") + } + + async fn from_stream(idevice: Idevice) -> Result { + let mut client = Self::new(idevice); + // Perform DeviceLink handshake first + client.dl_version_exchange().await?; + Ok(client) + } +} + +impl ScreenshotService { + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + async fn dl_version_exchange(&mut self) -> Result<(), IdeviceError> { + debug!("Starting DeviceLink version exchange"); + // 1) Receive DLMessageVersionExchange + let (msg, _arr) = self.receive_dl_message().await?; + if msg != "DLMessageVersionExchange" { + warn!("Expected DLMessageVersionExchange, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + // 2) Send DLVersionsOk with version 400 + let out = vec![ + plist::Value::String("DLMessageVersionExchange".into()), + plist::Value::String("DLVersionsOk".into()), + plist::Value::Integer(400u64.into()), + ]; + self.send_dl_array(out).await?; + + // 3) Receive DLMessageDeviceReady + let (msg2, _arr2) = self.receive_dl_message().await?; + if msg2 != "DLMessageDeviceReady" { + warn!("Expected DLMessageDeviceReady, got {msg2}"); + return Err(IdeviceError::UnexpectedResponse); + } + Ok(()) + } + + /// Sends a raw DL array as binary plist + async fn send_dl_array(&mut self, array: Vec) -> Result<(), IdeviceError> { + self.idevice.send_bplist(plist::Value::Array(array)).await + } + + /// Receives any DL* message and returns (message_tag, full_array_value) + pub async fn receive_dl_message(&mut self) -> Result<(String, plist::Value), IdeviceError> { + if let Some(socket) = &mut self.idevice.socket { + let mut buf = [0u8; 4]; + socket.read_exact(&mut buf).await?; + let len = u32::from_be_bytes(buf); + let mut body = vec![0; len as usize]; + socket.read_exact(&mut body).await?; + let value: plist::Value = plist::from_bytes(&body)?; + if let plist::Value::Array(arr) = &value + && let Some(plist::Value::String(tag)) = arr.first() + { + return Ok((tag.clone(), value)); + } + warn!("Invalid DL message format"); + Err(IdeviceError::UnexpectedResponse) + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + + pub async fn take_screenshot(&mut self) -> Result, IdeviceError> { + // Send DLMessageTakeScreenshot + + let message_type_dict = crate::plist!(dict { + "MessageType": "ScreenShotRequest" + }); + + let out = vec![ + plist::Value::String("DLMessageProcessMessage".into()), + plist::Value::Dictionary(message_type_dict), + ]; + self.send_dl_array(out).await?; + + // Receive DLMessageScreenshotData + let (msg, value) = self.receive_dl_message().await?; + if msg != "DLMessageProcessMessage" { + warn!("Expected DLMessageProcessMessage, got {msg}"); + return Err(IdeviceError::UnexpectedResponse); + } + + if let plist::Value::Array(arr) = &value + && arr.len() == 2 + { + if let Some(plist::Value::Dictionary(dict)) = arr.get(1) { + if let Some(plist::Value::Data(data)) = dict.get("ScreenShotData") { + Ok(data.clone()) + } else { + warn!("Invalid ScreenShotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } else { + warn!("Invalid DLMessageScreenshotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } else { + warn!("Invalid DLMessageScreenshotData format"); + Err(IdeviceError::UnexpectedResponse) + } + } +} diff --git a/tools/src/screenshot.rs b/tools/src/screenshot.rs index bb6f617..ead4a01 100644 --- a/tools/src/screenshot.rs +++ b/tools/src/screenshot.rs @@ -2,6 +2,8 @@ use clap::{Arg, Command}; use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake}; use std::fs; +use idevice::screenshotr::ScreenshotService; + mod common; #[tokio::main] @@ -62,31 +64,43 @@ async fn main() { return; } }; - let proxy = CoreDeviceProxy::connect(&*provider) + let mut res: Vec = Vec::new(); + if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await { + let rsd_port = proxy.handshake.server_rsd_port; + + let adapter = proxy.create_software_tunnel().expect("no software tunnel"); + let mut adapter = adapter.to_async_handle(); + let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); + + // Make the connection to RemoteXPC + let mut handshake = RsdHandshake::new(stream).await.unwrap(); + let mut ts_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd( + &mut adapter, + &mut handshake, + ) .await - .expect("no core proxy"); - let rsd_port = proxy.handshake.server_rsd_port; + .expect("Failed to connect"); + ts_client.read_message(0).await.expect("no read??"); - let adapter = proxy.create_software_tunnel().expect("no software tunnel"); - let mut adapter = adapter.to_async_handle(); - let stream = adapter.connect(rsd_port).await.expect("no RSD connect"); - - // Make the connection to RemoteXPC - let mut handshake = RsdHandshake::new(stream).await.unwrap(); - let mut ts_client = - idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake) + let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) .await - .expect("Failed to connect"); - ts_client.read_message(0).await.expect("no read??"); - - let mut ts_client = idevice::dvt::screenshot::ScreenshotClient::new(&mut ts_client) - .await - .expect("Unable to get channel for take screenshot"); - let res = ts_client - .take_screenshot() - .await - .expect("Failed to take screenshot"); - + .expect("Unable to get channel for take screenshot"); + res = ts_client + .take_screenshot() + .await + .expect("Failed to take screenshot"); + } else { + let mut screenshot_client = match ScreenshotService::connect(&*provider).await { + Ok(client) => client, + Err(e) => { + eprintln!( + "Unable to connect to screenshotr service: {e} Ensure Developer Disk Image is mounted." + ); + return; + } + }; + res = screenshot_client.take_screenshot().await.unwrap(); + } match fs::write(output_path, &res) { Ok(_) => println!("Screenshot saved to: {}", output_path), Err(e) => eprintln!("Failed to write screenshot to file: {}", e),