Skip to content
Open
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
8 changes: 6 additions & 2 deletions APK.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def assemble(self, target : str = None) -> str:
self.apk_path = out_apk
return out_apk

def apply_patches(self, version: Optional[str] = None, frida_gadget: bool = True, enable_user_certs: bool = True) -> str:
def apply_patches(self, version: Optional[str] = None, frida_gadget: bool = True, enable_user_certs: bool = True, gadget_config: Optional[str] = None, script_source: Optional[str] = None) -> str:

apkdir = self.decoded

Expand All @@ -97,6 +97,10 @@ def apply_patches(self, version: Optional[str] = None, frida_gadget: bool = True

if frida_gadget:
Log.info("Adding Frida gadget")
if gadget_config:
Log.info(f"Adding Frida gadget configuration: {gadget_config}")
if script_source:
Log.info(f"Adding Frida gadget script source: {script_source}")
# Ensure INTERNET
has_inet = any(el.tag == "uses-permission" and el.attrib.get(ns + "name") == "android.permission.INTERNET"
for el in root)
Expand Down Expand Up @@ -133,7 +137,7 @@ def apply_patches(self, version: Optional[str] = None, frida_gadget: bool = True
tree.write(manifest, encoding="utf-8", xml_declaration=True)

fg = FridaGadget()
fg.copy_android_gadgets(apkdir, version=version)
fg.copy_android_gadgets(apkdir, version=version, gadget_config=gadget_config, script_source=script_source)
else:
Log.warn("Not adding Frida Gadget.")

Expand Down
18 changes: 18 additions & 0 deletions FridaGadget.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ def copy_android_gadgets(
self,
dest_root: Path | str,
version: Optional[str] = None,
gadget_config: Optional[Path | str] = None,
script_source: Optional[Path | str] = None,
) -> List[Path]:
"""
Copy cached gadgets into an APK-like layout under dest_root:

dest_root/
lib/<abi>/libfrida-gadget.so
lib/<abi>/libfrida-gadget.config.so (optional)
lib/<abi>/libfrida-gadget.script.so (optional)

"""
dest_root = Path(dest_root).expanduser().resolve()
Expand Down Expand Up @@ -167,6 +171,20 @@ def copy_android_gadgets(
if self.verbose:
Log.info(f"Copied: {src_so} -> {dest_so}")

if gadget_config:
dest_config = dest_so_dir / "libfrida-gadget.config.so"
shutil.copyfile(gadget_config, dest_config)
copied.append(dest_config)
if self.verbose:
Log.info(f"Copied config: {gadget_config} -> {dest_config}")

if script_source:
dest_script = dest_so_dir / "libfrida-gadget.script.so"
shutil.copyfile(script_source, dest_script)
copied.append(dest_script)
if self.verbose:
Log.info(f"Copied script: {script_source} -> {dest_script}")

if not any_found:
Log.abort(f"No cached libfrida-gadget.so found under {tag_dir}")

Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ An APK patcher, for use with [objection](https://github.com/sensepost/objection)

### Changelog

* **7th September 2026:**
* Added `-c` / `--gadget-config` parameter to provide custom Frida Gadget configuration JSON
* Added `-l` / `--script-source` parameter to bundle and run a custom Frida script with the gadget

* **11th August 2026:**
* Added Certificate Transparency bypass for Android 17+
* Added `--apk` parameter to load a local APK instead of pulling from device
Expand Down Expand Up @@ -48,7 +52,7 @@ Install the target Android application on your device and connect it to your com

```
$ patch-apk.py -h
usage: patch-apk.py [-h] [--serial SERIAL] [--user USER] [--gadget-version GADGET_VERSION] [--no-user-certs] [--no-gadget] [--extract-only] [--disable-styles-hack] [--no-install] [--keep-splits] [--save-apk SAVE_APK] [-v] pkg_pattern
usage: patch-apk.py [-h] [--serial SERIAL] [--user USER] [--gadget-version GADGET_VERSION] [--no-user-certs] [--no-gadget] [--extract-only] [--disable-styles-hack] [--no-install] [--keep-splits] [-c GADGET_CONFIG] [-l SCRIPT_SOURCE] [--save-apk SAVE_APK] [-v] pkg_pattern

Pull, merge/patch, add gadget, build, align, sign, install.

Expand All @@ -68,6 +72,10 @@ options:
Skip duplicate <style><item> removal (merge step)
--no-install Do not install to device at the end
--keep-splits Keep split APKs when extracting
-c, --gadget-config GADGET_CONFIG
Path to Frida Gadget configuration JSON file
-l, --script-source SCRIPT_SOURCE
Path to a custom script to bundle with the gadget
--save-apk SAVE_APK Copy final APK to this path
-v, --verbose
```
Expand Down Expand Up @@ -188,6 +196,43 @@ $ python3 patch-apk.py org.proxydroid --extract-only
[*] Saved APK: org.proxydroid.apk
```

**Custom Frida Gadget Configuration (`-c` / `--gadget-config`):**

To provide custom Frida Gadget runtime parameters (e.g. listening address, interaction type, or loading scripts), pass a configuration JSON file using `-c`:

```bash
$ python3 patch-apk.py org.proxydroid -c gadget_config.json
```

Example `gadget_config.json`:
```json
{
"interaction": {
"type": "listen",
"address": "0.0.0.0",
"port": 27042,
"on_load": "resume"
}
}
```

To automatically load and run an embedded Frida script, specify `--script-source` / `-l`:

```bash
$ python3 patch-apk.py org.proxydroid -c gadget_config.json -l hook.js
```

In your `gadget_config.json`, reference the script path as `libfrida-gadget.script.so`:
```json
{
"interaction": {
"type": "script",
"path": "libfrida-gadget.script.so",
"on_change": "rescan"
}
}
```

## Original research

* NickstaDB - https://nickbloor.co.uk/2020/03/29/patching-android-split-apks/
Expand Down
20 changes: 18 additions & 2 deletions patch-apk.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def main():
help="Skip duplicate <style><item> removal (merge step)")
ap.add_argument("--no-install", action="store_true", help="Do not install to device at the end")
ap.add_argument("--keep-splits", action="store_true", help="Keep split APKs when extracting")
ap.add_argument("-c", "--gadget-config", help="Path to Frida Gadget configuration JSON file")
ap.add_argument("-l", "--script-source", help="Path to a custom script to bundle with the gadget")
ap.add_argument("--save-apk", help="Copy final APK to this path")
ap.add_argument("-v", "--verbose", action="store_true")
args = ap.parse_args()
Expand All @@ -105,6 +107,18 @@ def main():
if args.gadget_version and args.no_gadget:
Log.abort("Cannot specify --gadget-version when --no-gadget is set.")

if args.gadget_config and args.no_gadget:
Log.abort("Cannot specify --gadget-config when --no-gadget is set.")

if args.gadget_config and not os.path.isfile(args.gadget_config):
Log.abort(f"Gadget configuration file not found: {args.gadget_config}")

if args.script_source and not args.gadget_config:
Log.abort("A script source was specified (--script-source) but no gadget configuration was set (--gadget-config).")

if args.script_source and not os.path.isfile(args.script_source):
Log.abort(f"Script source file not found: {args.script_source}")

if args.apk:
if not os.path.isfile(args.apk):
Log.abort(f"APK file not found: {args.apk}")
Expand Down Expand Up @@ -202,8 +216,10 @@ def main():
# Apply patches
if not args.extract_only:
base.apply_patches(version=gadget_version,
enable_user_certs=not args.no_user_certs,
frida_gadget=not args.no_gadget)
enable_user_certs=not args.no_user_certs,
frida_gadget=not args.no_gadget,
gadget_config=args.gadget_config,
script_source=args.script_source)
# Build final APK
base.assemble()

Expand Down