From ef6a2196a77f3ade130a488e87155c0177ad9ddf Mon Sep 17 00:00:00 2001 From: Jean-Marie Mineau Date: Mon, 15 Apr 2024 15:13:30 +0200 Subject: [PATCH] add test --- TODO.md | 2 + androscalpel/src/dex_writer.rs | 42 ++- androscalpel/src/tests/mod.rs | 184 +++++++++++- androscalpel/src/tests/test_class.json | 324 +++++++++++++++++++++ androscalpel_serializer/src/file_reader.rs | 7 +- androscalpel_serializer/src/items/map.rs | 30 ++ 6 files changed, 580 insertions(+), 9 deletions(-) create mode 100644 androscalpel/src/tests/test_class.json diff --git a/TODO.md b/TODO.md index 940aa4b..a05d570 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,6 @@ - sanity checks +- SANITY CHECK (https://cs.android.com/android/platform/superproject/main/+/main:art/libdexfile/dex/dex_file_verifier.cc if check failed, .dex is not loaded but apk do not crash !!!) +- V41 https://cs.android.com/android/platform/superproject/main/+/main:art/libdexfile/dex/dex_file_verifier.cc;drc=e8da7cd1d0e7d3535c82f8d05adcef3edd43cd40;l=634 - tests - https://source.android.com/docs/core/runtime/dex-format#system-annotation - goto size computation diff --git a/androscalpel/src/dex_writer.rs b/androscalpel/src/dex_writer.rs index a54c3f9..f014cd8 100644 --- a/androscalpel/src/dex_writer.rs +++ b/androscalpel/src/dex_writer.rs @@ -2380,19 +2380,47 @@ impl DexWriter { debug!("Link the header section"); self.header.map_off = self.section_manager.get_offset(Section::MapList); self.header.string_ids_size = self.section_manager.get_nb_elt(Section::StringIdItem) as u32; - self.header.string_ids_off = self.section_manager.get_offset(Section::StringIdItem); + self.header.string_ids_off = if self.header.string_ids_size != 0 { + self.section_manager.get_offset(Section::StringIdItem) + } else { + 0 + }; self.header.type_ids_size = self.section_manager.get_nb_elt(Section::TypeIdItem) as u32; - self.header.type_ids_off = self.section_manager.get_offset(Section::TypeIdItem); + self.header.type_ids_off = if self.header.type_ids_size != 0 { + self.section_manager.get_offset(Section::TypeIdItem) + } else { + 0 + }; self.header.proto_ids_size = self.section_manager.get_nb_elt(Section::ProtoIdItem) as u32; - self.header.proto_ids_off = self.section_manager.get_offset(Section::ProtoIdItem); + self.header.proto_ids_off = if self.header.proto_ids_size != 0 { + self.section_manager.get_offset(Section::ProtoIdItem) + } else { + 0 + }; self.header.field_ids_size = self.section_manager.get_nb_elt(Section::FieldIdItem) as u32; - self.header.field_ids_off = self.section_manager.get_offset(Section::FieldIdItem); + self.header.field_ids_off = if self.header.field_ids_size != 0 { + self.section_manager.get_offset(Section::FieldIdItem) + } else { + 0 + }; self.header.method_ids_size = self.section_manager.get_nb_elt(Section::MethodIdItem) as u32; - self.header.method_ids_off = self.section_manager.get_offset(Section::MethodIdItem); + self.header.method_ids_off = if self.header.method_ids_size != 0 { + self.section_manager.get_offset(Section::MethodIdItem) + } else { + 0 + }; self.header.class_defs_size = self.section_manager.get_nb_elt(Section::ClassDefItem) as u32; - self.header.class_defs_off = self.section_manager.get_offset(Section::ClassDefItem); + self.header.class_defs_off = if self.header.class_defs_size != 0 { + self.section_manager.get_offset(Section::ClassDefItem) + } else { + 0 + }; self.header.data_size = self.section_manager.get_unaligned_size(Section::Data); - self.header.data_off = self.section_manager.get_offset(Section::Data); + self.header.data_off = if self.header.data_size != 0 { + self.section_manager.get_offset(Section::Data) + } else { + 0 + }; } /// Link the offsets in the call site id items. diff --git a/androscalpel/src/tests/mod.rs b/androscalpel/src/tests/mod.rs index cc6aa5b..3af3e3a 100644 --- a/androscalpel/src/tests/mod.rs +++ b/androscalpel/src/tests/mod.rs @@ -1,9 +1,10 @@ use super::*; use androscalpel_serializer::Instruction as InsFormat; use androscalpel_serializer::*; +use std::collections::HashSet; use std::fs::File; use std::io; -use std::io::Write; +use std::io::{Read, Write}; use std::ops::Deref; use std::sync::{Mutex, OnceLock}; use std::time::Instant; @@ -352,3 +353,184 @@ fn test_sort_strings() { assert_eq!(list1, list2); assert_eq!(list1, vec![s1.clone(), s2.clone()]); } + +/// Emulate test https://cs.android.com/android/platform/superproject/main/+/main:art/libdexfile/dex/dex_file_verifier.cc;drc=e8da7cd1d0e7d3535c82f8d05adcef3edd43cd40;l=581 +fn check_valid_offset_and_size( + dex: &DexFileReader, + offset: u32, + size: u32, + alignment: u32, + label: &str, +) { + if size == 0 { + if offset != 0 { + panic!("Offset 0x{offset:x} should be zero when size is zero for {label}"); + } + return; + } + // offset < hdr_offset is not relevent (we index from hdr_offset=0) + let file_size = dex.get_header().file_size; + if file_size <= offset { + panic!("Offset 0x{offset:x} sould be within file size 0x{file_size:x} for {label}"); + } + if file_size - offset < size { + // size + offset could overflow + panic!( + "Section end 0x{:x} should be within file size 0x{file_size:x} for {label}", + size + offset + ); + } + if alignment != 0 && !(offset & (alignment - 1) == 0) { + panic!("Offset 0x{offset:x} sould be aligned by {alignment} for {label}"); + } +} + +#[test] +fn test_load_from_json() { + let filename = "test_class.json"; + let hello_world_dex = format!("{}/src/tests/{}", env!("CARGO_MANIFEST_DIR"), filename); + let mut file = File::open(&hello_world_dex).expect(&format!("{} not found", filename)); + let mut json = String::new(); + file.read_to_string(&mut json).unwrap(); + let test_a: Class = serde_json::from_str(&json).unwrap(); + let mut apk = Apk::new(); + apk.add_class(test_a).unwrap(); + let dex = apk.gen_raw_dex().unwrap().pop().unwrap(); + let dex = DexFileReader::new(&dex).unwrap(); + + check_valid_offset_and_size( + &dex, + dex.get_header().link_off, + dex.get_header().link_size, + 0, + "link", + ); + check_valid_offset_and_size(&dex, dex.get_header().map_off, 4, 4, "map"); + check_valid_offset_and_size( + &dex, + dex.get_header().string_ids_off, + dex.get_header().string_ids_size, + 4, + "string-ids", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().type_ids_off, + dex.get_header().type_ids_size, + 4, + "type-ids", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().proto_ids_off, + dex.get_header().proto_ids_size, + 4, + "proto-ids", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().field_ids_off, + dex.get_header().field_ids_size, + 4, + "field-ids", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().method_ids_off, + dex.get_header().method_ids_size, + 4, + "method-ids", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().class_defs_off, + dex.get_header().class_defs_size, + 4, + "class-defs", + ); + check_valid_offset_and_size( + &dex, + dex.get_header().data_off, + dex.get_header().data_size, + 0, + "data", + ); + if dex.get_header().type_ids_size > (u16::MAX as u32) { + panic!( + "Size 0x{:x} should not exceed limit 0x{:x} for type-ids", + dex.get_header().type_ids_size, + u16::MAX + ) + } + if dex.get_header().proto_ids_size > (u16::MAX as u32) { + panic!( + "Size 0x{:x} should not exceed limit 0x{:x} for proto-ids", + dex.get_header().proto_ids_size, + u16::MAX + ) + } + + let map = dex.get_map_list(); + let mut last_offset = 0; + let mut last_type = MapItemType::UnkownType(0); + let mut data_item_count = 0; + let mut data_items_left = dex.get_header().data_size; + let file_size = dex.get_header().file_size; + let mut used_types = HashSet::new(); + for (i, item) in map.list.iter().enumerate() { + if last_offset >= item.offset && i != 0 { + panic!( + "Out of order map item: 0x{:x} then 0x{:x} for type {:?} (last type was {:?})", + last_offset, item.offset, item.type_, last_type + ) + } + if item.offset >= file_size { + panic!( + "Map item for type {:?} ends after file: 0x{:x} >= 0x{:x}", + item.type_, item.offset, file_size + ); + } + if item.type_.is_data_section_type() { + if item.size > data_items_left { + panic!( + "Too many items in data section: {} > {} (item type: {:?}", + item.size + data_item_count, + dex.get_header().data_size, + item.type_ + ); + } + data_items_left -= item.size; + data_item_count += item.size; + } + if used_types.contains(&item.type_) { + panic!("Duplicate map section of type {:?}", item.type_); + } + used_types.insert(item.type_); + last_offset = item.offset; + last_type = item.type_; + } + if !used_types.contains(&MapItemType::HeaderItem) { + panic!("Map is missing Header entry"); + } + if !used_types.contains(&MapItemType::MapList) { + panic!("Map is missing Map List entry"); + } + if !used_types.contains(&MapItemType::StringIdItem) && (dex.get_header().string_ids_off != 0) { + panic!("Map is missing String Id entry"); + } + if !used_types.contains(&MapItemType::TypeIdItem) && (dex.get_header().type_ids_off != 0) { + panic!("Map is missing Type Id entry"); + } + if !used_types.contains(&MapItemType::ProtoIdItem) && (dex.get_header().proto_ids_off != 0) { + panic!("Map is missing Proto Id entry"); + } + if !used_types.contains(&MapItemType::FieldIdItem) && (dex.get_header().field_ids_off != 0) { + panic!("Map is missing Field Id entry"); + } + if !used_types.contains(&MapItemType::MethodIdItem) && (dex.get_header().method_ids_off != 0) { + panic!("Map is missing Method Id entry"); + } + if !used_types.contains(&MapItemType::ClassDefItem) && (dex.get_header().class_defs_off != 0) { + panic!("Map is missing Class Def entry"); + } +} diff --git a/androscalpel/src/tests/test_class.json b/androscalpel/src/tests/test_class.json new file mode 100644 index 0000000..46f88b8 --- /dev/null +++ b/androscalpel/src/tests/test_class.json @@ -0,0 +1,324 @@ +{ + "descriptor": { + "String": "Lcom/example/testclassloader/TestB;" + }, + "is_public": true, + "is_final": false, + "is_interface": false, + "is_abstract": false, + "is_synthetic": false, + "is_annotation": false, + "is_enum": false, + "superclass": { + "String": "Ljava/lang/Object;" + }, + "interfaces": [], + "source_file": { + "String": "TestB.java" + }, + "static_fields": [], + "instance_fields": [], + "direct_methods": [ + [ + { + "class_": { + "String": "Lcom/example/testclassloader/TestB;" + }, + "proto": { + "shorty": { + "String": "V" + }, + "return_type": { + "String": "V" + }, + "parameters": [] + }, + "name": { + "String": "" + } + }, + { + "descriptor": { + "class_": { + "String": "Lcom/example/testclassloader/TestB;" + }, + "proto": { + "shorty": { + "String": "V" + }, + "return_type": { + "String": "V" + }, + "parameters": [] + }, + "name": { + "String": "" + } + }, + "visibility": "Public", + "is_static": false, + "is_final": false, + "is_synchronized": false, + "is_bridge": false, + "is_varargs": false, + "is_native": false, + "is_abstract": false, + "is_strictfp": false, + "is_synthetic": false, + "is_constructor": true, + "is_declared_syncrhonized": false, + "annotations": [], + "parameters_annotations": [], + "code": { + "registers_size": 1, + "ins_size": 1, + "outs_size": 1, + "debug_info": [ + 3, + [ + 14, + 0 + ] + ], + "parameter_names": [], + "insns": [ + { + "Label": { + "name": "label_00000000" + } + }, + { + "InvokeDirect": { + "method": { + "class_": { + "String": "Ljava/lang/Object;" + }, + "proto": { + "shorty": { + "String": "V" + }, + "return_type": { + "String": "V" + }, + "parameters": [] + }, + "name": { + "String": "" + } + }, + "args": [ + 0 + ] + } + }, + { + "Label": { + "name": "label_00000003" + } + }, + { + "ReturnVoid": null + } + ] + } + } + ] + ], + "virtual_methods": [ + [ + { + "class_": { + "String": "Lcom/example/testclassloader/TestB;" + }, + "proto": { + "shorty": { + "String": "L" + }, + "return_type": { + "String": "Ljava/lang/String;" + }, + "parameters": [] + }, + "name": { + "String": "val" + } + }, + { + "descriptor": { + "class_": { + "String": "Lcom/example/testclassloader/TestB;" + }, + "proto": { + "shorty": { + "String": "L" + }, + "return_type": { + "String": "Ljava/lang/String;" + }, + "parameters": [] + }, + "name": { + "String": "val" + } + }, + "visibility": "Public", + "is_static": false, + "is_final": false, + "is_synchronized": false, + "is_bridge": false, + "is_varargs": false, + "is_native": false, + "is_abstract": false, + "is_strictfp": false, + "is_synthetic": false, + "is_constructor": false, + "is_declared_syncrhonized": false, + "annotations": [], + "parameters_annotations": [], + "code": { + "registers_size": 2, + "ins_size": 1, + "outs_size": 1, + "debug_info": [ + 6, + [ + 14, + 0 + ] + ], + "parameter_names": [], + "insns": [ + { + "Label": { + "name": "label_00000000" + } + }, + { + "InvokeVirtual": { + "method": { + "class_": { + "String": "Ljava/lang/Object;" + }, + "proto": { + "shorty": { + "String": "L" + }, + "return_type": { + "String": "Ljava/lang/Class;" + }, + "parameters": [] + }, + "name": { + "String": "getClass" + } + }, + "args": [ + 1 + ] + } + }, + { + "Label": { + "name": "label_00000003" + } + }, + { + "MoveResultObject": { + "to": 0 + } + }, + { + "Label": { + "name": "label_00000004" + } + }, + { + "InvokeVirtual": { + "method": { + "class_": { + "String": "Ljava/lang/Class;" + }, + "proto": { + "shorty": { + "String": "L" + }, + "return_type": { + "String": "Ljava/lang/ClassLoader;" + }, + "parameters": [] + }, + "name": { + "String": "getClassLoader" + } + }, + "args": [ + 0 + ] + } + }, + { + "Label": { + "name": "label_00000007" + } + }, + { + "MoveResultObject": { + "to": 0 + } + }, + { + "Label": { + "name": "label_00000008" + } + }, + { + "InvokeVirtual": { + "method": { + "class_": { + "String": "Ljava/lang/Object;" + }, + "proto": { + "shorty": { + "String": "L" + }, + "return_type": { + "String": "Ljava/lang/String;" + }, + "parameters": [] + }, + "name": { + "String": "toString" + } + }, + "args": [ + 0 + ] + } + }, + { + "Label": { + "name": "label_0000000B" + } + }, + { + "MoveResultObject": { + "to": 0 + } + }, + { + "Label": { + "name": "label_0000000C" + } + }, + { + "ReturnObject": { + "reg": 0 + } + } + ] + } + } + ] + ], + "annotations": [] +} diff --git a/androscalpel_serializer/src/file_reader.rs b/androscalpel_serializer/src/file_reader.rs index d8c4636..20b4078 100644 --- a/androscalpel_serializer/src/file_reader.rs +++ b/androscalpel_serializer/src/file_reader.rs @@ -50,7 +50,9 @@ impl<'a> DexFileReader<'a> { method_handles: vec![], map_list: MapList { list: vec![] }, }; - tmp_file.map_list = tmp_file.get_struct_at_offset(tmp_file.header.map_off)?; + if tmp_file.header.map_off != 0 { + tmp_file.map_list = tmp_file.get_struct_at_offset(tmp_file.header.map_off)?; + } tmp_file.string_ids = tmp_file.get_item_list::( tmp_file.header.string_ids_off, tmp_file.header.string_ids_size, @@ -365,6 +367,9 @@ impl<'a> DexFileReader<'a> { } fn get_item_list(&self, offset: u32, size: u32) -> Result> { + if offset == 0 { + return Ok(vec![]); + } let mut buffer = Cursor::new(self.data); buffer.seek(SeekFrom::Start(offset as u64)).map_err(|err| { Error::DeserializationError(format!("Failed to seek 0x{offset:x} position: {err}")) diff --git a/androscalpel_serializer/src/items/map.rs b/androscalpel_serializer/src/items/map.rs index 46baddd..14a1926 100644 --- a/androscalpel_serializer/src/items/map.rs +++ b/androscalpel_serializer/src/items/map.rs @@ -71,6 +71,36 @@ pub enum MapItemType { UnkownType(u16), } +impl MapItemType { + /// If data of this type is stored in the data section + pub fn is_data_section_type(&self) -> bool { + match self { + Self::HeaderItem => false, + Self::StringIdItem => false, + Self::TypeIdItem => false, + Self::ProtoIdItem => false, + Self::FieldIdItem => false, + Self::MethodIdItem => false, + Self::ClassDefItem => false, + Self::CallSiteIdItem => true, + Self::MethodHandleItem => true, + Self::MapList => true, + Self::TypeList => true, + Self::AnnotationSetRefList => true, + Self::AnnotationSetItem => true, + Self::ClassDataItem => true, + Self::CodeItem => true, + Self::StringDataItem => true, + Self::DebugInfoItem => true, + Self::AnnotationItem => true, + Self::EncodedArrayItem => true, + Self::AnnotationsDirectoryItem => true, + Self::HiddenapiClassDataItem => true, + Self::UnkownType(_) => true, // Most likely + } + } +} + impl MapList { /// The size field of a MapList. pub fn size_field(&self) -> u32 {