diff --git a/patcher/src/reflection_patcher.rs b/patcher/src/reflection_patcher.rs index 634e5fa..e72ab51 100644 --- a/patcher/src/reflection_patcher.rs +++ b/patcher/src/reflection_patcher.rs @@ -83,9 +83,9 @@ pub fn transform_method( while move_ret.as_ref() != iter.next() {} } let end_label = if method == &*MTH_INVOKE { - format!("end_reflection_call_at_{}", "TODO_ADDR") + format!("end_reflection_call_at_{}", addr_label.clone()) } else if method == &*CLASS_NEW_INST || method == &*CNSTR_NEW_INST { - format!("end_reflection_instanciation_at_{}", "TODO_ADDR") + format!("end_reflection_instanciation_at_{}", addr_label.clone()) } else { panic!("Should not happen!") }; @@ -740,9 +740,9 @@ fn get_cnstr_new_inst_block( } let abort_label = format!( - "end_static_instance_with_{}_at_{}", + "end_static_instance_with_{}_at_{:08X}", ref_data.constructor.try_to_smali()?, - "TODO_ADDR" + ref_data.addr ); let mut insns = test_cnstr( @@ -854,9 +854,9 @@ fn get_class_new_inst_block( let class_reg = class_reg as u8; let abort_label = format!( - "end_static_instance_with_{}_at_{}", + "end_static_instance_with_{}_at_{:08X}", ref_data.constructor.try_to_smali()?, - "TODO_ADDR" + ref_data.addr ); let obj_reg = match move_result { diff --git a/theseus_autopatcher/.gitignore b/theseus_autopatcher/.gitignore new file mode 100644 index 0000000..d959d6c --- /dev/null +++ b/theseus_autopatcher/.gitignore @@ -0,0 +1,3 @@ +dist +__pycache__ +src/theseus_autopatcher/patcher_86_64_musl diff --git a/theseus_autopatcher/README.md b/theseus_autopatcher/README.md index e69de29..3180203 100644 --- a/theseus_autopatcher/README.md +++ b/theseus_autopatcher/README.md @@ -0,0 +1,36 @@ +# Android Theseus Patcher + +This is mostly glueware between the [theseus frida](../frida) python package (used to get runtime information) and the [theseus patcher](../patcher) (rust binary used tp patch the apk). + +This package embed the patcher binary for ease of use. The embedded version is build for linux x86_64, statically linked to musl. For other target platform (windows, arm, ect), a different patcher binary can provided at runtime. + +## Build + +TODO: use nix build the project + +Before building this package, the patcher binary must be built with the musl target. This require the `x86_64-unknown-linux-musl` to be installed, as well as `musl-gcc`: + +``` +rustup target add x86_64-unknown-linux-musl +doas pacman -S musl +``` + +Build the patcher: + +``` +cd ../patcher +cargo build --release --target=x86_64-unknown-linux-musl +cd - +``` + +Copy to patcher to the python directory: + +``` +cp ../patcher/target/x86_64-unknown-linux-musl/release/patcher src/theseus_autopatcher/patcher_86_64_musl +``` + +Build the package: + +``` +poetry build +``` diff --git a/theseus_autopatcher/pyproject.toml b/theseus_autopatcher/pyproject.toml index 1f7984a..c92208a 100644 --- a/theseus_autopatcher/pyproject.toml +++ b/theseus_autopatcher/pyproject.toml @@ -13,6 +13,9 @@ dependencies = [ [tool.poetry] packages = [{include = "theseus_autopatcher", from = "src"}] +include = [ + { path = "src/theseus_autopatcher/patcher_86_64_musl", format = ["sdist", "wheel"] }, +] [build-system] diff --git a/theseus_autopatcher/src/theseus_autopatcher/__init__.py b/theseus_autopatcher/src/theseus_autopatcher/__init__.py index 4ff235e..43c4685 100644 --- a/theseus_autopatcher/src/theseus_autopatcher/__init__.py +++ b/theseus_autopatcher/src/theseus_autopatcher/__init__.py @@ -1,2 +1,226 @@ +import os +import argparse +import subprocess +import tempfile +from pathlib import Path +from shutil import which + +from theseus_frida import collect_runtime + + +def get_android_sdk_path() -> Path | None: + if "ANDROID_HOME" in os.environ: + return os.environ["ANDROID_HOME"] + default = Path.home() / "Android" / "Sdk" + if default.exists(): + return default + return None + + +def get_build_tools_path(toolname: str) -> Path | None: + def score_version(name: str): + score = [] + for n in name.split("."): + if n.isdecimal(): + score.append(int(n)) + else: + score.append(-1) + return score + + path = which(toolname) + if path is not None: + return path + path = which(toolname + ".exe") + if path is not None: + return path + + sdk = get_android_sdk_path() + if sdk is None: + return None + tools = sdk / "build-tools" + if not tools.exists(): + return None + options = [] + for d in tools.iterdir(): + if (d / toolname).exists(): + options.append(d / toolname) + if (d / (toolname + ".exe")).exists(): + options.append(d / (toolname + ".exe")) + if not options: + return None + return max(options, key=lambda d: score_version(d.parent.name)) + + +def get_keytool_path() -> Path | None: + path = which("keytool") + if path is not None: + return path + return which("keytool.exe") + + +def gen_keystore(keytool: Path, storepath: Path): + print(f"{str(storepath)} does not exist, creating it.") + subprocess.run( + [ + str(keytool), + "-genkeypair", + "-validity", + "1000", + "-dname", + "CN=SomeKey,O=SomeOne,C=FR", + "-keystore", + str(storepath), + "-alias", + "SignKey", + "-keyalg", + "RSA", + "-v", + ] + ) + + +PATCHER_BIN_PATH = Path(__file__).parent / "patcher_86_64_musl" + + +def patch_apk( + runtime_data: Path, + apk: Path, + apkout: Path, + zipalign: Path, + apksigner: Path, + keystore: Path, +): + def dbg(l): + print(" ".join(l)) + return l + + subprocess.run( + dbg( + [ + str(PATCHER_BIN_PATH.absolute()), + "--runtime-data", + str(runtime_data.absolute()), + "--path", + str(apk.absolute()), + "--out", + str(apkout.absolute()), + "-k", + str(keystore.absolute()), + "-z", + str(zipalign.absolute()), + "-a", + str(apksigner.absolute()), + ] + ) + ) + + def main(): - print("hello void") + parser = argparse.ArgumentParser(prog="Android Theseus project") + parser.add_argument( + "-a", "--apk", required=True, help="Target application", type=Path + ) + parser.add_argument( + "-s", + "--device", + default="", + help="The android device to connect to, eg: 'emulator-5554'", + type=str, + ) + parser.add_argument( + "-o", + "--output-apk", + required=True, + help="Where to write the repackaged apk", + type=Path, + ) + parser.add_argument( + "--zipalign", + required=False, + help="Path to the zipalign executable to use", + type=Path, + ) + parser.add_argument( + "--apksigner", + required=False, + help="Path to the apksigner executable to use", + type=Path, + ) + parser.add_argument( + "-k", + "--keystore", + required=False, + help="Path to the apksigner executable to use", + type=Path, + default=Path(".") / "TheseusKey.keystore", + ) + parser.add_argument( + "--keytool", + required=False, + help="Path to the keytool executable to use", + type=Path, + ) + parser.add_argument( + "--patch", + required=False, + help="Path to the patcher executable to use. By default, use the one embeded with \ + the package. (static x86_64 linux build with musl)", + type=Path, + ) + args = parser.parse_args() + + if args.zipalign is None: + zipalign = get_build_tools_path("zipalign") + else: + zipalign = args.zipalign + if args.apksigner is None: + apksigner = get_build_tools_path("apksigner") + else: + apksigner = args.apksigner + if args.keytool is None: + keytool = get_keytool_path() + else: + keytool = args.keytool + + if zipalign is None: + print( + "Could not find zipalign, please install an android build-tools package. " + "If one is already installed, please use `--zipalign` to provide the path " + "to the zipalign executable." + ) + exit(1) + if apksigner is None: + print( + "Could not find apksigner, please install an android build-tools package. " + "If one is already installed, please use `--apksigner` to provide the path " + "to the apksigner executable." + ) + exit(1) + if keytool is None and not args.keystore.exists(): + print( + f"Could not find keytool and {str(args.keystore)} does not exist. Either " + "provide an existing keystore with -k or install a JDK. If one is already installed, " + "please use --keytool to provide the path to the keytool executable." + ) + exit(1) + + if not args.keystore.exists(): + gen_keystore(keytool, args.keystore) + + tmpdname = "/tmp/tmp.xzq9jLxUbQ/tmp" + if True: + # with tempfile.TemporaryDirectory() as tmpdname: + tmpd = Path(tmpdname) + (tmpd / "dex").mkdir() + with (tmpd / "runtime.json").open("w") as fp: + collect_runtime( + apk=args.apk, device=args.device, file_storage=tmpd / "dex", output=fp + ) + patch_apk( + runtime_data=tmpd / "runtime.json", + apk=args.apk, + apkout=args.output_apk, + zipalign=zipalign, + apksigner=apksigner, + keystore=args.keystore, + )