From 3996bf1b2e6bd71b1cc478c2d360896c2e2a1eaf Mon Sep 17 00:00:00 2001 From: Jean-Marie 'Histausse' Mineau Date: Fri, 31 Jan 2025 18:41:29 +0100 Subject: [PATCH] wip --- patcher/src/lib.rs | 157 ++++++++++++++++-- test_apks/tests/.gitignore | 3 + test_apks/tests/AndroidManifest.xml | 21 +++ test_apks/tests/Makefile | 67 ++++++++ .../example/theseus/tests/MainActivity.java | 110 ++++++++++++ .../com/example/theseus/tests/Reflectee.java | 19 +++ .../com/example/theseus/tests/Utils.java | 26 +++ 7 files changed, 390 insertions(+), 13 deletions(-) create mode 100644 test_apks/tests/.gitignore create mode 100644 test_apks/tests/AndroidManifest.xml create mode 100644 test_apks/tests/Makefile create mode 100644 test_apks/tests/java/classes/com/example/theseus/tests/MainActivity.java create mode 100644 test_apks/tests/java/classes/com/example/theseus/tests/Reflectee.java create mode 100644 test_apks/tests/java/classes/com/example/theseus/tests/Utils.java diff --git a/patcher/src/lib.rs b/patcher/src/lib.rs index 35db0cb..263e9cf 100644 --- a/patcher/src/lib.rs +++ b/patcher/src/lib.rs @@ -1,5 +1,6 @@ -use androscalpel::{IdMethod, Instruction, Method}; +use androscalpel::{IdMethod, IdType, Instruction, Method}; use anyhow::{bail, Context, Result}; +use std::sync::LazyLock; pub mod get_apk; @@ -19,6 +20,7 @@ struct RegistersInfo { pub array_index: u8, //pub array: u8, pub array_val: u8, + pub array: u8, //pub original_array_index_reg: Option, //pub original_array_reg: Option, pub first_arg: u16, @@ -26,19 +28,39 @@ struct RegistersInfo { } impl RegistersInfo { - const NB_U8_REG: u16 = 2; + const NB_U8_REG: u16 = 3; fn get_nb_added_reg(&self) -> u16 { - 2 + self.nb_arg_reg + 3 + self.nb_arg_reg } } -const INVOKE: &str = - "Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"; +static MTH_INVOKE: LazyLock = LazyLock::new(|| { + IdMethod::from_smali( + "Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", +) +.unwrap() +}); +static MTH_GET_NAME: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/reflect/Method;->getName()Ljava/lang/String;").unwrap() +}); +static MTH_GET_PARAMS_TY: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/reflect/Method;->getParameterTypes()[Ljava/lang/Class;") + .unwrap() +}); +static MTH_GET_RET_TY: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/reflect/Method;->getReturnType()Ljava/lang/Class;").unwrap() +}); +static MTH_GET_DEC_TY: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/reflect/Method;->getDeclaringClass()Ljava/lang/Class;") + .unwrap() +}); +static STR_EQ: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/String;->equals(Ljava/lang/Object;)Z").unwrap() +}); // Interesting stuff: https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/verifier/reg_type.h;drc=83db0626fad8c6e0508754fffcbbd58e539d14a5;l=94 // https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/verifier/method_verifier.cc;drc=83db0626fad8c6e0508754fffcbbd58e539d14a5;l=5328 pub fn transform_method(meth: &mut Method, ref_data: &ReflectionData) -> Result<()> { - let invoke = IdMethod::from_smali(INVOKE)?; // checking meth.annotations might be usefull at some point let code = meth .code @@ -55,24 +77,31 @@ pub fn transform_method(meth: &mut Method, ref_data: &ReflectionData) -> Result< let mut register_info = RegistersInfo { array_index: code.registers_size as u8, array_val: (code.registers_size + 1) as u8, + array: (code.registers_size + 2) as u8, //array: 0, - first_arg: code.registers_size + 2, + first_arg: code.registers_size + 3, nb_arg_reg: 0, }; let mut new_insns = vec![]; for ins in &code.insns { match ins { - Instruction::InvokeVirtual { method, args } if method == &invoke => { + Instruction::InvokeVirtual { method, args } if method == &*MTH_INVOKE => { // TODO move ret ? // TODO: rever from get_invoke_block failure - for ins in - get_invoke_block(ref_data, args.as_slice(), &mut register_info)?.into_iter() + let label: String = "TODO_NAME_THIS".into(); + for ins in get_invoke_block(ref_data, args.as_slice(), &mut register_info, &label)? + .into_iter() { println!(" \x1b[92m{}\x1b[0m", ins.__str__()); new_insns.push(ins); } - //new_insns.push(ins.clone()); + new_insns.push(ins.clone()); println!(" \x1b[91m{}\x1b[0m", ins.__str__()); + let lab = Instruction::Label { + name: format!("{label}_END"), + }; + new_insns.push(lab.clone()); + println!(" \x1b[91m{}\x1b[0m", lab.__str__()); } ins => { println!(" {}", ins.__str__()); @@ -104,8 +133,9 @@ fn get_invoke_block( ref_data: &ReflectionData, invoke_arg: &[u16], reg_inf: &mut RegistersInfo, + label: &str, ) -> Result> { - let (_method_obj, obj_inst, arg_arr) = if let &[a, b, c] = invoke_arg { + let (method_obj, obj_inst, arg_arr) = if let &[a, b, c] = invoke_arg { (a, b, c) } else { bail!( @@ -121,7 +151,102 @@ fn get_invoke_block( if reg_inf.nb_arg_reg < nb_args as u16 + 1 { reg_inf.nb_arg_reg = nb_args as u16 + 1; } - let mut insns = vec![]; + let mut insns = vec![ + // Check the runtime method is the right one + // Check Name + Instruction::InvokeVirtual { + method: MTH_GET_NAME.clone(), + args: vec![method_obj], + }, + Instruction::MoveResultObject { + to: reg_inf.array_index, // wrong name, but available for tmp val + }, + Instruction::ConstString { + reg: reg_inf.array_val, // wrong name, but available for tmp val + lit: ref_data.method.name.clone(), + }, + Instruction::InvokeVirtual { + method: STR_EQ.clone(), + args: vec![reg_inf.array_index as u16, reg_inf.array_val as u16], + }, + Instruction::MoveResult { + to: reg_inf.array_index, // wrong name, but available for tmp val + }, + Instruction::IfEqZ { + a: reg_inf.array_index, + label: format!("{label}_END_OF_CALL_1"), // TODO: rename 1 + }, + // Check Return Type + Instruction::InvokeVirtual { + method: MTH_GET_RET_TY.clone(), + args: vec![method_obj], + }, + Instruction::MoveResultObject { + to: reg_inf.array_index, // wrong name, but available for tmp val + }, + Instruction::ConstClass { + reg: reg_inf.array_val, // wrong name, but available for tmp val + lit: ref_data.method.proto.get_return_type(), + }, + Instruction::IfNe { + a: reg_inf.array_index, + b: reg_inf.array_val, + label: format!("{label}_END_OF_CALL_1"), // TODO: rename 1 + }, + // Check Declaring Type + Instruction::InvokeVirtual { + method: MTH_GET_DEC_TY.clone(), + args: vec![method_obj], + }, + Instruction::MoveResultObject { + to: reg_inf.array_index, // wrong name, but available for tmp val + }, + Instruction::ConstClass { + reg: reg_inf.array_val, // wrong name, but available for tmp val + lit: ref_data.method.class_.clone(), + }, + Instruction::IfNe { + a: reg_inf.array_index, + b: reg_inf.array_val, + label: format!("{label}_END_OF_CALL_1"), // TODO: rename 1 + }, + ]; + // Check for arg type + insns.push(Instruction::InvokeVirtual { + method: MTH_GET_PARAMS_TY.clone(), + args: vec![method_obj], + }); + insns.push(Instruction::MoveResultObject { + to: reg_inf.array, // wrong name, but available for tmp val + }); + for (i, param) in ref_data + .method + .proto + .get_parameters() + .into_iter() + .enumerate() + { + insns.push(Instruction::Const { + reg: reg_inf.array_index, + lit: i as i32, + }); + insns.push(Instruction::AGetObject { + dest: reg_inf.array_val, + arr: reg_inf.array, + idx: reg_inf.array_index, + }); + insns.push(Instruction::ConstClass { + reg: reg_inf.array_index, // wrong name, but available for tmp val + lit: param, + }); + insns.push(Instruction::IfNe { + a: reg_inf.array_index, + b: reg_inf.array_val, + label: format!("{label}_END_OF_CALL_1"), // TODO: rename 1 + }) + } + + // Move 'this' to fist arg insns.push(Instruction::MoveObject { from: obj_inst, to: reg_inf.first_arg, @@ -149,6 +274,12 @@ fn get_invoke_block( method: ref_data.method.clone(), args: (reg_inf.first_arg..reg_inf.first_arg + 1 + nb_args as u16).collect(), }); + insns.push(Instruction::Goto { + label: format!("{label}_END"), + }); + insns.push(Instruction::Label { + name: format!("{label}_END_OF_CALL_1"), + }); // We need a few u8 regs here. For now, we assumes we work with less than 256 reg. Ok(insns) } diff --git a/test_apks/tests/.gitignore b/test_apks/tests/.gitignore new file mode 100644 index 0000000..de18d4e --- /dev/null +++ b/test_apks/tests/.gitignore @@ -0,0 +1,3 @@ +build +ToyKey.keystore +java/classes/com/example/theseus/reflection/R.java diff --git a/test_apks/tests/AndroidManifest.xml b/test_apks/tests/AndroidManifest.xml new file mode 100644 index 0000000..ca2efdd --- /dev/null +++ b/test_apks/tests/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/test_apks/tests/Makefile b/test_apks/tests/Makefile new file mode 100644 index 0000000..17e3f33 --- /dev/null +++ b/test_apks/tests/Makefile @@ -0,0 +1,67 @@ +VERSION=34.0.0 +SDK_TOOLS=$(HOME)/Android/Sdk +JAVA_PATH=/usr/lib/jvm/java-17-openjdk/bin +JAVAC=/usr/lib/jvm/java-17-openjdk/bin/javac +JAR=/usr/lib/jvm/java-17-openjdk/bin/jar +PYTHON=python3 +APP=tests + +PACKAGE=com.example.theseus.tests +MAIN_ACTIVITY=MainActivity + +JAVAC_ARGS = +D8_ARGS = + + +VERSION_B=$(basename $(basename $(VERSION))) + +pass=ahahah + +export PATH := $(JAVA_PATH):$(PATH) + +all: $(shell mkdir -p build) +all: clean build/$(APP).apk +signature_v1: clean build/$(APP).v1.apk + +debug: JAVAC_ARGS += -g +debug: D8_ARGS += --debug +debug: all + +test: all + adb install build/$(APP).apk + adb shell am start -n $(PACKAGE)/.$(MAIN_ACTIVITY) + +build/%.v1signed.apk: ./build/%.unsigned.apk ./ToyKey.keystore + jarsigner -verbose -keystore ./ToyKey.keystore -storepass $(pass) -keypass $(pass) -signedjar $@ $< SignKey + +build/%.v1.apk: ./build/%.v1signed.apk + $(SDK_TOOLS)/build-tools/$(VERSION)/zipalign -v -f 4 $< $@ + +# TODO: fix dep somehow? cannot find a way to use % or $* in (shell ..) +build/%/classes: $(shell find java/ -type f -regex ".*\.java" ) + mkdir -p ./build/$*/classes + $(JAVAC) $(JAVAC_ARGS) -d ./build/$*/classes -classpath build/deps.jar:$(SDK_TOOLS)/platforms/android-$(VERSION_B)/android.jar $$(find java/$*/ -type f -regex ".*\.java") + +build/%/classes.dex: build/%/classes + mkdir -p ./build/$* + $(SDK_TOOLS)/build-tools/$(VERSION)/d8 $(D8_ARGS) --classpath $(SDK_TOOLS)/platforms/android-$(VERSION_B)/android.jar $(shell find build/$*/classes -type f -regex ".*\.class" -printf "'%p'\n") --output ./build/$*/ + +build/%.unsigned.apk: build/classes/classes.dex + mkdir -p ./build/$*_files + mv ./build/classes/classes.dex ./build/$*_files/classes.dex + $(SDK_TOOLS)/build-tools/$(VERSION)/aapt package -v -f -M ./AndroidManifest.xml -I $(SDK_TOOLS)/platforms/android-$(VERSION_B)/android.jar -F $@ ./build/$*_files + +build/%.v2aligned.apk: ./build/%.unsigned.apk ./ToyKey.keystore + $(SDK_TOOLS)/build-tools/$(VERSION)/zipalign -v -f 4 $< $@ + +build/%.apk: ./build/%.v2aligned.apk + $(SDK_TOOLS)/build-tools/$(VERSION)/apksigner sign -ks ./ToyKey.keystore --v2-signing-enabled true --in $< --out $@ --ks-pass pass:$(pass) + +ToyKey.keystore : + keytool -genkeypair -validity 1000 -dname "CN=SomeKey,O=SomeOne,C=FR" -keystore $@ -storepass $(pass) -keypass $(pass) -alias SignKey -keyalg RSA -v + +clean: + $(RM) -r build/* + +clean_all: clean + $(RM) ToyKey.keystore diff --git a/test_apks/tests/java/classes/com/example/theseus/tests/MainActivity.java b/test_apks/tests/java/classes/com/example/theseus/tests/MainActivity.java new file mode 100644 index 0000000..2d3573f --- /dev/null +++ b/test_apks/tests/java/classes/com/example/theseus/tests/MainActivity.java @@ -0,0 +1,110 @@ +package com.example.theseus.tests; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; + +import java.lang.ClassLoader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Constructor; +import java.lang.ClassNotFoundException; +import java.util.Arrays; + +import android.util.Log; + +import com.example.theseus.Utils; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + //callVirtualMethod(); + callVirtualMethodReflectCall(); + //callConstructorVirtualMethodReflectConstr(); + //callVirtualMethodReflectOldConst(); + } catch(Exception e) { + Log.e("THESEUS", "Error: ", e); + } + } + + // A normal virtual method call + public void callVirtualMethod() { + String data = Utils.source("no reflect virt call"); + Reflectee r = new Reflectee("R1"); + String newData = r.transfer(data); + Utils.sink(this, newData); + } + + // A call to a virtual method through reflection + public void callVirtualMethodReflectCall() throws + ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException + { + String data = Utils.source("reflect virt call"); + Reflectee r = new Reflectee("R2"); + ClassLoader cl = MainActivity.class.getClassLoader(); + Class clz = cl.loadClass("com.example.theseus.tests.Reflectee"); + Method mth = clz.getMethod("transfer", String.class); + String name = mth.getName(); + Class[] params = mth.getParameterTypes(); + Class ret = mth.getReturnType(); + Class dec = mth.getDeclaringClass(); + Log.e("[TEST]", "---------------------------------"); + Log.e("[TEST]", name); + Log.e("[TEST]", params.toString()); + Log.e("[TEST]", ret.toString()); + Log.e("[TEST]", dec.toString()); + Log.e("[TEST]", "---------------------------------"); + if (name.equals("transfer") && Arrays.equals(params, new Class[] {String.class}) && ret == String.class && dec == Reflectee.class) { + Log.e("[TEST]", "OK"); + } + String newData = (String) mth.invoke(r, data); + Utils.sink(this, newData); + } + + // A call to a virtual method through reflection using an object instanciated + // through reflection. The sensitive data is passed to the constructor. + public void callConstructorVirtualMethodReflectConstr() throws + ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException, + InstantiationException + { + String data = Utils.source("no reflect constr"); + ClassLoader cl = MainActivity.class.getClassLoader(); + Class clz = cl.loadClass("com.example.theseus.reflection.Reflectee"); + Constructor cst = clz.getDeclaredConstructor(String.class); + Object r = cst.newInstance(data); + Method mth = clz.getMethod("transfer", String.class); + String newData = (String) mth.invoke(r, ""); + Utils.sink(this, newData); + } + + // A call to a virtual method through reflection using an object instanciated + // through reflection using a deprecated method. + public void callVirtualMethodReflectOldConst() throws + ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException, + InstantiationException + { + String data = Utils.source("no reflect constr"); + ClassLoader cl = MainActivity.class.getClassLoader(); + Class clz = cl.loadClass("com.example.theseus.reflection.Reflectee"); + Object r = clz.newInstance(); + Method mth = clz.getMethod("transfer", String.class); + String newData = (String) mth.invoke(r, data); + Utils.sink(this, newData); + } + + // TODO: many argument methods + // TODO: static + // TODO: factory patern +} diff --git a/test_apks/tests/java/classes/com/example/theseus/tests/Reflectee.java b/test_apks/tests/java/classes/com/example/theseus/tests/Reflectee.java new file mode 100644 index 0000000..883aa43 --- /dev/null +++ b/test_apks/tests/java/classes/com/example/theseus/tests/Reflectee.java @@ -0,0 +1,19 @@ +package com.example.theseus.tests; + + +public class Reflectee { + + String name; + + public Reflectee() { + this.name = ""; + } + + public Reflectee(String name) { + this.name = "[" + name + "] "; + } + + public String transfer(String data) { + return name + data; + } +} diff --git a/test_apks/tests/java/classes/com/example/theseus/tests/Utils.java b/test_apks/tests/java/classes/com/example/theseus/tests/Utils.java new file mode 100644 index 0000000..0c8ca2a --- /dev/null +++ b/test_apks/tests/java/classes/com/example/theseus/tests/Utils.java @@ -0,0 +1,26 @@ +package com.example.theseus; + +import android.app.Activity; +import android.app.AlertDialog; + + +public class Utils { + public static String source() { + return "Secret"; + } + public static String source(String tag) { + return "[" + tag + "] Secret"; + } + + public static void popup(Activity ac, String title, String msg) { + (new AlertDialog.Builder(ac)) + .setMessage(msg) + .setTitle(title) + .create() + .show(); + } + + public static void sink(Activity ac, String data) { + popup(ac, "Data leak:", data); + } +}