【Android逆向工程】第2章:APK 文件结构与打包流程深度解析

297 阅读20分钟

第2章:APK 文件结构与打包流程深度解析

目录


2.1 APK 文件的本质:ZIP 格式

2.1.1 APK 即 ZIP

APK(Android Package)文件本质上是一个 ZIP 压缩文件,只是扩展名改为了 .apk。这意味着你可以使用任何 ZIP 工具打开、解压和查看 APK 文件的内容。

验证 APK 是 ZIP 文件:

# 方法1:使用 file 命令查看文件类型(Linux/macOS)
file app.apk
# 输出:app.apk: Zip archive data, at least v2.0 to extract

# 方法2:使用 unzip 命令测试
unzip -l app.apk

# 方法3:直接重命名扩展名
cp app.apk app.zip
unzip -l app.zip

ZIP 文件结构:

ZIP 文件由以下几个主要部分组成:

ZIP 文件结构:
├── Local File Header 1    # 本地文件头(文件1)
├── File Data 1            # 文件数据(文件1)
├── Local File Header 2    # 本地文件头(文件2)
├── File Data 2            # 文件数据(文件2)
├── ...
├── Central Directory      # 中央目录(文件索引)
└── End of Central Directory Record  # 中央目录结束记录

2.1.2 ZIP 文件格式详解

Local File Header(本地文件头)

每个文件在 ZIP 中都有一个本地文件头,位于文件数据之前。

结构定义(C 语言):

struct LocalFileHeader {
    uint32_t signature;        // 0x04034b50 (PK\x03\x04)
    uint16_t version;          // 解压所需版本
    uint16_t flags;             // 通用位标志
    uint16_t compression;       // 压缩方法
    uint16_t mod_time;          // 最后修改时间
    uint16_t mod_date;          // 最后修改日期
    uint32_t crc32;             // CRC-32 校验和
    uint32_t compressed_size;   // 压缩后大小
    uint32_t uncompressed_size; // 未压缩大小
    uint16_t filename_length;   // 文件名长度
    uint16_t extra_length;      // 扩展字段长度
    char filename[filename_length];  // 文件名
    char extra[extra_length];        // 扩展字段
    char file_data[compressed_size];  // 文件数据
};

十六进制查看示例:

[图示:ZIP Local File Header 的十六进制表示]
偏移    十六进制值                    含义
0x0000: 50 4B 03 04                  signature (PK\x03\x04)
0x0004: 14 00                        version (20)
0x0006: 00 00                        flags
0x0008: 00 00                        compression (无压缩)
0x000A: 4A 5E                        mod_time
0x000C: 21 4D                        mod_date
0x000E: 12 34 56 78                  crc32
0x0012: 00 00 00 00                  compressed_size
0x0016: 00 00 00 00                  uncompressed_size
0x001A: 0F 00                        filename_length (15)
0x001C: 00 00                        extra_length
0x001E: 41 6E 64 72 6F 69 64...     filename ("AndroidManifest.xml")
Central Directory(中央目录)

中央目录包含了 ZIP 文件中所有文件的索引信息,位于文件末尾。

结构定义:

struct CentralDirectoryEntry {
    uint32_t signature;        // 0x02014b50 (PK\x01\x02)
    uint16_t version_made;     // 创建版本
    uint16_t version_needed;   // 所需版本
    uint16_t flags;             // 通用位标志
    uint16_t compression;       // 压缩方法
    uint16_t mod_time;          // 最后修改时间
    uint16_t mod_date;          // 最后修改日期
    uint32_t crc32;             // CRC-32 校验和
    uint32_t compressed_size;   // 压缩后大小
    uint32_t uncompressed_size; // 未压缩大小
    uint16_t filename_length;   // 文件名长度
    uint16_t extra_length;      // 扩展字段长度
    uint16_t comment_length;    // 注释长度
    uint16_t disk_number;       // 磁盘号
    uint16_t internal_attrs;    // 内部属性
    uint32_t external_attrs;    // 外部属性
    uint32_t local_header_offset; // 本地文件头偏移
    char filename[filename_length];
    char extra[extra_length];
    char comment[comment_length];
};
End of Central Directory Record(中央目录结束记录)

位于 ZIP 文件的最末尾,标记中央目录的结束。

结构定义:

struct EndOfCentralDirectory {
    uint32_t signature;           // 0x06054b50 (PK\x05\x06)
    uint16_t disk_number;         // 当前磁盘号
    uint16_t cd_disk_number;      // 中央目录所在磁盘号
    uint16_t cd_entries_this_disk; // 本磁盘中央目录条目数
    uint16_t cd_total_entries;    // 中央目录总条目数
    uint32_t cd_size;             // 中央目录大小
    uint32_t cd_offset;           // 中央目录偏移
    uint16_t comment_length;      // ZIP 注释长度
    char comment[comment_length]; // ZIP 注释
};

2.1.3 使用工具查看 APK 的 ZIP 结构

使用 7-Zip(Windows)
  1. 右键点击 APK 文件
  2. 选择 "7-Zip" → "打开压缩包"
  3. 查看内部文件列表
使用 WinRAR
  1. 右键点击 APK 文件
  2. 选择 "用 WinRAR 打开"
  3. 查看内部结构
使用命令行工具
# 列出 APK 内容
unzip -l app.apk

# 解压 APK
unzip app.apk -d apk_extracted/

# 查看特定文件
unzip -p app.apk AndroidManifest.xml > manifest.xml
使用 Python 脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
APK ZIP 结构分析工具
"""

import zipfile
import struct

def analyze_zip_structure(apk_path):
    """分析 APK 的 ZIP 结构"""
    with zipfile.ZipFile(apk_path, 'r') as zip_file:
        print("=" * 60)
        print("APK ZIP 结构分析")
        print("=" * 60)
        print(f"\n文件总数: {len(zip_file.namelist())}")
        print(f"\n文件列表:")
        print("-" * 60)
        
        for name in zip_file.namelist():
            info = zip_file.getinfo(name)
            print(f"{name}")
            print(f"  压缩前大小: {info.file_size} 字节")
            print(f"  压缩后大小: {info.compress_size} 字节")
            print(f"  压缩率: {(1 - info.compress_size/info.file_size)*100:.2f}%")
            print(f"  压缩方法: {info.compress_type}")
            print()

if __name__ == "__main__":
    analyze_zip_structure("app.apk")

2.2 AndroidManifest.xml 二进制格式解析

2.2.1 AXML 格式简介

AndroidManifest.xml 在 APK 中是以二进制格式(AXML,Android XML)存储的,而不是普通的文本 XML。这种格式可以:

  • 减少文件大小
  • 加快解析速度
  • 支持资源 ID 引用

2.2.2 AXML 二进制结构

AXML 文件由以下几个部分组成:

AXML 文件结构:
├── Header                    # 文件头
├── String Pool              # 字符串池
├── Resource ID Map          # 资源 ID 映射(可选)
├── Namespace Start          # 命名空间开始
├── Element Start            # 元素开始
├── Attribute                # 属性
├── Element End              # 元素结束
└── Namespace End            # 命名空间结束
AXML Header(文件头)

结构定义:

struct AXMLHeader {
    uint32_t magic;           // 0x00080003 (AXML 魔数)
    uint32_t file_size;       // 文件大小
};

十六进制示例:

[图示:AXML Header 的十六进制表示]
偏移    十六进制值        含义
0x0000: 03 00 08 00        magic (0x00080003)
0x0004: 12 34 56 78        file_size
String Pool(字符串池)

字符串池存储了 XML 中所有的字符串,通过索引引用。

结构定义:

struct StringPoolHeader {
    uint32_t chunk_type;      // 0x0001 (STRING_POOL)
    uint32_t chunk_size;      // 块大小
    uint32_t string_count;    // 字符串数量
    uint32_t style_count;     // 样式数量
    uint32_t flags;           // 标志(UTF-8 或 UTF-16)
    uint32_t strings_offset;  // 字符串数据偏移
    uint32_t styles_offset;   // 样式数据偏移
    uint32_t string_offsets[string_count];  // 字符串偏移数组
    uint32_t style_offsets[style_count];   // 样式偏移数组
    char string_data[];       // 字符串数据
};

2.2.3 使用工具解析 AndroidManifest.xml

使用 aapt/aapt2

aapt(Android Asset Packaging Tool):

# 查看 AndroidManifest.xml(文本格式)
aapt dump xmltree app.apk AndroidManifest.xml

# 查看权限
aapt dump permissions app.apk

# 查看包信息
aapt dump badging app.apk

aapt2(新版工具):

# 使用 aapt2 需要先解压
unzip app.apk AndroidManifest.xml
aapt2 dump xmltree app.apk AndroidManifest.xml
使用 Python 脚本解析
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AndroidManifest.xml 解析工具
使用 axmlparserpy 库
"""

import zipfile
from axmlparserpy import axmlparserpy

def parse_manifest(apk_path):
    """解析 AndroidManifest.xml"""
    with zipfile.ZipFile(apk_path, 'r') as zip_file:
        # 提取 AndroidManifest.xml
        manifest_data = zip_file.read('AndroidManifest.xml')
        
        # 解析 AXML
        parser = axmlparserpy.AXML(manifest_data)
        
        # 获取 XML 字符串
        xml_string = parser.get_xml()
        
        print("=" * 60)
        print("AndroidManifest.xml 内容")
        print("=" * 60)
        print(xml_string)
        
        # 解析权限
        print("\n" + "=" * 60)
        print("权限列表")
        print("=" * 60)
        # 使用 XML 解析器解析权限
        import xml.etree.ElementTree as ET
        root = ET.fromstring(xml_string)
        
        for permission in root.findall('.//uses-permission'):
            name = permission.get('{http://schemas.android.com/apk/res/android}name')
            if name:
                print(f"  - {name}")

if __name__ == "__main__":
    parse_manifest("app.apk")

安装依赖:

pip install axmlparserpy
使用在线工具
  • APK Analyzer:Android Studio 内置工具
  • 在线 APK 分析器www.apkmonk.com/

2.2.4 手动解析 AXML(高级)

Python 实现示例:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
手动解析 AXML 格式
"""

import struct

class AXMLParser:
    def __init__(self, data):
        self.data = data
        self.offset = 0
        
    def read_uint32(self):
        """读取 32 位无符号整数"""
        value = struct.unpack('<I', self.data[self.offset:self.offset+4])[0]
        self.offset += 4
        return value
    
    def read_uint16(self):
        """读取 16 位无符号整数"""
        value = struct.unpack('<H', self.data[self.offset:self.offset+2])[0]
        self.offset += 2
        return value
    
    def read_string(self, length):
        """读取字符串"""
        string = self.data[self.offset:self.offset+length].decode('utf-8')
        self.offset += length
        return string
    
    def parse_header(self):
        """解析文件头"""
        magic = self.read_uint32()
        file_size = self.read_uint32()
        
        print(f"Magic: 0x{magic:08X}")
        print(f"File Size: {file_size} bytes")
        
        if magic != 0x00080003:
            raise ValueError("Invalid AXML magic number")
        
        return file_size
    
    def parse_string_pool(self):
        """解析字符串池"""
        chunk_type = self.read_uint32()
        chunk_size = self.read_uint32()
        string_count = self.read_uint32()
        style_count = self.read_uint32()
        flags = self.read_uint32()
        strings_offset = self.read_uint32()
        styles_offset = self.read_uint32()
        
        print(f"\n字符串池:")
        print(f"  字符串数量: {string_count}")
        print(f"  样式数量: {style_count}")
        print(f"  标志: 0x{flags:08X}")
        
        # 读取字符串偏移数组
        string_offsets = []
        for i in range(string_count):
            offset = self.read_uint32()
            string_offsets.append(offset)
        
        # 读取字符串数据
        strings = []
        string_data_start = self.offset
        for offset in string_offsets:
            self.offset = string_data_start + offset
            # 读取字符串长度(UTF-16)
            if flags & 0x00000100:  # UTF-8
                length = self.read_uint16()
                string = self.read_string(length)
            else:  # UTF-16
                length = self.read_uint16()
                string = self.data[self.offset:self.offset+length*2].decode('utf-16le')
                self.offset += length * 2
            strings.append(string)
        
        return strings

def parse_axml(apk_path):
    """解析 APK 中的 AndroidManifest.xml"""
    import zipfile
    
    with zipfile.ZipFile(apk_path, 'r') as zip_file:
        manifest_data = zip_file.read('AndroidManifest.xml')
    
    parser = AXMLParser(manifest_data)
    parser.parse_header()
    strings = parser.parse_string_pool()
    
    print("\n字符串列表:")
    for i, s in enumerate(strings):
        print(f"  [{i}] {s}")

if __name__ == "__main__":
    parse_axml("app.apk")

2.3 DEX 文件格式深度解析

2.3.1 DEX 文件简介

DEX(Dalvik Executable)是 Android 平台上的字节码格式,类似于 Java 的 .class 文件,但针对移动设备进行了优化。

2.3.2 DEX 文件结构

DEX 文件由以下几个主要部分组成:

DEX 文件结构:
├── DEX Header              # DEX 文件头
├── String Table            # 字符串表
├── Type Table              # 类型表
├── Proto Table             # 原型表
├── Field Table             # 字段表
├── Method Table            # 方法表
├── Class Table             # 类表
├── Data Section            # 数据区
└── Link Data               # 链接数据(可选)

2.3.3 DEX Header(文件头)

结构定义(C 语言):

struct DexHeader {
    uint8_t magic[8];         // DEX 魔数 "dex\n035\0" 或 "dex\n037\0"
    uint32_t checksum;        // 校验和(adler32)
    uint8_t signature[20];   // SHA-1 签名
    uint32_t file_size;       // 文件大小
    uint32_t header_size;     // 头大小(0x70)
    uint32_t endian_tag;      // 字节序标记(0x12345678)
    uint32_t link_size;       // 链接数据大小
    uint32_t link_off;        // 链接数据偏移
    uint32_t map_off;         // Map 列表偏移
    uint32_t string_ids_size; // 字符串 ID 数量
    uint32_t string_ids_off;  // 字符串 ID 偏移
    uint32_t type_ids_size;   // 类型 ID 数量
    uint32_t type_ids_off;    // 类型 ID 偏移
    uint32_t proto_ids_size;  // 原型 ID 数量
    uint32_t proto_ids_off;   // 原型 ID 偏移
    uint32_t field_ids_size;  // 字段 ID 数量
    uint32_t field_ids_off;   // 字段 ID 偏移
    uint32_t method_ids_size; // 方法 ID 数量
    uint32_t method_ids_off;  // 方法 ID 偏移
    uint32_t class_defs_size; // 类定义数量
    uint32_t class_defs_off;  // 类定义偏移
    uint32_t data_size;       // 数据大小
    uint32_t data_off;        // 数据偏移
};

十六进制查看示例:

[图示:DEX Header 的十六进制表示]
偏移    十六进制值                    含义
0x0000: 64 65 78 0A 30 33 35 00      magic ("dex\n035\0")
0x0008: 12 34 56 78                  checksum
0x000C: AB CD EF ...                 signature (SHA-1, 20 bytes)
0x0020: 00 01 23 45                  file_size
0x0024: 70 00 00 00                  header_size (0x70)
0x0028: 78 56 34 12                  endian_tag (0x12345678)
0x002C: 00 00 00 00                  link_size
0x0030: 00 00 00 00                  link_off
0x0034: 12 34 56 78                  map_off
0x0038: 00 00 01 23                  string_ids_size
0x003C: 70 00 00 00                  string_ids_off
...

Python 解析 DEX Header:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DEX Header 解析工具
"""

import struct

def parse_dex_header(dex_path):
    """解析 DEX 文件头"""
    with open(dex_path, 'rb') as f:
        data = f.read()
    
    # 解析 Header
    magic = data[0:8]
    checksum = struct.unpack('<I', data[8:12])[0]
    signature = data[12:32]
    file_size = struct.unpack('<I', data[32:36])[0]
    header_size = struct.unpack('<I', data[36:40])[0]
    endian_tag = struct.unpack('<I', data[40:44])[0]
    
    # 解析各个表的偏移和大小
    string_ids_size = struct.unpack('<I', data[56:60])[0]
    string_ids_off = struct.unpack('<I', data[60:64])[0]
    type_ids_size = struct.unpack('<I', data[64:68])[0]
    type_ids_off = struct.unpack('<I', data[68:72])[0]
    
    print("=" * 60)
    print("DEX Header 信息")
    print("=" * 60)
    print(f"Magic: {magic}")
    print(f"Checksum: 0x{checksum:08X}")
    print(f"Signature: {signature.hex()}")
    print(f"File Size: {file_size} bytes")
    print(f"Header Size: {header_size} bytes")
    print(f"Endian Tag: 0x{endian_tag:08X}")
    print(f"\n字符串表:")
    print(f"  数量: {string_ids_size}")
    print(f"  偏移: 0x{string_ids_off:08X}")
    print(f"\n类型表:")
    print(f"  数量: {type_ids_size}")
    print(f"  偏移: 0x{type_ids_off:08X}")

if __name__ == "__main__":
    parse_dex_header("classes.dex")

2.3.4 String Table(字符串表)

字符串表存储了 DEX 文件中所有的字符串常量。

String ID 结构:

struct StringId {
    uint32_t string_data_off;  // 字符串数据偏移
};

String Data 结构:

// 字符串数据格式(ULEB128 编码)
// 第一个字节:字符串长度(ULEB128)
// 后续字节:UTF-8 编码的字符串
// 最后一个字节:0x00(结束符)

Python 解析字符串表:

def read_uleb128(data, offset):
    """读取 ULEB128 编码的整数"""
    result = 0
    shift = 0
    while True:
        byte = data[offset]
        offset += 1
        result |= (byte & 0x7f) << shift
        if (byte & 0x80) == 0:
            break
        shift += 7
    return result, offset

def parse_string_table(dex_path):
    """解析 DEX 字符串表"""
    with open(dex_path, 'rb') as f:
        data = f.read()
    
    # 读取 Header 中的字符串表信息
    string_ids_size = struct.unpack('<I', data[56:60])[0]
    string_ids_off = struct.unpack('<I', data[60:64])[0]
    
    strings = []
    
    # 读取所有 String ID
    for i in range(string_ids_size):
        string_id_offset = string_ids_off + i * 4
        string_data_off = struct.unpack('<I', data[string_id_offset:string_id_offset+4])[0]
        
        # 读取字符串数据
        offset = string_data_off
        length, offset = read_uleb128(data, offset)
        string_data = data[offset:offset+length].decode('utf-8')
        strings.append(string_data)
    
    print("=" * 60)
    print(f"字符串表(共 {len(strings)} 个字符串)")
    print("=" * 60)
    for i, s in enumerate(strings[:20]):  # 只显示前20个
        print(f"  [{i}] {s}")
    if len(strings) > 20:
        print(f"  ... (还有 {len(strings) - 20} 个字符串)")

if __name__ == "__main__":
    parse_string_table("classes.dex")

2.3.5 Type Table(类型表)

类型表存储了所有类型的字符串索引。

Type ID 结构:

struct TypeId {
    uint32_t descriptor_idx;  // 描述符字符串索引(指向 String Table)
};

2.3.6 Method Table(方法表)

方法表存储了所有方法的定义信息。

Method ID 结构:

struct MethodId {
    uint16_t class_idx;       // 类索引(指向 Type Table)
    uint16_t proto_idx;       // 原型索引(指向 Proto Table)
    uint32_t name_idx;        // 方法名索引(指向 String Table)
};

2.3.7 使用工具解析 DEX 文件

使用 dexdump
# 查看 DEX 文件基本信息
dexdump -f classes.dex

# 反汇编 DEX 文件
dexdump -d classes.dex > classes_dex.txt

# 查看字符串
dexdump classes.dex | grep "String table"
使用 Python 工具
# 安装 androguard
pip install androguard

# 使用 androguard 分析
from androguard.core.bytecodes import dvm

# 加载 DEX 文件
dalvik = dvm.DalvikVMFormat(open("classes.dex", "rb").read())

# 获取所有字符串
for string in dalvik.get_strings():
    print(string)

2.4 resources.arsc 资源索引表解析

2.4.1 resources.arsc 简介

resources.arsc 是 Android 资源索引表,以二进制格式存储,用于快速查找和访问应用资源。

2.4.2 resources.arsc 结构

resources.arsc 结构:
├── Table Header            # 表头
├── String Pool             # 字符串池
├── Package Group           # 包组
│   ├── Package             # 包
│   │   ├── Type Spec       # 类型规格
│   │   │   └── Type        # 类型
│   │   │       └── Entry   # 条目

2.4.3 使用工具解析 resources.arsc

使用 aapt dump resources
# 查看所有资源
aapt dump resources app.apk

# 查看特定资源类型
aapt dump resources app.apk | grep "string"

# 查看资源配置
aapt dump configurations app.apk
使用 Python 解析
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
resources.arsc 解析工具
使用 aapt2 或 androguard
"""

from androguard.core.axml import AXML
from androguard.core.bytecodes import apk

def parse_resources(apk_path):
    """解析 APK 资源"""
    a = apk.APK(apk_path)
    
    print("=" * 60)
    print("资源信息")
    print("=" * 60)
    
    # 获取所有字符串资源
    print("\n字符串资源:")
    for key, value in a.get_strings_resources().items():
        print(f"  {key}: {value}")
    
    # 获取权限
    print("\n权限列表:")
    for perm in a.get_permissions():
        print(f"  - {perm}")
    
    # 获取主 Activity
    print("\n主 Activity:")
    print(f"  {a.get_main_activity()}")

if __name__ == "__main__":
    parse_resources("app.apk")

2.5 APK 签名机制(V1/V2/V3)

2.5.1 APK 签名的作用

  • 完整性验证:确保 APK 未被篡改
  • 身份认证:标识 APK 的发布者
  • 权限控制:系统根据签名授予权限

2.5.2 V1 签名(JAR 签名)

V1 签名是传统的 JAR 签名方式,兼容性好但安全性较低。

签名文件位置:

META-INF/
├── MANIFEST.MF      # 清单文件
├── CERT.SF          # 签名文件
└── CERT.RSA         # 证书文件

签名流程:

  1. 计算每个文件的 SHA-1 摘要
  2. 写入 MANIFEST.MF
  3. 计算 MANIFEST.MF 的摘要,写入 CERT.SF
  4. 使用私钥签名 CERT.SF,生成 CERT.RSA

2.5.3 V2 签名(APK Signature Scheme v2)

V2 签名在 APK 的 ZIP 结构中添加了签名块,提高了安全性。

签名块位置:

APK 文件:
├── ZIP 内容
├── APK Signing Block  # V2 签名块
└── ZIP Central Directory

特点:

  • ✅ 更快的验证速度
  • ✅ 防止 ZIP 元数据篡改
  • ✅ 支持 APK 增量更新

2.5.4 V3 签名(APK Signature Scheme v3)

V3 签名在 V2 基础上增加了密钥轮换支持。

特点:

  • ✅ 支持密钥升级
  • ✅ 向后兼容 V1/V2
  • ✅ 更安全的密钥管理

2.5.5 使用工具查看签名信息

使用 keytool
# 查看证书信息
keytool -printcert -jarfile app.apk

# 或从 META-INF 提取证书
unzip -p app.apk META-INF/CERT.RSA | openssl pkcs7 -inform DER -print
使用 apksigner
# 验证签名
apksigner verify --verbose app.apk

# 查看签名版本
apksigner verify --print-certs app.apk
使用 Python 脚本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
APK 签名信息查看工具
"""

import zipfile
from cryptography import x509
from cryptography.hazmat.backends import default_backend

def view_signature(apk_path):
    """查看 APK 签名信息"""
    with zipfile.ZipFile(apk_path, 'r') as zip_file:
        # 检查 V1 签名
        if 'META-INF/MANIFEST.MF' in zip_file.namelist():
            print("V1 签名: 存在")
            
            # 读取证书
            cert_data = zip_file.read('META-INF/CERT.RSA')
            cert = x509.load_der_x509_certificate(cert_data, default_backend())
            
            print(f"\n证书信息:")
            print(f"  颁发者: {cert.issuer}")
            print(f"  主题: {cert.subject}")
            print(f"  有效期: {cert.not_valid_before}{cert.not_valid_after}")
        else:
            print("V1 签名: 不存在")
        
        # 检查 V2/V3 签名(需要解析 APK Signing Block)
        print("\n提示: 使用 apksigner 查看 V2/V3 签名信息")

if __name__ == "__main__":
    view_signature("app.apk")

2.6 APK 打包流程详解

2.6.1 完整打包流程

源码 → 编译 → 打包 → 签名 → 对齐 → APK

详细步骤:

  1. 编译 Java 源码

    • Java 源码 → .class 文件
    • 工具:javac
  2. 转换为 DEX

    • .class 文件 → classes.dex
    • 工具:dx/d8
  3. 处理资源

    • 资源文件 → resources.arsc
    • 工具:aapt/aapt2
  4. 打包 APK

    • 所有文件 → APK(ZIP)
    • 工具:apkbuilder/zipalign
  5. 签名 APK

    • APK → 签名 APK
    • 工具:jarsigner/apksigner
  6. 对齐 APK(可选)

    • 优化 APK 结构
    • 工具:zipalign

2.6.2 使用 Android Studio 打包

Gradle 构建流程:

// build.gradle
android {
    compileSdkVersion 33
    
    defaultConfig {
        applicationId "com.example.app"
        minSdkVersion 21
        targetSdkVersion 33
    }
    
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
    signingConfigs {
        release {
            storeFile file('keystore.jks')
            storePassword 'password'
            keyAlias 'key'
            keyPassword 'password'
        }
    }
}

2.6.3 手动打包 APK

# 1. 编译 Java 源码
javac -d build/classes src/**/*.java

# 2. 转换为 DEX
d8 build/classes/**/*.class --output build/

# 3. 处理资源
aapt2 compile -o build/res.zip res/**/*
aapt2 link -o build/app.apk \
    -I android.jar \
    --manifest AndroidManifest.xml \
    build/res.zip \
    build/classes.dex

# 4. 签名
apksigner sign --ks keystore.jks --ks-key-alias key build/app.apk

# 5. 对齐
zipalign -v 4 build/app.apk build/app-aligned.apk

2.7 实战案例:手动解析 APK 文件

2.7.1 准备工作

目标: 手动解析一个 APK 文件,提取出:

  • 所有字符串资源
  • 权限声明
  • 入口 Activity

需要工具:

  • Python 3
  • 十六进制编辑器(010 Editor、HxD)
  • aapt/aapt2
  • dexdump

2.7.2 步骤 1:查看 APK 的 ZIP 结构

# 解压 APK
unzip app.apk -d apk_extracted/

# 查看文件列表
ls -la apk_extracted/

输出示例:

apk_extracted/
├── AndroidManifest.xml
├── classes.dex
├── resources.arsc
├── res/
└── META-INF/

2.7.3 步骤 2:解析 AndroidManifest.xml

# 使用 aapt 解析
aapt dump xmltree app.apk AndroidManifest.xml > manifest.txt

# 或使用 Python 脚本
python parse_manifest.py app.apk

提取权限:

import zipfile
from androguard.core.bytecodes import apk

a = apk.APK("app.apk")
print("权限列表:")
for perm in a.get_permissions():
    print(f"  - {perm}")

print("\n入口 Activity:")
print(f"  {a.get_main_activity()}")

2.7.4 步骤 3:解析 DEX 文件,提取字符串

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
提取 DEX 文件中的所有字符串
"""

import struct
import sys

def read_uleb128(data, offset):
    """读取 ULEB128"""
    result = 0
    shift = 0
    while offset < len(data):
        byte = data[offset]
        offset += 1
        result |= (byte & 0x7f) << shift
        if (byte & 0x80) == 0:
            break
        shift += 7
    return result, offset

def extract_strings(dex_path):
    """提取 DEX 文件中的所有字符串"""
    with open(dex_path, 'rb') as f:
        data = f.read()
    
    # 检查魔数
    magic = data[0:8]
    if not magic.startswith(b'dex'):
        print("错误: 不是有效的 DEX 文件")
        return
    
    # 读取字符串表信息
    string_ids_size = struct.unpack('<I', data[56:60])[0]
    string_ids_off = struct.unpack('<I', data[60:64])[0]
    
    strings = []
    
    # 读取所有字符串
    for i in range(string_ids_size):
        string_id_offset = string_ids_off + i * 4
        string_data_off = struct.unpack('<I', data[string_id_offset:string_id_offset+4])[0]
        
        # 读取字符串
        offset = string_data_off
        length, offset = read_uleb128(data, offset)
        if offset + length <= len(data):
            string_data = data[offset:offset+length].decode('utf-8', errors='ignore')
            strings.append(string_data)
    
    # 保存到文件
    with open('dex_strings.txt', 'w', encoding='utf-8') as f:
        for s in strings:
            f.write(s + '\n')
    
    print(f"提取了 {len(strings)} 个字符串,已保存到 dex_strings.txt")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python extract_strings.py <dex_file>")
        sys.exit(1)
    
    extract_strings(sys.argv[1])

使用方法:

# 从 APK 中提取 DEX
unzip app.apk classes.dex

# 提取字符串
python extract_strings.py classes.dex

2.7.5 步骤 4:解析 resources.arsc

from androguard.core.bytecodes import apk

a = apk.APK("app.apk")

print("=" * 60)
print("资源信息")
print("=" * 60)

# 字符串资源
print("\n字符串资源:")
for key, value in a.get_strings_resources().items():
    print(f"  {key}: {value}")

# 获取所有资源
print("\n所有资源类型:")
for resource_type in a.get_resources_types():
    print(f"  - {resource_type}")
    for resource_id, resource_name in a.get_resources_names(resource_type):
        print(f"    [{resource_id}] {resource_name}")

2.7.6 步骤 5:完整解析脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
完整的 APK 解析工具
提取所有关键信息
"""

import zipfile
from androguard.core.bytecodes import apk
from androguard.core.bytecodes import dvm

def parse_apk_complete(apk_path):
    """完整解析 APK"""
    print("=" * 60)
    print(f"解析 APK: {apk_path}")
    print("=" * 60)
    
    # 使用 androguard 解析
    a = apk.APK(apk_path)
    
    # 基本信息
    print("\n[1] 基本信息")
    print("-" * 60)
    print(f"包名: {a.get_package()}")
    print(f"版本: {a.get_androidversion_name()} ({a.get_androidversion_code()})")
    print(f"最小 SDK: {a.get_min_sdk_version()}")
    print(f"目标 SDK: {a.get_target_sdk_version()}")
    
    # 权限
    print("\n[2] 权限列表")
    print("-" * 60)
    for perm in a.get_permissions():
        print(f"  - {perm}")
    
    # 组件
    print("\n[3] 组件信息")
    print("-" * 60)
    print("Activities:")
    for activity in a.get_activities():
        print(f"  - {activity}")
        if activity == a.get_main_activity():
            print("    (主 Activity)")
    
    print("\nServices:")
    for service in a.get_services():
        print(f"  - {service}")
    
    print("\nReceivers:")
    for receiver in a.get_receivers():
        print(f"  - {receiver}")
    
    # 字符串资源
    print("\n[4] 字符串资源(前20个)")
    print("-" * 60)
    strings = a.get_strings_resources()
    for i, (key, value) in enumerate(list(strings.items())[:20]):
        print(f"  {key}: {value}")
    
    # DEX 字符串
    print("\n[5] DEX 字符串(前20个)")
    print("-" * 60)
    dalvik = dvm.DalvikVMFormat(a.get_dex())
    for i, string in enumerate(list(dalvik.get_strings())[:20]):
        print(f"  [{i}] {string}")
    
    # 签名信息
    print("\n[6] 签名信息")
    print("-" * 60)
    certs = a.get_certificates()
    if certs:
        cert = certs[0]
        print(f"颁发者: {cert.issuer}")
        print(f"主题: {cert.subject}")
    else:
        print("未找到签名信息")

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("用法: python parse_apk.py <apk_file>")
        sys.exit(1)
    
    parse_apk_complete(sys.argv[1])

2.8 常见问题与解决方案

2.8.1 DEX 文件解析问题

问题 1:混淆后的字符串表被加密

症状: 解析出的字符串是乱码或加密的

解决方案:

  1. 使用动态分析提取运行时字符串
  2. 分析字符串解密逻辑
  3. 使用 Frida Hook 字符串获取函数
问题 2:不同 Android 版本的 DEX 格式差异

症状: 解析工具报错或解析不完整

解决方案:

  1. 检查 DEX 版本号(magic 中的版本号)
  2. 使用对应版本的解析工具
  3. 参考官方 DEX 格式文档

2.8.2 resources.arsc 解析问题

问题 1:二进制格式复杂

症状: 解析 resources.arsc 时出错

解决方案:

  1. 使用成熟的工具(aapt、androguard)
  2. 参考 Android 源码中的解析实现
  3. 逐步解析,验证每个步骤

2.8.3 签名验证问题

问题 1:签名验证失败

症状: apksigner verify 报错

解决方案:

  1. 检查签名文件是否完整
  2. 验证证书是否有效
  3. 检查 APK 是否被篡改

2.9 本章总结

2.9.1 知识点回顾

  1. APK 本质:ZIP 格式的压缩文件
  2. AndroidManifest.xml:二进制 AXML 格式
  3. DEX 文件:Dalvik 字节码格式
  4. resources.arsc:资源索引表
  5. APK 签名:V1/V2/V3 三种签名方式
  6. 打包流程:源码 → 编译 → 打包 → 签名

2.9.2 实践要点

  • ✅ 理解 APK 的内部结构
  • ✅ 掌握二进制格式的解析方法
  • ✅ 能够使用工具提取关键信息
  • ✅ 理解 APK 的完整生命周期

2.9.3 下一步学习

  • 第 3 章:深入学习 Smali 语法
  • 第 4 章:使用工具进行静态分析
  • 第 5 章:了解 Android 安全机制

附录:工具和资源

工具下载

参考文档


本章完成! 🎉

现在你已经深入理解了 APK 文件的结构和格式。在下一章中,我们将学习 Java 字节码和 Smali 语法,这是进行代码分析和修改的基础。