MT 管理器「去除签名校验」原理分析

前言

MT 管理器的「去除签名校验」功能允许用户在修改 APK 后无需原始签名即可重新安装。本文通过构造一个极简应用来分析该功能的实现原理,并探讨相应的对抗方案。

有趣的是,反编译后的代码中发现了 KillerApplication80 类中硬编码的 URL:https://github.com/L-JINBIN/ApkSignatureKillerEx。进一步调查发现,MT 的该功能实际上是基于开源项目 ApkSignatureKillerEx 实现——该项目 README 明确说明「演示了 MT 去除签名校验功能的原理」。

实验设计

测试应用

为排除干扰,设计一个极简应用:

  • 单一 Activity,仅校验自身签名
  • 无第三方依赖
  • 使用自定义密钥签名

实验流程

  1. 构建原始 APK,记录签名信息
  2. 使用 MT 管理器「去除签名校验」功能处理
  3. MT 重新签名后导出
  4. 对比处理前后的 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;

// 替换为自定义 Creator,在 createFromParcel 中篡改签名
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; // 替换签名
}
// Android 9+ 还要替换 signingInfo
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;
}
// ...
};

// 反射替换系统的 CREATOR
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
// 使用 xhook 对 open 系列调用进行 PLT Hook
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);
}

// Hook 实现:当打开 APK 文件时,重定向到原始 APK 副本
static int openImpl(const char *pathname, int flags, mode_t mode) {
if (strcmp(pathname, apkPath__) == 0){
return old_open(repPath__, flags, mode); // 替换为原始 APK 路径
}
return old_open(pathname, flags, mode);
}

实现原理

总结一下实现原理,该技术采用代理 Application + 双层 Hook 的方案:

Java 层流程

  1. 修改 AndroidManifest.xml,将 Application 指向注入的 KillerApplication
  2. 在静态代码块中调用 killPM(),替换 PackageInfo.CREATOR 为自定义 Creator
  3. 当应用调用 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
// 定位 ZIP Central Directory
long centralDirOffset = findCentralDirectoryOffset(raf);

// 读取 APK Signing Block(v2/v3 签名)
raf.seek(centralDirOffset - 24);
// ... 解析 magic、size、ID-value pairs

// 提取证书并计算 SHA-256 fingerprint
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
// 原始路径: /data/app/xyz.mufanc.dbg-1/base.apk
// 改造后: /./data/./app/./xyz.mufanc.dbg-1/./base.apk
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 的防护有其盲区,而我们的检测手段也有进一步扩展的空间。

对于需要签名校验保护的应用,建议采用多种检测手段组合,并保持对新型攻击方式的关注。

参考与延伸


MT 管理器「去除签名校验」原理分析
https://neo.mufanc.xyz/posts/11627/
作者
Mufanc
发布于
2026年4月10日
许可协议