一、 问题链条与现象还原
在 Android 应用或游戏二次打包(破解、修改、换皮)的过程中,破解者为了绕过应用内部的本地签名校验机制,往往利用逆向工具(如 MT 管理器、NP 管理器、ARMPro 等)提供的 “去除签名校验” 功能进行自动化处理。
由此产生的问题链条与假象如下:
[二次打包修改 APK]
│
▼
物理真实签名变更为破解者证书 (如 testkey / 弱密钥 d09e6e... / e89b15...)
│
▼
应用启动阶段注入去签 Hook 逻辑 (Java / Native 双层拦截)
│
▼
┌────────────────────────────────────────────────────────┐
│ 欺骗方式 A: 动态代理拦截 ActivityThread.sPackageManager │
│ 欺骗方式 B: 反射替换 PackageInfo.CREATOR 序列化构造器 │
│ 欺骗方式 C: xhook 拦截 libc open() 将物理 APK 偷换为原版 │
└────────────────────────────────────────────────────────┘
│
▼
SDK / App 获取签名,返回被冻结硬编码的原版合法签名 (86d195...)
│
▼
App 内计算的 MD5 / SHA1 依然显示为原版合法值
(表象:换包换签“没生效”;实质:应用处于深度欺骗环境下,传统校验全线阵亡)
二、 欺骗原理深度剖析(从初级到高级)
通过对大量实战篡改样本(如 2026-8-26.apk、InsideTest_kill.apk)反编译分析,我们从注入的 classes.dex 和 .so 库中提取出其底层开源核心:https://github.com/L-JINBIN/ApkSignatureKillerEx(即 MT 管理器“去除签名校验 2.0 ~ 4.0”的底层技术)。
攻击者主要通过以下三代手段逐步升级欺骗:
1. 初级:PMS 动态代理拦截(killPM 基础版)
- 原理:在
Application.attachBaseContext阶段利用 Java 反射,将ActivityThread中的sPackageManager替换成 Java 动态代理对象(Proxy)。 - 表现:当应用调用
context.getPackageManager().getPackageInfo(..., GET_SIGNATURES)时,代理对象直接阻断系统 IPC 远程调用,强行返回预置的原版假签名。
2. 中级:PackageInfo.CREATOR 序列化劫持(killPM 增强版)
-
原理:为了避开对
sPackageManager的代理检测,攻击工具直接通过反射偷换了 Android 框架底层的PackageInfo.CREATOR:// 伪代码:直接篡改系统 Parcelable 反序列化构造器 findField(PackageInfo.class, "CREATOR").set(null, new Parcelable.Creator<PackageInfo>() { @Override public PackageInfo createFromParcel(Parcel parcel) { PackageInfo info = originalCreator.createFromParcel(parcel); if (info.packageName.equals(targetPkg)) { info.signatures[0] = fakeSignature; // 强行换回原版证书 } return info; } }); -
表现:系统底层 IPC 调用依然正常执行,但在应用层将 Parcel 字节流反序列化为 Java 对象的那一瞬间,证书被匿名内部类(如
bin.mt.signature.KillerApplication$1)恶意篡改。
3. 高级:Native 层 IO 重定向(killOpen 终极版)
-
原理:针对很多开发者尝试“绕开 PackageManager、直接读取物理 APK(
base.apk)文件提取证书”的做法,攻击工具实现了物理层偷梁换柱:- 工具在 APK 的
assets/SignatureKiller/origin.apk内藏匿一个原版未修改的完整 APK。 - 应用启动时,自动将原版 APK 释放到沙箱私有目录:
/data/data/<包名>/origin.apk。 - 加载
libSignatureKiller.so,利用xhook框架 Hook 了底层 C 运行时库(libc)的文件打开函数:open()、open64()、openat()、fopen()。 - 当 Java 层(如
JarFile、ZipFile、RandomAccessFile)试图打开真实物理路径/data/app/.../base.apk时,被 Native 钩子强行将路径重定向到沙箱里的origin.apk。
- 工具在 APK 的
-
表现:开发者自以为穿透到了物理文件,底层读取的其实是攻击者预留在沙箱里的原版假包。
三、 为什么微信、QQ 等第三方登录能天然免疫?
许多开发者常问:“为什么微信/QQ 登录能准确识别篡改并拦截,而游戏自己的本地代码却会被骗?”
| 场景 | 谁在执行签名校验? | 运行在哪个进程? | 破解者的 Hook 能否影响它? | 结果 |
|---|---|---|---|---|
| 应用/游戏本地校验 | App 自身 SDK 代码 | 应用自身进程 | 能(因为破解代码就在同一个进程内存里) | 签名被劫持,返回假签名 |
| 微信 / QQ 登录 | 微信 App / QQ App | 微信 / QQ 独立进程 | 绝对不能(Linux 跨进程沙箱内存隔离) | 微信拿到真实签名,直接拦截 |
-
底层鉴权链路:
- 游戏拉起微信登录时,通过 Binder 跨进程向微信发起请求。
- Linux 内核机制向微信提供不可伪造的调用方真实身份(
Binder.getCallingUid())。 - 微信在其干净、未被 Hook 的独立进程中向系统 PMS 查询该 UID 的签名。
- 微信拿到的永远是系统底层真实登记的安装包证书(testkey / 破解者证书),与开放平台后台登记的合法证书比对不符,立即弹窗拦截。
四、 降维对抗与破局方案(ApkSignUtil 生产级实战)
针对 ApkSignatureKillerEx 和 MT 管理器的攻击矩阵,我们在 SDK 中构建了多维穿透与反制闭环:
┌── 针对 killPM (代理 Hook) ────► 1. 反射检测 Proxy.isProxyClass
│
对抗防御矩阵 ────┼── 针对 killPM (CREATOR 劫持) ─► 2. 校验 PackageInfo.CREATOR 类名包名
│
├── 针对 killOpen (libc 重定向) ─► 3. 路径等价变形绕过 strcmp + 沙箱假包侦测
│
└── 针对 V1/V2/V3 多种签名模式 ──► 4. 纯 Java 物理签名块二进制穿透解析
1. 穿透 libc IO 重定向:利用 strcmp 破绽进行【路径等价变形】
-
破绽发现:逆向分析
libSignatureKiller.so的二进制符号表,发现其重定向比对函数使用的是标准的strcmp(精确字符串比对) 。它监控的目标路径是绝对路径/data/app/.../base.apk。 -
反制破局:在 Java 读取物理文件时,对路径实施等价规范化变形(在最后一个文件名之前插入
/./):// 原始路径: /data/app/~~xxx/com.pkg-xxx/base.apk // 变形路径: /data/app/~~xxx/com.pkg-xxx/./base.apk int lastSlash = apkPath.lastIndexOf('/'); String bypassPath = apkPath.substring(0, lastSlash) + "/./" + apkPath.substring(lastSlash + 1); File apkFile = new File(bypassPath); -
效果:
- 对
libSignatureKiller.so:strcmp("/..././base.apk", "/.../base.apk") != 0,字符串比对不匹配,Hook 钩子直接落空,重定向失效! - 对 Linux 底层内核:
/./指向的完全是同一个文件物理节点(inode),直接打开了真正的被篡改物理包,成功提取真实物理证书!
- 对
2. 双重特征识别:识破 CREATOR 劫持与沙箱假包
在 isPackageManagerHooked(Context) 中建立多重防线:
-
CREATOR 命名空间检测:
// 原生系统类必为 android.content.pm.PackageInfo$1 // 若被篡改,类名会变成 bin.mt.signature.KillerApplication$1 等外部包名 if (PackageInfo.CREATOR != null && !PackageInfo.CREATOR.getClass().getName().startsWith("android.content.pm.")) { return true; // 100% 抓获 CREATOR 劫持 } -
沙箱伪造假包检测:
// 正常 App 绝不会在私有目录下存放 origin.apk File originApk = new File(context.getApplicationInfo().dataDir, "origin.apk"); if (originApk.exists()) { return true; // 100% 抓获 killOpen 释放的原版假包 } -
动态代理与特征常量检测:
- 检测
ActivityThread.sPackageManager及pm.mPM是否为Proxy.isProxyClass。 - 检测
System.getProperty("mt.signature.killer.path1")等特有属性。
- 检测
3. 物理签名全版本兼容提取(V1 + V2/V3 Signing Block)
不调用任何受 Android 限制的私有类(如 ApkSignatureSchemeV2Verifier),纯通过标准 Java IO 流式解析:
-
优先 V1 提取:流式读取
AndroidManifest.xml触发JarVerifier填充公钥。 -
兜底 V2/V3 物理块解析:
- 通过尾部
EOCD定位Central Directory Offset。 - 向前读取 16 字节 Magic
APK Sig Block 42。 - 按照 ID
0x7109871a(V2) 与0xf05368c0(V3) 定位 signer 序列,直接提取证书公钥 DER 字节并计算 MD5。 - 兼容所有 Android 版本(Android 4.4 ~ Android 15)。
- 通过尾部
五、 实战样本验证对照表
以实际被二次打包去签名的样本(InsideTest_kill.apk)为例,实测效果如下:
| 校验维度 | 系统原生 API (被欺骗) | 本方案 ApkSignUtil (穿透对抗) |
|---|---|---|
| 证书 Subject | CN=rn, OU=rn, O=rn, C=CN (原版伪造) | E=android@android.com, CN=Android, C=US (真实testkey) |
| 证书 MD5 | 447...... (假) | e89.... (真实重签MD5) |
| 证书 SHA1 | 97:bf:f6:2a:d3:04:66... (假) | 61:ed:37:7e:85:d3:86... (真实重签SHA1) |
| Hook 状态识别 | 认为环境正常 (false) | 精准报警 (isHooked = true) |
| CREATOR 识别 | 未知 | 识别为 bin.mt.signature.KillerApplication$1 |
六、 业务落地与防御协同体系
| 层级 | 对抗手段 | 业务建议行为 | 收益与破解代价 |
|---|---|---|---|
| 客户端 P0 | 物理路径变形直读 + 多维 Hook 识别 | 接入 ApkSignUtil 替代原有 PackageManager 签名读取方法 | 彻底破解现存的 killPM 与 killOpen,直接取得真实重打包签名。 |
| 风控层 P1 | 静默风控上报 | 将 real_md5 与 is_tampered 随 SDK 初始化/登录埋点上报 | 不影响普通玩家体验,后台一眼掌握篡改包分发量,便于黑产排查与封号。 |
| 强管控 P2 | 本地直接阻断 / 降级 | isPackageManagerHooked() == true 时弹窗阻断或限制付费功能 | 防止单机游戏内购被一键破解或资源被盗用。 |
| 终极防线 P3 | 服务端登录白名单校验 | 登录时携带真实签名 Hash,由服务端私钥验签比对 | 形成类似微信/QQ登录的安全闭环,客户端无论如何单点修改均无法绕过。 |