apk.py
ofrak.core.apk
ApkAnalyzer (Analyzer)
Analyzes Android APK packages using aapt to extract package metadata including package name, application name, version information, SDK requirements, permissions, and launchable activity. The analyzer parses structured output from aapt dump badging to extract key APK attributes useful for understanding app identity, requirements, and capabilities. Use when you need to identify an APK's package name, determine version and SDK requirements, audit requested permissions, or find the main activity without unpacking the entire APK.
analyze(self, resource, config=None)
async
Analyze a resource for to extract specific ResourceAttributes.
Users should not call this method directly; rather, they should run Resource.run or Resource.analyze.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource |
Resource |
The resource that is being analyzed |
required |
config |
Optional config for analyzing. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run. |
None |
Returns:
| Type | Description |
|---|---|
ApkAttributes |
The analysis results |
Source code in ofrak/core/apk.py
async def analyze(self, resource: Resource, config=None) -> ApkAttributes:
async with resource.temp_to_disk(suffix=".apk") as temp_path:
cmd = ["aapt", "dump", "badging", temp_path]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
output = stdout.decode("utf-8")
return self._parse_aapt_output(output)
_parse_aapt_output(self, output)
private
Parse aapt dump badging output to extract APK attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output |
str |
Raw output from aapt dump badging command |
required |
Returns:
| Type | Description |
|---|---|
ApkAttributes |
Parsed APK attributes |
Source code in ofrak/core/apk.py
def _parse_aapt_output(self, output: str) -> ApkAttributes:
"""
Parse aapt dump badging output to extract APK attributes.
:param output: Raw output from aapt dump badging command
:return: Parsed APK attributes
"""
lines = output.split("\n")
package_name = None
version_code = None
sdk_version = None
target_sdk_version = None
application_name = None
launchable_activity = None
permissions = []
for line in lines:
# Parse package line: package: name='com.example' versionCode='123' ...
if line.startswith("package:"):
package_match = re.search(r"name='([^']+)'", line)
if package_match:
package_name = package_match.group(1)
version_code_match = re.search(r"versionCode='([^']+)'", line)
if version_code_match:
version_code = int(version_code_match.group(1))
# Parse sdkVersion line: sdkVersion:'23'
elif line.startswith("sdkVersion:"):
sdk_match = re.search(r"sdkVersion:'(\d+)'", line)
if sdk_match:
sdk_version = int(sdk_match.group(1))
# Parse targetSdkVersion line: targetSdkVersion:'30'
elif line.startswith("targetSdkVersion:"):
target_match = re.search(r"targetSdkVersion:'(\d+)'", line)
if target_match:
target_sdk_version = int(target_match.group(1))
# Parse application-label line: application-label:'MyApp'
elif line.startswith("application-label:") and not line.startswith(
"application-label-"
):
label_match = re.search(r"application-label:'([^']+)'", line)
if label_match:
application_name = label_match.group(1)
# Parse launchable-activity line: launchable-activity: name='com.example.MainActivity' ...
elif line.startswith("launchable-activity:"):
activity_match = re.search(r"name='([^']+)'", line)
if activity_match:
launchable_activity = activity_match.group(1)
# Parse uses-permission line: uses-permission: name='android.permission.INTERNET'
elif line.startswith("uses-permission:"):
permission_match = re.search(r"name='([^']+)'", line)
if permission_match:
permissions.append(permission_match.group(1))
if package_name is None:
raise ValueError("Failed to extract package name from aapt output")
if sdk_version is None:
raise ValueError("Failed to extract SDK version from aapt output")
if target_sdk_version is None:
raise ValueError("Failed to extract target SDK version from aapt output")
return ApkAttributes(
package_name=package_name,
application_name=application_name,
version_code=version_code,
sdk_version=sdk_version,
target_sdk_version=target_sdk_version,
permissions=permissions,
launchable_activity=launchable_activity,
)
ApkAttributes (ResourceAttributes)
dataclass
Attributes extracted from an Android APK package using aapt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
package_name |
The unique package identifier (e.g., 'com.example.app') |
required | |
application_name |
The human-readable application name |
required | |
version_code |
Integer version code for internal versioning |
required | |
sdk_version |
Minimum SDK version required to run the app |
required | |
target_sdk_version |
Target SDK version the app was built for |
required | |
permissions |
List of Android permissions requested by the app |
required | |
launchable_activity |
Main activity that launches the app |
required |
ApkIdentifier (Identifier)
Identifier for ApkArchive.
Some Apks are recognized by the MagicMimePattern; others are tagged as JavaArchive or ZipArchive. This identifier inspects those files, and tags any with an androidmanifest.xml as an ApkArchive.
identify(self, resource, config=None)
async
Perform identification on the given resource.
Users should not call this method directly; rather, they should run Resource.identify.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource |
Resource |
required | |
config |
Optional config for identifying. If an implementation provides a default, this default will always be used when config would otherwise be None. Note that a copy of the default config will be passed, so the default config values cannot be modified persistently by a component run. |
None |
Source code in ofrak/core/apk.py
async def identify(self, resource: Resource, config=None) -> None:
async with resource.temp_to_disk(suffix=".zip") as temp_path:
unzip_cmd = [
"unzip",
"-l",
temp_path,
]
unzip_proc = await asyncio.create_subprocess_exec(
*unzip_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await unzip_proc.communicate()
if unzip_proc.returncode:
raise CalledProcessError(returncode=unzip_proc.returncode, cmd=unzip_cmd)
if b"androidmanifest.xml" in stdout.lower():
resource.add_tag(Apk)
ApkPacker (Packer)
Repackages decoded Android APK resources into a complete, signed APK file using apktool for compilation and uber-apk-signer for signing. The process recompiles resources, repackages DEX files, updates the manifest, and creates cryptographic signatures required for Android installation. Use after modifying Android app resources, Smali code, or manifest to create an installable APK.
This unpacker is a wrapper for two tools:
apktoolrepacks the APK resources. See https://ibotpeaches.github.io/Apktool/.uber-apk-signersigns the packed APK file. See https://github.com/patrickfav/uber-apk-signer.
Another helpful overview of the process: https://github.com/vaibhavpandeyvpz/apkstudio.
pack(self, resource, config=ApkPackerConfig(sign_apk=True))
async
Pack disassembled APK resources into an APK.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource |
Resource |
required | |
config |
ApkPackerConfig |
ApkPackerConfig(sign_apk=True) |
Source code in ofrak/core/apk.py
async def pack(
self, resource: Resource, config: ApkPackerConfig = ApkPackerConfig(sign_apk=True)
):
"""
Pack disassembled APK resources into an APK.
:param resource:
:param config:
"""
apk = await resource.view_as(Apk)
temp_flush_dir = await apk.flush_to_disk()
apk_suffix = ".apk"
with tempfile.NamedTemporaryFile(suffix=apk_suffix, delete_on_close=False) as temp_apk:
temp_apk.close()
apk_cmd = [
"apktool",
"build",
"--force-all",
temp_flush_dir,
"--output",
temp_apk.name,
]
apk_proc = await asyncio.create_subprocess_exec(
*apk_cmd,
)
apk_returncode = await apk_proc.wait()
if apk_proc.returncode:
raise CalledProcessError(returncode=apk_returncode, cmd=apk_cmd)
if not config.sign_apk:
# Close the file handle and reopen, to avoid observed situations where temp.read()
# was not returning data
with open(temp_apk.name, "rb") as file_handle:
new_data = file_handle.read()
else:
with tempfile.TemporaryDirectory() as signed_apk_temp_dir:
java_cmd = [
"java",
"-jar",
_UberApkSignerTool.JAR_PATH,
"--apks",
temp_apk.name,
"--out",
signed_apk_temp_dir,
"--allowResign",
]
java_proc = await asyncio.create_subprocess_exec(
*java_cmd,
)
java_returncode = await java_proc.wait()
if java_proc.returncode:
raise CalledProcessError(returncode=java_returncode, cmd=java_cmd)
signed_apk_filename = (
os.path.basename(temp_apk.name)[: -len(apk_suffix)]
+ "-aligned-debugSigned.apk"
)
signed_file_name = os.path.join(
signed_apk_temp_dir,
signed_apk_filename,
)
with open(signed_file_name, "rb") as file_handle:
new_data = file_handle.read()
assert len(new_data) != 0
resource.queue_patch(Range(0, await resource.get_data_length()), new_data)
ApkPackerConfig (ComponentConfig)
dataclass
ApkPackerConfig(sign_apk: bool)
ApkUnpacker (Unpacker)
Decodes Android APK application packages into their component files and resources using apktool (see https://ibotpeaches.github.io/Apktool/). This tool decodes the AndroidManifest.xml back to readable XML, extracts resources (images, layouts, strings) in their original format, converts DEX bytecode to Smali assembly, and preserves the complete directory structure. Use when reverse engineering Android applications, analyzing app behavior, examining resource files, or preparing to modify and repackage an APK. The decoded files are much easier to read and modify than the compiled APK format.
unpack(self, resource, config=None)
async
Decode Android APK files.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource |
Resource |
required | |
config |
None |
Source code in ofrak/core/apk.py
async def unpack(self, resource: Resource, config=None):
"""
Decode Android APK files.
:param resource:
:param config:
"""
apk = await resource.view_as(Apk)
async with resource.temp_to_disk() as temp_path:
with tempfile.TemporaryDirectory() as temp_flush_dir:
cmd = [
"apktool",
"decode",
"--output",
temp_flush_dir,
"--force",
temp_path,
]
proc = await asyncio.create_subprocess_exec(
*cmd,
)
returncode = await proc.wait()
if proc.returncode:
raise CalledProcessError(returncode=returncode, cmd=cmd)
await apk.initialize_from_disk(temp_flush_dir)
_UberApkSignerTool (ComponentExternalTool)
private
__init__(self)
special
Initialize self. See help(type(self)) for accurate signature.
Source code in ofrak/core/apk.py
def __init__(self):
super().__init__(
_UberApkSignerTool.JAR_PATH,
"https://github.com/patrickfav/uber-apk-signer",
install_check_arg="",
)
is_tool_installed(self)
async
Check if a tool is installed by running it with the install_check_arg.
This method runs <tool> <install_check_arg>.
Returns:
| Type | Description |
|---|---|
bool |
True if the |
Source code in ofrak/core/apk.py
async def is_tool_installed(self) -> bool:
if not os.path.exists(_UberApkSignerTool.JAR_PATH):
return False
try:
cmd = [
"java",
"-jar",
_UberApkSignerTool.JAR_PATH,
"--help",
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
returncode = await proc.wait()
except FileNotFoundError:
return False
return 0 == returncode