add support for read hiddenapi

This commit is contained in:
Jean-Marie Mineau 2024-06-10 18:20:45 +02:00
parent df9d258780
commit 437ecbeecc
Signed by: histausse
GPG key ID: B66AEEDA9B645AD2
17 changed files with 764 additions and 92 deletions

32
Cargo.lock generated
View file

@ -335,6 +335,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "portable-atomic"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -352,9 +358,9 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.20.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b"
checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
dependencies = [
"anyhow",
"cfg-if",
@ -362,6 +368,7 @@ dependencies = [
"libc",
"memoffset",
"parking_lot",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
@ -370,9 +377,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.20.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5"
checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
dependencies = [
"once_cell",
"target-lexicon",
@ -380,9 +387,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.20.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b"
checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
dependencies = [
"libc",
"pyo3-build-config",
@ -390,9 +397,9 @@ dependencies = [
[[package]]
name = "pyo3-log"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c10808ee7250403bedb24bc30c32493e93875fef7ba3e4292226fe924f398bd"
checksum = "2af49834b8d2ecd555177e63b273b708dea75150abc6f5341d0a6e1a9623976c"
dependencies = [
"arc-swap",
"log",
@ -401,9 +408,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.20.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b"
checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@ -413,12 +420,13 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.20.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424"
checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]

View file

@ -14,8 +14,8 @@ androscalpel_serializer = { version = "0.1.0", path = "../androscalpel_serialize
anyhow = { version = "1.0.75", features = ["backtrace"] }
apk_frauder = { version = "0.1.0", path = "../apk_frauder" }
log = "0.4.20"
pyo3 = { version = "0.20.0", features = ["anyhow", "abi3-py38"] }
pyo3-log = "0.9.0"
pyo3 = { version = "0.21.0", features = ["anyhow", "abi3-py38"] }
pyo3-log = "0.10.0"
rayon = "1.9.0"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"

View file

@ -35,7 +35,8 @@ impl Apk {
let classes = dex
.get_class_defs()
.par_iter()
.map(|class| self.get_class_from_dex_file(class, &dex))
.enumerate()
.map(|(idx, class)| self.get_class_from_dex_file(class, idx, &dex))
.map(|class| class.map(|class| (class.descriptor.clone(), class)))
.collect::<Result<Vec<_>, _>>()?;
self.classes.par_extend(classes);
@ -45,9 +46,12 @@ impl Apk {
}
/// Extract a class from a dex file reader.
/// `class_item_idx` if the index of the `class_def_item` of the class, **not** the
/// `class_idx`.
fn get_class_from_dex_file(
&self,
class_item: &ClassDefItem,
class_item_idx: usize,
dex: &DexFileReader,
) -> Result<Class> {
let descriptor = Self::get_id_type_from_idx(class_item.class_idx as usize, dex)?;
@ -112,6 +116,8 @@ impl Apk {
}
}
let hiddenapi = dex.get_class_hiddenapi_flags(class_item_idx)?;
let mut hiddenapi_i = 0;
let mut static_fields_list = vec![];
let mut instance_fields_list = vec![];
let mut direct_methods = HashMap::new();
@ -123,12 +129,32 @@ impl Apk {
Self::get_field_list_from_encoded_field_list(&data.static_fields, dex)?;
instance_fields_list =
Self::get_field_list_from_encoded_field_list(&data.instance_fields, dex)?;
for method in Self::get_method_list_from_encoded_field_list(&data.direct_methods, dex)?
if let Some(hiddenapi) = &hiddenapi {
for field in &mut static_fields_list {
field.hiddenapi = Some((&hiddenapi[hiddenapi_i]).into());
hiddenapi_i += 1;
}
for field in &mut instance_fields_list {
field.hiddenapi = Some((&hiddenapi[hiddenapi_i]).into());
hiddenapi_i += 1;
}
}
for mut method in
Self::get_method_list_from_encoded_field_list(&data.direct_methods, dex)?
{
if let Some(hiddenapi) = &hiddenapi {
method.hiddenapi = Some((&hiddenapi[hiddenapi_i]).into());
hiddenapi_i += 1;
}
direct_methods.insert(method.descriptor.clone(), method);
}
for method in Self::get_method_list_from_encoded_field_list(&data.virtual_methods, dex)?
for mut method in
Self::get_method_list_from_encoded_field_list(&data.virtual_methods, dex)?
{
if let Some(hiddenapi) = &hiddenapi {
method.hiddenapi = Some((&hiddenapi[hiddenapi_i]).into());
hiddenapi_i += 1;
}
virtual_methods.insert(method.descriptor.clone(), method);
}
}
@ -617,6 +643,7 @@ impl Apk {
is_synthetic,
is_enum,
value: None,
hiddenapi: None,
annotations: vec![],
})
}
@ -746,6 +773,7 @@ impl Apk {
is_declared_syncrhonized,
annotations: vec![],
parameters_annotations: vec![],
hiddenapi: None,
code,
})
}
@ -2444,7 +2472,7 @@ impl Apk {
Ok(self
.gen_raw_dex()?
.into_iter()
.map(|bytes| PyBytes::new(py, &bytes).into())
.map(|bytes| PyBytes::new_bound(py, &bytes).into())
.collect())
}

View file

@ -316,7 +316,7 @@ impl Class {
.any(|field| field.value.is_some())
}
/// If the class or its fields/methods have annotations
/// Check if the class or its fields/methods have annotations
pub fn has_annotations(&self) -> bool {
!self.annotations.is_empty()
|| self
@ -330,11 +330,30 @@ impl Class {
|| self
.direct_methods
.values()
.any(|field| field.has_annotations())
.any(|method| method.has_annotations())
|| self
.virtual_methods
.values()
.any(|field| field.has_annotations())
.any(|method| method.has_annotations())
}
/// Check if the class have a field of method with hiddenapi information
pub fn has_hiddenapi(&self) -> bool {
self.static_fields
.values()
.any(|field| field.hiddenapi.is_some())
|| self
.instance_fields
.values()
.any(|field| field.hiddenapi.is_some())
|| self
.direct_methods
.values()
.any(|method| method.hiddenapi.is_some())
|| self
.virtual_methods
.values()
.any(|method| method.hiddenapi.is_some())
}
/// Return the binary representation of access flags.

View file

@ -667,11 +667,11 @@ impl IdField {
/// ```
/// use androscalpel::IdField;
///
/// let proto = IdField::from_smali("Ljava/lang/annotation/ElementType;->METHOD:Ljava/lang/annotation/ElementType;").unwrap();
/// let proto = IdField::from_smali("Ljava/lang/annotation/ElementType;->FIELD:Ljava/lang/annotation/ElementType;").unwrap();
/// assert_eq!(
/// proto,
/// IdField::new(
/// "METHOD".into(),
/// "FIELD".into(),
/// IdType::class("java/lang/annotation/ElementType"),
/// IdType::class("java/lang/annotation/ElementType"),
/// )

View file

@ -138,10 +138,10 @@ impl Hash for DexString {
}
}
pub fn as_dex_string(obj: &PyAny) -> PyResult<DexString> {
if let Ok(string) = DexString::extract(obj) {
pub fn as_dex_string(obj: &Bound<'_, PyAny>) -> PyResult<DexString> {
if let Ok(string) = DexString::extract_bound(obj) {
Ok(string)
} else if let Ok(string) = String::extract(obj) {
} else if let Ok(string) = String::extract_bound(obj) {
Ok(string.into())
} else {
Err(PyErr::new::<PyTypeError, _>(format!(
@ -198,10 +198,10 @@ impl DexString {
strings
}
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
let other: Self = other
.extract()
.or(<String as FromPyObject>::extract(other).map(|string| string.into()))?;
.or(<String as FromPyObject>::extract_bound(other).map(|string| string.into()))?;
Ok(op.matches(self.0.cmp(&other.0)))
}
}

View file

@ -6,7 +6,7 @@ use std::io::{Cursor, Seek, SeekFrom, Write};
use adler::Adler32;
use anyhow::{anyhow, bail, Context};
use log::debug;
use log::{debug, warn};
use sha1::{Digest, Sha1};
use crate::Result;
@ -180,6 +180,12 @@ impl DexWriter {
pub fn add_class(&mut self, class: &Class) -> Result<(), DexWritterError> {
debug!("Adding class {} to dex builder", class.descriptor.__str__());
if class.has_hiddenapi() {
warn!(
"Class {} has hidden API data: serialization of hidden API no yet implemented",
class.descriptor.__str__()
);
}
let new_strings = class.get_all_strings();
let new_types = class.get_all_types();

View file

@ -6,8 +6,8 @@ use std::collections::HashSet;
use pyo3::prelude::*;
use crate::{
DexAnnotationItem, DexString, DexValue, IdField, IdMethod, IdMethodType, IdType, MethodHandle,
Result,
DexAnnotationItem, DexString, DexValue, HiddenApiData, IdField, IdMethod, IdMethodType, IdType,
MethodHandle, Result,
};
use androscalpel_serializer::consts::*;
@ -44,6 +44,9 @@ pub struct Field {
/// The annotations for this field
#[pyo3(get)]
pub annotations: Vec<DexAnnotationItem>,
/// Hidden Api data.
#[pyo3(get)]
pub hiddenapi: Option<HiddenApiData>,
}
/// Represent the visibility of a field
@ -80,6 +83,7 @@ impl Field {
is_enum: false,
value: None,
annotations: vec![],
hiddenapi: None,
}
}
@ -127,7 +131,7 @@ impl Field {
/// Set the value from a python object
#[setter]
pub fn set_value(&mut self, ob: &PyAny) -> PyResult<()> {
pub fn set_value(&mut self, ob: &Bound<'_, PyAny>) -> PyResult<()> {
self.value = Some(ob.extract()?);
// TODO: check type match
Ok(())

View file

@ -0,0 +1,191 @@
//! Representation of the hidden API data
use log::warn;
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
#[pyclass]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize, Serialize)]
pub struct HiddenApiData {
pub permission: HiddenApiPermission,
pub domain: HiddenApiDomain,
}
impl From<&androscalpel_serializer::HiddenApiFlags> for HiddenApiData {
fn from(flags: &androscalpel_serializer::HiddenApiFlags) -> Self {
Self {
permission: flags.value.into(),
domain: flags.domain_api.into(),
}
}
}
impl From<androscalpel_serializer::HiddenApiFlags> for HiddenApiData {
fn from(flags: androscalpel_serializer::HiddenApiFlags) -> Self {
flags.into()
}
}
impl From<&HiddenApiData> for androscalpel_serializer::HiddenApiFlags {
fn from(flags: &HiddenApiData) -> Self {
Self {
value: (&flags.permission).into(),
domain_api: (&flags.domain).into(),
}
}
}
impl From<HiddenApiData> for androscalpel_serializer::HiddenApiFlags {
fn from(flags: HiddenApiData) -> Self {
flags.into()
}
}
#[pyclass]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize, Serialize)]
pub enum HiddenApiPermission {
/// Interfaces that can be freely used and are supported as
/// part of the officially documented Android framework Package Index
Whitelist {},
/// Non-SDK interfaces that can be used regardless of the
/// application's target API level
Greylist {},
/// Non-SDK interfaces that cannot be used regardless of the
/// application's target API level. Accessing one of these
/// interfaces causes a runtime error.
Blacklist {},
/// Non-SDK interfaces that can be used for Android 8.x and
/// below unless they are restricted (targetSdkVersion <= 27 (O_MR1)).
GreylistMaxO {},
/// Non-SDK interfaces that can be used for Android 9.x unless
/// they are restricted.
GreylistMaxP {},
/// Non-SDK interfaces that can be used for Android 10.x unless
/// they are restricted.
GreylistMaxQ {},
/// Non-SDK interfaces that can be used for Android 11.x unless
/// they are restricted.
GreylistMaxR {},
GreylistMaxS {},
/// Unknown flag, either an error or this crate is out of date.
Unknwon {
value: u8,
},
}
impl HiddenApiPermission {
pub fn to_smali_name(&self) -> &'static str {
match self {
Self::Whitelist {} => "whitelist",
Self::Greylist {} => "greylist",
Self::Blacklist {} => "blacklist",
Self::GreylistMaxO {} => "greylist-max-o",
Self::GreylistMaxP {} => "greylist-max-p",
Self::GreylistMaxQ {} => "greylist-max-q",
Self::GreylistMaxR {} => "greylist-max-r",
Self::GreylistMaxS {} => "greylist-max-s",
Self::Unknwon { .. } => "???list",
}
}
}
impl From<&androscalpel_serializer::HiddenApiValue> for HiddenApiPermission {
fn from(permission: &androscalpel_serializer::HiddenApiValue) -> Self {
match permission {
androscalpel_serializer::HiddenApiValue::Whitelist => Self::Whitelist {},
androscalpel_serializer::HiddenApiValue::Greylist => Self::Greylist {},
androscalpel_serializer::HiddenApiValue::Blacklist => Self::Blacklist {},
androscalpel_serializer::HiddenApiValue::GreylistMaxO => Self::GreylistMaxO {},
androscalpel_serializer::HiddenApiValue::GreylistMaxP => Self::GreylistMaxP {},
androscalpel_serializer::HiddenApiValue::GreylistMaxQ => Self::GreylistMaxQ {},
androscalpel_serializer::HiddenApiValue::GreylistMaxR => Self::GreylistMaxR {},
androscalpel_serializer::HiddenApiValue::GreylistMaxS => Self::GreylistMaxS {},
androscalpel_serializer::HiddenApiValue::Unknwon(val) => Self::Unknwon { value: *val },
}
}
}
impl From<androscalpel_serializer::HiddenApiValue> for HiddenApiPermission {
fn from(permission: androscalpel_serializer::HiddenApiValue) -> Self {
(&permission).into()
}
}
impl From<&HiddenApiPermission> for androscalpel_serializer::HiddenApiValue {
fn from(permission: &HiddenApiPermission) -> Self {
match permission {
HiddenApiPermission::Whitelist {} => Self::Whitelist,
HiddenApiPermission::Greylist {} => Self::Greylist,
HiddenApiPermission::Blacklist {} => Self::Blacklist,
HiddenApiPermission::GreylistMaxO {} => Self::GreylistMaxO,
HiddenApiPermission::GreylistMaxP {} => Self::GreylistMaxP,
HiddenApiPermission::GreylistMaxQ {} => Self::GreylistMaxQ,
HiddenApiPermission::GreylistMaxR {} => Self::GreylistMaxR,
HiddenApiPermission::GreylistMaxS {} => Self::GreylistMaxS,
HiddenApiPermission::Unknwon { value } => Self::Unknwon(*value),
}
}
}
impl From<HiddenApiPermission> for androscalpel_serializer::HiddenApiValue {
fn from(permission: HiddenApiPermission) -> Self {
(&permission).into()
}
}
#[pyclass]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize, Serialize)]
pub struct HiddenApiDomain {
pub is_core_platform_api: bool,
pub is_test_api: bool,
/// Unknown domains.
pub unknown_flags: u32,
}
impl HiddenApiDomain {
pub fn to_smali_name(&self) -> String {
let mut value = String::new();
if self.is_core_platform_api {
value += "core-platform-api";
}
if self.is_test_api {
if !value.is_empty() {
value += "|";
}
value += "test-api";
}
if self.unknown_flags != 0 {
warn!("This attribut has an unknown hiddenapi domain set, this will not be display for compatibility with apktool");
}
value
}
}
impl From<&androscalpel_serializer::HiddenApiDomain> for HiddenApiDomain {
fn from(domain: &androscalpel_serializer::HiddenApiDomain) -> Self {
Self {
is_core_platform_api: domain.is_core_platform_api,
is_test_api: domain.is_test_api,
unknown_flags: domain.unknown_flags,
}
}
}
impl From<androscalpel_serializer::HiddenApiDomain> for HiddenApiDomain {
fn from(domain: androscalpel_serializer::HiddenApiDomain) -> Self {
(&domain).into()
}
}
impl From<&HiddenApiDomain> for androscalpel_serializer::HiddenApiDomain {
fn from(domain: &HiddenApiDomain) -> androscalpel_serializer::HiddenApiDomain {
Self {
is_core_platform_api: domain.is_core_platform_api,
is_test_api: domain.is_test_api,
unknown_flags: domain.unknown_flags,
}
}
}
impl From<HiddenApiDomain> for androscalpel_serializer::HiddenApiDomain {
fn from(domain: HiddenApiDomain) -> androscalpel_serializer::HiddenApiDomain {
(&domain).into()
}
}

View file

@ -14,6 +14,7 @@ pub mod dex_string;
pub mod dex_writer;
pub mod field;
mod hashmap_vectorize;
pub mod hiddenapi;
pub mod instructions;
pub mod method;
pub mod method_handle;
@ -29,6 +30,7 @@ pub use dex_id::*;
pub use dex_string::*;
pub use dex_writer::*;
pub use field::*;
pub use hiddenapi::*;
pub use instructions as ins;
pub use method::*;
pub use method_handle::*;
@ -40,7 +42,7 @@ mod tests;
/// Androscalpel.
#[pymodule]
fn androscalpel(py: Python, m: &PyModule) -> PyResult<()> {
fn androscalpel(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
pyo3_log::init();
m.add_class::<DexNull>()?;
m.add_class::<DexBoolean>()?;
@ -69,6 +71,10 @@ fn androscalpel(py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<InvokeDirect>()?;
m.add_class::<InvokeInterface>()?;
m.add_class::<HiddenApiData>()?;
m.add_class::<HiddenApiPermission>()?;
m.add_class::<HiddenApiDomain>()?;
m.add_class::<DexAnnotationItem>()?;
m.add_class::<DexAnnotation>()?;
m.add_class::<DexArray>()?;
@ -80,18 +86,18 @@ fn androscalpel(py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Code>()?;
m.add_class::<Apk>()?;
let ins_module = PyModule::new(py, "ins")?;
androscalpel_ins(py, ins_module)?;
m.add_submodule(ins_module)?;
let utils_module = PyModule::new(py, "utils")?;
py_utils::export_module(py, utils_module)?;
m.add_submodule(utils_module)?;
let ins_module = PyModule::new_bound(py, "ins")?;
androscalpel_ins(py, &ins_module)?;
m.add_submodule(&ins_module)?;
let utils_module = PyModule::new_bound(py, "utils")?;
py_utils::export_module(py, &utils_module)?;
m.add_submodule(&utils_module)?;
Ok(())
}
/// Dalvik opcode for Androscalpel.
fn androscalpel_ins(_py: Python, m: &PyModule) -> PyResult<()> {
fn androscalpel_ins(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<ins::CallSite>()?;
m.add_class::<ins::Nop>()?;
m.add_class::<ins::Move>()?;

View file

@ -6,8 +6,8 @@ use std::collections::HashSet;
use pyo3::prelude::*;
use crate::{
Code, DexAnnotationItem, DexString, IdField, IdMethod, IdMethodType, IdType, MethodHandle,
Result,
Code, DexAnnotationItem, DexString, HiddenApiData, IdField, IdMethod, IdMethodType, IdType,
MethodHandle, Result,
};
use androscalpel_serializer::consts::*;
@ -62,6 +62,9 @@ pub struct Method {
/// The annotations for the parameters of this method method
#[pyo3(get)]
pub parameters_annotations: Vec<Vec<DexAnnotationItem>>,
/// Hidden Api data.
#[pyo3(get)]
pub hiddenapi: Option<HiddenApiData>,
/// The code of the method
#[pyo3(get)]
@ -109,6 +112,7 @@ impl Method {
annotations: vec![],
parameters_annotations: vec![],
code: None,
hiddenapi: None,
}
}

View file

@ -17,18 +17,18 @@ use apk_frauder::{end_of_central_directory::EndCentralDirectory, ZipFileReader};
/// Convert an integer to the uleb128 byte encoding
#[pyfunction]
pub fn int_to_uleb128(py: Python, x: u32) -> Result<PyObject> {
Ok(PyBytes::new(py, &Uleb128(x).serialize_to_vec()?).into())
Ok(PyBytes::new_bound(py, &Uleb128(x).serialize_to_vec()?).into())
}
/// Convert an integer to the uleb128p1 byte encoding
#[pyfunction]
pub fn int_to_uleb128p1(py: Python, x: u32) -> Result<PyObject> {
Ok(PyBytes::new(py, &Uleb128p1(x).serialize_to_vec()?).into())
Ok(PyBytes::new_bound(py, &Uleb128p1(x).serialize_to_vec()?).into())
}
/// Convert an integer to the sleb128 byte encoding
#[pyfunction]
pub fn int_to_sleb128(py: Python, x: i32) -> Result<PyObject> {
Ok(PyBytes::new(py, &Sleb128(x).serialize_to_vec()?).into())
Ok(PyBytes::new_bound(py, &Sleb128(x).serialize_to_vec()?).into())
}
/// Decode an uleb128 encoded integer
@ -112,17 +112,21 @@ pub fn is_zip(file: PathBuf) -> bool {
pub fn replace_dex(
apk: PathBuf,
dst: PathBuf,
dexfiles: Vec<&[u8]>,
dexfiles: Vec<&PyBytes>,
keystore: PathBuf,
zipalign: Option<PathBuf>,
apksigner: Option<PathBuf>,
additionnal_files: Option<HashMap<String, Option<&[u8]>>>,
additionnal_files: Option<HashMap<String, Option<&PyBytes>>>,
) {
let mut dexfiles: Vec<_> = dexfiles.iter().map(Cursor::new).collect();
let mut dexfiles: Vec<_> = dexfiles
.into_iter()
.map(PyBytes::as_bytes)
.map(Cursor::new)
.collect();
let additionnal_files: Option<HashMap<_, _>> = additionnal_files.map(|additionnal_files| {
additionnal_files
.into_iter()
.map(|(k, v)| (k, v.map(Cursor::new)))
.map(|(k, v)| (k, v.map(PyBytes::as_bytes).map(Cursor::new)))
.collect()
});
apk_frauder::replace_dex(
@ -137,7 +141,7 @@ pub fn replace_dex(
}
/// export the function in a python module
pub(crate) fn export_module(_py: Python, m: &PyModule) -> PyResult<()> {
pub(crate) fn export_module(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(int_to_uleb128, m)?)?;
m.add_function(wrap_pyfunction!(int_to_uleb128p1, m)?)?;
m.add_function(wrap_pyfunction!(int_to_sleb128, m)?)?;

View file

@ -0,0 +1,103 @@
#!/usr/bin/python3
# require zip and apktool installed
import sys
import os
import tempfile
from pathlib import Path
import json
def parse_smali(path: Path) -> tuple[str, dict, dict]:
class_name: str | None = None
fields = {}
methods = {}
with path.open() as file:
for line in file.readlines():
if line.startswith(".class"):
if class_name is None:
class_name = line.strip().split(" ")[-1]
else:
raise RuntimeError(f"Two classes found in {path}")
if line.startswith(".field"):
k, v = parse_field(line)
fields[k] = v
elif line.startswith(".method"):
k, v = parse_method(line)
methods[k] = v
if class_name is None:
raise RuntimeError(f"No classe found in {path}")
return (class_name, fields, methods)
def parse_field(line: str) -> tuple[str, dict]:
data: dict[str, None | str | list[str]] = {
"value": None,
"other": [],
"hiddenapi": None,
"hiddenapi_domain": None,
}
if "=" in line:
line, val = line.split("=")[0], "=".join(line.split("=")[1:])
data["value"] = val.strip()
line, ty = line.split(":")
vals = list(map(str.strip, line.split(" ")))
name = f"{vals[-1]}:{ty.strip()}"
vals = vals[:-1]
if vals:
if "api" in vals[-1]:
data["hiddenapi_domain"] = vals[-1]
vals = vals[:-1]
if vals:
if "list" in vals[-1]:
data["hiddenapi"] = vals[-1]
vals = vals[:-1]
data["other"] = vals
return name, data
def parse_method(line: str) -> tuple[str, dict]:
data: dict[str, None | str | list[str]] = {
"other": [],
"hiddenapi": None,
"hiddenapi_domain": None,
}
vals = list(map(str.strip, line.split(" ")))
name = vals[-1]
vals = vals[:-1]
if vals:
if "api" in vals[-1]:
data["hiddenapi_domain"] = vals[-1]
vals = vals[:-1]
if vals:
if "list" in vals[-1]:
data["hiddenapi"] = vals[-1]
vals = vals[:-1]
data["other"] = vals
return name, data
def main():
if len(sys.argv) != 3:
print("rought smali parser")
print("usage:")
print(f" {sys.argv[0]} some_classes.dex output.json")
exit()
file = sys.argv[1]
dst = sys.argv[2]
data = {}
with tempfile.TemporaryDirectory() as tmp:
os.system(f"zip {tmp}/apk.apk {file}")
os.system(f"apktool d -o {tmp}/apktool.out {tmp}/apk.apk")
smalidir = Path(f"{tmp}/apktool.out/smali_{file.removesuffix('.dex')}")
for root, dirs, files in smalidir.walk():
for file in files:
class_name, fields, methods = parse_smali(root / file)
data[class_name] = {"fields": fields, "methods": methods}
with open(dst, "w") as file:
json.dump(data, file)
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
use super::*;
use androscalpel_serializer::Instruction as InsFormat;
use androscalpel_serializer::*;
use serde_json as sj;
use std::collections::HashSet;
use std::fs::File;
use std::io;
@ -548,3 +549,130 @@ fn test_2_from_json() {
new_apk.add_dex_file(&dex).unwrap();
assert_eq!(apk, new_apk);
}
#[test]
fn test_hidden_api() {
let dex_raw = get_dex("core-oj-33_classes.dex");
// The data are generated using src/tests/apktool_parse_hiddenapi.py src/tests/core-oj-33_classes.dex src/tests/core-oj-33_hiddenapi.json
let apktool_result = format!(
"{}/src/tests/{}",
env!("CARGO_MANIFEST_DIR"),
"core-oj-33_hiddenapi.json"
);
fn f_desc_to_pointer(f_dsc: &IdField) -> String {
let name: String = String::try_from(&f_dsc.name.0).unwrap().replace("/", "~1");
let ty: String = f_dsc.type_.try_to_smali().unwrap().replace("/", "~1");
format!(
"/{}/fields/{name}:{ty}",
f_dsc.class_.try_to_smali().unwrap().replace("/", "~1")
)
}
fn m_desc_to_pointer(m_dsc: &IdMethod) -> String {
let name: String = String::try_from(&m_dsc.name.0).unwrap().replace("/", "~1");
let ty: String = m_dsc.proto.try_to_smali().unwrap().replace("/", "~1");
format!(
"/{}/methods/{name}{ty}",
m_dsc.class_.try_to_smali().unwrap().replace("/", "~1")
)
}
fn compare_hidden_api(
apktool_hiddenapi: &sj::Value,
apktool_hiddenapi_domain: &sj::Value,
parsed_api: &Option<HiddenApiData>,
name: &str,
) {
match (apktool_hiddenapi, apktool_hiddenapi_domain, parsed_api) {
(sj::Value::Null, sj::Value::Null, None) => (),
(
sj::Value::String(apktool_hiddenapi),
sj::Value::String(apktool_domain),
Some(HiddenApiData { permission, domain }),
) if (permission.to_smali_name() == apktool_hiddenapi)
&& (&domain.to_smali_name() == apktool_domain) =>
{
()
}
(
sj::Value::String(apktool_hiddenapi),
sj::Value::Null,
Some(HiddenApiData {
permission,
domain,
}),
) if (permission.to_smali_name() == apktool_hiddenapi) && &domain.to_smali_name() == "" => (),
_ => panic!("Expected {apktool_hiddenapi:?} and {apktool_hiddenapi_domain:?}, found {parsed_api:?} in {name}"),
}
}
let apktool_result = File::open(&apktool_result).expect("core-oj-33_hiddenapi.json not found");
let apktool_result = std::io::BufReader::new(apktool_result);
let apktool_result: sj::Value = sj::from_reader(apktool_result).unwrap();
let mut apk = Apk::new();
apk.add_dex_file(&dex_raw).unwrap();
for cls in apktool_result.as_object().unwrap().keys() {
assert!(
apk.classes
.get(&dex_id::IdType(cls.as_str().into()))
.is_some(),
"{cls} not found in core-oj-33_classes.dex"
);
}
for cls in apk.classes.keys() {
assert!(
apktool_result.get::<String>((&cls.0).into()).is_some(),
"{} not found in core-oj-33_hiddenapi.json",
cls.__str__()
);
}
for (_, cls) in &apk.classes {
for (f_dsc, field) in &cls.static_fields {
let pointer = f_desc_to_pointer(f_dsc);
let apktool_field = apktool_result.pointer(&pointer).unwrap();
let apktool_hiddenapi = apktool_field.get("hiddenapi").unwrap();
let apktool_hiddenapi_domain = apktool_field.get("hiddenapi_domain").unwrap();
compare_hidden_api(
apktool_hiddenapi,
&apktool_hiddenapi_domain,
&field.hiddenapi,
&f_dsc.try_to_smali().unwrap(),
);
}
for (f_dsc, field) in &cls.instance_fields {
let pointer = f_desc_to_pointer(f_dsc);
let apktool_field = apktool_result.pointer(&pointer).unwrap();
let apktool_hiddenapi = apktool_field.get("hiddenapi").unwrap();
let apktool_hiddenapi_domain = apktool_field.get("hiddenapi_domain").unwrap();
compare_hidden_api(
apktool_hiddenapi,
&apktool_hiddenapi_domain,
&field.hiddenapi,
&f_dsc.try_to_smali().unwrap(),
);
}
for (m_dsc, method) in &cls.direct_methods {
let pointer = m_desc_to_pointer(m_dsc);
let apktool_method = apktool_result.pointer(&pointer).unwrap();
let apktool_hiddenapi = apktool_method.get("hiddenapi").unwrap();
let apktool_hiddenapi_domain = apktool_method.get("hiddenapi_domain").unwrap();
compare_hidden_api(
apktool_hiddenapi,
&apktool_hiddenapi_domain,
&method.hiddenapi,
&m_dsc.try_to_smali().unwrap(),
);
}
for (m_dsc, method) in &cls.virtual_methods {
let pointer = m_desc_to_pointer(m_dsc);
let apktool_method = apktool_result.pointer(&pointer).unwrap();
let apktool_hiddenapi = apktool_method.get("hiddenapi").unwrap();
let apktool_hiddenapi_domain = apktool_method.get("hiddenapi_domain").unwrap();
compare_hidden_api(
apktool_hiddenapi,
&apktool_hiddenapi_domain,
&method.hiddenapi,
&m_dsc.try_to_smali().unwrap(),
);
}
}
}

View file

@ -1,9 +1,9 @@
//! Parser for a .dex file.
use crate::{
CallSiteIdItem, ClassDefItem, EndianConstant, Error, FieldIdItem, HeaderItem, MapItemType,
MapList, MethodHandleItem, MethodIdItem, ProtoIdItem, Result, Serializable, StringDataItem,
StringIdItem, TypeIdItem,
CallSiteIdItem, ClassDataItem, ClassDefItem, EndianConstant, Error, FieldIdItem, HeaderItem,
HiddenApiFlags, HiddenapiClassDataItem, MapItemType, MapList, MethodHandleItem, MethodIdItem,
ProtoIdItem, Result, Serializable, StringDataItem, StringIdItem, TypeIdItem,
};
use log::{error, info, warn};
use std::io::{Cursor, Seek, SeekFrom};
@ -28,6 +28,7 @@ pub struct DexFileReader<'a> {
class_defs: Vec<ClassDefItem>,
call_site_ids: Vec<CallSiteIdItem>,
method_handles: Vec<MethodHandleItem>,
hiddenapi_class_data: Option<HiddenapiClassDataItem>,
map_list: MapList,
}
@ -48,6 +49,7 @@ impl<'a> DexFileReader<'a> {
class_defs: vec![],
call_site_ids: vec![],
method_handles: vec![],
hiddenapi_class_data: None,
map_list: MapList { list: vec![] },
};
if tmp_file.header.map_off != 0 {
@ -99,6 +101,15 @@ impl<'a> DexFileReader<'a> {
tmp_file.method_handles =
tmp_file.get_item_list::<MethodHandleItem>(item.offset, item.size)?
}
if let Some(item) = tmp_file
.map_list
.list
.iter()
.find(|item| item.type_ == MapItemType::HiddenapiClassDataItem)
{
tmp_file.hiddenapi_class_data =
Some(tmp_file.get_struct_at_offset::<HiddenapiClassDataItem>(item.offset)?);
}
tmp_file.sanity_check()?;
Ok(tmp_file)
}
@ -107,39 +118,39 @@ impl<'a> DexFileReader<'a> {
pub fn get_header(&self) -> &HeaderItem {
&self.header
}
/// Retunr the file [`StringIdItem`] list.
/// Return the file [`StringIdItem`] list.
pub fn get_string_ids(&self) -> &[StringIdItem] {
&self.string_ids
}
/// Retunr the file [`TypeIdItem`] list.
/// Return the file [`TypeIdItem`] list.
pub fn get_type_ids(&self) -> &[TypeIdItem] {
&self.type_ids
}
/// Retunr the file [`ProtoIdItem`] list.
/// Return the file [`ProtoIdItem`] list.
pub fn get_proto_ids(&self) -> &[ProtoIdItem] {
&self.proto_ids
}
/// Retunr the file [`FieldIdItem`] list.
/// Return the file [`FieldIdItem`] list.
pub fn get_field_ids(&self) -> &[FieldIdItem] {
&self.field_ids
}
/// Retunr the file [`MethodIdItem`] list.
/// Return the file [`MethodIdItem`] list.
pub fn get_method_ids(&self) -> &[MethodIdItem] {
&self.method_ids
}
/// Retunr the file [`ClassDefItem`] list.
/// Return the file [`ClassDefItem`] list.
pub fn get_class_defs(&self) -> &[ClassDefItem] {
&self.class_defs
}
/// Retunr the file [`CallSiteIdItem`] list.
/// Return the file [`CallSiteIdItem`] list.
pub fn get_call_site_ids(&self) -> &[CallSiteIdItem] {
&self.call_site_ids
}
/// Retunr the file [`MethodHandleItem`] list.
/// Return the file [`MethodHandleItem`] list.
pub fn get_method_handles(&self) -> &[MethodHandleItem] {
&self.method_handles
}
/// Retunr the file [`MapList`].
/// Return the file [`MapList`].
pub fn get_map_list(&self) -> &MapList {
&self.map_list
}
@ -421,6 +432,45 @@ impl<'a> DexFileReader<'a> {
r
}
/// Return the hiddenapi flags list for the given class.
///
/// The list of flags is composed of one [`HiddenApiFlags`] for each static field, instance
/// field, direct method and virtual method of the class, in that order.
///
/// `class_def_item_idx` if the idx of the `class_def_item`, **not** the `class_idx` (contrary
/// to what <https://source.android.com/docs/core/runtime/dex-format#hiddenapi-class-data-item>
/// says)
pub fn get_class_hiddenapi_flags(
&self,
class_def_item_idx: usize,
) -> Result<Option<Vec<HiddenApiFlags>>> {
if class_def_item_idx >= self.class_defs.len() {
return Err(Error::InconsistantStruct(format!(
"idx 0x{class_def_item_idx:x} is out of bound of class_defs"
)));
}
let class_def = self.class_defs[class_def_item_idx];
if class_def.class_data_off == 0 {
if self.hiddenapi_class_data.is_some() {
return Ok(Some(vec![]));
} else {
return Ok(None);
}
}
let class_data = self.get_struct_at_offset::<ClassDataItem>(class_def.class_data_off)?;
let nb_flags = class_data.static_fields.len()
+ class_data.instance_fields.len()
+ class_data.direct_methods.len()
+ class_data.virtual_methods.len();
if let Some(hidden_api_data) = &self.hiddenapi_class_data {
hidden_api_data
.get_flags(nb_flags, class_def_item_idx)
.map(Some)
} else {
Ok(None)
}
}
/// Return the strings that where not referenced.
pub fn get_not_resolved_strings(&mut self) -> Result<Vec<StringDataItem>> {
// use `&mut self` because using this method at the same time as performing

View file

@ -1,13 +1,12 @@
//! Hidden api items.
use crate as androscalpel_serializer;
use crate::{Error, ReadSeek, Result, Serializable, Uleb128};
use std::io::{Cursor, Write};
use std::io::{Cursor, Seek, SeekFrom, Write};
/// <https://source.android.com/docs/core/runtime/dex-format#hiddenapi-class-data-item>
/// Hard to serialize/deserialize without additional data like the number of classes
/// Hard to serialize/deserialize without additional data like the number of classes defs
/// or the method/field of the classes.
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct HiddenapiClassDataItem {
//pub size: u32,
pub data: Vec<u8>,
@ -18,7 +17,7 @@ impl HiddenapiClassDataItem {
self.data.len() as u32
}
/// Return `hiddenapi_class_data_item.offsets[class_idx]`.
/// Return `hiddenapi_class_data_item.offsets[class_def_item_idx]`.
///
/// If `0`: Either no data for this class or all API flags are zero.
/// Else: offset from the begining of the [`HiddenapiClassDataItem`]
@ -26,13 +25,17 @@ impl HiddenapiClassDataItem {
///
/// # Warning
///
/// They are no check weither the `class_idx` is valid one or not.
/// Giving an invalid idx (like an idx >= nb class) is UB.
pub fn get_offset(&self, class_idx: u32) -> Result<u32> {
let index = (class_idx as usize) * 4;
if self.data.len() < index - 4 {
/// `class_def_item_idx` is **NOT** `class_idx` (Contrary to what
/// <https://source.android.com/docs/core/runtime/dex-format#hiddenapi-class-data-item> says).
/// `class_def_item_idx` is the index of the `class_def_item`.
///
/// They are no check weither the `class_def_item_idx` is valid one or not.
/// Giving an invalid idx (like an idx >= nb class def) is UB.
pub fn get_offset(&self, class_def_item_idx: usize) -> Result<u32> {
let index = class_def_item_idx * 4;
if self.data.len() <= index {
Err(Error::InconsistantStruct(format!(
"class index 0x{class_idx:x} out of bound of HiddenapiClassDataItem data"
"class index 0x{class_def_item_idx:x} out of bound of HiddenapiClassDataItem data"
)))
} else {
u32::deserialize_from_slice(&self.data[index..])
@ -44,7 +47,7 @@ impl HiddenapiClassDataItem {
/// # Warning
///
/// They are no check weither the `nb_class` is valid one or not.
/// Giving an invalid `nb_class`.
/// Giving an invalid `nb_class` is UB.
pub fn get_offsets(&self, nb_class: u32) -> Result<Vec<u32>> {
let mut offsets = vec![];
let mut buffer = Cursor::new(self.data.as_slice());
@ -60,19 +63,37 @@ impl HiddenapiClassDataItem {
///
/// # Warning
///
/// They are no check weither the `nb_flags` or `offset`
/// `class_def_item_idx` is **NOT** `class_idx` (Contrary to what
/// <https://source.android.com/docs/core/runtime/dex-format#hiddenapi-class-data-item> says).
/// `class_def_item_idx` is the index of the `class_def_item`.
///
/// They are no check weither the `nb_flags` or `class_def_item_idx`
/// are valid. Providing invalid values is UB.
pub fn get_flags(&self, nb_flags: usize, offset: u32) -> Result<Vec<Uleb128>> {
pub fn get_flags(
&self,
nb_flags: usize,
class_def_item_idx: usize,
) -> Result<Vec<HiddenApiFlags>> {
let offset = self.get_offset(class_def_item_idx)?;
if offset == 0 {
Ok(vec![Uleb128(0); nb_flags])
Ok(vec![HiddenApiFlags::DEFAULT; nb_flags])
} else if offset < 4 {
// < 8 is almost certainly false
panic!()
} else {
let mut buffer = Cursor::new(self.data.as_slice());
buffer
.seek(SeekFrom::Start(offset as u64 - 4))
.map_err(|_| {
Error::InconsistantStruct(format!(
"{offset} if out of data bound for HiddenApiClassData"
))
})?;
let mut flags = vec![];
for _ in 0..nb_flags {
flags.push(Uleb128::deserialize(&mut buffer)?);
flags.push(HiddenApiFlags::from_uleb128(Uleb128::deserialize(
&mut buffer,
)?));
}
Ok(flags)
}
@ -102,40 +123,139 @@ impl Serializable for HiddenapiClassDataItem {
}
}
/// Flags for hidden api
#[derive(Serializable, Debug, PartialEq, Eq, Copy, Clone)]
#[prefix_type(Uleb128)]
pub enum HiddenApiFlag {
#[derive(Clone, PartialEq, Eq)]
pub struct HiddenApiFlags {
pub value: HiddenApiValue,
pub domain_api: HiddenApiDomain,
}
impl HiddenApiFlags {
pub const DEFAULT: Self = Self {
value: HiddenApiValue::Whitelist,
domain_api: HiddenApiDomain {
is_test_api: false,
is_core_platform_api: false,
unknown_flags: 0,
},
};
pub fn from_uleb128(Uleb128(val): Uleb128) -> Self {
Self {
value: HiddenApiValue::from_u32(val),
domain_api: HiddenApiDomain::from_u32(val),
}
}
pub fn to_uleb128(&self) -> Uleb128 {
Uleb128(self.value.to_u32() | self.domain_api.to_u32())
}
}
/// Flags for hidden api flag value
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum HiddenApiValue {
/// Interfaces that can be freely used and are supported as
/// part of the officially documented Android framework Package Index
#[prefix(Uleb128(0x00))]
Whitelist,
/// Non-SDK interfaces that can be used regardless of the
/// application's target API level
#[prefix(Uleb128(0x01))]
Greylist,
/// Non-SDK interfaces that cannot be used regardless of the
/// application's target API level. Accessing one of these
/// interfaces causes a runtime error.
#[prefix(Uleb128(0x02))]
Blacklist,
/// Non-SDK interfaces that can be used for Android 8.x and
/// below unless they are restricted.
#[prefix(Uleb128(0x03))]
/// below unless they are restricted (targetSdkVersion <= 27 (O_MR1)).
GreylistMaxO,
/// Non-SDK interfaces that can be used for Android 9.x unless
/// they are restricted.
#[prefix(Uleb128(0x04))]
GreylistMaxP,
/// Non-SDK interfaces that can be used for Android 10.x unless
/// they are restricted.
#[prefix(Uleb128(0x05))]
GreylistMaxQ,
/// Non-SDK interfaces that can be used for Android 11.x unless
/// they are restricted.
#[prefix(Uleb128(0x06))]
GreylistMaxR,
GreylistMaxS,
/// Unknown flag, either an error or this crate is out of date.
#[default_variant]
Unknwon(Uleb128),
Unknwon(u8),
}
impl HiddenApiValue {
pub const MASK: u32 = 0b111;
pub fn from_u32(flags: u32) -> Self {
match flags & Self::MASK {
0x00 => Self::Whitelist,
0x01 => Self::Greylist,
0x02 => Self::Blacklist,
0x03 => Self::GreylistMaxO,
0x04 => Self::GreylistMaxP,
0x05 => Self::GreylistMaxQ,
0x06 => Self::GreylistMaxR,
0x07 => Self::GreylistMaxS,
other => Self::Unknwon(other as u8),
}
}
pub fn to_u32(&self) -> u32 {
match self {
Self::Whitelist => 0x00,
Self::Greylist => 0x01,
Self::Blacklist => 0x02,
Self::GreylistMaxO => 0x03,
Self::GreylistMaxP => 0x04,
Self::GreylistMaxQ => 0x05,
Self::GreylistMaxR => 0x06,
Self::GreylistMaxS => 0x07,
Self::Unknwon(other) => {
if (0b1111_1000 & *other) != 0 {
panic!("HiddenApiValue is encoded on 3 bits but found value {other}");
}
*other as u32
}
}
}
}
/// Flags for hidden api flag domain
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct HiddenApiDomain {
pub is_core_platform_api: bool,
pub is_test_api: bool,
pub unknown_flags: u32,
}
impl HiddenApiDomain {
pub const CORE_PLATFORM_API_FLAG: u32 = 0x08;
pub const TEST_API_FLAG: u32 = 0x10;
pub fn from_u32(flags: u32) -> Self {
let flags = flags & !HiddenApiValue::MASK;
Self {
is_core_platform_api: (flags & Self::CORE_PLATFORM_API_FLAG) != 0,
is_test_api: (flags & Self::TEST_API_FLAG) != 0,
unknown_flags: flags & (!Self::CORE_PLATFORM_API_FLAG) & (!Self::TEST_API_FLAG),
}
}
pub fn to_u32(&self) -> u32 {
if ((0b111 | Self::CORE_PLATFORM_API_FLAG | Self::CORE_PLATFORM_API_FLAG)
& self.unknown_flags)
!= 0
{
panic!(
"The first 5 bits of HiddenApiDomain are reserved and \
shoud be set to 0, but value 0x{:x} was found",
self.unknown_flags
);
}
self.unknown_flags
| if self.is_core_platform_api {
Self::CORE_PLATFORM_API_FLAG
} else {
0
}
| if self.is_test_api {
Self::TEST_API_FLAG
} else {
0
}
}
}