use androscalpel_serializer::Serializable; use std::env; use std::fs; use std::fs::File; use std::io::{Read, Seek}; use std::path::Path; use std::process::Command; pub mod apk_signing_block; pub mod compression; mod cp437; pub mod end_of_central_directory; pub mod error; pub mod extra_fields; pub mod file_header; pub mod local_file_header; pub mod zip_reader; pub mod zip_writer; use extra_fields::{ExtraField, Zip64ExtraField}; use file_header::FileHeader; use local_file_header::LocalFileHeader; pub use zip_reader::ZipFileReader; pub use zip_writer::ZipFileWriter; #[derive(Debug, Clone, PartialEq, Eq, Serializable, Default)] pub struct Signature(pub u32); pub enum Encoding { CP437, UTF8, } pub mod general_purpose_flags { pub const MASK_ENCRYPTED: u16 = 1 << 0; pub const MASK_COMPRESS_OPTION_1: u16 = 1 << 1; pub const MASK_COMPRESS_OPTION_2: u16 = 1 << 2; pub const MASK_USE_DATA_DESCRIPTOR: u16 = 1 << 3; pub const MASK_STRONG_ENCRYPTION: u16 = 1 << 6; pub const MASK_UTF8_FILENAME: u16 = 1 << 11; pub const MASK_ENCRYPTED_CENTRAL_DIR: u16 = 1 << 13; } pub mod external_file_attributes { pub const FIFO_FILE: u32 = 0b00010000 << 24; // #define S_IFCHR 0020000 /* character special */ ? pub const DIR_FILE: u32 = 0b01000000 << 24; // #define S_IFBLK 0060000 /* block special */ ? pub const SYMBOLIC_LINK_FILE: u32 = 0b10100000 << 24; pub const SOCKET_FILE: u32 = 0b11000000 << 24; pub const REGULAR_FILE: u32 = 0b10000000 << 24; pub const MASK_FILE_TYPE: u32 = 0b11110000 << 24; pub const SETUID: u32 = 0b0000100000000000 << 16; pub const SETGID: u32 = 0b0000010000000000 << 16; pub const STICKY: u32 = 0b0000001000000000 << 16; pub const PERM_UR: u32 = 0b0000000100000000 << 16; pub const PERM_UW: u32 = 0b0000000010000000 << 16; pub const PERM_UX: u32 = 0b0000000001000000 << 16; pub const PERM_GR: u32 = 0b0000000000100000 << 16; pub const PERM_GW: u32 = 0b0000000000010000 << 16; pub const PERM_GX: u32 = 0b0000000000001000 << 16; pub const PERM_OR: u32 = 0b0000000000000100 << 16; pub const PERM_OW: u32 = 0b0000000000000010 << 16; pub const PERM_OX: u32 = 0b0000000000000001 << 16; } #[derive(Debug, Clone, PartialEq, Eq)] pub struct FileInfo { pub local_header: LocalFileHeader, pub header: FileHeader, } // TODO: support data descriptor (MASK_USE_DATA_DESCRIPTOR) impl FileInfo { pub fn get_name(&self) -> String { self.header.get_name() } pub fn set_name(&mut self, name: &str) { self.header.set_name(name); self.local_header.set_name(name); } pub fn external_file_attributes_set_flag(&mut self, flag: u32) { self.header.external_file_attributes_set_flag(flag); } pub fn external_file_attributes_unset_flag(&mut self, flag: u32) { self.header.external_file_attributes_unset_flag(flag); } pub fn set_file_type(&mut self, file_type: u32) { self.header.set_file_type(file_type); } pub fn get_offset_local_header(&self) -> u64 { self.header.get_offset_local_header() } pub fn get_compressed_size(&self) -> u64 { self.header.get_compressed_size() } pub fn get_file_offset(&self) -> u64 { self.get_offset_local_header() + self.local_header.size() as u64 } pub fn set_offset_local_header(&mut self, new_offset: u64) { let use_z64 = new_offset > u32::MAX as u64; for extra in &mut self.local_header.extra_field { if let ExtraField::Zip64(extra) = extra { if use_z64 { extra.offset_header = Some(new_offset); } else { extra.offset_header = None; } } } let mut z64found = false; for extra in &mut self.header.extra_field { if let ExtraField::Zip64(extra) = extra { if use_z64 { extra.offset_header = Some(new_offset); } else { extra.offset_header = None; } z64found = true; } } if use_z64 && !z64found { self.header .extra_field .push(ExtraField::Zip64(Zip64ExtraField { original_size: Some(self.header.uncompressed_size as u64), compressed_size: Some(self.header.compressed_size as u64), offset_header: Some(new_offset), disk_number: None, })); self.header.uncompressed_size = u32::MAX; self.header.compressed_size = u32::MAX; } if use_z64 { self.header.offset_local_header = u32::MAX; } else { self.header.offset_local_header = new_offset as u32; } } } /// Replace the dex files a an apk an resigned the apk. /// /// # Warning /// /// For now, only jks keystore are allowed. /// /// The `zipalign` and `apksigner` args allow to use a specific version of the /// tools instead of the one in the PATH (if it even exist) pub fn replace_dex( apk: impl AsRef, dst: impl AsRef, dexfiles: &mut [impl Read + Seek], keystore: impl AsRef, // TODO enum for handling p11 and generating a random cert // `keytool -genkey -v -keystore KEYSTORE.jks -keyalg RSA -keysize // 2048 -validity 10000 -alias ALIAS` zipalign: Option>, apksigner: Option>, ) { let zipalign = if let Some(path) = &zipalign { path.as_ref().as_os_str() } else { "zipalign".as_ref() }; let apksigner = if let Some(path) = &apksigner { path.as_ref().as_os_str() } else { "apksigner".as_ref() }; let tmp_dir = env::temp_dir().join(format!("apk_frauder_{:x}", rand::random::())); let unaligned_path = tmp_dir.join("stripped.apk"); let aligned_path = tmp_dir.join("aligned.apk"); fs::create_dir_all(&tmp_dir).expect("Failed to create temporary directory"); let file = File::open(apk).expect("failed to open file"); let mut apk = ZipFileReader::new(file); let file = File::create(&unaligned_path).expect("failed to create file"); let mut apk_out = ZipFileWriter::new(file, apk.zip64_end_of_central_directory.clone()); let mut file_info_ref = (*apk .get_classes_file_info() .first() .expect("No dex file found in apk")) .clone(); apk.unlink_signature_files(); apk.unlink_bytecode_files(); for f in apk.files.clone() { apk_out.insert_file_from_zip(f, &mut apk); } for (i, mut dex) in dexfiles.iter_mut().enumerate() { if i == 0 { file_info_ref.set_name("classes.dex"); } else { file_info_ref.set_name(&format!("classes{}.dex", i + 1)); } apk_out.insert_file( &mut dex, file_info_ref.header.clone(), Some(file_info_ref.local_header.clone()), ); } apk_out.write_central_directory(); // TODO: we can probably do that ourself an spare ourself the trouble of finding zipalign Command::new(zipalign) .arg("-v") .arg("-p") .arg("4") .arg(unaligned_path.as_os_str()) .arg(aligned_path.as_os_str()) .status() .unwrap(); Command::new(apksigner) .arg("sign") .arg("--ks") .arg(keystore.as_ref().as_os_str()) .arg("--out") .arg(dst.as_ref().as_os_str()) .arg(aligned_path.as_os_str()) .status() .unwrap(); fs::remove_dir_all(tmp_dir).expect("Failled to remove tmp dir"); }