from __future__ import annotations import argparse import os import struct from enum import IntEnum from io import BufferedReader from typing import List from zipfile import ZipFile def main() -> None: from frida_tools.application import ConsoleApplication class ApkApplication(ConsoleApplication): def _usage(self) -> str: return "%(prog)s [options] path.apk" def _add_options(self, parser: argparse.ArgumentParser) -> None: parser.add_argument("-o", "--output", help="output path", metavar="OUTPUT") parser.add_argument("apk", help="apk file") def _needs_device(self) -> bool: return False def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: self._output_path = options.output self._path = options.apk if not self._path.endswith(".apk"): parser.error("path must end in .apk") if self._output_path is None: self._output_path = self._path.replace(".apk", ".d.apk") def _start(self) -> None: try: debug(self._path, self._output_path) except Exception as e: self._update_status(f"Error: {e}") self._exit(1) self._exit(0) app = ApkApplication() app.run() def debug(path: str, output_path: str) -> None: with ZipFile(path, "r") as iz, ZipFile(output_path, "w") as oz: for info in iz.infolist(): with iz.open(info) as f: if info.filename == "AndroidManifest.xml": manifest = BinaryXML(f) pool = None debuggable_index = None size = 8 for header in manifest.chunk_headers[1:]: if header.type == ChunkType.STRING_POOL: pool = StringPool(header) debuggable_index = pool.append_str("debuggable") if header.type == ChunkType.RESOURCE_MAP: # The "debuggable" attribute name is not only a reference to the string pool, but # also to the resource map. We need to extend the resource map with a valid entry. # refs https://justanapplication.wordpress.com/category/android/android-binary-xml/android-xml-startelement-chunk/ resource_map = ResourceMap(header) resource_map.add_debuggable(debuggable_index) if header.type == ChunkType.START_ELEMENT: start = StartElement(header) name = pool.get_string(start.name) if name == "application": start.insert_debuggable(debuggable_index, resource_map) size += header.size header = manifest.chunk_headers[0] header_data = bytearray(header.chunk_data) header_data[4 : 4 + 4] = struct.pack(" None: self.stream = stream self.chunk_headers = [] self.parse() def parse(self) -> None: chunk_header = ChunkHeader(self.stream, False) if chunk_header.type != ChunkType.XML: raise BadHeader() self.chunk_headers.append(chunk_header) size = chunk_header.size while self.stream.tell() < size: chunk_header = ChunkHeader(self.stream) self.chunk_headers.append(chunk_header) class ChunkType(IntEnum): STRING_POOL = 0x001 XML = 0x003 START_ELEMENT = 0x102 RESOURCE_MAP = 0x180 class ResourceType(IntEnum): BOOL = 0x12 class StringType(IntEnum): UTF8 = 1 << 8 class BadHeader(Exception): pass class ChunkHeader: FORMAT = " None: self.stream = stream data = self.stream.peek(struct.calcsize(self.FORMAT)) (self.type, self.header_size, self.size) = struct.unpack_from(self.FORMAT, data) if consume_data: self.chunk_data = self.stream.read(self.size) else: self.chunk_data = self.stream.read(struct.calcsize(self.FORMAT)) class StartElement: FORMAT = " None: self.header = header self.stream = self.header.stream self.header_size = struct.calcsize(self.FORMAT) data = struct.unpack_from(self.FORMAT, self.header.chunk_data) if data[0] != ChunkType.START_ELEMENT: raise BadHeader() self.name = data[6] self.attribute_count = data[8] attributes_data = self.header.chunk_data[self.header_size :] if len(attributes_data[-20:]) == 20: previous_attribute = struct.unpack(self.ATTRIBUTE_FORMAT, attributes_data[-20:]) self.namespace = previous_attribute[0] else: # There are no other attributes in the application tag self.namespace = -1 def insert_debuggable(self, name: int, resource_map: ResourceMap) -> None: # TODO: Instead of using the previous attribute to determine the probable # namespace for the debuggable tag we could scan the strings section # for the AndroidManifest schema tag if self.namespace == -1: raise BadHeader() chunk_data = bytearray(self.header.chunk_data) resource_size = 8 resource_type = ResourceType.BOOL # Denotes a True value in AXML, 0 is used for False resource_data = -1 debuggable = struct.pack( self.ATTRIBUTE_FORMAT, self.namespace, name, -1, resource_size, 0, resource_type, resource_data ) # Some parts of Android expect this to be sorted by resource ID. attr_offset = None for insert_pos in range(self.attribute_count + 1): attr_offset = 0x24 + 20 * insert_pos idx = int.from_bytes(chunk_data[attr_offset + 4 : attr_offset + 8], "little") if resource_map.get_resource(idx) > ResourceMap.DEBUGGING_RESOURCE: break chunk_data[attr_offset:attr_offset] = debuggable self.header.size = len(chunk_data) chunk_data[4 : 4 + 4] = struct.pack(" None: self.header = header def add_debuggable(self, idx: int) -> None: assert idx is not None data_size = len(self.header.chunk_data) - 8 target = (idx + 1) * 4 self.header.chunk_data += b"\x00" * (target - data_size - 4) + self.DEBUGGING_RESOURCE.to_bytes(4, "little") self.header.size = len(self.header.chunk_data) self.header.chunk_data = ( self.header.chunk_data[:4] + struct.pack(" int: offset = index * 4 + 8 return int.from_bytes(self.header.chunk_data[offset : offset + 4], "little") class StringPool: FORMAT = " str: offset = self.offsets[index] # HACK: We subtract 4 because we insert a string offset during append_str # but we do not update the original stream and thus it reads stale data. if self.dirty: offset -= 4 position = self.stream.tell() self.stream.seek(self.strings_offset + 8 + offset, os.SEEK_SET) string = None if self.utf8: # Ignore number of characters n = struct.unpack(" int: data_size = len(self.header.chunk_data) # Reserve data for our new offset data_size += 4 chunk_data = bytearray(data_size) end = self.header_size + self.string_count * 4 chunk_data[:end] = self.header.chunk_data[:end] chunk_data[end + 4 :] = self.header.chunk_data[end:] # Add 4 since we have added a string offset offset = len(chunk_data) - 8 - self.strings_offset + 4 if self.utf8: assert len(add.encode("utf-8")) < 128 # multi-byte len strings not supported yet length_in_characters = len(add) length_in_bytes = len(add.encode("utf-8")) chunk_data.extend(struct.pack("