前言
MT 管理器的「去除签名校验」功能允许用户在修改 APK 后无需原始签名即可重新安装。本文通过构造一个极简应用来分析该功能的实现原理,并探讨相应的对抗方案。
有趣的是,反编译后的代码中发现了 KillerApplication80 类中硬编码的 URL:https://github.com/L-JINBIN/ApkSignatureKillerEx。进一步调查发现,MT 的该功能实际上是基于开源项目 ApkSignatureKillerEx 实现——该项目 README 明确说明「演示了 MT 去除签名校验功能的原理」。
实验设计
测试应用
为排除干扰,设计一个极简应用:
- 单一 Activity,仅校验自身签名
- 无第三方依赖
- 使用自定义密钥签名
实验流程
- 构建原始 APK,记录签名信息
- 使用 MT 管理器「去除签名校验」功能处理
- MT 重新签名后导出
- 对比处理前后的 APK 差异
APK 差异分析
签名变化
MT 处理后的 APK 使用了一个公钥证书重新签名。对比原签名和新签名:
- 原签名:使用自定义测试密钥(
CN=Test),采用 SHA384withRSA 算法
- 新签名:使用 AOSP 默认调试密钥(
CN=Android, O=Android),采用 SHA1withRSA 算法,有效期始于 2008 年
从签名信息可以确认,APK 的签名块确实发生了改变——这意味着 MT 并非突破了系统的签名校验机制,而是通过某种手段让应用自身检测不到签名已发生变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 类型: X.509 版本: 3 序列号: 0x7590441d55c52a32 主题: CN=SignTest, OU=Dev, O=Mufanc, L=Beijing, ST=Beijing, C=CN 有效期始: Thu Apr 16 22:11:10 CST 2026 有效期至: Mon Sep 01 22:11:10 CST 2053 公钥类型: RSA 指数: 65537 模数大小(位): 2048 模数: 22412618850145490529823490816873913033398295940467386241054193047726765996987676931747077672115080877661410000132885905157479301074589978705579690225710717166183480116581937848702455277767962013509333763317217482100626383284798160490120519864703322574840226498545144004731298769137369572517114382406314052189148016609917631393228554877332795218553328420507789254870558653164924780705015189201647778172909591769160131839515170500490958800519615556204527815016381242784793964061059562494789267255263412943203017804865237034044036400646262211562138569951979265198538393585669849903803987509391928589396971207315767001927 签名算法: SHA384withRSA 签名 OID: 1.2.840.113549.1.1.12 MD5 签名: F8 4B 40 E7 DF 19 28 14 A4 80 06 A8 66 A0 39 D2 SHA-1 签名: 3B C8 B6 7B E5 A6 DB 17 2A 97 3E 76 AC E6 48 8A 39 F0 FE 63 SHA-256 签名: B9 53 81 6D A0 6D 9B 1C 6B ED 14 9A 5F B6 2A D6 1A CA 01 FC D2 60 87 01 D4 ED FF 4D 54 D8 D9 70
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 类型: X.509 版本: 3 序列号: 0x936eacbe07f201df 主题: EMAILADDRESS=android@android.com, CN=Android, OU=Android, O=Android, L=Mountain View, ST=California, C=US 有效期始: Fri Feb 29 09:33:46 CST 2008 有效期至: Tue Jul 17 09:33:46 CST 2035 公钥类型: RSA 指数: 3 模数大小(位): 2048 模数: 27087533857153302906822427244451835680271467139433638657402420676788772368468316411790577780743478815329574319010356420647651577255214076320764054962227698091591190998224183931185609609820277016242603583619929549819986490809257050240250723681109660718403959925449702875642189909904608631689243630431349528603016850515510838951987672075344238987930639179476225895129710043944157373677589593772202003591689051650854123572660036810919613063456337914746959297660631038090097224838665758049737111657080826771808365050815496720770905152230613652255807956565630323299366925404317303221604342657788982549334320910974026967327 签名算法: SHA1withRSA 签名 OID: 1.2.840.113549.1.1.5 MD5 签名: E8 9B 15 8E 4B CF 98 8E BD 09 EB 83 F5 37 8E 87 SHA-1 签名: 61 ED 37 7E 85 D3 86 A8 DF EE 6B 86 4B D8 5B 0B FA A5 AF 81 SHA-256 签名: A4 0D A8 0A 59 D1 70 CA A9 50 CF 15 C1 8C 45 4D 47 A3 9B 26 98 9D 8B 64 0E CD 74 5B A7 1B F5 DC
|
文件结构变化
对比原始 APK 和处理后的 APK,主要变化如下:
| 项目 |
原始 APK |
处理后 APK |
AndroidManifest.xml |
无 Application 类 |
替换入口为 bin.mt.signature.KillerApplication80 |
assets/ |
不存在 |
新增 origin80.apk(原始 APK 副本) |
lib/ |
不存在 |
新增 libSignatureKiller80.so(多架构) |
| DEX |
仅包含应用代码 |
新增 KillerApplication80 及相关类 |
DEX 变化
观察注入的 KillerApplication80 类,注意到一个硬编码的字符串:
1 2 3 4
| public class KillerApplication extends Application { public static final String URL = "https://github.com/L-JINBIN/ApkSignatureKillerEx"; }
|
顺着这个链接找到项目,发现 MT 的「去除签名校验」功能正是基于该项目实现。对比源码后,其核心逻辑分为两层:
Java 层(KillerApplication.killPM):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| private static void killPM(String packageName, String signatureData) { Signature fakeSignature = new Signature(Base64.decode(signatureData, Base64.DEFAULT)); Parcelable.Creator<PackageInfo> originalCreator = PackageInfo.CREATOR; Parcelable.Creator<PackageInfo> creator = new Parcelable.Creator<PackageInfo>() { @Override public PackageInfo createFromParcel(Parcel source) { PackageInfo packageInfo = originalCreator.createFromParcel(source); if (packageInfo.packageName.equals(packageName)) { if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { packageInfo.signatures[0] = fakeSignature; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (packageInfo.signingInfo != null) { Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); if (signaturesArray != null && signaturesArray.length > 0) { signaturesArray[0] = fakeSignature; } } } } return packageInfo; } }; findField(PackageInfo.class, "CREATOR").set(null, creator); }
|
Native 层(mt_jni.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| JNIEXPORT void JNICALL Java_bin_mt_signature_KillerApplication_hookApkPath(JNIEnv *env, jclass clazz, jstring apkPath, jstring repPath) { apkPath__ = (*env)->GetStringUTFChars(env, apkPath, 0); repPath__ = (*env)->GetStringUTFChars(env, repPath, 0);
xhook_register(".*\\.so$", "openat64", openat64Impl, (void **) &old_openat64); xhook_register(".*\\.so$", "openat", openatImpl, (void **) &old_openat); xhook_register(".*\\.so$", "open64", open64Impl, (void **) &old_open64); xhook_register(".*\\.so$", "open", openImpl, (void **) &old_open);
xhook_refresh(0); }
static int openImpl(const char *pathname, int flags, mode_t mode) { if (strcmp(pathname, apkPath__) == 0){ return old_open(repPath__, flags, mode); } return old_open(pathname, flags, mode); }
|
实现原理
总结一下实现原理,该技术采用代理 Application + 双层 Hook 的方案:
Java 层流程:
- 修改
AndroidManifest.xml,将 Application 指向注入的 KillerApplication
- 在静态代码块中调用
killPM(),替换 PackageInfo.CREATOR 为自定义 Creator
- 当应用调用
getPackageSignatures() 时,系统通过 Parcel 反序列化得到 PackageInfo,自定义 Creator 将签名数组替换为硬编码的原始值
Native 层流程:
4. 调用 killOpen(),从 assets/SignatureKiller/origin.apk 提取原始 APK 到应用数据目录
5. 加载 libSignatureKiller.so,Hook open 系列系统调用
6. 当 Native 代码尝试打开 APK 文件进行签名校验时,重定向到提取的原始 APK 路径
对抗方案
为了验证上述分析,我们在测试应用中实现了两种签名校验方案,观察 MT 的处理效果。
方案一:系统 API 验签
最常规的做法,通过 PackageManager 获取签名:
1 2 3 4
| PackageManager pm = getPackageManager(); var packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); SigningInfo signingInfo = packageInfo.signingInfo; var signatures = signingInfo.getApkContentsSigners();
|
结果:被 MT 轻松绕过。因为 PackageInfo 是通过 Parcel 反序列化得到的,而 MT 已经替换了 PackageInfo.CREATOR,返回的签名早已被篡改。
方案二:自验签(绕过 PackageManager)
不依赖系统 API,直接读取 APK 文件解析 Signing Block:
1 2 3 4 5 6 7 8 9 10 11
| long centralDirOffset = findCentralDirectoryOffset(raf);
raf.seek(centralDirOffset - 24);
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(...); MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(cert.getEncoded());
|
结果:依然被绕过。MT 的 Native Hook 拦截了 open 系统调用,当应用尝试打开 APK 文件时,实际打开的是 assets/origin.apk(原始 APK 副本)。
方案三:路径规范化绕过
既然 MT 在 Native 层拦截了文件访问,我们尝试在路径上做文章。观察 MT 的 Hook 实现:
1 2 3 4 5 6
| static int openImpl(const char *pathname, int flags, mode_t mode) { if (strcmp(pathname, apkPath__) == 0) { return old_open(repPath__, flags, mode); } return old_open(pathname, flags, mode); }
|
MT 使用的是简单的字符串比较。既然它只认精确的字符串匹配,那我们可以对路径做点变形——在路径中插入 /./,/data/app/xxx/base.apk 和 /./data/./app/./xxx/./base.apk 对内核来说是同一个文件,但对 MT 的 Hook 来说是两个不同的字符串。
1 2 3 4 5
|
SignatureResult result = verifySignatureFromApkFile( apkPath.replace("/", "/./") );
|
结果:成功绕过。MT 的 Hook 没有识别这个「等价但不同」的路径,校验逻辑直接读取了真实的 APK 文件,检测到了签名异常。
这种绕过方式的优雅之处在于:没有对抗,没有检测,只是利用了一个简单的路径特性,让 MT 的拦截规则形同虚设。
更多检测思路
路径变形只是众多检测手段中的一种。实际上,Android 系统在加载 APK 时已经为我们打开了文件描述符,通过 /proc/self/fd 可以直接访问真实的 APK 文件,完全绕过 MT 的 Hook:
1 2 3
| OP5D0DL1:/ # ls -l /proc/$(pidof xyz.mufanc.dbg)/fd | grep base.apk lr-x------ 1 root root 64 2026-04-18 00:03 100 -> /data/app/~~ANyiD9tBDcnIwA--d6XgPg==/xyz.mufanc.dbg-oO4sPPmnuHx2GZNrneOKrA==/base.apk lr-x------ 1 root root 64 2026-04-18 00:03 95 -> /data/app/~~ANyiD9tBDcnIwA--d6XgPg==/xyz.mufanc.dbg-oO4sPPmnuHx2GZNrneOKrA==/base.apk
|
除此之外,还可以:
- 检测 PLT/GOT 表是否被篡改
- 识别 xhook 等 Hook 框架的特征
- 校验关键系统函数的代码段完整性
- 甚至 直接检测注入行为本身
MT 的「去除签名校验」并非不可战胜,关键在于找到它防护的盲区。
结语
MT 管理器的「去除签名校验」功能基于 ApkSignatureKillerEx 项目,通过代理 Application + 双层 Hook 实现:Java 层篡改 PackageInfo 的签名数据,Native 层重定向 APK 文件读取路径。
这种方案看似周全,但防御方并非无计可施。从路径变形绕过 Native Hook,到利用系统 fd 直接读取真实 APK,再到检测注入行为本身——攻防博弈中,没有绝对的安全,也没有绝对的破解。MT 的防护有其盲区,而我们的检测手段也有进一步扩展的空间。
对于需要签名校验保护的应用,建议采用多种检测手段组合,并保持对新型攻击方式的关注。
参考与延伸