加固APK的AndroidManifest.xml修复方法总结


加固APK的AndroidManifest.xml修复方法总结

分析加固后的 Android 应用时,解包得到的 AndroidManifest.xml 经常无法解码或显示为乱码,导致包名、组件声明等关键信息无法获取。本文以 X加密加固的银行类样本(cn.com.x.bank)为例,分析乱码的成因,整理三种修复方案的原理、操作方法和适用场景。

1. 简介

APK 内的 AndroidManifest.xml 本身就不是文本文件,而是 AXML(Android Binary XML)格式——Android 为安装时快速解析设计的二进制编码。所以直接解包后用编辑器打开看到”乱码”是正常现象,正常情况下解码工具可以还原。一般可以通过jadx工具解析APK查看AndroidManifest.xml配置文件:

jadx 正常解码未加固 APK 的 AndroidManifest.xml

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

jadx 打开加固 APK 显示乱码

这种加固混淆一般是在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 工具包发布:

build-tools 目录中的 aapt2 工具

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 badging 解析加固包输出

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

aapt2 dump xmltree 输出的 manifest 树形结构

实测包名、版本号、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

drozer 还原出的完整 manifest XML

输出为完整、标准的 XML,包含全部组件声明、intent-filter、meta-data,信息完备性优于方案一。

局限:需要设备、drozer Agent 和端口转发,环境搭建成本最高;原版模块输出在控制台,只能手动复制保存;不适合批量分析。适合对少量重点样本做深度分析。

3.1 drozer 模块实现原理

模块源码位于 PC 端 drozer\modules\app\package.pyclass 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
  1. createPackageContext(package).getAssets() —— 在设备端为目标应用创建 Context,取得指向它的 AssetManager;
  2. openXmlResourceParser("AndroidManifest.xml") —— 由 Android 框架自带的资源解析器(AssetManager / ResourceTypes 层,运行时读资源用的同一套代码)把二进制 AXML 解析成 XmlResourceParser。严格说干活的是它而不是安装阶段的 PackageParser,但效果相同:系统解析什么,我们就拿到什么;
  3. XmlAssetReader.read() —— drozer 打包的小 Java 助手,遍历 XmlPullParser 事件流序列化回 XML 字符串。

输出层:字符串回传 PC 后,__write_manifest 给它加缩进和 [color green] 之类的标记,console 再渲染成终端着色——原版只能复制终端内容就是这么来的。

这里有一个容易误解的关键点:模块的 execute() 是在 PC 端 console 进程里执行的drozer\modules\base.pyrun() 直接本地调用 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 literalis not 比较的是对象身份而非值,对字符串字面量应使用 !=,已一并修正。

改进后的用法——一条命令直接得到标准 XML 文件:

drozer console connect -c "run app.package.manifest cn.com.x.bank -o m_raw.xml"

实测输出 m_raw.xml(79241 字节)即为标准 XML,可直接用编辑器打开:

改造后的 drozer 模块将 manifest 落盘为 m_raw.xml

编辑器打开 m_raw.xml 为标准明文 XML

两点注意:

  1. console 用 shlex.split 解析参数,会把 \ 当转义符吃掉,Windows 路径请写 C:/1.xmlC:\\1.xml
  2. 模块代码每次运行由 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 控制台的中文乱码。

fix_axml.py 直接输入 APK 一键提取并修复

4.7 修复结果

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

fix_axml.py 修复混淆的 AndroidManifest.xml

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

修复后的 AndroidManifest_recovered.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

jadx-fix-axml 插件效果

正确性上与 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 资源引用)反而要回退到它们。

最后是几条预防措施,都比修复省事:

  1. 从 APK 提取文件一律用二进制方式:unzip -p、Python zipfile7z x,不要用记事本打开另存,不要走文本模式传输;
  2. 长期保存可读版本时保存解码后的文本 XML,不要保存二进制;
  3. 新构建的 APK 用老工具前先 xxd file | head -1 看文件头,03 00 0c 00 即新格式。

6. 总结

加固 APK 的 manifest 乱码是多种问题叠加:AXML 格式演进、文本提取造成的不可逆损坏、加固厂商混淆。三种方案分别从”官方工具绕过”、”系统代为解析”、”格式分析修字节”三个角度应对,覆盖从快速摸底到深度分析的链路。

核心结论还是那句话:Android 系统必须能读懂这份 manifest,所以永远存在一条还原路径。

7. 参考链接

  1. aapt2 官方文档
  2. drozer
  3. AOSP ResourceTypes.h(AXML chunk 结构定义)

8. 源码和附件

yanghaoi/hardened-apk-manifest-fix


文章作者: YangHao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YangHao !
评论
 本篇
加固APK的AndroidManifest.xml修复方法总结 加固APK的AndroidManifest.xml修复方法总结
加固APK的AndroidManifest.xml修复方法总结分析加固后的 Android 应用时,解包得到的 AndroidManifest.xml 经常无法解码或显示为乱码,导致包名、组件声明等关键信息无法获取。本文以 X加密加固的银行
2026-08-31
下一篇 
PHP filters和CVE-2024-2961组合:从文件读取到RCE PHP filters和CVE-2024-2961组合:从文件读取到RCE
在PHP中可能通过CVE-2024-2961构造php://filter的数据使PHP在转换ISO-2022-CN-EXT字符集时触发漏洞将文件读取原语提升到远程代码执行。
2024-12-04
  目录