From 6d77df2b79d7c8e2d2fe5ade02c8d78ca2ecf8d8 Mon Sep 17 00:00:00 2001 From: Jean-Marie 'Histausse' Mineau Date: Wed, 14 Feb 2024 17:59:10 +0100 Subject: [PATCH] add parsing of smali id --- androscalpel/src/dex_id.rs | 212 +++++++++++++++++++++++++++++++++- androscalpel/src/tests/mod.rs | 130 ++++++++++++++++++--- 2 files changed, 325 insertions(+), 17 deletions(-) diff --git a/androscalpel/src/dex_id.rs b/androscalpel/src/dex_id.rs index 2054e51..e115f99 100644 --- a/androscalpel/src/dex_id.rs +++ b/androscalpel/src/dex_id.rs @@ -6,7 +6,7 @@ use std::collections::hash_map::DefaultHasher; use std::collections::HashSet; use std::hash::{Hash, Hasher}; -use anyhow::anyhow; +use anyhow::{anyhow, bail, Context}; use pyo3::prelude::*; use crate::{scalar::*, DexString, DexValue, Result}; @@ -59,6 +59,53 @@ impl IdMethodType { } } + /// Try to parse a smali representation of a prototype into a IdMethodType. + /// + /// ``` + /// use androscalpel::IdMethodType; + /// + /// let proto = IdMethodType::from_smali("(Landroidx/core/util/Predicate;Landroidx/core/util/Predicate;Ljava/lang/Object;)Z").unwrap(); + /// assert_eq!( + /// proto, + /// IdMethodType( + /// IdType::boolean(), + /// vec![ + /// IdType::class("androidx/core/util/Predicate"), + /// IdType::class("androidx/core/util/Predicate"), + /// IdType::class("java/lang/Object") + /// ] + /// ) + /// ); + /// ``` + #[staticmethod] + pub fn from_smali(smali_repr: &str) -> Result { + if smali_repr.len() < 2 { + bail!("'{smali_repr}' is to short to be a prototype"); + } + if &smali_repr[0..1] != "(" { + bail!("'{smali_repr}' does not begin by a '('"); + } + let closing_par_i = smali_repr.find(')'); + if closing_par_i.is_none() { + bail!("'{smali_repr}' does not contians a ')'"); + } + let closing_par_i = closing_par_i.unwrap(); + let parameters = + IdType::get_list_from_str(&smali_repr[1..closing_par_i]).with_context(|| { + format!("Failed to parse smali repr of parameters of prototype {smali_repr}") + })?; + let return_type = + IdType::new((&smali_repr[closing_par_i + 1..]).into()).with_context(|| { + format!("Failed to parse smali repr of return type of {smali_repr}") + })?; + let shorty = Self::compute_shorty(&return_type, ¶meters); + Ok(Self { + shorty, + return_type, + parameters, + }) + } + pub fn __str__(&self) -> String { format!( "({}){}", @@ -180,6 +227,76 @@ impl IdType { Self(ty) } + /// Return a list of types from a string of concatenated types (like the ones between + /// parentheses in the repr of a prototype) + /// + /// # Example + /// + /// ``` + /// use androscalpel::IdType; + /// + /// let types = IdType::get_list_from_str("IILjava/lang/Object;Ljava/lang/Object;Z").unwrap(); + /// assert_eq!(types, vec![ + /// IdType::int(), + /// IdType::int(), + /// IdType::class("java/lang/Object"), + /// IdType::class("java/lang/Object"), + /// IdType::boolean(), + /// ]) + /// ``` + #[staticmethod] + pub fn get_list_from_str(type_list: &str) -> Result> { + let mut lst = vec![]; + let mut chars = type_list.chars(); + let mut array_dimmention = 0; + while let Some(c) = chars.next() { + let new_type = match c { + 'V' => Some(Self::void()), + 'Z' => Some(Self::boolean()), + 'B' => Some(Self::byte()), + 'S' => Some(Self::short()), + 'C' => Some(Self::char()), + 'I' => Some(Self::int()), + 'J' => Some(Self::long()), + 'F' => Some(Self::float()), + 'D' => Some(Self::double()), + '[' => { array_dimmention += 1; None }, + 'L' => { + let mut class_name = String::new(); + for cc in chars.by_ref(){ + if cc == ';' { break;} + else { + class_name.push(cc); + } + + } + Some(Self::class(&class_name)) + } + _ => bail!( + "Could not parse {type_list} as a list of types, found invalid char {c} outside a class name" + ), + }; + if let Some(mut new_type) = new_type { + if array_dimmention != 0 { + let mut data = vec![0u8; array_dimmention + new_type.0 .0.data.len()]; + for c in &mut data[..array_dimmention] { + *c = 0x5b; + } + data[array_dimmention..].copy_from_slice(&new_type.0 .0.data); + new_type.0 .0.data = data; + new_type.0 .0.utf16_size.0 += array_dimmention as u32; + } + + lst.push(new_type); + array_dimmention = 0; + } + } + if array_dimmention != 0 { + bail!("Could not parse {type_list}: finishing with [") + } + Ok(lst) + } + /// Return the void type (for return type) #[staticmethod] pub fn void() -> Self { @@ -236,7 +353,7 @@ impl IdType { /// Return the type for the class of fully qualified name `name` #[staticmethod] pub fn class(name: &str) -> Self { - Self(format!("L{name}").into()) + Self(format!("L{name};").into()) } /// Return the type for an array of the specify `type_` @@ -491,6 +608,46 @@ impl IdField { } } + /// Try to parse a smali representation of a field into a IdField. + /// + /// ``` + /// use androscalpel::IdField; + /// + /// let proto = IdField::from_smali("Ljava/lang/annotation/ElementType;->METHOD:Ljava/lang/annotation/ElementType;").unwrap(); + /// assert_eq!( + /// proto, + /// IdField::new( + /// "METHOD".into(), + /// IdType::class("java/lang/annotation/ElementType"), + /// IdType::class("java/lang/annotation/ElementType"), + /// ) + /// ); + /// ``` + #[staticmethod] + pub fn from_smali(smali_repr: &str) -> Result { + let arrow_i = smali_repr.find("->"); + if arrow_i.is_none() { + bail!("'{smali_repr}' does not contains a '->'"); + } + let arrow_i = arrow_i.unwrap(); + let dots_i = smali_repr.find(':'); + if dots_i.is_none() { + bail!("'{smali_repr}' does not contains a ':("); + } + let dots_i = dots_i.unwrap(); + let type_ = IdType::new((&smali_repr[dots_i + 1..]).into()) + .with_context(|| format!("Failed to parse smali repr of type of field {smali_repr}"))?; + let class_ = IdType::new((&smali_repr[..arrow_i]).into()).with_context(|| { + format!("Failed to parse smali repr of class type of method {smali_repr}") + })?; + let name = (&smali_repr[arrow_i + 2..dots_i]).into(); + Ok(Self { + name, + type_, + class_, + }) + } + pub fn __str__(&self) -> String { let class: String = self.class_.get_name().into(); let name: String = (&self.name).into(); @@ -594,6 +751,57 @@ impl IdMethod { } } + /// Try to parse a smali representation of method into a IdMethod. + /// + /// ``` + /// use androscalpel::IdMethod; + /// + /// let id_method = IdMethod::from_smali( + /// "Landroidx/core/util/Predicate;->lambda$and$0(Landroidx/core/util/Predicate;Landroidx/core/util/Predicate;Ljava/lang/Object;)Z" + /// ).unwrap(); + /// + /// assert_eq!( + /// id_method, + /// IdMethod::new( + /// "lambda$and$0".into(), + /// IdMethodType::new( + /// IdType::boolean(), + /// vec![ + /// IdType::class("androidx/core/util/Predicate"), + /// IdType::class("androidx/core/util/Predicate"), + /// IdType::class("java/lang/Object") + /// ] + /// ), + /// IdType::class("androidx/core/util/Predicate") + /// ) + /// ); + /// ``` + #[staticmethod] + pub fn from_smali(smali_repr: &str) -> Result { + let arrow_i = smali_repr.find("->"); + if arrow_i.is_none() { + bail!("'{smali_repr}' does not contains a '->'"); + } + let arrow_i = arrow_i.unwrap(); + let openning_par_i = smali_repr.find('('); + if openning_par_i.is_none() { + bail!("'{smali_repr}' does not contains a '('"); + } + let openning_par_i = openning_par_i.unwrap(); + let proto = IdMethodType::from_smali(&smali_repr[openning_par_i..]).with_context(|| { + format!("Failed to parse smali repr of prototype of method {smali_repr}") + })?; + let class_ = IdType::new((&smali_repr[..arrow_i]).into()).with_context(|| { + format!("Failed to parse smali repr of class type of method {smali_repr}") + })?; + let name = (&smali_repr[arrow_i + 2..openning_par_i]).into(); + Ok(Self { + name, + proto, + class_, + }) + } + pub fn __str__(&self) -> String { format!( "{}->{}({}){}", diff --git a/androscalpel/src/tests/mod.rs b/androscalpel/src/tests/mod.rs index 2e7d73d..4392ae3 100644 --- a/androscalpel/src/tests/mod.rs +++ b/androscalpel/src/tests/mod.rs @@ -2,6 +2,7 @@ use super::*; use androscalpel_serializer::*; use std::fs::File; use std::io; +use std::sync::OnceLock; fn get_dex(filename: &str) -> Vec { let hello_world_dex = format!("{}/src/tests/{}", env!("CARGO_MANIFEST_DIR"), filename); @@ -11,19 +12,33 @@ fn get_dex(filename: &str) -> Vec { data } +fn get_hello_world_dex() -> &'static [u8] { + static HELLO_WORLD_DEX: OnceLock> = OnceLock::new(); + HELLO_WORLD_DEX.get_or_init(|| get_dex("classes_hello_world.dex")) +} + +fn get_hello_world_apk() -> &'static Apk { + static HELLO_WORLD_APK: OnceLock = OnceLock::new(); + HELLO_WORLD_APK.get_or_init(|| { + let mut apk = Apk::new(); + apk.add_dex_file(get_hello_world_dex()).unwrap(); + apk + }) +} + +fn get_hello_world_recompilled() -> &'static [u8] { + static HELLO_WORLD_RECOMP: OnceLock> = OnceLock::new(); + HELLO_WORLD_RECOMP.get_or_init(|| get_hello_world_apk().gen_raw_dex().unwrap().pop().unwrap()) +} + #[test] fn test_generated_data_size() { - let mut apk = Apk::new(); - let dex_data = get_dex("classes_hello_world.dex"); - apk.add_dex_file(&dex_data).unwrap(); - let new_dex = apk.gen_raw_dex().unwrap(); - assert_eq!(new_dex.len(), 1); - let new_dex = new_dex.first().unwrap(); - let dex = DexFileReader::new(&new_dex).unwrap(); + let dex_bin = get_hello_world_recompilled(); + let dex = DexFileReader::new(dex_bin).unwrap(); assert_eq!( dex.get_header().data_off + dex.get_header().data_size, - new_dex.len() as u32 + dex_bin.len() as u32 ) // TODO: check for all pool concerned if the pool span outside the data section? //for item in dex.get_map_list().list() {} @@ -31,13 +46,98 @@ fn test_generated_data_size() { #[test] fn test_generated_apk_equivalence() { - let mut apk = Apk::new(); - let dex_data = get_dex("classes_hello_world.dex"); - apk.add_dex_file(&dex_data).unwrap(); - let new_dex = apk.gen_raw_dex().unwrap(); - assert_eq!(new_dex.len(), 1); - let new_dex = new_dex.first().unwrap(); + let new_dex = get_hello_world_recompilled(); let mut new_apk = Apk::new(); new_apk.add_dex_file(&new_dex).unwrap(); - assert_eq!(apk, new_apk); + assert_eq!(get_hello_world_apk(), &new_apk); +} + +#[test] +fn test_string_order() { + use std::collections::HashSet; + let dex = DexFileReader::new(get_hello_world_dex()).unwrap(); + let new_dex = DexFileReader::new(get_hello_world_recompilled()).unwrap(); + let mut string_set = HashSet::new(); + for i in 0..new_dex.get_string_ids().len() { + string_set.insert(DexString(new_dex.get_string(i as u32).unwrap())); + } + for i in 0..dex.get_string_ids().len() { + let string = DexString(dex.get_string(i as u32).unwrap()); + if !string_set.contains(&string) { + println!("{}", string.__str__()); + } + } + assert_eq!(dex.get_string_ids().len(), new_dex.get_string_ids().len()); +} + +#[test] +fn test_parse_type_list() { + let types = IdType::get_list_from_str("IILjavalangObject;LjavalangObject;Z").unwrap(); + assert_eq!( + types, + vec![ + IdType::int(), + IdType::int(), + IdType::class("javalangObject"), + IdType::class("javalangObject"), + IdType::boolean(), + ] + ) +} + +#[test] +fn test_parse_proto_smali() { + let proto = IdMethodType::from_smali( + "(LandroidxcoreutilPredicate;LandroidxcoreutilPredicate;LjavalangObject;)Z", + ) + .unwrap(); + assert_eq!( + proto, + IdMethodType::new( + IdType::boolean(), + vec![ + IdType::class("androidxcoreutilPredicate"), + IdType::class("androidxcoreutilPredicate"), + IdType::class("javalangObject") + ] + ) + ); +} + +#[test] +fn test_parse_method_smali() { + let id_method = IdMethod::from_smali( + "Landroidx/core/util/Predicate;->lambda$and$0(Landroidx/core/util/Predicate;Landroidx/core/util/Predicate;Ljava/lang/Object;)Z" + ).unwrap(); + + assert_eq!( + id_method, + IdMethod::new( + "lambda$and$0".into(), + IdMethodType::new( + IdType::boolean(), + vec![ + IdType::class("androidx/core/util/Predicate"), + IdType::class("androidx/core/util/Predicate"), + IdType::class("java/lang/Object") + ] + ), + IdType::class("androidx/core/util/Predicate") + ) + ); +} +#[test] +fn test_parse_field_smali() { + let proto = IdField::from_smali( + "Ljava/lang/annotation/ElementType;->METHOD:Ljava/lang/annotation/ElementType;", + ) + .unwrap(); + assert_eq!( + proto, + IdField::new( + "METHOD".into(), + IdType::class("java/lang/annotation/ElementType"), + IdType::class("java/lang/annotation/ElementType"), + ) + ); }