apkpatcher package
Submodules
apkpatcher.cli module
- apkpatcher.cli.main() int
cli function
apkpatcher.conf module
conf file
Module contents
- class apkpatcher.Patcher(apk: str | Path, version_buildtools: str = None, sdktools: str | Path = None, logger: Logger | None = None, nb_jobs: int = 4)
Bases:
objectPatches 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