diff --git a/frida/theseus_frida/__init__.py b/frida/theseus_frida/__init__.py index 271397a..e5ab6ca 100644 --- a/frida/theseus_frida/__init__.py +++ b/frida/theseus_frida/__init__.py @@ -1,5 +1,7 @@ import argparse +import base64 import os +import hashlib import subprocess import time import json @@ -16,7 +18,7 @@ STACK_CONSUMER_B64 = Path(__file__).parent / "StackConsumer.dex.b64" # Define handler to event generated by the scripts -def on_message(message, data, data_storage: dict): +def on_message(message, data, data_storage: dict, file_storage: Path): if message["type"] == "error": print(f"[error] {message['description']}") print(message["stack"]) @@ -26,6 +28,8 @@ def on_message(message, data, data_storage: dict): handle_class_new_inst_data(message["payload"]["data"], data_storage) elif message["type"] == "send" and message["payload"]["type"] == "cnstr-new-isnt": handle_cnstr_new_inst_data(message["payload"]["data"], data_storage) + elif message["type"] == "send" and message["payload"]["type"] == "load-dex": + handle_load_dex(message["payload"]["data"], data_storage, file_storage) else: print("[on_message] message:", message) @@ -130,6 +134,43 @@ def handle_cnstr_new_inst_data(data, data_storage: dict): ) +def handle_load_dex(data, data_storage: dict, file_storage: Path): + dex = data["dex"] + classloader_class = data["classloader_class"] + classloader = data["classloader"] + short_class = classloader_class.split("/")[-1].removesuffix(";") + files = [] + print("DEX file loaded:") + print(f" by: {classloader_class} ({classloader})") + for file in dex: + print(f"{file=}") + file_bin = base64.b64decode(file) + hasher = hashlib.sha1() + hasher.update(file_bin) + h = hasher.digest().hex() + print(f" hash: {h}") + fname = ( + file_storage / f"{short_class}_{classloader}_{h[:16]}.bytecode" + ) # not .dex, can also be .jar or .apk or .oat or ... + i = 1 + while fname.exists(): + fname = file_storage / f"{short_class}_{classloader}_{h[:16]}_{i}.bytecode" + i += 1 + fname = fname.absolute().resolve() + + with fname.open("wb") as fp: + fp.write(file_bin) + print(f" stored: {str(fname)}") + files.append(str(fname)) + data_storage["dyn_code_load"].append( + { + "classloader_class": classloader_class, + "classloader": classloader, + "files": files, + } + ) + + def main(): parser = argparse.ArgumentParser( prog="Android Theseus project", @@ -151,9 +192,23 @@ def main(): help="where to dump the collected data, default is stdout", type=Path, ) + parser.add_argument( + "-d", + "--dex-dir", + default=Path("."), + help="where to store dynamically loaded bytecode", + type=Path, + ) args = parser.parse_args() env = dict(os.environ) + file_storage = args.dex_dir + if not file_storage.exists(): + file_storage.mkdir(parents=True) + if not file_storage.is_dir(): + print("--dex-dir must be a directory") + exit() + if args.device != "": device = frida.get_device(args.device) env["ANDROID_SERIAL"] = args.device @@ -177,17 +232,26 @@ def main(): pid = device.spawn([app]) session = device.attach(pid) - script = session.create_script(script) + try: + script = session.create_script(script) + except frida.InvalidArgumentError as e: + print( + "\n".join( + map(lambda v: f"{v[0]+1: 3} {v[1]}", enumerate(script.split("\n"))) + ) + ) + raise e data_storage = { "invoke_data": [], "class_new_inst_data": [], "cnstr_new_inst_data": [], + "dyn_code_load": [], } script.on( "message", - lambda msg, data: on_message(msg, data, data_storage), + lambda msg, data: on_message(msg, data, data_storage, file_storage), ) # Load script diff --git a/frida/theseus_frida/hook.js b/frida/theseus_frida/hook.js index 612ce6a..3f293c3 100644 --- a/frida/theseus_frida/hook.js +++ b/frida/theseus_frida/hook.js @@ -68,6 +68,17 @@ Java.perform(() => { const Class = Java.use("java.lang.Class"); const Constructor = Java.use("java.lang.reflect.Constructor"); const Modifier = Java.use("java.lang.reflect.Modifier"); + const DexFile = Java.use("dalvik.system.DexFile"); + + const File = Java.use('java.io.File'); + const Files = Java.use('java.nio.file.Files'); + const Path = Java.use('java.nio.file.Path'); + const System = Java.use('java.lang.System'); + const Arrays = Java.use('java.util.Arrays'); + + // ****** Reflexive Method Calls ****** + + // Method.invoke(obj, ..args) Method.invoke.overload( "java.lang.Object", "[Ljava.lang.Object;" // the Frida type parser is so cursted... ).implementation = function (obj, args) { @@ -87,6 +98,10 @@ Java.perform(() => { }); return this.invoke(obj, args); }; + + // ****** Reflexive Class Instantiation ****** + + // Class.newInstance() Class.newInstance.overload( ).implementation = function () { send({ @@ -106,6 +121,7 @@ Java.perform(() => { }); return this.newInstance(); }; + // Constructor.newInstance(..args) Constructor.newInstance.overload( "[Ljava.lang.Object;" ).implementation = function (args) { @@ -129,5 +145,120 @@ Java.perform(() => { return this.newInstance(args); }; + // ****** Dynamic Class Loading ****** + + // DexFile.openDexFileNative(sourceName, outputName, flags, loader, elements): load .dex from file + // See https://cs.android.com/android/platform/superproject/main/+/main:libcore/dalvik/src/main/java/dalvik/system/DexFile.java;drc=2f8a31e93fc238a88a48bfeed82557e07e1d5003;l=477 + // https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/native/dalvik_system_DexFile.cc;drc=3d19fbcc09b1b44928639b06cd0b88f735cd988d;l=368 + DexFile.openDexFileNative.overload( + 'java.lang.String', + 'java.lang.String', + 'int', + 'java.lang.ClassLoader', + '[Ldalvik.system.DexPathList$Element;', + ).implementation = function ( + sourceName, + outputName, + flags, + loader, + elements, + ) { + let file = File.$new(sourceName); + + let path = Path.of(sourceName, []); + let dex = Files.readAllBytes(path); + let b64 = Base64.encodeToString(dex, Base64.DEFAULT.value); + let classloader_class = ""; + let classloader_id = System.identityHashCode(loader); + if (loader !== null) { + classloader_class = loader.getClass().descriptorString(); + } + send({ + "type": "load-dex", + "data": { + "dex": [b64], + "classloader_class": classloader_class, + "classloader": classloader_id, + } + }); + + let is_wr = file.canWrite(); + if (is_wr) { + file.setReadOnly(); + } + let result = this.openDexFileNative( + sourceName, + outputName, + flags, + loader, + elements, + ); + /* TODO: FIX + if (is_wr) { + file.setWritable(true, false); + } + */ + return result; + }; + // DexFile.openInMemoryDexFilesNative(bufs, arrays, starts, ends, loader,elements): load .dex from memory + // See https://cs.android.com/android/platform/superproject/main/+/main:libcore/dalvik/src/main/java/dalvik/system/DexFile.java;drc=2f8a31e93fc238a88a48bfeed82557e07e1d5003;l=431 + // https://cs.android.com/android/platform/superproject/main/+/main:art/runtime/native/dalvik_system_DexFile.cc;l=253;drc=3d19fbcc09b1b44928639b06cd0b88f735cd988d + DexFile.openInMemoryDexFilesNative.overload( + '[Ljava.nio.ByteBuffer;', + '[[B', + '[I', + '[I', + 'java.lang.ClassLoader', + '[Ldalvik.system.DexPathList$Element;', + ).implementation = function ( + bufs, + arrays, + starts, + ends, + loader, + elements, + ) { + let dex = []; + // openInMemoryDexFilesNative() checks bufs.length == arrays.length == starts.length === ends.length + for (let i = 0; i < bufs.length; i++) { + let s = starts[i]; + let e = starts[i]; + // openInMemoryDexFilesNative() checks s < e + let array = arrays[i]; + let buf = bufs[i]; + let raw = []; + // match code from art/runtime/native/dalvik_system_DexFile.cc commit 3d19fbcc09b1b44928639b06cd0b88f735cd988d + if (array === null) { + raw = Arrays.copyOf([], e-s); + raw = buf.get(s, raw, 0, e-s); + } else { + raw = Arrays.copyOfRange(array, s, e); + } + let b64 = Base64.encodeToString(raw, Base64.DEFAULT.value); + dex.push(b64); + } + + let classloader_class = ""; + let classloader_id = System.identityHashCode(loader); + if (loader !== null) { + classloader_class = loader.getClass().descriptorString(); + } + send({ + "type": "load-dex", + "data": { + "dex": dex, + "classloader_class": classloader_class, + "classloader": classloader_id, + } + }); + return this.openInMemoryDexFilesNative( + bufs, + arrays, + starts, + ends, + loader, + elements, + ); + }; });