Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apk: Allow injecting and configuring the Frida Gadget #167

Merged
merged 2 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions completions/frida.fish
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ complete --command frida-create --no-files --require-parameter --short-option=t
######## frida-apk ########
add_base_arguments frida-apk
complete --command frida-apk --force-files --short-option=o --long-option=output --description="output path"
complete --command frida-apk --force-files --short-option=g --long-option=gadget --description="inject the specified gadget library"
complete --command frida-apk --force-files --short-option=c --long-option=gadget-config --description="set the given key=value gadget interaction config"


######## frida-compile ########
Expand Down
80 changes: 79 additions & 1 deletion frida_tools/apk.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
from __future__ import annotations

import argparse
import json
import os
import struct
from enum import IntEnum
from io import BufferedReader
from typing import List
from typing import BinaryIO, Dict, List
from zipfile import ZipFile

GADGET_NAME = "libfridagadget.so"

WRAP_SCRIPT = f"""#!/bin/sh
LD_PRELOAD="$(dirname "$0")/{GADGET_NAME}" "$@"
"""

GADGET_INTERACTION_CONFIG = {
"type": "listen",
"on_load": "wait",
}


def main() -> None:
from frida_tools.application import ConsoleApplication
Expand All @@ -18,6 +30,29 @@ def _usage(self) -> str:

def _add_options(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument("-o", "--output", help="output path", metavar="OUTPUT")
parser.add_argument(
"-g",
"--gadget",
type=argparse.FileType("rb"),
help="inject the specified gadget library",
metavar="GADGET",
)

def key_val_type(arg: str) -> tuple[str, str]:
split = arg.split("=", 1)
if len(split) == 1:
raise argparse.ArgumentTypeError("config entry must be of form key=value")
return (split[0], split[1])

parser.add_argument(
"-c",
"--gadget-config",
type=key_val_type,
action="append",
help="set the given key=value gadget interaction config",
metavar="GADGET_CONFIG",
)

parser.add_argument("apk", help="apk file")

def _needs_device(self) -> bool:
Expand All @@ -26,6 +61,11 @@ def _needs_device(self) -> bool:
def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None:
self._output_path = options.output
self._path = options.apk
self._gadget = options.gadget
self._gadget_config = options.gadget_config

if self._gadget_config and self._gadget is None:
parser.error("cannot configure gadget without injecting gadget")

if not self._path.endswith(".apk"):
parser.error("path must end in .apk")
Expand All @@ -36,6 +76,13 @@ def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespa
def _start(self) -> None:
try:
debug(self._path, self._output_path)
if self._gadget is not None:
gadget_arch = get_gadget_arch(self._gadget)
lib_dir = f"lib/{gadget_arch}/"

config = {"interaction": {**GADGET_INTERACTION_CONFIG, **dict(self._gadget_config or [])}}

inject(self._gadget.name, lib_dir, config, self._output_path)
except Exception as e:
self._update_status(f"Error: {e}")
self._exit(1)
Expand Down Expand Up @@ -96,6 +143,37 @@ def debug(path: str, output_path: str) -> None:
oz.writestr(info.filename, f.read(), info.compress_type)


def inject(gadget_so: str, lib_dir: str, config: Dict[str, Dict[str, str]], output_apk: str) -> None:
config_name = GADGET_NAME.removesuffix(".so") + ".config.so"
with ZipFile(output_apk, "a") as oz:
oz.writestr(lib_dir + "wrap.sh", WRAP_SCRIPT)
oz.writestr(lib_dir + config_name, json.dumps(config))
oz.write(gadget_so, lib_dir + GADGET_NAME)


def get_gadget_arch(gadget: BinaryIO) -> str:
ELF_HEADER = struct.Struct("<B3sB13xH")

(m1, m2, bits, machine) = ELF_HEADER.unpack(gadget.read(ELF_HEADER.size))
if m1 != 0x7F or m2 != b"ELF":
raise ValueError("gadget is not an ELF file")

# ABI names from https://android.googlesource.com/platform/ndk.git/+/refs/heads/main/meta/abis.json,
# ELF machine values (and header) from /usr/include/elf.h.
if machine == 0x28 and bits == 1:
return "armeabi-v7a"
elif machine == 0xB7 and bits == 2:
return "arm64-v8a"
elif machine == 0x03 and bits == 1:
return "x86"
elif machine == 0x3E and bits == 2:
return "x86_64"
elif machine == 0xF3 and bits == 2:
return "riscv64"
else:
raise ValueError(f"unknown ELF e_machine 0x{machine:02x}")


class BinaryXML:
def __init__(self, stream: BufferedReader) -> None:
self.stream = stream
Expand Down
Loading