From 9f7e57bb21d31f0ecf107eda36c19640c2e056cb Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 25 Sep 2025 10:04:55 -0600 Subject: [PATCH] Add cpp bindings for image mounter --- Cargo.lock | 4 +- cpp/examples/mounter.cpp | 230 ++++++++++++++++ cpp/include/idevice++/lockdown.hpp | 1 - .../idevice++/mobile_image_mounter.hpp | 87 +++++++ cpp/include/idevice++/option.hpp | 29 +++ cpp/src/mobile_image_mounter.cpp | 246 ++++++++++++++++++ ffi/Cargo.toml | 2 +- 7 files changed, 595 insertions(+), 4 deletions(-) create mode 100644 cpp/examples/mounter.cpp create mode 100644 cpp/include/idevice++/mobile_image_mounter.hpp create mode 100644 cpp/src/mobile_image_mounter.cpp diff --git a/Cargo.lock b/Cargo.lock index ae33fa5..9e59f73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,9 +1825,9 @@ dependencies = [ [[package]] name = "plist_ffi" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a5ca928241bc2e8c5fd28b81772962389efdbfcb71dfc9ec694369e063cb3a" +checksum = "35ed070b06d9f2fdd7e816ef784fb07b09672f2acf37527f810dbedf450b7769" dependencies = [ "cbindgen", "cc", diff --git a/cpp/examples/mounter.cpp b/cpp/examples/mounter.cpp new file mode 100644 index 0000000..b6598e8 --- /dev/null +++ b/cpp/examples/mounter.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include + +// Idevice++ library headers +#include +#include +#include +#include +#include + +// --- Helper Functions --- + +/** + * @brief Reads an entire file into a byte vector. + * @param path The path to the file. + * @return A vector containing the file's data. + */ +std::vector read_file(const std::string& path) { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + throw std::runtime_error("Failed to open file: " + path); + } + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(size); + if (!file.read(reinterpret_cast(buffer.data()), size)) { + throw std::runtime_error("Failed to read file: " + path); + } + return buffer; +} + +/** + * @brief Prints the command usage instructions. + */ +void print_usage(const char* prog_name) { + std::cerr << "Usage: " << prog_name << " [options] \n\n" + << "A tool to manage developer images on a device.\n\n" + << "Options:\n" + << " --udid Target a specific device by its UDID.\n\n" + << "Subcommands:\n" + << " list List mounted images.\n" + << " unmount Unmount the developer image.\n" + << " mount [mount_options] Mount a developer image.\n\n" + << "Mount Options:\n" + << " --image (Required) Path to the DeveloperDiskImage.dmg.\n" + << " --signature (Required for iOS < 17) Path to the .signature file.\n" + << " --manifest (Required for iOS 17+) Path to the BuildManifest.plist.\n" + << " --trustcache (Required for iOS 17+) Path to the trust cache file.\n" + << std::endl; +} + +// --- Main Logic --- + +int main(int argc, char** argv) { + idevice_init_logger(Debug, Disabled, NULL); + // --- 1. Argument Parsing --- + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + std::string udid_arg; + std::string subcommand; + std::string image_path; + std::string signature_path; + std::string manifest_path; + std::string trustcache_path; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--udid" && i + 1 < argc) { + udid_arg = argv[++i]; + } else if (arg == "--image" && i + 1 < argc) { + image_path = argv[++i]; + } else if (arg == "--signature" && i + 1 < argc) { + signature_path = argv[++i]; + } else if (arg == "--manifest" && i + 1 < argc) { + manifest_path = argv[++i]; + } else if (arg == "--trustcache" && i + 1 < argc) { + trustcache_path = argv[++i]; + } else if (arg == "list" || arg == "mount" || arg == "unmount") { + subcommand = arg; + } else if (arg == "--help" || arg == "-h") { + print_usage(argv[0]); + return 0; + } + } + + if (subcommand.empty()) { + std::cerr << "Error: No subcommand specified. Use 'list', 'mount', or 'unmount'." + << std::endl; + print_usage(argv[0]); + return 1; + } + + try { + // --- 2. Device Connection --- + auto u = + IdeviceFFI::UsbmuxdConnection::default_new(0).expect("Failed to connect to usbmuxd"); + auto devices = u.get_devices().expect("Failed to get devices from usbmuxd"); + if (devices.empty()) { + throw std::runtime_error("No devices connected."); + } + + IdeviceFFI::UsbmuxdDevice* target_dev = nullptr; + if (!udid_arg.empty()) { + for (auto& dev : devices) { + if (dev.get_udid().unwrap_or("") == udid_arg) { + target_dev = &dev; + break; + } + } + if (!target_dev) { + throw std::runtime_error("Device with UDID " + udid_arg + " not found."); + } + } else { + target_dev = &devices[0]; // Default to the first device + } + + auto udid = target_dev->get_udid().expect("Device has no UDID"); + auto id = target_dev->get_id().expect("Device has no ID"); + + IdeviceFFI::UsbmuxdAddr addr = IdeviceFFI::UsbmuxdAddr::default_new(); + auto prov = IdeviceFFI::Provider::usbmuxd_new(std::move(addr), 0, udid, id, "mounter-tool") + .expect("Failed to create provider"); + + // --- 3. Connect to Lockdown & Get iOS Version --- + auto lockdown_client = + IdeviceFFI::Lockdown::connect(prov).expect("Lockdown connect failed"); + + auto pairing_file = prov.get_pairing_file().expect("Failed to get pairing file"); + lockdown_client.start_session(pairing_file).expect("Failed to start session"); + + auto version_plist = lockdown_client.get_value("ProductVersion", NULL) + .expect("Failed to get ProductVersion"); + PList::String version_node(version_plist); + std::string version_str = version_node.GetValue(); + std::cout << "Version string: " << version_str << std::endl; + + if (version_str.empty()) { + throw std::runtime_error( + "Failed to get a valid ProductVersion string from the device."); + } + int major_version = std::stoi(version_str); + + // --- 4. Connect to MobileImageMounter --- + auto mounter_client = IdeviceFFI::MobileImageMounter::connect(prov).expect( + "Failed to connect to image mounter"); + + // --- 5. Execute Subcommand --- + if (subcommand == "list") { + auto images = mounter_client.copy_devices().expect("Failed to get images"); + std::cout << "Mounted Images:\n"; + for (plist_t p : images) { + PList::Dictionary dict(p); + std::cout << dict.ToXml() << std::endl; + } + + } else if (subcommand == "unmount") { + const char* unmount_path = (major_version < 17) ? "/Developer" : "/System/Developer"; + mounter_client.unmount_image(unmount_path).expect("Failed to unmount image"); + std::cout << "Successfully unmounted image from " << unmount_path << std::endl; + + } else if (subcommand == "mount") { + if (image_path.empty()) { + throw std::runtime_error("Mount command requires --image "); + } + auto image_data = read_file(image_path); + + if (major_version < 17) { + if (signature_path.empty()) { + throw std::runtime_error("iOS < 17 requires --signature "); + } + auto signature_data = read_file(signature_path); + mounter_client + .mount_developer(image_data.data(), + image_data.size(), + signature_data.data(), + signature_data.size()) + .expect("Failed to mount developer image"); + } else { // iOS 17+ + if (manifest_path.empty() || trustcache_path.empty()) { + throw std::runtime_error("iOS 17+ requires --manifest and --trustcache paths"); + } + auto manifest_data = read_file(manifest_path); + auto trustcache_data = read_file(trustcache_path); + + auto chip_id_plist = lockdown_client.get_value(nullptr, "UniqueChipID") + .expect("Failed to get UniqueChipID"); + PList::Integer chip_id_node(chip_id_plist); + uint64_t unique_chip_id = chip_id_node.GetValue(); + + std::function progress_callback = [](size_t n, size_t d) { + if (d == 0) { + return; + } + double percent = (static_cast(n) / d) * 100.0; + std::cout << "\rProgress: " << std::fixed << std::setprecision(2) << percent + << "%" << std::flush; + if (n == d) { + std::cout << std::endl; + } + }; + + mounter_client + .mount_personalized_with_callback(prov, + image_data.data(), + image_data.size(), + trustcache_data.data(), + trustcache_data.size(), + manifest_data.data(), + manifest_data.size(), + nullptr, // info_plist + unique_chip_id, + progress_callback) + .expect("Failed to mount personalized image"); + } + std::cout << "Successfully mounted image." << std::endl; + } + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/cpp/include/idevice++/lockdown.hpp b/cpp/include/idevice++/lockdown.hpp index 14dfdff..baa60d0 100644 --- a/cpp/include/idevice++/lockdown.hpp +++ b/cpp/include/idevice++/lockdown.hpp @@ -1,4 +1,3 @@ - #pragma once #include #include diff --git a/cpp/include/idevice++/mobile_image_mounter.hpp b/cpp/include/idevice++/mobile_image_mounter.hpp new file mode 100644 index 0000000..96637e4 --- /dev/null +++ b/cpp/include/idevice++/mobile_image_mounter.hpp @@ -0,0 +1,87 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace IdeviceFFI { + +using MobileImageMounterPtr = + std::unique_ptr>; + +class MobileImageMounter { + public: + // Factory: connect via Provider + static Result connect(Provider& provider); + + // Factory: wrap an existing Idevice socket (consumes it on success) + static Result from_socket(Idevice&& socket); + + // Ops + Result, FfiError> copy_devices(); + Result, FfiError> lookup_image(std::string image_type); + Result upload_image(std::string image_type, + const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size); + Result mount_image(std::string image_type, + const uint8_t* signature_data, + size_t signature_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + plist_t info_plist); + Result unmount_image(std::string mount_path); + Result query_developer_mode_status(); + Result mount_developer(const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size); + Result, FfiError> query_personalization_manifest( + std::string image_type, const uint8_t* signature_data, size_t signature_size); + Result, FfiError> query_nonce(std::string personalized_image_type); + Result query_personalization_identifiers(std::string image_type); + Result roll_personalization_nonce(); + Result roll_cryptex_nonce(); + Result mount_personalized(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id); + Result + mount_personalized_with_callback(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id, + std::function& lambda); + + // RAII / moves + ~MobileImageMounter() noexcept = default; + MobileImageMounter(MobileImageMounter&&) noexcept = default; + MobileImageMounter& operator=(MobileImageMounter&&) noexcept = default; + MobileImageMounter(const MobileImageMounter&) = delete; + MobileImageMounter& operator=(const MobileImageMounter&) = delete; + + ImageMounterHandle* raw() const noexcept { return handle_.get(); } + static MobileImageMounter adopt(ImageMounterHandle* h) noexcept { + return MobileImageMounter(h); + } + + private: + explicit MobileImageMounter(ImageMounterHandle* h) noexcept : handle_(h) {} + MobileImageMounterPtr handle_{}; +}; + +} // namespace IdeviceFFI diff --git a/cpp/include/idevice++/option.hpp b/cpp/include/idevice++/option.hpp index ccc9f09..c2a44e6 100644 --- a/cpp/include/idevice++/option.hpp +++ b/cpp/include/idevice++/option.hpp @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -115,6 +116,34 @@ template class Option { return has_ ? std::move(*ptr()) : static_cast(f()); } + T expect(const char* message) && { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + T tmp = std::move(*ptr()); + reset(); + return tmp; + } + + // Returns a mutable reference from an lvalue Result + T& expect(const char* message) & { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return *ptr(); + } + + // Returns a const reference from a const lvalue Result + const T& expect(const char* message) const& { + if (is_none()) { + std::fprintf(stderr, "Fatal (expect) error: %s\n", message); + std::terminate(); + } + return *ptr(); + } + // map template auto map(F&& f) const& -> Option::type> { diff --git a/cpp/src/mobile_image_mounter.cpp b/cpp/src/mobile_image_mounter.cpp new file mode 100644 index 0000000..adb3dfc --- /dev/null +++ b/cpp/src/mobile_image_mounter.cpp @@ -0,0 +1,246 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// -------- Anonymous Namespace for Helpers -------- +namespace { +/** + * @brief A C-style trampoline function to call back into a C++ std::function. + * + * This function is passed to the Rust FFI layer. It receives a void* context, + * which it casts back to the original std::function object to invoke it. + */ +extern "C" void progress_trampoline(size_t progress, size_t total, void* context) { + if (context) { + auto& callback_fn = *static_cast*>(context); + callback_fn(progress, total); + } +} +} // namespace + +// -------- Factory Methods -------- + +Result MobileImageMounter::connect(Provider& provider) { + ImageMounterHandle* handle = nullptr; + FfiError e(::image_mounter_connect(provider.raw(), &handle)); + if (e) { + return Err(e); + } + return Ok(MobileImageMounter::adopt(handle)); +} + +Result MobileImageMounter::from_socket(Idevice&& socket) { + ImageMounterHandle* handle = nullptr; + // The Rust FFI function consumes the socket, so we must release it from the + // C++ RAII wrapper's control. An `Idevice::release()` method is assumed here. + FfiError e(::image_mounter_new(socket.release(), &handle)); + if (e) { + return Err(e); + } + return Ok(MobileImageMounter::adopt(handle)); +} + +// -------- Ops -------- + +Result, FfiError> MobileImageMounter::copy_devices() { + plist_t* devices_raw = nullptr; + size_t devices_len = 0; + + FfiError e(::image_mounter_copy_devices(this->raw(), &devices_raw, &devices_len)); + if (e) { + return Err(e); + } + + std::vector devices; + if (devices_raw) { + devices.assign(devices_raw, devices_raw + devices_len); + } + + return Ok(std::move(devices)); +} + +Result, FfiError> MobileImageMounter::lookup_image(std::string image_type) { + uint8_t* signature_raw = nullptr; + size_t signature_len = 0; + + FfiError e(::image_mounter_lookup_image( + this->raw(), image_type.c_str(), &signature_raw, &signature_len)); + if (e) { + return Err(e); + } + + std::vector signature(signature_len); + std::memcpy(signature.data(), signature_raw, signature_len); + idevice_data_free(signature_raw, signature_len); + + return Ok(std::move(signature)); +} + +Result MobileImageMounter::upload_image(std::string image_type, + const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size) { + FfiError e(::image_mounter_upload_image( + this->raw(), image_type.c_str(), image_data, image_size, signature_data, signature_size)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::mount_image(std::string image_type, + const uint8_t* signature_data, + size_t signature_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + plist_t info_plist) { + FfiError e(::image_mounter_mount_image(this->raw(), + image_type.c_str(), + signature_data, + signature_size, + trust_cache_data, + trust_cache_size, + info_plist)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::unmount_image(std::string mount_path) { + FfiError e(::image_mounter_unmount_image(this->raw(), mount_path.c_str())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::query_developer_mode_status() { + int status_c = 0; + FfiError e(::image_mounter_query_developer_mode_status(this->raw(), &status_c)); + if (e) { + return Err(e); + } + return Ok(status_c != 0); +} + +Result MobileImageMounter::mount_developer(const uint8_t* image_data, + size_t image_size, + const uint8_t* signature_data, + size_t signature_size) { + FfiError e(::image_mounter_mount_developer( + this->raw(), image_data, image_size, signature_data, signature_size)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result, FfiError> MobileImageMounter::query_personalization_manifest( + std::string image_type, const uint8_t* signature_data, size_t signature_size) { + uint8_t* manifest_raw = nullptr; + size_t manifest_len = 0; + FfiError e(::image_mounter_query_personalization_manifest(this->raw(), + image_type.c_str(), + signature_data, + signature_size, + &manifest_raw, + &manifest_len)); + if (e) { + return Err(e); + } + + std::vector manifest(manifest_len); + std::memcpy(manifest.data(), manifest_raw, manifest_len); + idevice_data_free(manifest_raw, manifest_len); + + return Ok(std::move(manifest)); +} + +Result, FfiError> +MobileImageMounter::query_nonce(std::string personalized_image_type) { + uint8_t* nonce_raw = nullptr; + size_t nonce_len = 0; + const char* image_type_c = + personalized_image_type.empty() ? nullptr : personalized_image_type.c_str(); + + FfiError e(::image_mounter_query_nonce(this->raw(), image_type_c, &nonce_raw, &nonce_len)); + if (e) { + return Err(e); + } + + std::vector nonce(nonce_len); + std::memcpy(nonce.data(), nonce_raw, nonce_len); + idevice_data_free(nonce_raw, nonce_len); + + return Ok(std::move(nonce)); +} + +Result +MobileImageMounter::query_personalization_identifiers(std::string image_type) { + plist_t identifiers = nullptr; + const char* image_type_c = image_type.empty() ? nullptr : image_type.c_str(); + + FfiError e( + ::image_mounter_query_personalization_identifiers(this->raw(), image_type_c, &identifiers)); + if (e) { + return Err(e); + } + + // The caller now owns the returned `plist_t` and is responsible for freeing it. + return Ok(identifiers); +} + +Result MobileImageMounter::roll_personalization_nonce() { + FfiError e(::image_mounter_roll_personalization_nonce(this->raw())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::roll_cryptex_nonce() { + FfiError e(::image_mounter_roll_cryptex_nonce(this->raw())); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result MobileImageMounter::mount_personalized(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id) { + FfiError e(::image_mounter_mount_personalized(this->raw(), + provider.raw(), + image_data, + image_size, + trust_cache_data, + trust_cache_size, + build_manifest_data, + build_manifest_size, + info_plist, + unique_chip_id)); + return e ? Result(Err(e)) : Result(Ok()); +} + +Result +MobileImageMounter::mount_personalized_with_callback(Provider& provider, + const uint8_t* image_data, + size_t image_size, + const uint8_t* trust_cache_data, + size_t trust_cache_size, + const uint8_t* build_manifest_data, + size_t build_manifest_size, + plist_t info_plist, + uint64_t unique_chip_id, + std::function& lambda) { + + FfiError e(::image_mounter_mount_personalized_with_callback(this->raw(), + provider.raw(), + image_data, + image_size, + trust_cache_data, + trust_cache_size, + build_manifest_data, + build_manifest_size, + info_plist, + unique_chip_id, + progress_trampoline, + &lambda /* context */)); + + return e ? Result(Err(e)) : Result(Ok()); +} + +} // namespace IdeviceFFI diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 833c700..57e6ef5 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -13,7 +13,7 @@ once_cell = "1.21.1" tokio = { version = "1.44.1", features = ["full"] } libc = "0.2.171" plist = "1.7.1" -plist_ffi = { version = "0.1.5" } +plist_ffi = { version = "0.1.6" } uuid = { version = "1.12", features = ["v4"], optional = true } [target.'cfg(windows)'.dependencies]