加固APK的AndroidManifest.xml修复方法总结
分析加固后的 Android 应用时,解包得到的 AndroidManifest.xml 经常无法解码或显示为乱码,导致包名、组件声明等关键信息无法获取。本文以 X加密加固的银行类样本(cn.com.x.bank)为例,分析乱码的成因,整理三种修复方案的原理、操作方法和适用场景。
1. 简介
APK 内的 AndroidManifest.xml 本身就不是文本文件,而是 AXML(Android Binary XML)格式——Android 为安装时快速解析设计的二进制编码。所以直接解包后用编辑器打开看到”乱码”是正常现象,正常情况下解码工具可以还原。一般可以通过jadx工具解析APK查看AndroidManifest.xml配置文件:

当APK(cn.com.x.bank)经过加固后,AndroidManifest.xml可能被混淆,这时候使用jadx工具查看就是乱码形式了:

这种加固混淆一般是在AndroidManifest.xml中插入垃圾字节或非常规格式,破坏工具的解析。但是对于这种混淆的场景,有一个底层事实:Android 系统安装时必须能解析 manifest,厂商可以对 dex 加密、对资源混淆,但 manifest 必须保持 PackageManager 可读的形态。Android 系统必须能读懂这份 manifest,所以永远存在一条还原路径。原因是框架自带的解析器对规范外的脏数据相当宽容(跳过、忽略、按剩余有效信息继续解析),而 jadx、AXMLPrinter2 这类严格按规范实现的解析器一遇到偏差就直接报错放弃。
2. 方案一:aapt2 直接提取
系统必须能读懂 manifest,官方解析工具自然也能。AAPT2(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。AAPT2工具随 Android SDK Build Tools 工具包发布:

aapt2 对新旧格式原生兼容,工具可直接对 APK 操作(注意 cmd 下使用命令 chcp 65001 切换到 UTF-8 编码再执行,否则中文信息可能乱码):
# 关键摘要:包名、版本、SDK、入口 Activity、权限
aapt2 dump badging your_app.apk
# 完整 manifest 的 XML 树
aapt2 dump xmltree --file AndroidManifest.xml your_app.apk
# 老版 aapt 同样可用
aapt dump badging your_app.apk
如使用aapt2 dump badging解析加固包配置:

使用aapt2 dump xmltree --file AndroidManifest.xml your_app.apk输出AndroidManifest.xml:

实测包名、版本号、minSdk/targetSdk、启动 Activity、全部 uses-permission 可以 100% 拿到。但是输出数据是 aapt2 自定义的摘要/树形格式,不是标准 XML 文件,无法直接喂给需要 XML 输入的下游工具。
3. 方案二:drozer 运行时还原
run app.package.manifest 模块的思路是把 APK 装进真实设备,由 Android 框架自带的资源解析器代为解析 manifest(细节见 3.1),drozer 再输出解析结果。任何格式差异、加固混淆,只要系统能装能跑,输出的就是完整明文。
前置条件:设备上安装 drozer Agent 并启动(进入 Embedded Server),然后建立端口转发和连接 Agent(实测 drozer Console v3.1.0):
adb forward tcp:31415 tcp:31415
drozer console connect
然后在 drozer 控制台中执行:
dz> run app.package.manifest cn.com.x.bank

输出为完整、标准的 XML,包含全部组件声明、intent-filter、meta-data,信息完备性优于方案一。
局限:需要设备、drozer Agent 和端口转发,环境搭建成本最高;原版模块输出在控制台,只能手动复制保存;不适合批量分析。适合对少量重点样本做深度分析。
3.1 drozer 模块实现原理
模块源码位于 PC 端 drozer\modules\app\package.py(class Manifest),实现分两层:
解析层:getAndroidManifest()(定义在 drozer\modules\common\assets.py)通过反射操作设备端 Java 对象,三步拿到清单:
def getAndroidManifest(self, package):
XmlAssetReader = self.loadClass("common/XmlAssetReader.apk", "XmlAssetReader")
asset_manager = self.getAssetManager(package)
xml = asset_manager.openXmlResourceParser("AndroidManifest.xml")
xml_string = str(XmlAssetReader.read(xml))
return xml_string
createPackageContext(package).getAssets()—— 在设备端为目标应用创建 Context,取得指向它的 AssetManager;openXmlResourceParser("AndroidManifest.xml")—— 由 Android 框架自带的资源解析器(AssetManager / ResourceTypes 层,运行时读资源用的同一套代码)把二进制 AXML 解析成 XmlResourceParser。严格说干活的是它而不是安装阶段的 PackageParser,但效果相同:系统解析什么,我们就拿到什么;XmlAssetReader.read()—— drozer 打包的小 Java 助手,遍历 XmlPullParser 事件流序列化回 XML 字符串。
输出层:字符串回传 PC 后,__write_manifest 给它加缩进和 [color green] 之类的标记,console 再渲染成终端着色——原版只能复制终端内容就是这么来的。
这里有一个容易误解的关键点:模块的 execute() 是在 PC 端 console 进程里执行的(drozer\modules\base.py 的 run() 直接本地调用 self.execute(arguments)),只有 Java 相关操作通过 reflector 代理到设备端。也就是说,raw XML 字符串返回时已经在 PC 的内存里了——这为直接落盘创造了条件。
3.2 drozer module 改造
基于上面的结论,可以在 Manifest 模块中增加 -o/--output 参数实现AndroidManifest.xml配置本地写入,共两处改动:
改动一:新增 -o 参数,raw XML 直接写 PC 本地文件
def add_arguments(self, parser):
parser.add_argument("package", help="the identifier of the package")
parser.add_argument("-o", "--output", default=None, metavar="PATH",
help="write the raw manifest to a local file on the PC, e.g. C:/1.xml")
def execute(self, arguments):
if arguments.package == None or arguments.package == "":
self.stderr.write("No package provided.\n")
return
manifest = self.getAndroidManifest(arguments.package)
if arguments.output != None:
self.__save_manifest(manifest, arguments.output)
else:
self.__write_manifest(manifest) # 原行为不变
def __save_manifest(self, manifest, path):
data = manifest.encode("utf-8")
with open(path, "wb") as f:
f.write(data)
self.stdout.write("[+] manifest written to %s (%d bytes)\n" % (path, len(data)))
原理:execute() 既然跑在 PC 进程里,open() 写的就是本地路径——数据在模块内部就已经落盘,不经过控制台渲染,天然无横幅、无颜色标记。不带 -o 时行为与原版完全一致。
改动二:顺带修复上游 bug
package.py:319 有一处上游代码 if out is not "",在 Python 3.8+ 会触发 SyntaxWarning: "is not" with a literal。is not 比较的是对象身份而非值,对字符串字面量应使用 !=,已一并修正。
改进后的用法——一条命令直接得到标准 XML 文件:
drozer console connect -c "run app.package.manifest cn.com.x.bank -o m_raw.xml"
实测输出 m_raw.xml(79241 字节)即为标准 XML,可直接用编辑器打开:


两点注意:
- console 用
shlex.split解析参数,会把\当转义符吃掉,Windows 路径请写C:/1.xml或C:\\1.xml; - 模块代码每次运行由 console 下发,改动只需重启 console 生效,无需重装 Agent;但 drozer 升级会覆盖
package.py,建议把改过的Manifest类放进自己常用的 module repository。
改造后的完整模块文件见随文附件 drozer_module/package.py,直接覆盖 site-packages/drozer/modules/app/package.py 即可。
4. 方案三:AXML 字节级修复
前两种方案都是绕过文件,但如果手头只有一份损坏的 manifest(来源 APK 都不在了),或者需要离线批量处理,就得正面修文件。这就需要先理解 AXML 的格式。
4.1 AXML 格式结构
AXML 是线性的块(chunk)序列:
偏移 结构
0x00 根块 RES_XML_TYPE (type=0x0003)
├─ 老格式: headerSize=0x08 [type:2][headerSize:2][size:4]
└─ 新格式: headerSize=0x0C [type:2][headerSize:2][size:4][保留:4]
0x08* 字符串池 RES_STRING_POOL_TYPE (type=0x0001, headerSize=0x1C)
[块头 28B][字符串偏移表 4*N][可能的对齐填充][字符串数据]
随后 资源映射块 → 命名空间 → 元素 ...
* 字符串池紧跟根块头之后:老格式在 0x08,新格式在 0x0C。
老版解码工具(以 AXMLPrinter2.jar 为代表)失败的原因是新版 aapt2(AGP 7+ / Android 12+ 构建链)的两处格式变化:
- 坑① 根块头从 8 字节扩为 12 字节:多出 4 字节保留字段(通常为
00 00 00 00),老工具按 8 字节读头后整体错位,报Expected chunk of type 0x80003, read 0xc0003; - 坑② 字符串池新增对齐填充:偏移表与字符串数据之间插入若干字节 0 填充(
stringsStart > 28 + 4*字符串数),老工具不跳过填充,报Invalid chunk type之类的怪错误。
4.2 文件头诊断
| 文件头特征 | 状态 | 处理 |
|---|---|---|
03 00 08 00 |
标准老格式 AXML | 直接 java -jar AXMLPrinter2.jar 解码 |
03 00 0c 00 |
新 aapt2 格式(根块头 12 字节) | 坑①②修补后解码 |
开头正常但大量 EF BF BD |
文本编码损坏 | 不可逆,回 APK 二进制重提取 |
50 4b 03 04 (PK) |
这是 ZIP/APK 本体 | 二进制提取其中的 manifest |
3c 3f 78 6d (<?xm) |
已是文本 XML | 无需处理 |
4.3 修补坑①:根块头
import struct
d = bytearray(open('clean.xml','rb').read())
t, hs, size = struct.unpack_from('<HHI', d, 0)
if hs == 0x0C:
# 头改回 0x08,删掉 4 字节保留字段,根块 size 减 4
d = d[:2] + struct.pack('<HI', 8, size - 4) + d[12:]
4.4 修补坑②:字符串池填充
以下代码假设坑①已修补完毕(字符串池位于偏移 8);若直接处理未修坑①的新格式文件,需把各偏移整体 +4(字符串池在 0x0C):
pt, phs, psize = struct.unpack_from('<HHI', d, 8)
cnt, sty, flags, sstart, styoff = struct.unpack_from('<IIIII', d, 16)
gap = sstart - (28 + 4 * (cnt + sty)) # 填充字节数
if gap > 0:
cut = 8 + 28 + 4 * (cnt + sty)
d = d[:cut] + d[cut + gap:]
# 同步修正 4 处尺寸/偏移字段
struct.pack_into('<I', d, 12, psize - gap)
struct.pack_into('<I', d, 28, sstart - gap)
if styoff: struct.pack_into('<I', d, 32, styoff - gap)
struct.pack_into('<I', d, 4, struct.unpack_from('<I', d, 4)[0] - gap)
修补能成立的原理:字符串偏移表中的偏移都是相对 stringsStart 的,删掉填充不影响任何引用,只需修正外围 4 个字段。
4.5 损坏文件溯源
如果文件已被文本方式损坏,还可以对候选 APK 的干净 manifest 模拟同样的损坏过程,逐字节比对定位来源:
simulate = orig.decode('utf-8', errors='replace').encode('utf-8')
if simulate == corrupted_bytes: # 定位到来源 APK
4.6 一键脚本
以上流程已封装为 fix_axml.py(Python 3 标准库,无第三方依赖,配套 AXMLPrinter2.jar,见随文附件 FIX_AXML 目录,内含 README 与详细修复文档):
# 输入为 APK:提取并解码
python fix_axml.py xxx.apk
# 输入为 manifest:自动修补
python fix_axml.py AndroidManifest.xml
# 手动指定候选来源
python fix_axml.py AndroidManifest.xml a.apk b.apk
脚本包含两项安全机制:修补后先按 AXMLPrinter2 的解析逻辑模拟走查整个文件,确认字节对齐才调用 Java;调用时强制 -Dfile.encoding=UTF-8 规避 Windows GBK 控制台的中文乱码。

4.7 修复结果
使用python fix_axml.py AndroidManifest.xml修复混淆的配置文件:

修复后的AndroidManifest.xml可直接解析:

实测修复了多个加固银行类样本,还原后的清单中 Application 类为 s.h.e.l.l.S(X加密壳特征)——manifest 是壳外真实配置,可直接用于组件面分析,运行时逻辑仍需先脱壳。
局限:字符串池为 UTF-8 编码(flags & 0x100)时 AXMLPrinter2 不支持,改用 apktool / androguard / jadx;@7F110215 这类资源引用只显示原始 ID,需还原值时用 apktool / jadx;少数厂商对 manifest 做结构性破坏时字节修补失效,回退方案一/二。
4.8 jadx 插件:把修复搬进日常工具
上面反复提到”需要还原资源引用 / 处理 UTF-8 字符串池时换用 jadx”,但 jadx(实测 1.5.5)同样是严格解析器:遇到坑①的新格式 manifest,它会把二进制当文本输出乱码,或静默输出空 XML(isBinaryXml() 只认 8 字节根块头)。也就是说日常逆向中用 jadx 打开这类加固包,manifest 依然是废的——1. 节截图里那片乱码就是这么来的。
为此我把坑①修补逻辑封装成了 jadx 插件 **jadx-fix-axml**(源码与构建方法见附件仓库 jadx-fix-axml/ 目录,内含技术文档):
- 原理:通过 jadx 官方扩展点
IResContainerFactory挂入资源解码链路,仅对AndroidManifest.xml生效——检测到根块 headerSize=0x0C 时就地修补字节流(逻辑同 4.3),再交给 jadx 自带的BinaryXMLParser解码。由于最终用的是 jadx 自己的解析器,输出质量与内置解码完全一致,资源引用直接还原为@style/...、@string/...,优于 AXMLPrinter2 的裸 ID; - 零干扰:正常格式的 APK 门控直接放行,不读流、不改变任何原有行为;修补发生时日志面板输出
[fix-axml]前缀的 INFO 信息; - 局限:只修坑①(jadx 对坑②的字符串池填充天然免疫,无需处理);manifest 已被文本模式损坏(大量
EF BF BD)时字节已永久丢失,插件只告警,仍需fix_axml.py走来源 APK 重提取。
安装只需把插件 jar 放入 jadx 的 dropins 目录(GUI / CLI 通用,重启即生效):
copy /y jadx-fix-axml.jar "%APPDATA%\skylot\jadx\config\plugins\dropins\"
再次用 jadx 打开 1. 节中乱码的同一样本,manifest 已直接正常显示,日志提示插件已加载、修补工厂已注册,插件管理器中也能看到 Fix AXML Manifest:

正确性上与 fix_axml.py 做了交叉验证:对同一个约 216MB 的加固样本,插件与脚本两条链路独立解码,组件计数(activity 316 / service 51 / receiver 12 / provider 10 / meta-data 98 / uses-permission 68)完全一致。
5. 方案对比与选型
| 维度 | 方案一 aapt2 | 方案二 drozer | 方案三 字节修复 |
|---|---|---|---|
| 核心思路 | 官方工具直接读 APK | 系统运行时解析还原 | 理解格式,修补字节 |
| 输出 | 摘要 / XML 树 | 完整标准 XML | 完整 XML 文件 |
| 需要设备 | 否 | 是 | 否 |
| 需要原 APK | 是 | 是 | 不必须(可溯源) |
| 离线批量 | 一般 | 差 | 好 |
| 环境成本 | build-tools | 设备+Agent+转发 | Python3+Java |
| 上手难度 | ★ | ★★ | ★★★ |
选型建议:
- 只要包名/版本/入口/权限 → 方案一,一步到位;
- 需要完整组件树且具备设备 + drozer Agent 环境 → 方案二;
- 离线 / 批量 / 只有损坏文件 / 需要 XML 交付 → 方案三;
- 日常以 jadx 为主力逆向工具 → 直接装 4.8 的
jadx-fix-axml插件,坑①格式一次性解决,配合fix_axml.py覆盖损坏场景。
另注:apktool、androguard 等工具也内置 AXML 解码,可以离线输出文本 XML,本可作为又一候选,但它们与 jadx 一样是严格解析器,对加固混淆样本的容忍度参差,失败时即可按本文方案处理;方案三的局限场景(UTF-8 字符串池、需要还原 @7F110215 资源引用)反而要回退到它们。
最后是几条预防措施,都比修复省事:
- 从 APK 提取文件一律用二进制方式:
unzip -p、Pythonzipfile、7z x,不要用记事本打开另存,不要走文本模式传输; - 长期保存可读版本时保存解码后的文本 XML,不要保存二进制;
- 新构建的 APK 用老工具前先
xxd file | head -1看文件头,03 00 0c 00即新格式。
6. 总结
加固 APK 的 manifest 乱码是多种问题叠加:AXML 格式演进、文本提取造成的不可逆损坏、加固厂商混淆。三种方案分别从”官方工具绕过”、”系统代为解析”、”格式分析修字节”三个角度应对,覆盖从快速摸底到深度分析的链路。
核心结论还是那句话:Android 系统必须能读懂这份 manifest,所以永远存在一条还原路径。
7. 参考链接
- aapt2 官方文档
- drozer
- AOSP ResourceTypes.h(AXML chunk 结构定义)