Skip to content

Commit

Permalink
Merge pull request #1 from erev0s/manifest_dec_and_cli_flags
Browse files Browse the repository at this point in the history
Manifest dec and cli flags
  • Loading branch information
erev0s authored Oct 8, 2023
2 parents a6fe14e + 48468dc commit 8f1ebb5
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 75 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file.

## [1.0.3] - 08-10-2023
- New method get_manifest() in manifestDecoder module for convenience when getting the decoded AndroidManifest from an apk or a file.
- added flag in the cli to be able to pass an encoded AndroidManifest.xml file and decode it.
- Slight corrections in docstrings and in versioning between cli and lib.

## [1.0.2] - 25-09-2023
- Updated decoding in central and local header to ignore non decodable bytes
- fixed a forgotten flag in the cli
Expand Down
2 changes: 1 addition & 1 deletion apkInspector/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.2"
__version__ = "1.0.3"
2 changes: 1 addition & 1 deletion apkInspector/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def extract_file_based_on_header_info(apk_file, offset, header_info):
"""
Extracts a single file from the apk_file based on the information provided from the offset and the header_info.
It takes into account that the compression method provided might not be STORED or DEFLATED and in that case
it treats it as STORED -> TODO: inspect other cases
it attempts to inflate it (Android < 9) and if that fails it considers it as STORED (Android >= 9).
:param apk_file: The already read/loaded data of the APK file e.g. with open('test.apk', 'rb') as apk_file
:param offset: The offset at which the local header for that file is.
:param header_info: The local header dictionary info for that specific filename (parse_local_header)
Expand Down
20 changes: 13 additions & 7 deletions apkInspector/manifestDecoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def process_headers(file):
return elements


def get_manifest(elements, string_data):
def create_manifest(elements, string_data):
"""
Method to go over all elements and attempt to create the AndroidManifest.xml file.
:param elements: Elements as retrieved from process_headers()
Expand Down Expand Up @@ -421,12 +421,18 @@ def get_manifest(elements, string_data):
return android_manifest_xml


# def beautify_validate_xml(xml_string):
# import xml.etree.ElementTree as ET
# ET.register_namespace("android", "http://schemas.android.com/apk/res/android")
# root = ET.fromstring(xml_string)
# xml_pretty = ET.tostring(root, encoding='utf-8', method='xml').decode("utf-8")
# return xml_pretty
def get_manifest(file_like_object):
"""
Method to return the AndroidManifest file as created by create_manifest()
:param file_like_object: expects the encoded AndroidManifest.xml file as a file-like object
:return: returns the decoded AndroidManifest file
"""
ResChunkHeader.from_file(file_like_object)
string_pool = StringPoolType.from_file(file_like_object)
string_data = string_pool.strdata
elements = process_headers(file_like_object)
manifest = create_manifest(elements, string_data)
return manifest



2 changes: 1 addition & 1 deletion cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.2"

122 changes: 62 additions & 60 deletions cli/apkInspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import io
import os

from apkInspector import __version__ as lib_version
from . import __version__ as cli_version
from apkInspector import __version__ as version
from apkInspector.extract import extract_file_based_on_header_info, extract_all_files_from_central_directory
from apkInspector.headers import find_eocd, parse_central_directory, headers_of_filename, print_headers_of_filename, \
get_and_save_local_headers_of_all, show_and_save_info_of_central
from apkInspector.helpers import save_data_to_file
from apkInspector.manifestDecoder import ResChunkHeader, StringPoolType, process_headers, get_manifest
from apkInspector.manifestDecoder import get_manifest


def main():
Expand All @@ -31,6 +30,7 @@ def main():
'the central directory header')
parser.add_argument('-m', '--manifest', action='store_true',
help='Extract and decode the AndroidManifest.xml')
parser.add_argument('-sm', '--specify-manifest', help='Pass an encoded AndroidManifest.xml file to be decoded')
parser.add_argument('-v', '--version', action='store_true', help='Retrieves version information')
args = parser.parse_args()

Expand All @@ -46,70 +46,72 @@ def main():
# |_| |_|
"""
print(mm)
print(f"apkInspector CLI Version: {cli_version}")
print(f"apkInspector Library Version: {lib_version}")
print(f"apkInspector Library Version: {version}")
print(f"Copyright 2023 erev0s <projects@erev0s.com>\n")
return
print(f"apkInspector CLI Version: {cli_version}")
print(f"apkInspector Library Version: {lib_version}")
print(f"apkInspector Version: {version}")
print(f"Copyright 2023 erev0s <projects@erev0s.com>\n")
if args.apk is None:
print("APK file is required")
return
apk_name = os.path.splitext(args.apk)[0]
with open(args.apk, 'rb') as apk_file:
eocd = find_eocd(apk_file)
if eocd is None:
print("Are you sure you are trying to parse an APK file?")
return
central_directory_entries = parse_central_directory(apk_file, eocd["Offset of start of central directory"])
if args.apk is None and args.specify_manifest is None:
parser.error('APK file or AndroidManifest.xml file is required')
if not (args.specify_manifest is None) != (args.apk is None):
parser.error('Please specify an apk file with flag "-apk" or an AndroidManifest.xml file with flag "-sm", but not both.')
if args.apk:
apk_name = os.path.splitext(args.apk)[0]
with open(args.apk, 'rb') as apk_file:
eocd = find_eocd(apk_file)
if eocd is None:
print("Are you sure you are trying to parse an APK file?")
return
central_directory_entries = parse_central_directory(apk_file, eocd["Offset of start of central directory"])

if args.filename and args.extract:
try:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries, args.filename)
except TypeError as e:
print(f"Are you sure the filename: {args.filename} exists?")
exit()
offset = cd_h_of_file["Relative offset of local file header"]
print_headers_of_filename(cd_h_of_file, local_header_of_file)
extracted_data = extract_file_based_on_header_info(apk_file, offset, local_header_of_file)
save_data_to_file(f"EXTRACTED_{args.filename}", extracted_data)
elif args.filename:
try:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries, args.filename)
except TypeError as e:
print(f"Are you sure the filename: {args.filename} exists?")
exit()
print_headers_of_filename(cd_h_of_file, local_header_of_file)
elif args.extract_all:
print(f"Number of entries: {len(central_directory_entries)}")
if not extract_all_files_from_central_directory(apk_file, central_directory_entries, apk_name):
print(f"Extraction successful for: {apk_name}")
elif args.list_local:
get_and_save_local_headers_of_all(apk_file, central_directory_entries, apk_name, args.export)
print(f"Local headers list complete. Export: {args.export}")
elif args.list_central:
show_and_save_info_of_central(central_directory_entries, apk_name, args.export)
print(f"Central header list complete. Export: {args.export}")
elif args.list_all:
show_and_save_info_of_central(central_directory_entries, apk_name, args.export)
get_and_save_local_headers_of_all(apk_file, central_directory_entries, apk_name, args.export)
print(f"Central and local headers list complete. Export: {args.export}")
elif args.manifest:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries,
"AndroidManifest.xml")
offset = cd_h_of_file["Relative offset of local file header"]
extracted_data = io.BytesIO(extract_file_based_on_header_info(apk_file, offset, local_header_of_file))
ResChunkHeader.from_file(extracted_data)
string_pool = StringPoolType.from_file(extracted_data)
string_data = string_pool.strdata
elements = process_headers(extracted_data)
manifest = get_manifest(elements, string_data)
if args.filename and args.extract:
try:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries, args.filename)
except TypeError as e:
print(f"Are you sure the filename: {args.filename} exists?")
exit()
offset = cd_h_of_file["Relative offset of local file header"]
print_headers_of_filename(cd_h_of_file, local_header_of_file)
extracted_data = extract_file_based_on_header_info(apk_file, offset, local_header_of_file)
save_data_to_file(f"EXTRACTED_{args.filename}", extracted_data)
elif args.filename:
try:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries, args.filename)
except TypeError as e:
print(f"Are you sure the filename: {args.filename} exists?")
exit()
print_headers_of_filename(cd_h_of_file, local_header_of_file)
elif args.extract_all:
print(f"Number of entries: {len(central_directory_entries)}")
if not extract_all_files_from_central_directory(apk_file, central_directory_entries, apk_name):
print(f"Extraction successful for: {apk_name}")
elif args.list_local:
get_and_save_local_headers_of_all(apk_file, central_directory_entries, apk_name, args.export)
print(f"Local headers list complete. Export: {args.export}")
elif args.list_central:
show_and_save_info_of_central(central_directory_entries, apk_name, args.export)
print(f"Central header list complete. Export: {args.export}")
elif args.list_all:
show_and_save_info_of_central(central_directory_entries, apk_name, args.export)
get_and_save_local_headers_of_all(apk_file, central_directory_entries, apk_name, args.export)
print(f"Central and local headers list complete. Export: {args.export}")
elif args.manifest:
cd_h_of_file, local_header_of_file = headers_of_filename(apk_file, central_directory_entries,
"AndroidManifest.xml")
offset = cd_h_of_file["Relative offset of local file header"]
extracted_data = io.BytesIO(extract_file_based_on_header_info(apk_file, offset, local_header_of_file))
manifest = get_manifest(extracted_data)
with open("decoded_AndroidManifest.xml", "w", encoding="utf-8") as xml_file:
xml_file.write(manifest)
print("AndroidManifest was saved as: decoded_AndroidManifest.xml")
else:
parser.print_help()
elif args.specify_manifest:
with open(args.specify_manifest, 'rb') as enc_manifest:
manifest = get_manifest(enc_manifest)
with open("decoded_AndroidManifest.xml", "w", encoding="utf-8") as xml_file:
xml_file.write(manifest)
print("AndroidManifest was saved as: decoded_AndroidManifest.xml")
else:
parser.print_help()


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "apkInspector"
version = "1.0.2"
version = "1.0.3"
description = "apkInspector is a tool designed to provide detailed insights into the central directory and local headers of APK files, offering the capability to extract content and decode the AndroidManifest.xml file."
authors = ["erev0s <projects@erev0s.com>"]
license = "GPL-3.0-or-later"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='apkInspector',
version='1.0.2',
version='1.0.3',
author='erev0s',
author_email='projects@erev0s.com',
description='apkInspector is a tool designed to provide detailed insights into '
Expand Down
6 changes: 3 additions & 3 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from apkInspector.extract import extract_file_based_on_header_info, extract_all_files_from_central_directory
from apkInspector.headers import find_eocd, parse_central_directory, parse_local_header, headers_of_filename
from apkInspector.manifestDecoder import ResChunkHeader, StringPoolType, process_headers, get_manifest
from apkInspector.manifestDecoder import ResChunkHeader, StringPoolType, process_headers, create_manifest


class ApkInspectorTestCase(unittest.TestCase):
Expand Down Expand Up @@ -86,7 +86,7 @@ def test_android_manifest_decoding_orig(self):
string_pool = StringPoolType.from_file(extracted_data)
string_data = string_pool.strdata
elements = process_headers(extracted_data)
manifest = get_manifest(elements, string_data)
manifest = create_manifest(elements, string_data)
manifest_orig = '4e5928e06a5bcaf3373fd5dc0ff1cd5686c465d80ebd7f6d8b2883d58f180bb7'
self.assertEqual(hashlib.sha256(str(manifest).encode('utf-8')).hexdigest(), manifest_orig)

Expand All @@ -101,7 +101,7 @@ def test_android_manifest_decoding_mod(self):
string_pool = StringPoolType.from_file(extracted_data)
string_data = string_pool.strdata
elements = process_headers(extracted_data)
manifest = get_manifest(elements, string_data)
manifest = create_manifest(elements, string_data)
manifest_mod = '4e5928e06a5bcaf3373fd5dc0ff1cd5686c465d80ebd7f6d8b2883d58f180bb7'
self.assertEqual(hashlib.sha256(str(manifest).encode('utf-8')).hexdigest(), manifest_mod)

Expand Down

0 comments on commit 8f1ebb5

Please sign in to comment.