apkpatcher package

Submodules

apkpatcher.cli module

apkpatcher.cli.main() int

cli function

apkpatcher.conf module

conf file

Module contents

class apkpatcher.Manifest(file: Path)

Bases: object

read()
update(xml)
write()
class apkpatcher.Patcher(apk: str | Path, version_buildtools: str = None, sdktools: str | Path = None, logger: Logger | None = None, nb_jobs: int = 4)

Bases: object

Patches an Android APK: extracts it, injects a Frida gadget and/or runs plugins/community patches against the unpacked smali/resources, repackages the dex/zip, then signs and zipaligns the result.

Typical usage: construct with the APK path plus SDK build-tools info, call whichever set_*/add_* setters apply (arch, certificates, Frida gadget options, …), then call patching() to run the whole pipeline and produce a patched APK.

ARCH_ARM = 'arm'
ARCH_ARM64 = 'arm64'
ARCH_X64 = 'x64'
ARCH_X86 = 'x86'
ARCH_X86_64 = 'x86_64'
AUTOLOAD_BIT = 2
CONFIG_BIT = 1
DEFAULT_HOOKFILE_NAME = 'libhook.js.so'
INTERNET_PERMISSION = 'android.permission.INTERNET'
add_certificate(keycertificate: Path | str, keyalias: str, keypass: str)

Add signature certificate

Args:

keycertificate (Path | str): certificate path keyalias (str): aliasname keypass (str): password

add_network_certificate(cert: Path)

add network certificate

Args:

cert (Path): the certificate to inject

add_plugin(plugin: str | Path | Callable[[List[str | Path]], int]) None

add a plugin to the chain, keeping any already set/added

Plugins run in the order added, right before repackaging, each operating on the same unpacked directories the previous one left them in – so a later plugin sees an earlier plugin’s file changes. Execution stops at the first plugin that returns/exits non-zero.

Args:

plugin (str | Path | Callable[str | Path]): same contract as set_plugin().

calculate_sha256(file_path: Path)

Calculate the SHA-256 hash of a file.

Parameters:

file_path – Path to the file.

Returns:

SHA-256 hash as a hexadecimal string.

check_libextract() bool

Force extractNativeLibs=true on the base AND every split manifest.

compute_directory_hashes(base_directory, hash_dict: dict | None = None) None

Compute SHA-256 hashes for all files in a directory and store them in a nested dictionary.

Parameters:

base_directory – Path to the base directory.

create_lib_arch_folders(arch)

make lib folder in the apk to put native lib

create_security_config_xml(path_network: str) bool

Create security config file for add certificate

Args:

path_network (str): path network

delete_existing_gadget(arch_folder: Path, lib_name: str, delete_custom_files: int = 0)

delete existing gadget inside the apk

disable_dex_split()

disable automatic dex splitting on a 65536-reference overflow.

By default, if reassembling a modified smali directory overflows a dex file’s 65536 constant-pool references, apkpatcher will try to move the modified/added classes (and whatever else in that dex depends on them) into a second, brand-new dex file and retry. This opts back out to the previous behavior: a clear, immediate error.

download_frida() str | None

Download frida binary

Returns:

str | None: return the name of lib

enable_debug_mode() bool

Enable debug mode

Returns:

bool: success to enable debug mode

enable_user_certificates(rsc: ARSC)

Enable user certificate

Args:

rsc (pyaxml.ARSC): return ARSC file return 2 if rsc modified return 1 if success return 0 if failed

enable_v4_signature(file: Path | str)

enable v4 signature

Args:

file (Path | str): v4_signature file

extract_apk(apk: Path, final_dir: Path)

Extract the apk on the temporary folder

extract_dex(dex_file: Path)

Extract Dex

Args:

dex_file (Path): the dexfile to extract

get_entrypoint_class_name() str | None

get the class name of the entrypoint

get_entrypoint_smali_path() Path | None

get the path of apk entrypoint on the smali files

get_package() str
has_permission(permission_name: str) bool

Check if the apk have ‘permission_name’ as permission

Parameters:

permission_name (str): name of the permission with format: android.permission.XXX

Returns:

has_permission (bool): permission is present

inject_custom_network_certificate(rsc, path_network: str) bool

Inject custom network certificate

inject_permission_manifest(permission: str)

Inject permission on the Manifest

inject_user_certificates_label(rsc: ARSC) -> (<class 'str'>, <class 'int'>)

Inject a proxy certificate directly inside the application

Args:

rsc (pyaxml.ARSC): ARSC file (resource file of Android)

Raises:

FileNotFoundError: raise when manifest is not found

Returns:

str: return the path of network file

insert_lib(gadget_path: str | Path, arch: str, dst: str | Path, config_file_path=None, auto_load_script_path=None)

Insert native lib inside the apk

Parameters:
  • gadget_path (str): the path of the gadget to insert

insert_lib_loader(lib_name='libfrida-gadget')

inject snippet to load lib in smali code

keep_certificate()

keep certificate after use it

keep_debug_info()

keep .line/.local/.parameter debug directives when disassembling.

Off by default: debug info is pure overhead in a patched build and eats into a dex file’s 65536-reference budget (each variable/param name is a string constant) for no benefit to Frida/smali-level instrumentation. Enable this only if you need Java/Kotlin source-level debugging of the patched APK.

missing_sdktools()

raise an error when sdktools is missing

Raises:

ValueError: The error

property need_full_extraction: bool

need full extraction

Returns:

bool: return true if needed full extraction

patching(gadget_to_use: str | Path | None = None, output_file: Path | None = None, user_certificate: bool = False, splits_apk: list[Path] | None = None, entrypoint=None, permissions: list[str] = None) bool

patch the apk with gadget ‘gadget_to_use’

property pause: bool

get Pause status

Returns:

bool: pause

repack_from(source_dir: Path, output_apk: Path, splits_apk: List[Path] | None = None) bool

Repackage an already-unpacked tree (as produced by unpack_to(), hand-edited or not) back into an APK, then sign and zipalign it.

Only the main APK’s unpacked tree (source_dir/base) is repackaged; splits_apk are re-signed/zipaligned as-is alongside it, matching unpack_to()’s split handling (extracted for inspection, not rebuilt from their unpacked folders).

Args:

source_dir (Path): the same directory a prior unpack_to() call was given (containing a base subdirectory). output_apk (Path): path of the APK to repackage/sign in place. splits_apk (List[Path] | None): split APKs to sign alongside it.

Returns:

bool: True on success.

repackage_apk(target_file: Path | None = None) Path | None

repackage the apk

Parameters:
  • target_file (str) : the path of the new apk created if none, a new apk will be created with suffix “_patched.apk”

repackage_dex(classes_dir: Path) bool

Repackage Dex

Args:

classes_dir (Path): classes directory

Returns:

bool: whether classes_dir was (re)packaged successfully

run_plugins() None

Run every configured plugin (set_plugin/add_plugin), in order.

Each plugin operates on the same unpacked directories the previous one left them in, so a later plugin sees an earlier plugin’s file changes – this is what lets –patch/–plugin be chained. Exits the process (sys.exit(1)) at the first plugin that returns/exits non-zero, leaving any already-applied changes in final_dir as-is.

set_arch(arch: str)

set architecture of target phone where apk would be installed

Args:

arch (str): architecture

set_compression(method: int, level: int)

set compression method for zip level

Args:

method (int): one of these method ( zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED, zipfile.ZIP_BZIP2, zipfile.ZIP_LZMA) level (int): The compresslevel parameter controls the compression level to use when writing files to the archive. When using ZIP_STORED or ZIP_LZMA it has no effect. When using ZIP_DEFLATED integers 0 through 9 are accepted (see zlib for more information). When using ZIP_BZIP2 integers 1 through 9 are accepted (see bz2 for more information).

set_debug()

set debug mode

set_frida_config(config_path: str | Path)

Use a custom Frida gadget config JSON, for advanced control over the gadget’s interaction type.

Args:

config_path (str | Path): path to the gadget config JSON file

Raises:

FileNotFoundError: if config_path doesn’t exist ValueError: if config_path isn’t valid JSON or lacks a top-level “interaction” key

set_frida_no_wait()

Make the injected Frida gadget resume the app immediately on startup instead of blocking until a Frida client attaches.

Generates a gadget config with interaction type “listen” and on_load=resume. Mutually exclusive with set_frida_script() and set_frida_config() (they all configure the same gadget interaction).

set_frida_script(script_path: str | Path)

Embed a Frida script that the gadget auto-loads on startup, with no network interaction required.

Args:

script_path (str | Path): path to the local .js script to embed

Raises:

FileNotFoundError: if script_path doesn’t exist

set_plugin(plugin: str | Path | Callable[[List[str | Path]], int]) None

set a plugin, replacing any plugins previously set/added

Args:

plugin (str | Path | Callable[str | Path]): set a plugin binary or python method that should be called just right before repackage the application. The method should take as parameter the list of directory where the apk has been unpacked.

set_use_download_frida(frida_version: str)

Specify frida version to download

Args:

frida_version (str): frida version

set_use_download_frida_from_installed()

Download whichever frida-gadget version matches the frida Python package installed in the current environment.

sign_and_zipalign(apk_path: Path, splits_apk: List[Path])

sign and zipalign file

split_dex_directory(classes_dir: Path, move_classes: set[str] | None = None, keep_classes: set[str] | None = None, exc=None) Path | None

Split classes_dir into two: the top-level classes named in move_classes (or every top-level class except those named in keep_classes, if that’s given instead) are moved into a brand-new sibling smali_classesK directory – its own future dex file, with a fresh 65536-reference budget – and the rest stay in classes_dir. Returns the new directory’s path, or None if no split could be determined (callers should treat that as “don’t retry, report the original error”).

move_classes/keep_classes name top-level classes (slashed package path, no “.smali”, no inner-class “$…” suffix) for manual/explicit use. When neither is given (the automatic case, on a 65536-reference overflow), the selection is guessed via _relocation_seed/_guess_relocation_set; if no seed can be determined at all, falls back to an even split of all top-level classes by name.

unpack_to(target_dir: Path, splits_apk: List[Path] | None = None)

Extract this APK (and any splits) to target_dir, without repackaging – for inspecting/hand-editing the unpacked tree before feeding it back through repack_from().

Args:
target_dir (Path): destination directory. The main APK is

extracted to target_dir/base, each split to target_dir/<split.stem>.

splits_apk (List[Path] | None): split APKs belonging to the

same app, extracted alongside the main one.

apkpatcher.download_baksmali(output_file: Path)

Download baksmali

Args:

output_file (Path): output location

apkpatcher.download_smali(output_file: Path)

Download smali

Args:

output_file (Path): output location

apkpatcher.get_latest_version_directory(base_path: Path) str | None

Get latest version directory

Args:

base_path (Path): base path

Returns:

str | None: the latest version directory

apkpatcher.new_logger(level: logging._Level) Logger

Instanciate Logger

Args:

level (logging._Level): level of logger

Returns:

logging.Logger: new logger

apkpatcher.plba(filename: str | Path, arch: str) str

Filename with architecture

Args:

filename (str | Path): base filename arch (str): architecture

Returns:

str: return filename with architecture