diff --git a/patcher/src/dex_types.rs b/patcher/src/dex_types.rs index 6d58314..110a388 100644 --- a/patcher/src/dex_types.rs +++ b/patcher/src/dex_types.rs @@ -42,6 +42,9 @@ pub(crate) static CNSTR_GET_DEC_CLS: LazyLock = LazyLock::new(|| { IdMethod::from_smali("Ljava/lang/reflect/Constructor;->getDeclaringClass()Ljava/lang/Class;") .unwrap() }); +pub(crate) static CLT_GET_DESCR_STRING: LazyLock = LazyLock::new(|| { + IdMethod::from_smali("Ljava/lang/Class;->descriptorString()Ljava/lang/String;").unwrap() +}); pub(crate) static OBJ_TO_SCAL_BOOL: LazyLock = LazyLock::new(|| IdMethod::from_smali("Ljava/lang/Boolean;->booleanValue()Z").unwrap()); pub(crate) static OBJ_TO_SCAL_BYTE: LazyLock = diff --git a/patcher/src/reflection_patcher.rs b/patcher/src/reflection_patcher.rs index e72ab51..bb85e86 100644 --- a/patcher/src/reflection_patcher.rs +++ b/patcher/src/reflection_patcher.rs @@ -344,11 +344,34 @@ fn gen_tester_method( reg: reg_arr_idx, // wrong name, but available for tmp val lit: param, }); - insns.push(Instruction::IfNe { + insns.push(Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_idx as u16], + }); + insns.push(Instruction::MoveResultObject { to: reg_arr_idx }); + insns.push(Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_val as u16], + }); + insns.push(Instruction::MoveResultObject { to: reg_arr_val }); + insns.push(Instruction::InvokeVirtual { + method: STR_EQ.clone(), + args: vec![reg_arr_idx as u16, reg_arr_val as u16], + }); + insns.push(Instruction::MoveResult { + to: reg_arr_idx, // wrong name, but available for tmp val + }); + insns.push(Instruction::IfEqZ { a: reg_arr_idx, - b: reg_arr_val, label: no_label.clone(), - }) + }); + // Comparing Type does not work when different types share the same name (eg type from + // another class loader) + //insns.push(Instruction::IfNe { + // a: reg_arr_idx, + // b: reg_arr_val, + // label: no_label.clone(), + //}) } if !is_constructor { insns.append(&mut vec![ @@ -388,11 +411,34 @@ fn gen_tester_method( reg: reg_arr_val, // wrong name, but available for tmp val lit: method_to_test.proto.get_return_type(), }, - Instruction::IfNe { + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_idx as u16], + }, + Instruction::MoveResultObject { to: reg_arr_idx }, + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_val as u16], + }, + Instruction::MoveResultObject { to: reg_arr_val }, + Instruction::InvokeVirtual { + method: STR_EQ.clone(), + args: vec![reg_arr_idx as u16, reg_arr_val as u16], + }, + Instruction::MoveResult { + to: reg_arr_idx, // wrong name, but available for tmp val + }, + Instruction::IfEqZ { a: reg_arr_idx, - b: reg_arr_val, label: no_label.clone(), }, + // Comparing Type does not work when different types share the same name (eg type from + // another class loader) + //Instruction::IfNe { + // a: reg_arr_idx, + // b: reg_arr_val, + // label: no_label.clone(), + //}, // Check Declaring Type Instruction::InvokeVirtual { method: MTH_GET_DEC_CLS.clone(), @@ -415,11 +461,34 @@ fn gen_tester_method( reg: reg_arr_val, // wrong name, but available for tmp val lit: method_to_test.class_.clone(), }, - Instruction::IfNe { + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_idx as u16], + }, + Instruction::MoveResultObject { to: reg_arr_idx }, + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_arr_val as u16], + }, + Instruction::MoveResultObject { to: reg_arr_val }, + Instruction::InvokeVirtual { + method: STR_EQ.clone(), + args: vec![reg_arr_idx as u16, reg_arr_val as u16], + }, + Instruction::MoveResult { + to: reg_arr_idx, // wrong name, but available for tmp val + }, + Instruction::IfEqZ { a: reg_arr_idx, - b: reg_arr_val, label: no_label.clone(), }, + // Comparing Type does not work when different types share the same name (eg type from + // another class loader) + //Instruction::IfNe { + // a: reg_arr_idx, + // b: reg_arr_val, + // label: no_label.clone(), + //}, Instruction::Const { reg: reg_arr_val, lit: 1, @@ -869,11 +938,36 @@ fn get_class_new_inst_block( reg: reg_inf.array_index, // wrong name, but available for tmp val lit: ref_data.constructor.class_.clone(), }, - Instruction::IfNe { + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![reg_inf.array_index as u16], + }, + Instruction::MoveResultObject { + to: reg_inf.array_index, + }, + Instruction::InvokeVirtual { + method: CLT_GET_DESCR_STRING.clone(), + args: vec![class_reg as u16], + }, + Instruction::MoveResultObject { to: class_reg }, + Instruction::InvokeVirtual { + method: STR_EQ.clone(), + args: vec![reg_inf.array_index as u16, class_reg as u16], + }, + Instruction::MoveResult { + to: reg_inf.array_index, // wrong name, but available for tmp val + }, + Instruction::IfEqZ { a: reg_inf.array_index, - b: class_reg, label: abort_label.clone(), }, + // Comparing Type does not work when different types share the same name (eg type from + // another class loader) + //Instruction::IfNe { + // a: reg_inf.array_index, + // b: class_reg, + // label: abort_label.clone(), + //}, Instruction::NewInstance { reg: obj_reg, lit: ref_data.constructor.class_.clone(), diff --git a/test_apks/dynloading/.gitignore b/test_apks/dynloading/.gitignore new file mode 100644 index 0000000..487375c --- /dev/null +++ b/test_apks/dynloading/.gitignore @@ -0,0 +1,3 @@ +build +ToyKey.keystore +java/classes/com/example/theseus/dynloading/R.java diff --git a/test_apks/dynloading/AndroidManifest.xml b/test_apks/dynloading/AndroidManifest.xml new file mode 100644 index 0000000..0e8e9df --- /dev/null +++ b/test_apks/dynloading/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/test_apks/dynloading/Makefile b/test_apks/dynloading/Makefile new file mode 100644 index 0000000..bcaa9fe --- /dev/null +++ b/test_apks/dynloading/Makefile @@ -0,0 +1,68 @@ +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=test_dynloading + +PACKAGE=com.example.theseus.dynloading +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 build/a/classes.dex + mkdir -p ./build/$*_files ./build/$*_files/assets + mv ./build/classes/classes.dex ./build/$*_files/classes.dex + mv build/a/classes.dex ./build/$*_files/assets/a.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/dynloading/java/a/com/example/theseus/dynloading/AMain.java b/test_apks/dynloading/java/a/com/example/theseus/dynloading/AMain.java new file mode 100644 index 0000000..fbdb5e6 --- /dev/null +++ b/test_apks/dynloading/java/a/com/example/theseus/dynloading/AMain.java @@ -0,0 +1,7 @@ +package com.example.theseus.dynloading; + +public class AMain { + public static String getColliderId() { + return Collider.getColliderId(); + } +} diff --git a/test_apks/dynloading/java/a/com/example/theseus/dynloading/Collider.java b/test_apks/dynloading/java/a/com/example/theseus/dynloading/Collider.java new file mode 100644 index 0000000..29d0002 --- /dev/null +++ b/test_apks/dynloading/java/a/com/example/theseus/dynloading/Collider.java @@ -0,0 +1,7 @@ +package com.example.theseus.dynloading; + +public class Collider { + public static String getColliderId() { + return "A"; + } +} diff --git a/test_apks/dynloading/java/classes/com/example/theseus/Utils.java b/test_apks/dynloading/java/classes/com/example/theseus/Utils.java new file mode 100644 index 0000000..1d02590 --- /dev/null +++ b/test_apks/dynloading/java/classes/com/example/theseus/Utils.java @@ -0,0 +1,36 @@ +package com.example.theseus; + +import android.app.Activity; +import android.app.AlertDialog; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +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); + } + public static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + while((read = in.read(buffer)) != -1){ + out.write(buffer, 0, read); + } + } +} diff --git a/test_apks/dynloading/java/classes/com/example/theseus/dynloading/Collider.java b/test_apks/dynloading/java/classes/com/example/theseus/dynloading/Collider.java new file mode 100644 index 0000000..21de7b5 --- /dev/null +++ b/test_apks/dynloading/java/classes/com/example/theseus/dynloading/Collider.java @@ -0,0 +1,7 @@ +package com.example.theseus.dynloading; + +public class Collider { + public static String getColliderId() { + return "MainAPK"; + } +} diff --git a/test_apks/dynloading/java/classes/com/example/theseus/dynloading/MainActivity.java b/test_apks/dynloading/java/classes/com/example/theseus/dynloading/MainActivity.java new file mode 100644 index 0000000..ef1480b --- /dev/null +++ b/test_apks/dynloading/java/classes/com/example/theseus/dynloading/MainActivity.java @@ -0,0 +1,272 @@ +package com.example.theseus.dynloading; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; + +import android.widget.RelativeLayout; +import android.widget.ScrollView; +import android.widget.LinearLayout; +import android.view.ViewGroup; +import android.view.View; +import android.widget.Button; +import android.content.res.ColorStateList; + +import java.lang.ClassLoader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Constructor; +import java.lang.ClassNotFoundException; + +import android.content.res.AssetManager; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.InvocationTargetException; +import android.content.Context; +import dalvik.system.PathClassLoader; +import java.lang.reflect.Method; + +import com.example.theseus.Utils; + + +import java.util.Arrays; + +public class MainActivity extends Activity { + + public void setup() { + AssetManager assetManager = getAssets(); + InputStream in = null; + OutputStream out = null; + File outFile = null; + try { + in = assetManager.open("a.dex"); + outFile = new File(getCacheDir(), "a.dex_"); // .dex_ because android does not like people writing .dex + out = new FileOutputStream(outFile); + Utils.copy(in, out); + outFile.renameTo(new File(getCacheDir(), "a.dex")); // security? + } catch (IOException e) {} + try { + in.close(); + } catch (IOException e) {} + try { + out.close(); + } catch (IOException e) {} + } + + public String getdexfile(String name) { + File dexfile = new File(getCacheDir(), name); + dexfile.setReadOnly(); + Log.e("DEBUG", dexfile.getPath()); + return dexfile.getPath(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setup(); + + ColorStateList buttonColor = ColorStateList.valueOf(0xff808080); + + RelativeLayout relLayout = new RelativeLayout(this); + relLayout.generateViewId(); + + ScrollView scrollView = new ScrollView(this); + scrollView.generateViewId(); + + RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + lp.addRule(RelativeLayout.CENTER_IN_PARENT); + + LinearLayout linLayout = new LinearLayout(this); + linLayout.generateViewId(); + linLayout.setLayoutParams(lp); + linLayout.setOrientation(LinearLayout.VERTICAL); + + + Button b1 = new Button(this); + b1.generateViewId(); + linLayout.addView(b1); + + Button b2 = new Button(this); + b2.generateViewId(); + linLayout.addView(b2); + + Button b3 = new Button(this); + b3.generateViewId(); + linLayout.addView(b3); + + Button b4 = new Button(this); + b4.generateViewId(); + linLayout.addView(b4); + + + scrollView.addView(linLayout); + relLayout.addView(scrollView); + setContentView(relLayout); + + Activity ac = this; + + b1.setText("Direct With Parent"); + b1.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.setBackgroundTintList(buttonColor); + try { + directWithParent(); + } catch(Exception e) { + Log.e("THESEUS", "Error: ", e); + } + } + }); + + b2.setText("Direct Without Parent"); + b2.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.setBackgroundTintList(buttonColor); + try { + directWithoutParent(); + } catch(Exception e) { + Log.e("THESEUS", "Error: ", e); + } + } + }); + + b3.setText("Indirect With Parent"); + b3.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.setBackgroundTintList(buttonColor); + try { + indirectWithParent(); + } catch(Exception e) { + Log.e("THESEUS", "Error: ", e); + } + } + }); + + b4.setText("Indirect Without Parent"); + b4.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.setBackgroundTintList(buttonColor); + try { + indirectWithoutParent(); + } catch(Exception e) { + Log.e("THESEUS", "Error: ", e); + } + } + }); + } + + public void directWithParent() { + try { + PathClassLoader cl = new PathClassLoader(getdexfile("a.dex"), MainActivity.class.getClassLoader()); + Class clz = cl.loadClass("com.example.theseus.dynloading.Collider"); + Method mth = clz.getMethod("getColliderId"); + String id = (String)mth.invoke(null); + //Utils.popup(this, "Result", id); + String expectedId = "MainAPK"; + if (id.equals(expectedId)) { + Utils.popup(this, "OK", "The right class was loaded: " + id); + } else { + Utils.popup(this, "BAD", "The wrong class was loaded: id = " + id + " expected id = " + expectedId); + } + } catch (ClassNotFoundException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (NoSuchMethodException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (IllegalAccessException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (InvocationTargetException e) { + Log.e("DEBUG", "ERROR: ", e); + } + } + + public void directWithoutParent() { + try { + PathClassLoader cl = new PathClassLoader(getdexfile("a.dex"), null); + Class clz = cl.loadClass("com.example.theseus.dynloading.Collider"); + Method mth = clz.getMethod("getColliderId"); + String id = (String)mth.invoke(null); + //Utils.popup(this, "Result", id); + Utils.popup(this, "TEST", clz.descriptorString()); + String expectedId = "A"; + if (id.equals(expectedId)) { + Utils.popup(this, "OK", "The right class was loaded: " + id); + } else { + Utils.popup(this, "BAD", "The wrong class was loaded: id = " + id + " expected id = " + expectedId); + } + } catch (ClassNotFoundException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (NoSuchMethodException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (IllegalAccessException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (InvocationTargetException e) { + Log.e("DEBUG", "ERROR: ", e); + } + } + + public void indirectWithParent() { + try { + PathClassLoader cl = new PathClassLoader(getdexfile("a.dex"), MainActivity.class.getClassLoader()); + Class clz = cl.loadClass("com.example.theseus.dynloading.AMain"); + Method mth = clz.getMethod("getColliderId"); + String id = (String)mth.invoke(null); + //Utils.popup(this, "Result", id); + String expectedId = "MainAPK"; + if (id.equals(expectedId)) { + Utils.popup(this, "OK", "The right class was loaded: " + id); + } else { + Utils.popup(this, "BAD", "The wrong class was loaded: id = " + id + " expected id = " + expectedId); + } + } catch (ClassNotFoundException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (NoSuchMethodException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (IllegalAccessException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (InvocationTargetException e) { + Log.e("DEBUG", "ERROR: ", e); + } + } + + public void indirectWithoutParent() { + try { + PathClassLoader cl = new PathClassLoader(getdexfile("a.dex"), null); + Class clz = cl.loadClass("com.example.theseus.dynloading.AMain"); + Method mth = clz.getMethod("getColliderId"); + String id = (String)mth.invoke(null); + //Utils.popup(this, "Result", id); + String expectedId = "A"; + if (id.equals(expectedId)) { + Utils.popup(this, "OK", "The right class was loaded: " + id); + } else { + Utils.popup(this, "BAD", "The wrong class was loaded: id = " + id + " expected id = " + expectedId); + } + } catch (ClassNotFoundException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (NoSuchMethodException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (IllegalAccessException e) { + Log.e("DEBUG", "ERROR: ", e); + } + catch (InvocationTargetException e) { + Log.e("DEBUG", "ERROR: ", e); + } + } +}