224 lines
7.5 KiB
Rust
224 lines
7.5 KiB
Rust
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<Path>,
|
|
dst: impl AsRef<Path>,
|
|
dexfiles: &mut [impl Read + Seek],
|
|
keystore: impl AsRef<Path>, // 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<impl AsRef<Path>>,
|
|
apksigner: Option<impl AsRef<Path>>,
|
|
) {
|
|
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::<u128>()));
|
|
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");
|
|
}
|