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..e6a946b --- /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_reflection + +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..f9bb1cd --- /dev/null +++ b/test_apks/dynloading/java/classes/com/example/theseus/dynloading/MainActivity.java @@ -0,0 +1,271 @@ +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"); + } 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); + String expectedId = "A"; + if (id.equals(expectedId)) { + Utils.popup(this, "OK", "The right class was loaded"); + } 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"); + } 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"); + } 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); + } + } +}