diff --git a/androscalpel/src/apk.rs b/androscalpel/src/apk.rs index 693037f..31d2331 100644 --- a/androscalpel/src/apk.rs +++ b/androscalpel/src/apk.rs @@ -2362,14 +2362,36 @@ impl Apk { } pub fn gen_raw_dex(&self) -> Result>> { + let mut dex_writers = vec![]; let mut dex_writer = DexWriter::new(); for class_ in self.classes.values() { - dex_writer.add_class(class_)?; + match dex_writer.add_class(class_) { + Ok(()) => (), + Err(DexWritterError::OutOfSpace(_)) => { + dex_writers.push(dex_writer); + dex_writer = DexWriter::new(); + match dex_writer.add_class(class_) { + Ok(()) => (), + Err(DexWritterError::OutOfSpace(err)) => { + bail!( + "Class {} contains to many references to fit in one .dex: {err}", + class_.__str__() + ) + } + } + } + } } - for string in &self.not_referenced_strings { - dex_writer.add_string(string.clone()); + dex_writers.push(dex_writer); + for dex_writer in dex_writers.iter_mut() { + for string in &self.not_referenced_strings { + dex_writer.add_string(string.clone()); + } } - Ok(vec![dex_writer.gen_dex_file_to_vec()?]) + dex_writers + .into_iter() + .map(|mut dex_writer| dex_writer.gen_dex_file_to_vec()) + .collect() } } diff --git a/androscalpel/src/dex_string.rs b/androscalpel/src/dex_string.rs index c86fe4c..3aab161 100644 --- a/androscalpel/src/dex_string.rs +++ b/androscalpel/src/dex_string.rs @@ -198,7 +198,10 @@ impl DexString { strings } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool { - op.matches(self.0.cmp(&other.0)) + fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult { + let other: Self = other + .extract() + .or(::extract(other).map(|string| string.into()))?; + Ok(op.matches(self.0.cmp(&other.0))) } } diff --git a/androscalpel/src/dex_writer.rs b/androscalpel/src/dex_writer.rs index 2408f6b..a54c3f9 100644 --- a/androscalpel/src/dex_writer.rs +++ b/androscalpel/src/dex_writer.rs @@ -169,12 +169,16 @@ impl Default for DexWriter { } } +pub enum DexWritterError { + OutOfSpace(String), +} + impl DexWriter { pub fn new() -> Self { Self::default() } - pub fn add_class(&mut self, class: &Class) -> Result<()> { + pub fn add_class(&mut self, class: &Class) -> Result<(), DexWritterError> { debug!("Adding class {} to dex builder", class.descriptor.__str__()); let new_strings = class.get_all_strings(); @@ -185,8 +189,9 @@ impl DexWriter { .filter(|ty| self.type_ids.get(ty).is_none()) .count(); if new_nb_types >= u16::MAX as usize { - // TODO return structured error to handle this case by generating multiple dex files - bail!("To many types for one dex file"); + return Err(DexWritterError::OutOfSpace( + "To many types for one dex file".into(), + )); } let new_protos = class.get_all_protos(); @@ -196,8 +201,9 @@ impl DexWriter { .filter(|proto| self.proto_ids.get(proto).is_none()) .count(); if new_nb_protos >= u16::MAX as usize { - // TODO return structured error to handle this case by generating multiple dex files - bail!("To many prototypes for one dex file"); + return Err(DexWritterError::OutOfSpace( + "To many prototypes for one dex file".into(), + )); } let new_field_ids = class.get_all_field_ids(); @@ -207,8 +213,9 @@ impl DexWriter { .filter(|field| self.field_ids.get(field).is_none()) .count(); if new_nb_field_ids >= u16::MAX as usize { - // TODO return structured error to handle this case by generating multiple dex files - bail!("To many field ids for one dex file"); + return Err(DexWritterError::OutOfSpace( + "To many field ids for one dex file".into(), + )); } let new_method_ids = class.get_all_method_ids(); @@ -218,8 +225,9 @@ impl DexWriter { .filter(|meth| self.method_ids.get(meth).is_none()) .count(); if new_nb_method_ids >= u16::MAX as usize { - // TODO return structured error to handle this case by generating multiple dex files - bail!("To many method ids for one dex file"); + return Err(DexWritterError::OutOfSpace( + "To many method ids for one dex file".into(), + )); } for string in new_strings { diff --git a/tests/test.py b/tests/test.py index 724355b..d17c555 100644 --- a/tests/test.py +++ b/tests/test.py @@ -75,10 +75,10 @@ for i, inst in enumerate(code.insns): case ins.InvokeVirtual(args=args, method=method) if is_evasion_method(method): print(f"{i:>03} {RED}{inst}{ENDC}") case ins.SGetObject(to=to, field=field): + field: IdField = field # type: ignore print(f"{i:>03} {inst}") - print( - f" val: {GREEN}{dyn_load_apk.classes[field.class_].static_fields[field].value}{ENDC}" - ) + val = dyn_load_apk.classes[field.class_].static_fields[field].value + print(f" val: {GREEN}{val}{ENDC}") case inst: print(f"{i:>03} {inst}") @@ -96,7 +96,7 @@ print(f"[+] Load bytecode in assets/classes") with z.ZipFile(DYN_LOAD_APK) as zipf: with zipf.open("assets/classes", "r") as dex_f: dex = dex_f.read() - # dyn_load_apk.add_dex_file(dex) + dyn_load_apk.add_dex_file(dex) mal_cls_in_apk = malicious_class_id in dyn_load_apk.classes if mal_cls_in_apk: @@ -108,231 +108,70 @@ print(f"[+] {malicious_class_id} in loaded apk: {color}{mal_cls_in_apk}{ENDC}") print( f"[+] -- very simplified steep, see https://developer.android.com/reference/java/lang/Class#getMethod(java.lang.String,%20java.lang.Class%3C?%3E[]) --" ) + + name = "a" args = [ IdType("Landroid/content/Context;"), IdType("Landroid/content/BroadcastReceiver;"), ] -# malicious_class = dyn_load_apk.classes[malicious_class_id] -# -# potential_meth = [] -# for m_id in malicious_class.direct_methods: -# if m_id.name == name and m_id.proto.get_parameters() == args: -# potential_meth.append((m_id, "direct")) -# for m_id in malicious_class.virtual_methods: -# if m_id.name == name and m_id.proto.get_parameters() == args: -# potential_meth.append((m_id, "virtual")) -# -# print("[+] Potential methods:") -# for m_id, t in potential_meth: -# print(f" {m_id}({t})") -# -# m_id, t = potential_meth[0] -# -# new_insns = ( -# code.insns[:42] -# + [ -# ins.NewInstance(1, IdType("Lcom/example/ut_dyn_load/SmsReceiver;")), -# ins.InvokeVirtual(m_id, [1, 8, 1]), -# ] -# + code.insns[62:] -# ) -# -# print(f"[+] New code ") -# for i, inst in enumerate(new_insns): -# if i >= 42 and i < 44: -# print(f"{i:>03} {GREEN}{inst}{ENDC}") -# continue -# match inst: -# case ins.InvokeVirtual(args=args, method=method) if is_evasion_method(method): -# print(f"{i:>03} {RED}{inst}{ENDC}") -# case ins.SGetObject(to=to, field=field): -# print(f"{i:>03} {inst}") -# print( -# f" val: {GREEN}{dyn_load_apk.classes[field.class_].static_fields[field].value}{ENDC}" -# ) -# case inst: -# print(f"{i:>03} {inst}") -# -# new_code = Code(code.registers_size, code.ins_size, code.outs_size, new_insns) -# dyn_load_apk.set_method_code(method_id, code) +malicious_class = dyn_load_apk.classes[malicious_class_id] -NB_APK = 8665 +potential_meth = [] +for m_id in malicious_class.direct_methods: + if m_id.name == name and m_id.proto.get_parameters() == args: + potential_meth.append((m_id, "direct")) +for m_id in malicious_class.virtual_methods: + if m_id.name == name and m_id.proto.get_parameters() == args: + potential_meth.append((m_id, "virtual")) -list_cls = list(dyn_load_apk.classes.keys()) -list_cls.sort() -print(f"[+] NB classes: {len(list_cls)}") -for cls in list_cls[NB_APK:]: - dyn_load_apk.remove_class(cls) +print("[+] Potential methods:") +for m_id, t in potential_meth: + print(f" {m_id}({t})") + +m_id, t = potential_meth[0] + + +TRY_INDEX = 80 # try until label_0000007C Ljava/lang/Exception;: label_0000007D +LABEL_7C_INDEX = 121 +code_inserted = [ + ins.NewInstance(1, IdType("Lcom/example/ut_dyn_load/SmsReceiver;")), + ins.InvokeDirect( + IdMethod.from_smali("Lcom/example/ut_dyn_load/SmsReceiver;->()V"), [1] + ), + ins.InvokeStatic(m_id, [8, 1]), +] + +new_insns = code.insns[:TRY_INDEX] + code_inserted + code.insns[LABEL_7C_INDEX:] + +print(f"[+] New code ") +for i, inst in enumerate(new_insns): + if i >= TRY_INDEX and i < TRY_INDEX + len(code_inserted): + print(f"{i:>03} {GREEN}{inst}{ENDC}") + continue + match inst: + case ins.InvokeVirtual(args=args, method=method) if is_evasion_method(method): + print(f"{i:>03} {RED}{inst}{ENDC}") + case ins.SGetObject(to=to, field=field): + print(f"{i:>03} {inst}") + val = dyn_load_apk.classes[field.class_].static_fields[field].value + print(f" val: {GREEN}{val}{ENDC}") + case inst: + print(f"{i:>03} {inst}") + +new_code = Code(code.registers_size, code.ins_size, code.outs_size, new_insns) +dyn_load_apk.set_method_code(method_id, new_code) print("[+] Recompile") dex_raw = dyn_load_apk.gen_raw_dex() print("[+] Repackage") -# utils.replace_dex( -# DYN_LOAD_APK, -# DYN_LOAD_APK.parent -# / (DYN_LOAD_APK.name.removesuffix(".apk") + "-instrumented.apk"), -# dex_raw, -# Path(__file__).parent.parent / "my-release-key.jks", -# zipalign=Path.home() / "Android" / "Sdk" / "build-tools" / "34.0.0" / "zipalign", -# apksigner=Path.home() / "Android" / "Sdk" / "build-tools" / "34.0.0" / "apksigner", -# ) - - -MAX_REQ = 8 - - -def cmp(a, b, req=0): - if req > MAX_REQ: - return - if type(a) == dict: - cmp_dict(a, b, req) - elif type(a) == list: - cmp_list(a, b, req) - else: - cmp_other(a, b, req) - - -def nice_bool(b) -> str: - if b: - return "\033[32mTrue\033[0m" - else: - return "\033[31mFalse\033[0m" - - -def cmp_other(a, b, req=0): - ident = " " * req - for f in dir(a): - if getattr(getattr(a, f), "__call__", None) is None and ( - len(f) < 2 or f[:2] != "__" - ): - eq = getattr(a, f) == getattr(b, f) - print(f"{f'{ident}{f}: ':<150}{nice_bool(eq)}") - if not eq: - if "descriptor" in dir(a): - global last_id - last_id = a.descriptor - cmp(getattr(a, f), getattr(b, f), req + 1) - - -def cmp_dict(a, b, req=0): - ident = " " * req - keys_a = set(a.keys()) - keys_b = set(b.keys()) - if keys_a != keys_b: - print(f"{ident}a.keys() != b.keys()") - tot = 0 - nb_failed = 0 - for key in keys_a & keys_b: - eq = a[key] == b[key] - tot += 1 - if not eq: - print(f"{f'{ident}{str(key)}: ':<150}{nice_bool(eq)}") - nb_failed += 1 - global last_id - last_id = key - cmp(a[key], b[key], req + 1) - print(f"\033[32m{tot-nb_failed}\033[0m + \033[31m{nb_failed}\033[0m = {tot}") - - -def cmp_list(a, b, req=0): - ident = " " * req - la = len(a) - lb = len(b) - if la != lb: - print(f"{ident}len(a) != len(b)") - for i in range(min(la, lb)): - eq = a[i] == b[i] - print(f"{f'{ident}{str(i)}: ':<150}{nice_bool(eq)}") - if not eq: - print(f"{ident}: {str(a[i])} != {str(b[i])}") - cmp(a[i], b[i], req + 1) - - -instrumented_apk = Apk() -instrumented_apk.add_dex_file(dex_raw[0]) - -print("wtf?") - -# cmp(instrumented_apk, dyn_load_apk) - -MAX_REQ = 5 - -tys = [ - IdType("Landroidx/compose/foundation/MutatePriority;"), - IdType( - "Landroidx/compose/ui/input/pointer/PointerInteropFilter$DispatchToViewState;" - ), - IdType("Landroidx/compose/ui/autofill/AutofillType;"), - IdType("Landroidx/compose/material3/TextFieldType;"), - IdType("Landroidx/compose/material3/SnackbarResult;"), - IdType("Landroidx/compose/ui/layout/MeasuringIntrinsics$IntrinsicMinMax;"), - IdType("Landroidx/compose/foundation/text/Handle;"), - IdType("Landroidx/compose/foundation/text/selection/SelectionMode;"), - IdType("Landroidx/compose/material3/SliderComponents;"), - IdType("Landroidx/lifecycle/Lifecycle$Event;"), - IdType("Landroidx/compose/ui/focus/FocusStateImpl;"), - IdType("Landroidx/fragment/app/strictmode/FragmentStrictMode$Flag;"), - IdType("Landroidx/compose/ui/text/style/ResolvedTextDirection;"), - IdType("Landroidx/compose/foundation/text/KeyCommand;"), - IdType("Landroidx/compose/ui/text/AnnotationType;"), - IdType("Landroidx/compose/foundation/layout/SizeMode;"), - IdType("Landroidx/compose/ui/semantics/NodeLocationHolder$ComparisonStrategy;"), - IdType("Landroidx/compose/material3/DrawerValue;"), - IdType("Landroidx/compose/ui/input/pointer/PointerEventPass;"), - IdType("Landroidx/compose/foundation/layout/Direction;"), - IdType("Landroidx/compose/ui/node/LayoutNode$UsageByParent;"), - IdType("Landroidx/compose/material3/tokens/TypographyKeyTokens;"), - IdType("Landroidx/compose/animation/core/RepeatMode;"), - IdType("Landroidx/compose/ui/layout/IntrinsicMinMax;"), - IdType("Landroidx/compose/ui/text/input/TextInputServiceAndroid$TextInputCommand;"), - IdType("Landroidx/compose/ui/unit/LayoutDirection;"), - IdType("Landroidx/collection/SparseArrayCompat;"), - IdType("Landroidx/compose/material3/TabSlots;"), - IdType("Landroidx/compose/material3/tokens/ColorSchemeKeyTokens;"), - IdType("Landroidx/annotation/RestrictTo$Scope;"), - IdType("Landroidx/profileinstaller/FileSectionType;"), - IdType("Landroidx/lifecycle/Lifecycle$State;"), - IdType("Landroidx/compose/animation/EnterExitState;"), - IdType("Landroidx/compose/animation/core/MutatePriority;"), - IdType("Landroidx/compose/ui/layout/MeasuringIntrinsics$IntrinsicWidthHeight;"), - IdType("Landroidx/loader/content/ModernAsyncTask$Status;"), - IdType("Landroidx/annotation/InspectableProperty$ValueType;"), - IdType("Landroidx/compose/foundation/layout/LayoutOrientation;"), - IdType("Landroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicMinMax;"), - IdType("Landroidx/compose/material3/tokens/ShapeKeyTokens;"), - IdType("Landroidx/compose/ui/node/LayoutNode$LayoutState;"), - IdType("Landroidx/compose/runtime/InvalidationResult;"), - IdType("Landroidx/compose/ui/layout/IntrinsicWidthHeight;"), - IdType("Landroidx/compose/ui/node/NodeMeasuringIntrinsics$IntrinsicWidthHeight;"), - IdType("Landroidx/compose/runtime/Recomposer$State;"), - IdType("Landroidx/compose/foundation/gestures/Orientation;"), - IdType("Landroidx/compose/material3/ScaffoldLayoutContent;"), - IdType("Landroidx/compose/foundation/text/selection/HandleReferencePoint;"), - IdType("Landroidx/compose/material3/SnackbarDuration;"), - IdType("Landroidx/compose/ui/platform/TextToolbarStatus;"), - IdType("Landroidx/compose/material3/InputPhase;"), - IdType("Landroidx/compose/ui/window/SecureFlagPolicy;"), - IdType("Landroidx/collection/LongSparseArray;"), - IdType("Landroidx/compose/animation/core/AnimationEndReason;"), - IdType("Landroidx/compose/foundation/text/HandleState;"), - IdType("Landroidx/compose/ui/state/ToggleableState;"), - IdType("Landroidx/compose/ui/text/android/animation/SegmentType;"), - IdType("Landroidx/compose/ui/platform/actionmodecallback/MenuItemOption;"), - IdType("Landroidx/compose/foundation/layout/IntrinsicSize;"), - IdType("Landroidx/compose/ui/platform/actionmodecallback/MenuItemOption;"), -] -instrumented_apk_classes = instrumented_apk.classes -dyn_load_apk_classes = dyn_load_apk.classes -for ty in tys: - if instrumented_apk_classes[ty].annotations != dyn_load_apk_classes[ty].annotations: - print(f"-> {str(ty)}") - # invoke-virtual {0} Landroid/animation/Animator;->end()V != invoke-virtual {0} [Ljava/lang/Object;->clone()Ljava/lang/Object; - cmp( - instrumented_apk.classes[ty], - dyn_load_apk.classes[ty], - ) - - # direct: Landroidx/compose/foundation/layout/IntrinsicSize;->values()[Landroidx/compose/foundation/layout/IntrinsicSize; - # insns[3]: Landroidx/compose/foundation/layout/IntrinsicSize;->values()[Landroidx/compose/foundation/layout/IntrinsicSize;: +utils.replace_dex( + DYN_LOAD_APK, + DYN_LOAD_APK.parent + / (DYN_LOAD_APK.name.removesuffix(".apk") + "-instrumented.apk"), + dex_raw, + Path(__file__).parent.parent / "my-release-key.jks", + zipalign=Path.home() / "Android" / "Sdk" / "build-tools" / "34.0.0" / "zipalign", + apksigner=Path.home() / "Android" / "Sdk" / "build-tools" / "34.0.0" / "apksigner", +)