diff --git a/src/bin/playground/main.rs b/src/bin/playground/main.rs index ce504ac..ade9ad6 100644 --- a/src/bin/playground/main.rs +++ b/src/bin/playground/main.rs @@ -7,8 +7,15 @@ use rustwii::archive::u8; // use rustii::title::content; fn main() { - let data = fs::read("sm.wad").unwrap(); - let title = title::Title::from_bytes(&data).unwrap(); + let data = fs::read("ios9.wad").unwrap(); + let mut title = title::Title::from_bytes(&data).unwrap(); + + let index = title::iospatcher::ios_find_module(String::from("ES:"), &title).unwrap(); + println!("ES index: {}", index); + + let patch_count = title::iospatcher::ios_patch_sigchecks(&mut title, index).unwrap(); + println!("patches applied: {}", patch_count); + println!("Title ID from WAD via Title object: {}", hex::encode(title.tmd.title_id())); let wad = wad::WAD::from_bytes(&data).unwrap(); @@ -41,16 +48,9 @@ fn main() { let result = title.verify().unwrap(); println!("full title verified successfully: {}", result); - - let u8_archive = u8::U8Directory::from_bytes(fs::read("testu8.arc").unwrap().into_boxed_slice()).unwrap(); println!("{:#?}", u8_archive); - // println!("files and dirs counted: {}", u8_archive.node_tree.borrow().count()); - // fs::write("outfile.arc", u8_archive.to_bytes().unwrap()).unwrap(); - // println!("re-written"); - - // let mut content_map = content::SharedContentMap::from_bytes(&fs::read("content.map").unwrap()).unwrap(); // content_map.add(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap(); diff --git a/src/bin/rustwii/archive/ash.rs b/src/bin/rustwii/archive/ash.rs index 36fa732..2f5a64d 100644 --- a/src/bin/rustwii/archive/ash.rs +++ b/src/bin/rustwii/archive/ash.rs @@ -1,7 +1,7 @@ // archive/ash.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the ASH decompression command in the rustii CLI. +// Code for the ASH decompression command in the rustwii CLI. // Might even have the compression command someday if I ever write the compression code! use std::{str, fs}; diff --git a/src/bin/rustwii/archive/lz77.rs b/src/bin/rustwii/archive/lz77.rs index 28b602d..99df80a 100644 --- a/src/bin/rustwii/archive/lz77.rs +++ b/src/bin/rustwii/archive/lz77.rs @@ -1,7 +1,7 @@ // archive/lz77.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the LZ77 compression/decompression commands in the rustii CLI. +// Code for the LZ77 compression/decompression commands in the rustwii CLI. use std::{str, fs}; use std::path::{Path, PathBuf}; diff --git a/src/bin/rustwii/archive/theme.rs b/src/bin/rustwii/archive/theme.rs index 2c3c32a..b5082ab 100644 --- a/src/bin/rustwii/archive/theme.rs +++ b/src/bin/rustwii/archive/theme.rs @@ -1,7 +1,7 @@ // archive/theme.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the theme building commands in the rustii CLI. +// Code for the theme building commands in the rustwii CLI. use std::collections::HashMap; use std::fs; diff --git a/src/bin/rustwii/archive/u8.rs b/src/bin/rustwii/archive/u8.rs index a3b3fd8..8373364 100644 --- a/src/bin/rustwii/archive/u8.rs +++ b/src/bin/rustwii/archive/u8.rs @@ -1,7 +1,7 @@ // archive/u8.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the U8 packing/unpacking commands in the rustii CLI. +// Code for the U8 packing/unpacking commands in the rustwii CLI. use std::{str, fs}; use std::path::{Path, PathBuf}; diff --git a/src/bin/rustwii/info.rs b/src/bin/rustwii/info.rs index c82031e..46da4c7 100644 --- a/src/bin/rustwii/info.rs +++ b/src/bin/rustwii/info.rs @@ -1,7 +1,7 @@ // info.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the info command in the rustii CLI. +// Code for the info command in the rustwii CLI. use std::{str, fs}; use std::path::Path; diff --git a/src/bin/rustwii/main.rs b/src/bin/rustwii/main.rs index 66ac050..6645614 100644 --- a/src/bin/rustwii/main.rs +++ b/src/bin/rustwii/main.rs @@ -1,7 +1,7 @@ // main.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Base for the rustii CLI that handles argument parsing and directs execution to the proper module. +// Base for the rustwii CLI that handles argument parsing and directs execution to the proper module. mod archive; mod title; @@ -36,7 +36,7 @@ enum Commands { Fakesign { /// The path to a TMD, Ticket, or WAD input: String, - /// An (optional) output name; defaults to overwriting input file if not provided + /// An optional output path; defaults to overwriting input file if not provided #[arg(short, long)] output: Option, }, @@ -45,6 +45,25 @@ enum Commands { /// The path to a TMD, Ticket, or WAD input: String, }, + /// Apply patches to an IOS + IosPatch { + /// The IOS WAD to apply patches to + input: String, + #[arg(short, long)] + /// An optional output path; default to overwriting input file if not provided + output: Option, + /// Set a new IOS version (0-65535) + #[arg(short, long)] + version: Option, + /// Set the slot that this IOS will install into + #[arg(short, long)] + slot: Option, + /// Set all patched content to be non-shared + #[arg(short, long, action)] + no_shared: bool, + #[command(flatten)] + enabled_patches: title::iospatcher::EnabledPatches, + }, /// Compress/decompress data using LZ77 compression Lz77 { #[command(subcommand)] @@ -118,6 +137,24 @@ fn main() -> Result<()> { Some(Commands::Info { input }) => { info::info(input)? }, + Some(Commands::IosPatch { + input, + output, + version, + slot, + no_shared, + enabled_patches + } + ) => { + title::iospatcher::patch_ios( + input, + output, + version, + slot, + no_shared, + enabled_patches, + )? + } Some(Commands::Lz77 { command }) => { match command { archive::lz77::Commands::Compress { input, output } => { diff --git a/src/bin/rustwii/nand/emunand.rs b/src/bin/rustwii/nand/emunand.rs index 2690fef..49d3e45 100644 --- a/src/bin/rustwii/nand/emunand.rs +++ b/src/bin/rustwii/nand/emunand.rs @@ -1,7 +1,7 @@ // nand/emunand.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for EmuNAND-related commands in the rustii CLI. +// Code for EmuNAND-related commands in the rustwii CLI. use std::{str, fs}; use std::path::{absolute, Path}; diff --git a/src/bin/rustwii/nand/setting.rs b/src/bin/rustwii/nand/setting.rs index 9e3dc9f..45e65d3 100644 --- a/src/bin/rustwii/nand/setting.rs +++ b/src/bin/rustwii/nand/setting.rs @@ -1,7 +1,7 @@ // nand/setting.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for setting.txt-related commands in the rustii CLI. +// Code for setting.txt-related commands in the rustwii CLI. use std::{str, fs}; use std::path::{Path, PathBuf}; diff --git a/src/bin/rustwii/title/fakesign.rs b/src/bin/rustwii/title/fakesign.rs index 4c9788c..3f577c7 100644 --- a/src/bin/rustwii/title/fakesign.rs +++ b/src/bin/rustwii/title/fakesign.rs @@ -1,7 +1,7 @@ // title/fakesign.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for the fakesign command in the rustii CLI. +// Code for the fakesign command in the rustwii CLI. use std::{str, fs}; use std::path::{Path, PathBuf}; diff --git a/src/bin/rustwii/title/iospatcher.rs b/src/bin/rustwii/title/iospatcher.rs new file mode 100644 index 0000000..9982060 --- /dev/null +++ b/src/bin/rustwii/title/iospatcher.rs @@ -0,0 +1,129 @@ +// title/iospatcher.rs from ruswtii (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustwii +// +// Code for the iospatcher command in the rustwii CLI. + +use std::fs; +use std::path::{Path, PathBuf}; +use anyhow::{bail, Context, Result}; +use clap::Args; +use rustwii::title; +use rustwii::title::iospatcher; + +#[derive(Args)] +#[clap(next_help_heading = "Patches")] +#[group(multiple = true, required = true)] +/// Modifications that can be made to a title, shared between the WAD and TMD commands. +pub struct EnabledPatches { + /// Patch out signature checks + #[arg(long, action)] + sig_checks: bool, + /// Patch in access to ES_Identify + #[arg(long, action)] + es_identify: bool, + /// Patch in access to /dev/flash + #[arg(long, action)] + dev_flash: bool, + /// Patch out anti-downgrade checks + #[arg(long, action)] + allow_downgrade: bool, + /// Patch out drive inquiries (EXPERIMENTAL) + #[arg(long, action)] + drive_inquiry: bool, +} + +pub fn patch_ios( + input: &str, + output: &Option, + version: &Option, + slot: &Option, + no_shared: &bool, + enabled_patches: &EnabledPatches, +) -> Result<()> { + let in_path = Path::new(input); + if !in_path.exists() { + bail!("Source WAD \"{}\" does not exist.", in_path.display()); + } + let out_path = if output.is_some() { + PathBuf::from(output.clone().unwrap()).with_extension("wad") + } else { + in_path.to_path_buf() + }; + + let mut ios = title::Title::from_bytes(&fs::read(in_path)?).with_context(|| "The provided WAD file could not be parsed, and is likely invalid.")?; + let tid = hex::encode(ios.tmd.title_id()); + + // If the TID is not a valid IOS TID, then bail. + if !tid[..8].eq("00000001") || tid[8..].eq("00000001") || tid[8..].eq("00000002") { + bail!("The provided WAD does not appear to contain an IOS! No patches can be applied.") + } + + let mut patches_applied = 0; + + if let Some(version) = version { + ios.set_title_version(*version); + println!("Set new IOS version: {version}") + } + + if let Some(slot) = slot && *slot >= 3 { + let tid = hex::decode(format!("00000001{slot:08X}"))?; + ios.set_title_id(tid.try_into().unwrap()).expect("Failed to set IOS slot!"); + println!("Set new IOS slot: {slot}"); + } + + if enabled_patches.sig_checks || + enabled_patches.es_identify || + enabled_patches.dev_flash || + enabled_patches.allow_downgrade + { + let es_index = iospatcher::ios_find_module(String::from("ES:"), &ios) + .with_context(|| "The ES module could not be found. This WAD is not a valid IOS.")?; + if enabled_patches.sig_checks { + print!("Applying signature check patch... "); + let count = iospatcher::ios_patch_sigchecks(&mut ios, es_index)?; + println!("{} patch(es) applied", count); + patches_applied += count; + } + if enabled_patches.es_identify { + print!("Applying ES_Identify access patch... "); + let count = iospatcher::ios_patch_es_identify(&mut ios, es_index)?; + println!("{} patch(es) applied", count); + patches_applied += count; + } + if enabled_patches.dev_flash { + print!("Applying /dev/flash access patch... "); + let count = iospatcher::ios_patch_dev_flash(&mut ios, es_index)?; + println!("{} patch(es) applied", count); + patches_applied += count; + } + if enabled_patches.allow_downgrade { + print!("Applying allow downgrading patch... "); + let count = iospatcher::ios_patch_allow_downgrade(&mut ios, es_index)?; + println!("{} patch(es) applied", count); + patches_applied += count; + } + } + + if enabled_patches.drive_inquiry { + let dip_index = iospatcher::ios_find_module(String::from("DIP:"), &ios) + .with_context(|| "The DIP module could not be found. This WAD is not a valid IOS, \ + or this IOS version does not use the DIP module.")?; + print!("Applying (EXPERIMENTAL) drive inquiry patch... "); + let count = iospatcher::ios_patch_drive_inquiry(&mut ios, dip_index)?; + println!("{} patch(es) applied", count); + patches_applied += count; + } + + println!("\nTotal patches applied: {patches_applied}"); + + if patches_applied == 0 && version.is_none() && slot.is_none() { + bail!("No patchers were applied. Please make sure the specified patches are compatible \ + with this IOS.") + } + + ios.fakesign()?; + fs::write(out_path, ios.to_wad()?.to_bytes()?)?; + + println!("IOS successfully patched!"); + Ok(()) +} diff --git a/src/bin/rustwii/title/mod.rs b/src/bin/rustwii/title/mod.rs index 315bc46..de18bd0 100644 --- a/src/bin/rustwii/title/mod.rs +++ b/src/bin/rustwii/title/mod.rs @@ -6,3 +6,4 @@ pub mod nus; pub mod wad; pub mod tmd; mod shared; +pub mod iospatcher; diff --git a/src/bin/rustwii/title/nus.rs b/src/bin/rustwii/title/nus.rs index 4b3f9e1..b85c251 100644 --- a/src/bin/rustwii/title/nus.rs +++ b/src/bin/rustwii/title/nus.rs @@ -1,7 +1,7 @@ // title/nus.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for NUS-related commands in the rustii CLI. +// Code for NUS-related commands in the rustwii CLI. use std::{str, fs}; use std::path::PathBuf; diff --git a/src/bin/rustwii/title/shared.rs b/src/bin/rustwii/title/shared.rs index e8a5594..711f522 100644 --- a/src/bin/rustwii/title/shared.rs +++ b/src/bin/rustwii/title/shared.rs @@ -1,7 +1,7 @@ // title/shared.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code shared between title commands in the rustii CLI. +// Code shared between title commands in the rustwii CLI. use anyhow::bail; use clap::Args; diff --git a/src/bin/rustwii/title/tmd.rs b/src/bin/rustwii/title/tmd.rs index 75f8b7f..7d58520 100644 --- a/src/bin/rustwii/title/tmd.rs +++ b/src/bin/rustwii/title/tmd.rs @@ -1,7 +1,7 @@ // title/tmd.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for TMD-related commands in the rustii CLI. +// Code for TMD-related commands in the rustwii CLI. use std::{str, fs}; use std::path::{Path, PathBuf}; diff --git a/src/bin/rustwii/title/wad.rs b/src/bin/rustwii/title/wad.rs index fd27e4b..6efaeef 100644 --- a/src/bin/rustwii/title/wad.rs +++ b/src/bin/rustwii/title/wad.rs @@ -1,7 +1,7 @@ // title/wad.rs from ruswtii (c) 2025 NinjaCheetah & Contributors // https://github.com/NinjaCheetah/rustwii // -// Code for WAD-related commands in the rustii CLI. +// Code for WAD-related commands in the rustwii CLI. use std::{str, fs, fmt}; use std::path::{Path, PathBuf}; diff --git a/src/title/iospatcher.rs b/src/title/iospatcher.rs new file mode 100644 index 0000000..bb02579 --- /dev/null +++ b/src/title/iospatcher.rs @@ -0,0 +1,136 @@ +// title/iospatcher.rs from ruswtii (c) 2025 NinjaCheetah & Contributors +// https://github.com/NinjaCheetah/rustwii +// +// Module for applying patches to IOSes using a Title. + +use std::io::{Cursor, Seek, SeekFrom, Write}; +use thiserror::Error; +use crate::title; +use crate::title::content; + +#[derive(Debug, Error)] +pub enum IOSPatcherError { + #[error("this title is not an IOS")] + NotIOS, + #[error("the required module \"{0}\" could not be found, this may not be a valid IOS")] + ModuleNotFound(String), + #[error("failed to get IOS content")] + Content(#[from] content::ContentError), + #[error("failed to set content in Title")] + Title(#[from] title::TitleError), + #[error("IOS content is invalid")] + IO(#[from] std::io::Error), +} + +pub fn ios_find_module(module_keyword: String, ios: &title::Title) -> Result { + let content_records = ios.tmd.content_records(); + let tid = hex::encode(ios.tmd.title_id()); + + // If the TID is not a valid IOS TID, then return NotIOS. It's possible that this could catch + // some modified IOSes that currently have a non-IOS TID but that's weird and if you're doing + // that please stop. + if !tid[..8].eq("00000001") || tid[8..].eq("00000001") || tid[8..].eq("00000002") { + return Err(IOSPatcherError::NotIOS); + } + + // Find the module's keyword in the content, and return the (true) index of the content that + // it was found in. + let keyword = module_keyword.as_bytes(); + for record in content_records { + let content_decrypted = ios.get_content_by_index(record.index as usize)?; + let offset = content_decrypted + .windows(keyword.len()) + .position(|window| window == keyword); + if offset.is_some() { + return Ok(record.index as usize); + } + } + + // If we didn't return early by finding the offset, then return a ModuleNotFound error. + Err(IOSPatcherError::ModuleNotFound(module_keyword)) +} + +fn ios_apply_patches( + target_content: &mut Cursor>, + find_seq: Vec>, + replace_seq: Vec> +) -> Result { + let mut patch_count = 0; + for idx in 0..find_seq.len() { + let offset = target_content.get_ref() + .windows(find_seq[idx].len()) + .position(|window| window == find_seq[idx]); + if let Some(offset) = offset { + target_content.seek(SeekFrom::Start(offset as u64))?; + target_content.write_all(&replace_seq[idx])?; + patch_count += 1; + } + } + + Ok(patch_count) +} + +pub fn ios_patch_sigchecks(ios: &mut title::Title, es_index: usize) -> Result { + let target_content = ios.get_content_by_index(es_index)?; + let mut buf = Cursor::new(target_content); + + let find_seq = vec![vec![0x20, 0x07, 0x23, 0xa2], vec![0x20, 0x07, 0x4b, 0x0b]]; + let replace_seq: Vec> = vec![vec![0x20, 0x00, 0x23, 0xa2], vec![0x20, 0x00, 0x4b, 0x0b]]; + let patch_count = ios_apply_patches(&mut buf, find_seq, replace_seq)?; + + ios.set_content(buf.get_ref(), es_index, None, None)?; + + Ok(patch_count) +} + +pub fn ios_patch_es_identify(ios: &mut title::Title, es_index: usize) -> Result { + let target_content = ios.get_content_by_index(es_index)?; + let mut buf = Cursor::new(target_content); + + let find_seq = vec![vec![0x28, 0x03, 0xd1, 0x23]]; + let replace_seq = vec![vec![0x28, 0x03, 0x00, 0x00]]; + let patch_count = ios_apply_patches(&mut buf, find_seq, replace_seq)?; + + ios.set_content(buf.get_ref(), es_index, None, None)?; + + Ok(patch_count) +} + +pub fn ios_patch_dev_flash(ios: &mut title::Title, es_index: usize) -> Result { + let target_content = ios.get_content_by_index(es_index)?; + let mut buf = Cursor::new(target_content); + + let find_seq = vec![vec![0x42, 0x8b, 0xd0, 0x01, 0x25, 0x66]]; + let replace_seq = vec![vec![0x42, 0x8b, 0xe0, 0x01, 0x25, 0x66]]; + let patch_count = ios_apply_patches(&mut buf, find_seq, replace_seq)?; + + ios.set_content(buf.get_ref(), es_index, None, None)?; + + Ok(patch_count) +} + +pub fn ios_patch_allow_downgrade(ios: &mut title::Title, es_index: usize) -> Result { + let target_content = ios.get_content_by_index(es_index)?; + let mut buf = Cursor::new(target_content); + + let find_seq = vec![vec![0xd2, 0x01, 0x4e, 0x56]]; + let replace_seq = vec![vec![0xe0, 0x01, 0x4e, 0x56]]; + let patch_count = ios_apply_patches(&mut buf, find_seq, replace_seq)?; + + ios.set_content(buf.get_ref(), es_index, None, None)?; + + Ok(patch_count) +} + +pub fn ios_patch_drive_inquiry(ios: &mut title::Title, dip_index: usize) -> Result { + let target_content = ios.get_content_by_index(dip_index)?; + let mut buf = Cursor::new(target_content); + + let find_seq = vec![vec![0x49, 0x4c, 0x23, 0x90, 0x68, 0x0a]]; + let replace_seq = vec![vec![0x20, 0x00, 0xe5, 0x38, 0x68, 0x0a]]; + let patch_count = ios_apply_patches(&mut buf, find_seq, replace_seq)?; + + ios.set_content(buf.get_ref(), dip_index, None, None)?; + + Ok(patch_count) +} diff --git a/src/title/mod.rs b/src/title/mod.rs index 46cd5ea..97fb6dc 100644 --- a/src/title/mod.rs +++ b/src/title/mod.rs @@ -7,6 +7,7 @@ pub mod cert; pub mod commonkeys; pub mod content; pub mod crypto; +pub mod iospatcher; pub mod nus; pub mod ticket; pub mod tmd; @@ -137,6 +138,7 @@ impl Title { /// content type can be provided, with the existing values being preserved by default. pub fn set_content(&mut self, content: &[u8], index: usize, cid: Option, content_type: Option) -> Result<(), TitleError> { self.content.set_content(content, index, cid, content_type, self.ticket.title_key_dec())?; + self.tmd.set_content_records(self.content.content_records()); Ok(()) } @@ -146,6 +148,7 @@ impl Title { /// content records. pub fn add_content(&mut self, content: &[u8], cid: u32, content_type: tmd::ContentType) -> Result<(), TitleError> { self.content.add_content(content, cid, content_type, self.ticket.title_key_dec())?; + self.tmd.set_content_records(self.content.content_records()); Ok(()) } @@ -195,6 +198,11 @@ impl Title { Ok(()) } + pub fn set_title_version(&mut self, version: u16) { + self.tmd.set_title_version(version); + self.ticket.set_title_version(version); + } + pub fn set_cert_chain(&mut self, cert_chain: cert::CertificateChain) { self.cert_chain = cert_chain; } diff --git a/src/title/ticket.rs b/src/title/ticket.rs index d6d5ab6..f6ec023 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -212,6 +212,10 @@ impl Ticket { pub fn title_version(&self) -> u16 { self.title_version } + + pub fn set_title_version(&mut self, version: u16) { + self.title_version = version; + } /// Gets the permitted titles mask listed in the Ticket. pub fn permitted_titles_mask(&self) -> [u8; 4] { diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 4eda914..b1a9281 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -393,6 +393,10 @@ impl TMD { self.title_type = new_type; Ok(()) } + + pub fn set_title_version(&mut self, version: u16) { + self.title_version = version; + } /// Gets the type of content described by a content record in a TMD. pub fn content_type(&self, index: usize) -> ContentType {