From 0fdf619360fa16ad37cbf8d6b7d70e9ce7eb80e4 Mon Sep 17 00:00:00 2001 From: Jean-Marie Mineau Date: Wed, 17 Jan 2024 13:57:01 +0100 Subject: [PATCH] transfert file from zip to another (wip) --- Cargo.lock | 20 ++ apk_frauder/Cargo.toml | 1 + apk_frauder/README.md | 1 - apk_frauder/src/compression.rs | 64 ++++++ apk_frauder/src/file_header.rs | 7 +- apk_frauder/src/lib.rs | 305 ++++----------------------- apk_frauder/src/local_file_header.rs | 17 +- apk_frauder/src/main.rs | 12 +- apk_frauder/src/zip_reader.rs | 285 +++++++++++++++++++++++++ apk_frauder/src/zip_writer.rs | 73 +++++++ 10 files changed, 512 insertions(+), 273 deletions(-) delete mode 100644 apk_frauder/README.md create mode 100644 apk_frauder/src/compression.rs create mode 100644 apk_frauder/src/zip_reader.rs create mode 100644 apk_frauder/src/zip_writer.rs diff --git a/Cargo.lock b/Cargo.lock index ec3dea2..b5a603b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ name = "apk_frauder" version = "0.1.0" dependencies = [ "androscalpel_serializer", + "flate2", ] [[package]] @@ -130,6 +131,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -150,6 +160,16 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "generic-array" version = "0.14.7" diff --git a/apk_frauder/Cargo.toml b/apk_frauder/Cargo.toml index e58d9b2..375242c 100644 --- a/apk_frauder/Cargo.toml +++ b/apk_frauder/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] androscalpel_serializer = { version = "0.1.0", path = "../androscalpel_serializer" } +flate2 = { version = "1.0.28", features = ["rust_backend"] } diff --git a/apk_frauder/README.md b/apk_frauder/README.md deleted file mode 100644 index dbd9dda..0000000 --- a/apk_frauder/README.md +++ /dev/null @@ -1 +0,0 @@ -flate2/rust_backend diff --git a/apk_frauder/src/compression.rs b/apk_frauder/src/compression.rs new file mode 100644 index 0000000..b673b61 --- /dev/null +++ b/apk_frauder/src/compression.rs @@ -0,0 +1,64 @@ +use androscalpel_serializer::Serializable; + +#[derive(Serializable, Clone, Copy, PartialEq, Eq, Debug)] +#[prefix_type(u16)] +pub enum CompressionMethod { + #[prefix(0)] + Stored, + #[prefix(1)] + Shrunk, + #[prefix(2)] + ReducedF1, + #[prefix(3)] + ReducedF2, + #[prefix(4)] + ReducedF3, + #[prefix(5)] + ReducedF4, + #[prefix(6)] + Imploded, + #[prefix(7)] + ReservedTokenizing, + #[prefix(8)] + Deflated, + #[prefix(9)] + Deflate64, + #[prefix(10)] + PKWAREImploding, + #[prefix(11)] + Reserved1, + #[prefix(12)] + BZIP2, + #[prefix(13)] + Reserved2, + #[prefix(14)] + LZMA, + #[prefix(15)] + Reserved3, + #[prefix(16)] + CMPSC, + #[prefix(17)] + Reserved4, + #[prefix(18)] + TERSE, + #[prefix(19)] + LZ77, + #[prefix(20)] + Zstandard93, + #[prefix(93)] + Zstandard, + #[prefix(94)] + MP3, + #[prefix(95)] + XZ, + #[prefix(96)] + JPEG, + #[prefix(97)] + WavPack, + #[prefix(98)] + PPMdI1, + #[prefix(99)] + AEx, + #[default_variant] + Unknown(u16), +} diff --git a/apk_frauder/src/file_header.rs b/apk_frauder/src/file_header.rs index d7460ca..62bb5d3 100644 --- a/apk_frauder/src/file_header.rs +++ b/apk_frauder/src/file_header.rs @@ -1,5 +1,6 @@ use std::io::{SeekFrom, Write}; +use crate::compression::CompressionMethod; use crate::extra_fields::{ExtraField, GenericExtraField, Zip64ExtraField}; use crate::{cp437, general_purpose_flags, Encoding, Signature}; use androscalpel_serializer::{ReadSeek, Result, Serializable}; @@ -10,7 +11,7 @@ pub struct FileHeader { pub version_made_by: u16, pub version_needed_to_extract: u16, pub general_purpose_flags: u16, - pub compression_method: u16, + pub compression_method: CompressionMethod, pub last_mod_file_time: u16, pub last_mod_file_data: u16, pub crc_32: u32, @@ -70,7 +71,7 @@ impl Serializable for FileHeader { let version_made_by = u16::deserialize(input)?; let version_needed_to_extract = u16::deserialize(input)?; let general_purpose_flags = u16::deserialize(input)?; - let compression_method = u16::deserialize(input)?; + let compression_method = CompressionMethod::deserialize(input)?; let last_mod_file_time = u16::deserialize(input)?; let last_mod_file_data = u16::deserialize(input)?; let crc_32 = u32::deserialize(input)?; @@ -127,7 +128,7 @@ impl Serializable for FileHeader { } } if extra_size_read > extra_field_length as usize { - println!("Failed to parsed last extra field in {}", header.get_name()); + println!("Failed to parse last extra field in {}", header.get_name()); let size = header.extra_field.pop().unwrap().size(); input.seek(SeekFrom::Current(-(size as i64))).unwrap(); } diff --git a/apk_frauder/src/lib.rs b/apk_frauder/src/lib.rs index 3ccceed..e6206d0 100644 --- a/apk_frauder/src/lib.rs +++ b/apk_frauder/src/lib.rs @@ -1,18 +1,19 @@ -use std::io::{Read, Seek, SeekFrom}; - use androscalpel_serializer::Serializable; pub mod apk_signing_block; +pub mod compression; mod cp437; pub mod end_of_central_directory; pub mod extra_fields; pub mod file_header; pub mod local_file_header; +pub mod zip_reader; +pub mod zip_writer; -use apk_signing_block::*; -use end_of_central_directory::*; +use extra_fields::{ExtraField, Zip64ExtraField}; use file_header::FileHeader; use local_file_header::LocalFileHeader; +pub use zip_reader::ZipFileReader; #[derive(Debug, Clone, PartialEq, Eq, Serializable, Default)] pub struct Signature(pub u32); @@ -33,13 +34,13 @@ pub mod general_purpose_flags { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct File { +pub struct FileInfo { pub local_header: LocalFileHeader, pub header: FileHeader, } // TODO: support data descriptor (MASK_USE_DATA_DESCRIPTOR) -impl File { +impl FileInfo { pub fn get_name(&self) -> String { self.header.get_name() } @@ -49,267 +50,47 @@ impl File { pub fn get_compressed_size(&self) -> u64 { self.header.get_compressed_size() } -} - -pub struct ZipFile { - pub end_of_central_directory: EndCentralDirectory, - pub zip64_end_of_central_directory: Option, - pub files: Vec, - pub apk_sign_block: Option, - pub data: T, -} - -impl ZipFile { - pub fn new(mut reader: T) -> Self { - let end_of_central_directory_off = - Self::get_end_of_central_directory_offset(&mut reader).unwrap(); - reader - .seek(SeekFrom::Start(end_of_central_directory_off)) - .unwrap(); - let end_of_central_directory = EndCentralDirectory::deserialize(&mut reader).unwrap(); - reader - .seek(SeekFrom::Start( - end_of_central_directory_off - Zip64EndCentralDirectoryLocator::SIZE as u64, - )) - .unwrap(); - let zip64_ecd_locator = Zip64EndCentralDirectoryLocator::deserialize(&mut reader).ok(); - let zip64_end_of_central_directory = if let Some(zip64_ecd_locator) = zip64_ecd_locator { - assert_eq!( - zip64_ecd_locator.disk_number_of_zip64_end_central_directory_start, - 0 - ); - assert!(zip64_ecd_locator.total_number_of_disks <= 1); - let zip64_edc_record_off = - zip64_ecd_locator.offset_zip64_end_of_central_directory_record; - reader.seek(SeekFrom::Start(zip64_edc_record_off)).unwrap(); - Zip64EndCentralDirectory::deserialize(&mut reader).ok() - } else { - None - }; - - // At this point python's ziplib recompute the location of the central directory from the - // location of the end of central directory in case the zip was concanated after a file. - // We probably don't need that for now. - let mut zip_file = Self { - end_of_central_directory, - zip64_end_of_central_directory, - data: reader, - files: vec![], - apk_sign_block: None, - }; - zip_file - .data - .seek(SeekFrom::Start(zip_file.get_cd_offset())) - .unwrap(); - - let mut size_read = 0; - let cd_size = zip_file.get_cd_size(); - while size_read < cd_size { - let header = FileHeader::deserialize(&mut zip_file.data).unwrap(); - size_read += header.size() as u64; - let pos_in_dir = zip_file.data.stream_position().unwrap(); - if header.general_purpose_flags & general_purpose_flags::MASK_ENCRYPTED_CENTRAL_DIR != 0 - { - panic!("Central directory encryption not supported"); - } - zip_file - .data - .seek(SeekFrom::Start(header.get_offset_local_header())) - .unwrap(); - let local_header = LocalFileHeader::deserialize(&mut zip_file.data).unwrap(); - zip_file.data.seek(SeekFrom::Start(pos_in_dir)).unwrap(); - zip_file.files.push(File { - local_header, - header, - }); - } - assert_eq!(size_read, cd_size); - if zip_file.get_cd_offset() > 16 { - zip_file - .data - .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16)) - .unwrap(); - let magic = Magic::deserialize(&mut zip_file.data).unwrap(); - if magic == ApkSigningBlock::MAGIC { - zip_file - .data - .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16 - 8)) - .unwrap(); - let block_size = u64::deserialize(&mut zip_file.data).unwrap(); - zip_file - .data - .seek(SeekFrom::Start(zip_file.get_cd_offset() - block_size - 8)) - .unwrap(); - - zip_file.apk_sign_block = ApkSigningBlock::deserialize(&mut zip_file.data).ok(); + 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; + } } } - - zip_file - } - - pub fn is_zip64(&self) -> bool { - self.zip64_end_of_central_directory.is_some() - } - - pub fn get_disk_num(&self) -> u32 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.number_of_this_disk - } else { - self.end_of_central_directory.disk_number as u32 - } - } - - pub fn get_disk_ed_start(&self) -> u32 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.disk_number_of_central_directory_start - } else { - self.end_of_central_directory - .disk_number_of_central_directory_start as u32 - } - } - - pub fn get_number_entries_on_disk(&self) -> u64 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.number_entry_in_central_directory_on_this_disk - } else { - self.end_of_central_directory - .number_of_entries_in_central_directory_on_disk as u64 - } - } - - pub fn get_number_entries(&self) -> u64 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.number_entry_in_central_directory - } else { - self.end_of_central_directory - .number_of_entries_in_central_directory as u64 - } - } - - pub fn get_cd_size(&self) -> u64 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.size_of_central_directory - } else { - self.end_of_central_directory.size_central_directory as u64 - } - } - - pub fn get_cd_offset(&self) -> u64 { - if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { - zip64_end_of_central_directory.offset_central_directory - } else { - self.end_of_central_directory.offset_central_directory as u64 - } - } - - pub fn get_end_of_central_directory_offset(reader: &mut T) -> Option { - let file_size = reader.seek(SeekFrom::End(0)).unwrap(); - let mut sig = Signature::default(); - let mut comment_size = 0; - while sig != EndCentralDirectory::SIGNATURE { - reader - .seek(SeekFrom::End( - -(EndCentralDirectory::MIN_SIZE as i64) - comment_size, - )) - .unwrap(); - sig = Signature::deserialize(reader).unwrap(); - comment_size += 1; - if comment_size > 65536 - || comment_size as usize + EndCentralDirectory::MIN_SIZE > file_size as usize - { - return 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; } } - comment_size -= 1; - Some(file_size - comment_size as u64 - EndCentralDirectory::MIN_SIZE as u64) - } - - pub fn get_file_names(&self) -> Vec { - self.files.iter().map(|f| f.get_name()).collect() - } - - /// Test if the zipfile contains files used to signe jar file (apk signature v1): - /// META-INF/MANIFEST.MF - /// META-INF/*.SF - /// META-INF/*.DSA - /// META-INF/*.RSA - /// META-INF/SIG-* - pub fn get_jar_sig_files(&self) -> Vec<&File> { - self.files - .iter() - .filter(|file| { - let name = file.get_name(); - let l = name.len(); - (name == "META-INF/MANIFEST.MF") - || (l >= 13 && &name[..9] == "META-INF/" && &name[l - 3..] == ".SF") - || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".DSA") - || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".RSA") - || (l >= 14 && &name[..13] == "META-INF/SIG-") - }) - .collect() - } - - /// Test if the zipfile contains files used to signe jar file (apk signature v1): - /// META-INF/MANIFEST.MF - /// META-INF/*.SF - /// META-INF/*.DSA - /// META-INF/*.RSA - /// META-INF/SIG-* - /// - /// TODO: there is a field `X-Android-APK-Signed` in .SF that indicate the use of v2 and v3 - /// (and v4?) signature. - pub fn is_signed_v1(&self) -> bool { - !self.get_jar_sig_files().is_empty() - } - - /// Test if the zipfile as apk signature block. - pub fn is_signed_v2(&self) -> bool { - self.apk_sign_block.is_some() - } - - pub fn check_holes(&self) { - let mut files: Vec<&File> = self.files.iter().collect(); - files.sort_by_key(|f| f.get_offset_local_header()); - let mut lst_offset = 0; - for file in files.iter() { - if file.get_offset_local_header() != lst_offset { - println!( - "Hole before {} between 0x{:x} and 0x{:x}", - file.get_name(), - lst_offset, - file.get_offset_local_header() - ); - } - lst_offset += file.local_header.size() as u64; - lst_offset += file.get_compressed_size(); + 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 let Some(apk_sign_block) = &self.apk_sign_block { - let apk_sb_off = self.get_cd_offset() - apk_sign_block.size() as u64; - if apk_sb_off != lst_offset { - println!( - "Hole before apk signing block, between 0x{:x} and 0x{:x}", - lst_offset, apk_sb_off - ); - } - - lst_offset += apk_sign_block.size() as u64; + if use_z64 { + self.header.offset_local_header = u32::MAX; + } else { + self.header.offset_local_header = new_offset as u32; } - if self.get_cd_offset() != lst_offset { - println!( - "Hole before central directory between 0x{:x} and 0x{:x}", - lst_offset, - self.get_cd_offset() - ); - } - } - - pub fn get_bin(&mut self, offset: u64, size: usize) -> Vec { - self.data.seek(SeekFrom::Start(offset)).unwrap(); - let mut data = vec![]; - for _ in 0..size { - data.push(u8::deserialize(&mut self.data).unwrap()); - } - data } } diff --git a/apk_frauder/src/local_file_header.rs b/apk_frauder/src/local_file_header.rs index de54f43..c0f9ac7 100644 --- a/apk_frauder/src/local_file_header.rs +++ b/apk_frauder/src/local_file_header.rs @@ -1,5 +1,6 @@ use std::io::{SeekFrom, Write}; +use crate::compression::CompressionMethod; use crate::extra_fields::{ExtraField, GenericExtraField, Zip64ExtraField}; use crate::{cp437, general_purpose_flags, Encoding, Signature}; use androscalpel_serializer::{ReadSeek, Result, Serializable}; @@ -9,7 +10,7 @@ pub struct LocalFileHeader { // signature: Signature(0x04034b50) pub version_needed_to_extract: u16, pub general_purpose_flags: u16, - pub compression_method: u16, + pub compression_method: CompressionMethod, pub last_mod_file_time: u16, pub last_mod_file_data: u16, pub crc_32: u32, @@ -57,7 +58,7 @@ impl Serializable for LocalFileHeader { assert_eq!(signature, Self::SIGNATURE); // TODO let version_needed_to_extract = u16::deserialize(input)?; let general_purpose_flags = u16::deserialize(input)?; - let compression_method = u16::deserialize(input)?; + let compression_method = CompressionMethod::deserialize(input)?; let last_mod_file_time = u16::deserialize(input)?; let last_mod_file_data = u16::deserialize(input)?; let crc_32 = u32::deserialize(input)?; @@ -103,8 +104,10 @@ impl Serializable for LocalFileHeader { header.extra_field.push(field); } } + let mut failed_last_extra_field = false; if extra_size_read > extra_field_length as usize { - println!("Failed to parsed last extra field in {}", header.get_name()); + //println!("Failed to parse last extra field in {}", header.get_name()); + failed_last_extra_field = true; let size = header.extra_field.pop().unwrap().size(); input.seek(SeekFrom::Current(-(size as i64))).unwrap(); } @@ -113,6 +116,14 @@ impl Serializable for LocalFileHeader { header.malformed_extra_field.push(u8::deserialize(input)?); extra_size_read += 1; } + // If it is not padding from zipalign + if failed_last_extra_field + && header.malformed_extra_field != vec![0] + && header.malformed_extra_field != vec![0, 0] + && header.malformed_extra_field != vec![0, 0, 0] + { + println!("Failed to parse last extra field in {}", header.get_name()); + } //input.seek(SeekFrom::Start(end_of_extra_field)).unwrap(); for field in &mut header.extra_field { if let ExtraField::Generic(GenericExtraField { diff --git a/apk_frauder/src/main.rs b/apk_frauder/src/main.rs index 6d1b6bd..7306c1e 100644 --- a/apk_frauder/src/main.rs +++ b/apk_frauder/src/main.rs @@ -1,18 +1,20 @@ -use apk_frauder::ZipFile; +use apk_frauder::ZipFileReader; use std::fs::File; fn main() { let file = File::open("app-release.apk").expect("failed to open file"); //let file = File::open("tst_64.zip").expect("failed to open file"); - let zip_file = ZipFile::new(file); + let zip_file = ZipFileReader::new(file); //println!("{}", zip_file.get_file_names().join("\n")); + /* for file in &zip_file.files { println!("{}", file.get_name()); println!("local: {:?}", file.local_header.malformed_extra_field); println!("central dir: {:?}", file.header.malformed_extra_field); println!(); } - /*println!( + + println!( "uncompressed size: {}", zip_file.files[0].get_uncompressed_size() ); @@ -24,11 +26,13 @@ fn main() { .map(|f| f.get_name()) .collect::>() .join("\n") - );*/ + ); if zip_file.is_signed_v2() { println!("Signed >= v2"); } else { println!("Not signed whith scheme >= v2"); } zip_file.check_holes(); + */ + println!("{:#?}", zip_file.get_file_info("classes.dex")); } diff --git a/apk_frauder/src/zip_reader.rs b/apk_frauder/src/zip_reader.rs new file mode 100644 index 0000000..207982a --- /dev/null +++ b/apk_frauder/src/zip_reader.rs @@ -0,0 +1,285 @@ +use std::io::{Read, Seek, SeekFrom}; + +use crate::{ + apk_signing_block::ApkSigningBlock, apk_signing_block::Magic, + end_of_central_directory::EndCentralDirectory, + end_of_central_directory::Zip64EndCentralDirectory, + end_of_central_directory::Zip64EndCentralDirectoryLocator, general_purpose_flags, FileHeader, + FileInfo, LocalFileHeader, Signature, +}; +use androscalpel_serializer::Serializable; + +pub struct ZipFileReader { + pub end_of_central_directory: EndCentralDirectory, + pub zip64_end_of_central_directory: Option, + pub files: Vec, + pub apk_sign_block: Option, + pub data: T, +} + +impl ZipFileReader { + pub fn new(mut reader: T) -> Self { + let end_of_central_directory_off = + Self::get_end_of_central_directory_offset(&mut reader).unwrap(); + reader + .seek(SeekFrom::Start(end_of_central_directory_off)) + .unwrap(); + let end_of_central_directory = EndCentralDirectory::deserialize(&mut reader).unwrap(); + reader + .seek(SeekFrom::Start( + end_of_central_directory_off - Zip64EndCentralDirectoryLocator::SIZE as u64, + )) + .unwrap(); + let zip64_ecd_locator = Zip64EndCentralDirectoryLocator::deserialize(&mut reader).ok(); + let zip64_end_of_central_directory = if let Some(zip64_ecd_locator) = zip64_ecd_locator { + assert_eq!( + zip64_ecd_locator.disk_number_of_zip64_end_central_directory_start, + 0 + ); + assert!(zip64_ecd_locator.total_number_of_disks <= 1); + let zip64_edc_record_off = + zip64_ecd_locator.offset_zip64_end_of_central_directory_record; + reader.seek(SeekFrom::Start(zip64_edc_record_off)).unwrap(); + Zip64EndCentralDirectory::deserialize(&mut reader).ok() + } else { + None + }; + + // At this point python's ziplib recompute the location of the central directory from the + // location of the end of central directory in case the zip was concanated after a file. + // We probably don't need that for now. + let mut zip_file = Self { + end_of_central_directory, + zip64_end_of_central_directory, + data: reader, + files: vec![], + apk_sign_block: None, + }; + zip_file + .data + .seek(SeekFrom::Start(zip_file.get_cd_offset())) + .unwrap(); + + let mut size_read = 0; + let cd_size = zip_file.get_cd_size(); + while size_read < cd_size { + let header = FileHeader::deserialize(&mut zip_file.data).unwrap(); + size_read += header.size() as u64; + let pos_in_dir = zip_file.data.stream_position().unwrap(); + if header.general_purpose_flags & general_purpose_flags::MASK_ENCRYPTED_CENTRAL_DIR != 0 + { + panic!("Central directory encryption not supported"); + } + zip_file + .data + .seek(SeekFrom::Start(header.get_offset_local_header())) + .unwrap(); + let local_header = LocalFileHeader::deserialize(&mut zip_file.data).unwrap(); + zip_file.data.seek(SeekFrom::Start(pos_in_dir)).unwrap(); + if (local_header.general_purpose_flags + & general_purpose_flags::MASK_USE_DATA_DESCRIPTOR + != 0) + || (header.general_purpose_flags & general_purpose_flags::MASK_USE_DATA_DESCRIPTOR + != 0) + { + panic!("Data Descriptor not yet suported"); + } + zip_file.files.push(FileInfo { + local_header, + header, + }); + } + assert_eq!(size_read, cd_size); + if zip_file.get_cd_offset() > 16 { + zip_file + .data + .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16)) + .unwrap(); + let magic = Magic::deserialize(&mut zip_file.data).unwrap(); + if magic == ApkSigningBlock::MAGIC { + zip_file + .data + .seek(SeekFrom::Start(zip_file.get_cd_offset() - 16 - 8)) + .unwrap(); + let block_size = u64::deserialize(&mut zip_file.data).unwrap(); + zip_file + .data + .seek(SeekFrom::Start(zip_file.get_cd_offset() - block_size - 8)) + .unwrap(); + + zip_file.apk_sign_block = ApkSigningBlock::deserialize(&mut zip_file.data).ok(); + } + } + + zip_file + } + + pub fn is_zip64(&self) -> bool { + self.zip64_end_of_central_directory.is_some() + } + + pub fn get_disk_num(&self) -> u32 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.number_of_this_disk + } else { + self.end_of_central_directory.disk_number as u32 + } + } + + pub fn get_disk_ed_start(&self) -> u32 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.disk_number_of_central_directory_start + } else { + self.end_of_central_directory + .disk_number_of_central_directory_start as u32 + } + } + + pub fn get_number_entries_on_disk(&self) -> u64 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.number_entry_in_central_directory_on_this_disk + } else { + self.end_of_central_directory + .number_of_entries_in_central_directory_on_disk as u64 + } + } + + pub fn get_number_entries(&self) -> u64 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.number_entry_in_central_directory + } else { + self.end_of_central_directory + .number_of_entries_in_central_directory as u64 + } + } + + pub fn get_cd_size(&self) -> u64 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.size_of_central_directory + } else { + self.end_of_central_directory.size_central_directory as u64 + } + } + + pub fn get_cd_offset(&self) -> u64 { + if let Some(zip64_end_of_central_directory) = &self.zip64_end_of_central_directory { + zip64_end_of_central_directory.offset_central_directory + } else { + self.end_of_central_directory.offset_central_directory as u64 + } + } + + pub fn get_end_of_central_directory_offset(reader: &mut T) -> Option { + let file_size = reader.seek(SeekFrom::End(0)).unwrap(); + let mut sig = Signature::default(); + let mut comment_size = 0; + while sig != EndCentralDirectory::SIGNATURE { + reader + .seek(SeekFrom::End( + -(EndCentralDirectory::MIN_SIZE as i64) - comment_size, + )) + .unwrap(); + sig = Signature::deserialize(reader).unwrap(); + comment_size += 1; + if comment_size > 65536 + || comment_size as usize + EndCentralDirectory::MIN_SIZE > file_size as usize + { + return None; + } + } + comment_size -= 1; + Some(file_size - comment_size as u64 - EndCentralDirectory::MIN_SIZE as u64) + } + + pub fn get_file_names(&self) -> Vec { + self.files.iter().map(|f| f.get_name()).collect() + } + + /// List files used to signe jar file (apk signature v1) found in the zip file: + /// META-INF/MANIFEST.MF + /// META-INF/*.SF + /// META-INF/*.DSA + /// META-INF/*.RSA + /// META-INF/SIG-* + pub fn get_jar_sig_files(&self) -> Vec<&FileInfo> { + self.files + .iter() + .filter(|file| { + let name = file.get_name(); + let l = name.len(); + (name == "META-INF/MANIFEST.MF") + || (l >= 13 && &name[..9] == "META-INF/" && &name[l - 3..] == ".SF") + || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".DSA") + || (l >= 14 && &name[..9] == "META-INF/" && &name[l - 4..] == ".RSA") + || (l >= 14 && &name[..13] == "META-INF/SIG-") + }) + .collect() + } + + /// Test if the zipfile contains files used to signe jar file (apk signature v1): + /// META-INF/MANIFEST.MF + /// META-INF/*.SF + /// META-INF/*.DSA + /// META-INF/*.RSA + /// META-INF/SIG-* + /// + /// TODO: there is a field `X-Android-APK-Signed` in .SF that indicate the use of v2 and v3 + /// (and v4?) signature. + pub fn is_signed_v1(&self) -> bool { + !self.get_jar_sig_files().is_empty() + } + + /// Test if the zipfile as apk signature block. + pub fn is_signed_v2(&self) -> bool { + self.apk_sign_block.is_some() + } + + pub fn check_holes(&self) { + let mut files: Vec<&FileInfo> = self.files.iter().collect(); + files.sort_by_key(|f| f.get_offset_local_header()); + let mut lst_offset = 0; + for file in files.iter() { + if file.get_offset_local_header() != lst_offset { + println!( + "Hole before {} between 0x{:x} and 0x{:x}", + file.get_name(), + lst_offset, + file.get_offset_local_header() + ); + } + lst_offset += file.local_header.size() as u64; + lst_offset += file.get_compressed_size(); + } + if let Some(apk_sign_block) = &self.apk_sign_block { + let apk_sb_off = self.get_cd_offset() - apk_sign_block.size() as u64; + if apk_sb_off != lst_offset { + println!( + "Hole before apk signing block, between 0x{:x} and 0x{:x}", + lst_offset, apk_sb_off + ); + } + + lst_offset += apk_sign_block.size() as u64; + } + if self.get_cd_offset() != lst_offset { + println!( + "Hole before central directory between 0x{:x} and 0x{:x}", + lst_offset, + self.get_cd_offset() + ); + } + } + + pub fn get_bin(&mut self, offset: u64, size: usize) -> Vec { + self.data.seek(SeekFrom::Start(offset)).unwrap(); + let mut data = vec![]; + for _ in 0..size { + data.push(u8::deserialize(&mut self.data).unwrap()); + } + data + } + + pub fn get_file_info(&self, name: &str) -> Option<&FileInfo> { + self.files.iter().find(|&file| file.get_name() == name) + } +} diff --git a/apk_frauder/src/zip_writer.rs b/apk_frauder/src/zip_writer.rs new file mode 100644 index 0000000..28b3db1 --- /dev/null +++ b/apk_frauder/src/zip_writer.rs @@ -0,0 +1,73 @@ +use std::io::{Read, Seek, SeekFrom, Write}; + +use crate::{ + end_of_central_directory::EndCentralDirectory, + end_of_central_directory::Zip64EndCentralDirectory, FileInfo, ZipFileReader, +}; +use androscalpel_serializer::Serializable; + +pub struct ZipFileWriter { + files: Vec, + zip64_end_of_central_directory: Option, + current_offset: u64, + data: T, +} + +impl ZipFileWriter { + pub fn new(writer: T, zip64_info: Option) -> Self { + Self { + current_offset: 0, + zip64_end_of_central_directory: zip64_info, + files: vec![], + data: writer, + } + } + + /// Insert an file from an existing zip + pub fn insert_file_from_zip( + &mut self, + mut file_info: FileInfo, + zip_file: &mut ZipFileReader, + ) { + let file_off = file_info.get_file_offset(); + let file_size = file_info.get_compressed_size(); + file_info.set_offset_local_header(self.current_offset); + file_info.local_header.serialize(&mut self.data).unwrap(); + zip_file.data.seek(SeekFrom::Start(file_off)).unwrap(); + let mut remaining_size = file_size; + let mut buffer = [0u8; 4096]; + while remaining_size != 0 { + let read = zip_file + .data + .read(&mut buffer[0..core::cmp::min(4096, remaining_size as usize)]) + .unwrap(); + self.data.write_all(&buffer[0..read]).unwrap(); + remaining_size -= read as u64; + } + self.current_offset += file_info.local_header.size() as u64 + file_size; + self.files.push(file_info); + } + + /// Finish the zip file by writing the central directory. + pub fn write_central_directory(&mut self) { + for file_info in &self.files { + file_info.header.serialize(&mut self.data).unwrap(); + } + // TODO: zip64 + EndCentralDirectory { + disk_number: 0, + disk_number_of_central_directory_start: 0, + number_of_entries_in_central_directory_on_disk: self.files.len() as u16, + number_of_entries_in_central_directory: self.files.len() as u16, + size_central_directory: self + .files + .iter() + .map(|file| file.local_header.size() as u32 + file.get_compressed_size() as u32) + .sum(), + offset_central_directory: self.current_offset as u32, + comment: vec![], + } + .serialize(&mut self.data) + .unwrap(); + } +}