第2章:APK 文件结构与打包流程深度解析
目录
- 2.1 APK 文件的本质:ZIP 格式
- 2.2 AndroidManifest.xml 二进制格式解析
- 2.3 DEX 文件格式深度解析
- 2.4 resources.arsc 资源索引表解析
- 2.5 APK 签名机制(V1/V2/V3)
- 2.6 APK 打包流程详解
- 2.7 实战案例:手动解析 APK 文件
- 2.8 常见问题与解决方案
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)
- 右键点击 APK 文件
- 选择 "7-Zip" → "打开压缩包"
- 查看内部文件列表
使用 WinRAR
- 右键点击 APK 文件
- 选择 "用 WinRAR 打开"
- 查看内部结构
使用命令行工具
# 列出 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 # 证书文件
签名流程:
- 计算每个文件的 SHA-1 摘要
- 写入 MANIFEST.MF
- 计算 MANIFEST.MF 的摘要,写入 CERT.SF
- 使用私钥签名 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
详细步骤:
-
编译 Java 源码
- Java 源码 → .class 文件
- 工具:javac
-
转换为 DEX
- .class 文件 → classes.dex
- 工具:dx/d8
-
处理资源
- 资源文件 → resources.arsc
- 工具:aapt/aapt2
-
打包 APK
- 所有文件 → APK(ZIP)
- 工具:apkbuilder/zipalign
-
签名 APK
- APK → 签名 APK
- 工具:jarsigner/apksigner
-
对齐 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:混淆后的字符串表被加密
症状: 解析出的字符串是乱码或加密的
解决方案:
- 使用动态分析提取运行时字符串
- 分析字符串解密逻辑
- 使用 Frida Hook 字符串获取函数
问题 2:不同 Android 版本的 DEX 格式差异
症状: 解析工具报错或解析不完整
解决方案:
- 检查 DEX 版本号(magic 中的版本号)
- 使用对应版本的解析工具
- 参考官方 DEX 格式文档
2.8.2 resources.arsc 解析问题
问题 1:二进制格式复杂
症状: 解析 resources.arsc 时出错
解决方案:
- 使用成熟的工具(aapt、androguard)
- 参考 Android 源码中的解析实现
- 逐步解析,验证每个步骤
2.8.3 签名验证问题
问题 1:签名验证失败
症状:
apksigner verify 报错
解决方案:
- 检查签名文件是否完整
- 验证证书是否有效
- 检查 APK 是否被篡改
2.9 本章总结
2.9.1 知识点回顾
- APK 本质:ZIP 格式的压缩文件
- AndroidManifest.xml:二进制 AXML 格式
- DEX 文件:Dalvik 字节码格式
- resources.arsc:资源索引表
- APK 签名:V1/V2/V3 三种签名方式
- 打包流程:源码 → 编译 → 打包 → 签名
2.9.2 实践要点
- ✅ 理解 APK 的内部结构
- ✅ 掌握二进制格式的解析方法
- ✅ 能够使用工具提取关键信息
- ✅ 理解 APK 的完整生命周期
2.9.3 下一步学习
- 第 3 章:深入学习 Smali 语法
- 第 4 章:使用工具进行静态分析
- 第 5 章:了解 Android 安全机制
附录:工具和资源
工具下载
- aapt/aapt2:Android SDK 自带
- dexdump:Android SDK 自带
- 010 Editor:www.sweetscape.com/010editor/
- androguard:
pip install androguard
参考文档
- DEX 格式文档:source.android.com/devices/tec…
- APK 签名文档:source.android.com/security/ap…
- ZIP 格式文档:APPNOTE.TXT
本章完成! 🎉
现在你已经深入理解了 APK 文件的结构和格式。在下一章中,我们将学习 Java 字节码和 Smali 语法,这是进行代码分析和修改的基础。