This commit is contained in:
Jean-Marie Mineau 2025-03-31 17:32:23 +02:00
parent f8c70b576b
commit a374769389
Signed by: histausse
GPG key ID: B66AEEDA9B645AD2
6 changed files with 163 additions and 39 deletions

View file

@ -6,8 +6,11 @@ import subprocess
import time
import json
import sys
import tempfile
import shutil
import lzma
from pathlib import Path
from typing import TextIO
from typing import TextIO, Any
import frida # type: ignore
from androguard.core.apk import get_apkid # type: ignore
@ -23,6 +26,12 @@ STACK_CONSUMER_B64 = Path(__file__).parent / "StackConsumer.dex.b64"
HASH_NB_BYTES = 4
def spinner(symbs: str = "-\\|/"):
while True:
for s in symbs:
yield s
# Define handler to event generated by the scripts
def on_message(message, data, data_storage: dict, file_storage: Path):
if message["type"] == "error":
@ -36,6 +45,8 @@ def on_message(message, data, data_storage: dict, file_storage: Path):
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)
elif message["type"] == "send" and message["payload"]["type"] == "apk-cl":
handle_classloader_data(message["payload"]["data"], data_storage)
else:
print("[-] message:", message)
@ -48,8 +59,13 @@ def print_stack(stack, prefix: str):
print(f" {prefix}{frame['method']}:{frame['bytecode_index']}{native}")
def handle_classloader_data(data: dict, data_storage: dict):
data_storage["initial_classloaders"].append(data)
def handle_invoke_data(data, data_storage: dict):
method = data["method"]
method_cl_id = data["method_cl_id"]
# TODO: good idea?
if method in [
"Landroid/view/View;->getTranslationZ()F",
@ -59,6 +75,7 @@ def handle_invoke_data(data, data_storage: dict):
if len(data["stack"]) == 0:
return
caller_method = data["stack"][0]["method"]
caller_cl_id = data["stack"][0]["cl_id"]
addr = data["stack"][0]["bytecode_index"]
is_static = data["is_static"]
if is_static:
@ -66,8 +83,8 @@ def handle_invoke_data(data, data_storage: dict):
else:
is_static_str = ""
print("[+] Method.Invoke:")
print(f" called: {method}{is_static_str}")
print(f" by: {caller_method}")
print(f" called: [{method_cl_id}]{method}{is_static_str}")
print(f" by: [{caller_cl_id}]{caller_method}")
print(f" at: 0x{addr:08x}")
# print(f" stack:")
# print_stack(data["stack"], " ")
@ -76,7 +93,11 @@ def handle_invoke_data(data, data_storage: dict):
data_storage["invoke_data"].append(
{
"method": method,
"method_cl_id": method_cl_id,
"renamed_method": None,
"caller_method": caller_method,
"caller_cl_id": caller_cl_id,
"renamed_caller_method": None,
"addr": addr,
"is_static": is_static,
}
@ -85,6 +106,7 @@ def handle_invoke_data(data, data_storage: dict):
def handle_class_new_inst_data(data, data_storage: dict):
constructor = data["constructor"]
constructor_cl_id = data["constructor_cl_id"]
if len(data["stack"]) == 0:
return
if (
@ -97,10 +119,11 @@ def handle_class_new_inst_data(data, data_storage: dict):
else:
return
caller_method = frame["method"]
caller_cl_id = frame["cl_id"]
addr = frame["bytecode_index"]
print("[+] Class.NewInstance:")
print(f" called: {constructor}")
print(f" by: {caller_method}")
print(f" called: [{constructor_cl_id}]{constructor}")
print(f" by: [{caller_cl_id}]{caller_method}")
print(f" at: 0x{addr:08x}")
# print(f" stack:")
# print_stack(data["stack"], " ")
@ -109,7 +132,11 @@ def handle_class_new_inst_data(data, data_storage: dict):
data_storage["class_new_inst_data"].append(
{
"constructor": constructor,
"constructor_cl_id": constructor_cl_id,
"renamed_constructor": None,
"caller_method": caller_method,
"caller_cl_id": caller_cl_id,
"renamed_caller_method": None,
"addr": addr,
}
)
@ -117,15 +144,17 @@ def handle_class_new_inst_data(data, data_storage: dict):
def handle_cnstr_new_inst_data(data, data_storage: dict):
constructor = data["constructor"]
constructor_cl_id = data["constructor_cl_id"]
if not constructor.startswith("Lcom/example/theseus"):
return
if len(data["stack"]) == 0:
return
caller_method = data["stack"][0]["method"]
caller_cl_id = data["stack"][0]["cl_id"]
addr = data["stack"][0]["bytecode_index"]
print("[+] Constructor.newInstance:")
print(f" called: {constructor}")
print(f" by: {caller_method}")
print(f" called: [{constructor_cl_id}]{constructor}")
print(f" by: [{caller_cl_id}]{caller_method}")
print(f" at: 0x{addr:08x}")
# print(f" stack:")
# print_stack(data["stack"], " ")
@ -134,7 +163,11 @@ def handle_cnstr_new_inst_data(data, data_storage: dict):
data_storage["cnstr_new_inst_data"].append(
{
"constructor": constructor,
"constructor_cl_id": constructor_cl_id,
"renamed_constructor": None,
"caller_method": caller_method,
"caller_cl_id": caller_cl_id,
"renamed_caller_method": None,
"addr": addr,
}
)
@ -183,10 +216,10 @@ FRIDA_SERVER_BIN = Path(__file__).parent / "frida-server-16.7.0-android-x86_64.x
FRIDA_SERVER_ANDROID_PATH = "/data/local/tmp/frida-server"
def setup_frida(device: str, env: dict[str, str]) -> frida.core.Device:
if device != "":
device = frida.get_device(args.device)
env["ANDROID_SERIAL"] = args.device
def setup_frida(device_name: str, env: dict[str, str]) -> frida.core.Device:
if device_name != "":
device = frida.get_device(device_name)
env["ANDROID_SERIAL"] = device_name
else:
device = frida.get_usb_device()
@ -197,14 +230,19 @@ def setup_frida(device: str, env: dict[str, str]) -> frida.core.Device:
except frida.ServerNotRunningError:
pass
# Start server
proc = subprocess.run(
["adb", "shell", "whoami"], encoding="utf-8", stdout=subprocess.PIPE, env=env
proc: subprocess.CompletedProcess[str] | subprocess.CompletedProcess[bytes] = (
subprocess.run(
["adb", "shell", "whoami"],
encoding="utf-8",
stdout=subprocess.PIPE,
env=env,
)
)
if proc.stdout.strip() != "root":
proc = subprocess.run(["adb", "root"], env=env)
# Rooting adb will disconnect the device
if device != "":
device = frida.get_device(device)
if device_name != "":
device = frida.get_device(device_name)
else:
device = frida.get_usb_device()
perm = subprocess.run(
@ -221,20 +259,31 @@ def setup_frida(device: str, env: dict[str, str]) -> frida.core.Device:
"7",
] # int(perm[0]) & 1 == 1
if perm == "":
subprocess.run(
[
"adb",
"push",
str(FRIDA_SERVER_BIN.absolute()),
FRIDA_SERVER_ANDROID_PATH,
],
env=env,
)
with tempfile.TemporaryDirectory() as tmpdname:
tmpd = Path(tmpdname)
with (
lzma.open(str(FRIDA_SERVER_BIN.absolute())) as fin,
(tmpd / "frida-server").open("wb") as fout,
):
shutil.copyfileobj(fin, fout)
subprocess.run(
[
"adb",
"push",
str((tmpd / "frida-server").absolute()),
FRIDA_SERVER_ANDROID_PATH,
],
env=env,
)
if need_perm_resset:
subprocess.run(["adb", "chmod", "755", FRIDA_SERVER_ANDROID_PATH], env=env)
subprocess.run(
["adb", "shell", "chmod", "755", FRIDA_SERVER_ANDROID_PATH], env=env
)
subprocess.Popen(["adb", "shell", FRIDA_SERVER_ANDROID_PATH], env=env)
# The server take some time to start
# time.sleep(3)
t = spinner()
while True:
try:
s = device.attach(0)
@ -242,11 +291,11 @@ def setup_frida(device: str, env: dict[str, str]) -> frida.core.Device:
print("[*] Server started: begin analysis ")
return device
except frida.ServerNotRunningError:
print("[-] Waiting for frida server to start", end="\r")
print(f"[{t.__next__()}] Waiting for frida server to start", end="\r")
time.sleep(0.3)
def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
def collect_runtime(apk: Path, device_name: str, file_storage: Path, output: TextIO):
env = dict(os.environ)
if not file_storage.exists():
@ -255,7 +304,7 @@ def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
print("[!] file_storage must be a directory")
exit()
device = setup_frida(device, env)
device = setup_frida(device_name, env)
app = get_apkid(apk)[0]
@ -265,9 +314,9 @@ def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
subprocess.run(["adb", "install", str(apk.absolute())], env=env)
with FRIDA_SCRIPT.open("r") as file:
script = file.read()
jsscript = file.read()
with STACK_CONSUMER_B64.open("r") as file:
script = script.replace(
jsscript = jsscript.replace(
"<PYTHON REPLACE StackConsumer.dex.b64>",
file.read().replace("\n", "").strip(),
)
@ -275,7 +324,7 @@ def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
pid = device.spawn([app])
session = device.attach(pid)
try:
script = session.create_script(script)
script = session.create_script(jsscript)
except frida.InvalidArgumentError as e:
print("[!] Error:")
print(
@ -286,11 +335,12 @@ def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
)
raise e
data_storage = {
data_storage: dict[str, Any] = {
"invoke_data": [],
"class_new_inst_data": [],
"cnstr_new_inst_data": [],
"dyn_code_load": [],
"initial_classloaders": [],
}
script.on(
@ -305,6 +355,40 @@ def collect_runtime(apk: Path, device: str, file_storage: Path, output: TextIO):
print("==> Press ENTER to finish the analysis <==")
input()
main_class_loader: str | None = None
cls = {d["id"]: d for d in data_storage["initial_classloaders"]}
for load_data in data_storage["dyn_code_load"]:
if load_data["classloader"] in cls:
del cls[load_data["classloader"]]
for id_ in cls.keys():
if (
'dalvik.system.PathClassLoader[DexPathList[[directory "."],'
in cls[id_]["str"]
):
del cls[id_]
elif cls[id_]["cname"] == "java.lang.BootClassLoader":
del cls[id_]
if len(cls) == 0:
print("[!] No classloader found for the main APK")
elif len(cls) > 1:
print(
"[!] Multiple classloader found that could be the main APK, try to guess the right one"
)
nb_occ = {k: 0 for k in cls.keys()}
for data in data_storage["class_new_inst_data"]:
if data["caller_cl_id"] in nb_occ:
nb_occ[data["caller_cl_id"]] += 1
for data in data_storage["invoke_data"]:
if data["caller_cl_id"] in nb_occ:
nb_occ[data["caller_cl_id"]] += 1
for data in data_storage["cnstr_new_inst_data"]:
if data["caller_cl_id"] in nb_occ:
nb_occ[data["caller_cl_id"]] += 1
main_class_loader = max(cls.keys(), key=lambda x: nb_occ[x])
else:
main_class_loader = list(cls.keys())[0]
data_storage["apk_cl_id"] = main_class_loader
json.dump(data_storage, output, indent=" ")
@ -340,7 +424,7 @@ def main():
if args.output is None:
collect_runtime(
apk=args.apk,
device=args.device,
device_name=args.device,
file_storage=args.dex_dir,
output=sys.stdout,
)
@ -348,7 +432,7 @@ def main():
with args.output.open("w") as fp:
collect_runtime(
apk=args.apk,
device=args.device,
device_name=args.device,
file_storage=args.dex_dir,
output=fp,
)