目录
  1. 1. 一、攻防双方的战场
    1. 1.1. 1.1 APK 文件结构速览
  2. 2. 二、常见破解手段
    1. 2.1. 2.1 APK 反编译与 smali 代码修改
    2. 2.2. 2.2 使用 JADX 直接反编译为 Java
    3. 2.3. 2.3 License 校验绕过
    4. 2.4. 2.4 应用内购买(IAP)绕过
    5. 2.5. 2.5 广告移除
    6. 2.6. 2.6 二次打包(Repackaging)
  3. 3. 三、防破解技术体系
    1. 3.1. 3.1 代码混淆
    2. 3.2. 3.2 控制流混淆技术详解
    3. 3.3. 3.3 完整性检测
    4. 3.4. 3.4 反调试技术
    5. 3.5. 3.5 Root / Magisk 检测
  4. 4. 四、高级对抗与加固方案
    1. 4.1. 4.1 动态加载与代码分离
    2. 4.2. 4.2 第三方加固服务
    3. 4.3. 4.3 服务端验证
    4. 4.4. 4.4 代码虚拟化(VMP)
  5. 5. 五、AOSP 安全相关源码路径
  6. 6. 六、常见面试题
【深入理解JVM字节码】第十三篇、应用破解Vs防破解

一、攻防双方的战场

Android 应用安全领域有一句经典名言:”只要代码运行在用户的设备上,它就不再安全。” APK 文件本质上是一个 ZIP 压缩包,包含 DEX 字节码、资源文件和原生库。攻击者可以轻易提取、反编译、修改并重新打包你的应用。这场攻防战从 Android 诞生之初就一直在进行,至今仍在演进。

本章从攻击者和防御者两个视角,系统性地拆解 APK 破解的常见手段以及对应的防护策略。理解攻击手法,是设计有效防御的前提。

1.1 APK 文件结构速览

在深入破解技术之前,先快速回顾 APK 的物理结构:

APK (ZIP格式)
├── META-INF/
│ ├── MANIFEST.MF # 每个文件的 SHA-256 摘要
│ ├── CERT.SF # MANIFEST.MF 的签名
│ └── CERT.RSA/EC # 开发者证书和签名字段
├── AndroidManifest.xml # 二进制 XML(AXML 格式)
├── classes.dex # 主 DEX 文件(Dalvik 字节码)
├── classes2.dex ... # 多 DEX(MultiDex)
├── resources.arsc # 编译后的资源表
├── res/ # 资源文件(二进制 XML + 图片)
├── lib/armeabi-v7a/ # 原生库(.so)
└── assets/ # 原始资源文件

逆向分析的对应工具链:

APK 组件 逆向工具 输出格式
AndroidManifest.xml AXMLPrinter2, apktool, Androguard 可读 XML
classes.dex baksmali, jadx, dex2jar + JD-GUI smali / Java
resources.arsc apktool, aapt2 可读资源表
.so 文件 IDA Pro, Ghidra, radare2 汇编 / 伪C
签名信息 keytool, apksigner 证书信息

二、常见破解手段

2.1 APK 反编译与 smali 代码修改

这是最基础也最常见的攻击方式。攻击流程如下:

第一步:解包

# 使用 apktool 解包
apktool d target.apk -o output_dir/

# 输出目录结构:
# output_dir/
# ├── AndroidManifest.xml (可读文本)
# ├── smali/ (反编译的 smali 代码)
# ├── res/ (解码后的资源)
# └── ...

第二步:修改 smali 代码

攻击者可以在 smali 代码中找到关键逻辑并修改。例如,一个简单的 License 校验函数:

// 原始 Java 代码
public boolean isLicensed() {
return LicenseManager.check(getPackageName());
}

对应的 smali 代码:

.method public isLicensed()Z
.locals 1
invoke-virtual {p0}, Lcom/example/App;->getPackageName()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lcom/example/LicenseManager;->check(Ljava/lang/String;)Z
move-result v0
return v0
.end method

攻击者只需修改一行:

# 原始:invoke-static {v0}, Lcom/example/LicenseManager;->check(Ljava/lang/String;)Z→v0
# 修改为:
const/4 v0, 0x1 # 直接返回 true
return v0

第三步:重新打包并签名

# 重新打包
apktool b output_dir/ -o modified.apk

# 生成调试签名
keytool -genkey -v -keystore debug.keystore -alias debug -keyalg RSA \
-keysize 2048 -validity 10000
jarsigner -verbose -keystore debug.keystore modified.apk debug

2.2 使用 JADX 直接反编译为 Java

JADX 是目前最流行的 DEX-to-Java 反编译器,它直接将 DEX 字节码反编译为可读性极高的 Java 代码:

jadx target.apk -d decompiled/

JADX 的反编译质量远超 dex2jar+JD-GUI 组合,能够:

  • 还原大部分控制流结构(if/for/while)
  • 还原泛型参数
  • 识别常见库的特征(Retrofit、OkHttp、Glide 等)
  • 支持多 DEX 文件

2.3 License 校验绕过

很多付费应用采用本地 License 校验。常见的校验点:

方式一:PackageManager 校验

PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;
// 判断签名是否匹配开发者的证书

攻击者的绕过方式:在 smali 中找到 checkSignatures() 方法,将其返回值改为 true,或直接 Hook PackageManager.getPackageInfo() 方法返回伪造的 PackageInfo。

方式二:Google Play Licensing (LVL)

// LVL 通过与 Google Play 服务通信验证 License
mLicenseChecker.checkAccess(new LicenseCheckerCallback() {
@Override
public void allow() { /* 授权通过 */ }
@Override
public void dontAllow() { /* 授权拒绝 */ }
});

攻击方式:

  1. 修改 smali,让 checkAccess 的回调始终进入 allow() 分支
  2. 使用 Lucky Patcher 自动 patch LVL 和 Google Billing
  3. 通过 Xposed/Frida Hook LicenseChecker.checkAccess() 方法

2.4 应用内购买(IAP)绕过

Google Play Billing 是应用内购买的主要实现方式。攻击者通过:

  1. Lucky Patcher:自动 patch Google Billing 的响应,将购买状态修改为成功
  2. Freedom:拦截 Billing Service 通信,伪造购买凭证
  3. 修改 smali:找到购买回调 onPurchasesUpdated(),将 BillingResult.responseCode 修改为 OK
# 原始代码检查 responseCode == OK
if-ne v0, v1, :cond_fail
# 攻击者修改:删除条件跳转,直接进入成功分支
# if-ne v0, v1, :cond_fail <-- 注释掉或删除

更高阶的防护:购买状态不应仅依赖客户端的 BillingResult。应该在服务端验证 Google Play 的 Purchase 对象(通过 purchase.getPurchaseToken() 调用 Google Play Developer API 验证购买真实性)。

2.5 广告移除

免费应用通常通过展示广告盈利。攻击者移除广告的手段:

  1. 删除 AndroidManifest.xml 中的 AdActivity 声明
  2. 修改布局文件,将广告 View 的 visibility 改为 GONE
  3. 在 smali 中删除 loadAd() 调用
  4. 使用 AdAway 在 hosts 文件中屏蔽广告服务器域名

2.6 二次打包(Repackaging)

攻击者反编译应用后,注入恶意代码(如广告 SDK、信息窃取模块),然后重新打包并签名,通过第三方应用市场分发。这是渠道盗版的主要形式。

三、防破解技术体系

3.1 代码混淆

ProGuard / R8

ProGuard 是 Android 默认的混淆器,R8 是 Google 从 Android Gradle Plugin 3.4.0 开始引入的全新代码缩减和混淆器。R8 相比 ProGuard 的优势:

  • 同时处理 shrinking(移除无用代码)、optimization(优化字节码)和 obfuscation(混淆命名)
  • 与 D8 集成更紧密,减少构建时间
  • 支持更激进的优化(如方法内联、类合并)
# 基础混淆配置 (proguard-rules.pro)
-keepattributes Signature # 保留泛型签名
-keepattributes *Annotation* # 保留注解
-keepattributes SourceFile,LineNumberTable # 保留调试信息

# 保留关键入口(否则会被移除)
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver

# 字符串加密(需要第三方方案如 DexGuard)
# ProGuard/R8 本身不提供字符串加密

DexGuard(商业产品)

DexGuard 是 GuardSquare 的商业混淆和保护产品,提供比 ProGuard/R8 更高级的保护:

  • 字符串加密:将字符串常量加密,运行时解密
  • 类加密:加密整个类,运行时动态解密
  • 控制流混淆:插入虚假分支、将条件分支转换为间接跳转
  • 资源加密:加密 assets 和 raw 资源
  • Native 代码保护:调用混淆(将 Java 方法通过 JNI 调用 native 实现)
  • 完整性校验:检测 APK 是否被篡改

3.2 控制流混淆技术详解

控制混淆的目的是让反编译后的代码难以理解。常见技术:

(1)不透明谓词(Opaque Predicate)

插入一个结果永远是 true 或永远是 false 的条件判断,但反编译器无法推断:

// 原始代码
int result = a + b;

// 混淆后
int x = 15;
int y = 7;
int z = x * x - 2 * x * y + y * y; // 实际上 z = (x-y)^2 = 64
int result = (z == 64) ? (a + b) : -1; // 永远走 true 分支

(2)控制流平坦化(Control Flow Flattening)

将所有基本块放在同一个 switch-case 中,通过一个状态变量控制执行顺序:

// 原始代码
public int compute(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
if (i % 2 == 0) {
sum += i;
} else {
sum -= i;
}
}
return sum;
}

// 平坦化后(伪代码)
public int compute(int n) {
int state = 0;
int sum = 0, i = 0;
while (true) {
switch (state) {
case 0: sum = 0; i = 0; state = 1; break;
case 1: if (i < n) state = 2; else state = 6; break;
case 2: if (i % 2 == 0) state = 3; else state = 4; break;
case 3: sum += i; state = 5; break;
case 4: sum -= i; state = 5; break;
case 5: i++; state = 1; break;
case 6: return sum;
}
}
}

(3)虚假控制流(Bogus Control Flow)

插入永远不会执行的代码路径,干扰分析人员的理解:

// 插入一个永远为 false 的条件分支
if (("foo" + "bar").equals("foobar") && System.currentTimeMillis() < 0) {
// 这个分支永远不会被执行,但会增加分析的难度
executeEvilCode();
}

3.3 完整性检测

(1)APK 签名验证

public boolean isSignatureValid(Context context) {
try {
PackageInfo pi = context.getPackageManager()
.getPackageInfo(context.getPackageName(),
PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;

// 获取预期的签名哈希(编译时计算好,硬编码在这里)
String expectedSignature = "3082..."; // 发布证书的 SHA-256

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(signatures[0].toByteArray());
String actualSignature = bytesToHex(digest);

return expectedSignature.equals(actualSignature);
} catch (Exception e) {
return false;
}
}

安全增强建议

  • 不要直接比较签名字符串,因为这可以通过字符串搜索轻易定位
  • 将签名哈希拆分为多个部分,分散在不同的类中
  • 在 native 层进行签名验证(通过 JNI 调用 Java 的 PackageManager API)

(2)DEX 校验和检测

public boolean isDexIntegrityValid(Context context) {
try {
// 获取当前 APK 的路径
String apkPath = context.getPackageCodePath();
ZipFile zipFile = new ZipFile(apkPath);
ZipEntry dexEntry = zipFile.getEntry("classes.dex");

// 计算实际 CRC
CRC32 crc32 = new CRC32();
InputStream is = zipFile.getInputStream(dexEntry);
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) > 0) {
crc32.update(buffer, 0, len);
}
is.close();

// 与预期的 CRC 比较
long expectedCrc = 0xABCD1234L; // 编译时计算好的 CRC
return crc32.getValue() == expectedCrc;

} catch (IOException e) {
return false;
}
}

(3)Native 层的完整性校验

将校验逻辑放在 native 库中可以提高逆向门槛:

// native-lib.c
JNIEXPORT jboolean JNICALL
Java_com_example_IntegrityCheck_verifySignature(JNIEnv* env, jobject thiz) {
// 获取当前 APK 路径
// 读取 META-INF/CERT.RSA
// 解析 X.509 证书
// 验证签名链
// 返回验证结果
// 注意:使用内联汇编或控制混淆来保护关键常量
}

3.4 反调试技术

(1)ptrace 检测

Android 调试器(如 gdb、lldb)通过 ptrace 系统调用附加到目标进程。应用可以检测自身是否被 ptrace:

#include <unistd.h>
#include <sys/ptrace.h>

bool isBeingDebugged() {
// 尝试 ptrace 自身
// 如果已经有一个 tracer 附加,ptrace 会失败
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
return true; // 有其他进程正在 trace 本进程
}
// 分离 ptrace
ptrace(PTRACE_DETACH, 0, 0, 0);
return false;
}

(2)TracerPid 检测

Android 的 /proc/self/status 文件中包含一个 TracerPid 字段,如果该值不为 0,说明有进程正在调试当前进程:

bool checkTracerPid() {
char buf[4096];
FILE* fp = fopen("/proc/self/status", "r");
if (fp == NULL) return false;

while (fgets(buf, sizeof(buf), fp)) {
if (strncmp(buf, "TracerPid:", 10) == 0) {
int pid = atoi(buf + 10);
fclose(fp);
return pid != 0; // true 表示被调试
}
}
fclose(fp);
return false;
}

对抗提示:攻击者可以使用内核模块或 Xposed 模块 Hook fopenfgets 返回伪造的 /proc/self/status 内容,将 TracerPid 伪装为 0。因此不能仅依赖一种检测手段。

(3)时间检测

调试器在单步执行时会显著降低代码运行速度。通过检测两段代码之间的执行时间,可以判断是否被调试:

#include <time.h>

bool checkTiming() {
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);

// 执行一些简单的操作
volatile int x = 0;
for (int i = 0; i < 10000; i++) {
x += i;
}

clock_gettime(CLOCK_MONOTONIC, &end);

long diff_ns = (end.tv_sec - start.tv_sec) * 1000000000L
+ (end.tv_nsec - start.tv_nsec);

return diff_ns > 10000000L; // 超过 10ms 可能被单步调试
}

(4)断点检测

调试器通过插入断点指令(在 ARM 上是 BKPT 指令,编码为 0xE12000700xBE00 等)来暂停程序执行。可以在运行时扫描代码段的断点指令:

void checkBreakpoints() {
// 获取函数代码的起始地址
void* funcAddr = &myCriticalFunction;
unsigned char* ptr = (unsigned char*)funcAddr;

for (int i = 0; i < 64; i++) {
// 在 ARM 上检测 BKPT 指令
unsigned int instruction = *((unsigned int*)(ptr + i));
if ((instruction & 0xFFF000F0) == 0xE1200070) {
// 发现断点!
abort();
}
}
}

3.5 Root / Magisk 检测

Root 检测是安全防护的第一道门。没有 root 权限,攻击者的能力会受到很大限制。

方法一:检查 su 二进制文件

private boolean checkSu() {
String[] paths = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/system/sbin/su",
"/vendor/bin/su",
"/data/local/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
};
for (String path : paths) {
if (new File(path).exists()) {
return true; // 发现 su,可能已 root
}
}
return false;
}

方法二:检查 Magisk 特征

private boolean checkMagisk() {
// 检查 Magisk 挂载点的特征
// Magisk 通常在 /sbin/.magisk/ 下
return new File("/sbin/.magisk").exists();
}

方法三:检测已安装的 root 管理应用

private boolean isRootManagementAppInstalled() {
String[] knownApps = {
"com.topjohnwu.magisk", // Magisk Manager
"eu.chainfire.supersu", // SuperSU
"com.noshufou.android.su", // Superuser
"com.kingroot.kinguser", // KingRoot
};

PackageManager pm = getPackageManager();
for (String pkg : knownApps) {
try {
pm.getPackageInfo(pkg, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
// 未安装
}
}
return false;
}

方法四:执行 which 命令

try {
Process process = Runtime.getRuntime().exec("which su");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line = reader.readLine();
if (line != null && !line.isEmpty()) {
return true; // su 命令可用
}
} catch (IOException e) {
// 忽略
}

对抗技巧:Magisk 提供了 MagiskHide 功能,可以对特定应用隐藏 root 状态。因此 root 检测应该放在 native 层,并且结合多种检测手段。

四、高级对抗与加固方案

4.1 动态加载与代码分离

将核心功能编译为独立的 DEX/JAR 文件,不放在 APK 中。应用启动时从服务器下载(需要 HTTPS + 签名验证),通过 DexClassLoader 动态加载:

File dexDir = context.getDir("dex", Context.MODE_PRIVATE);
DexClassLoader loader = new DexClassLoader(
downloadedDexPath, // 下载的动态 DEX 路径
dexDir.getAbsolutePath(),
null,
context.getClassLoader()
);

Class<?> coreClass = loader.loadClass("com.example.CoreEngine");
Object coreInstance = coreClass.newInstance();

注意:动态加载的代码同样可以被 dump 出来分析(通过 DexClassLoader 加载的 DEX 文件以 odex 形式存在于应用私有目录中)。

4.2 第三方加固服务

国内主流加固服务商:

加固服务 特点 保护强度
360 加固保 DEX 加密 + SO 加密 + 反调试
腾讯乐固 DEX 壳保护 + 资源加密
梆梆加固 DEX 虚拟化 + 内存保护
阿里聚安全 DEX 隐藏 + VMP(虚拟机保护) 极高
网易易盾 DEX 加密 + SO 加固 + 反注入

加固的基本原理:

原始 APK

加固 SDK (打包阶段)

加固后 APK
├── classes.dex (壳 DEX,负责解密和加载真实代码)
├── assets/encrypted/ (加密后的真实 DEX 文件)
├── lib/libprotect.so (native 保护层)
└── ...

运行时
├── 壳 DEX 启动 → 加载 libprotect.so
├── libprotect.so 解密 assets/encrypted/ 中的真实 DEX
├── 将解密后的 DEX 加载到内存,或通过 DexClassLoader 加载
└── 执行真实应用代码

加固的攻防演进

  • DEX 整体加密 → 攻击者通过内存 dump(如 dex_dump)在运行时提取解密后的 DEX
  • DEX 分段解密 → 攻击者通过 Hook DexFile 相关方法逐段收集
  • VMP(虚拟机保护) → 将关键方法编译为自定义的虚拟机指令,攻击者需要逆向虚拟机的指令集
  • 对抗 VMP → 通过符号执行和污点分析推导 VMP 指令语义

4.3 服务端验证

最根本的安全原则:不信任客户端。所有关键逻辑都应在服务端做最终验证。

  • License 验证:客户端发起验证请求 → 服务端返回加密的 License Token → 客户端仅做基本检查
  • 应用内购买:客户端完成支付后 → 将 PurchaseToken 发送到服务端 → 服务端调用 Google Play Developer API 验证 → 服务端发货
  • 核心算法:将关键算法放在服务端,客户端仅做输入收集和结果展示
  • 配置文件:动态配置(如功能开关、广告策略)由服务端下发,不在 APK 中硬编码

4.4 代码虚拟化(VMP)

VMP(Virtual Machine Protection)是最强的代码保护技术之一。它将关键方法的字节码编译为自定义虚拟机(VM)的指令,然后在自定义 VM 中解释执行。这意味着:

  • 反编译器(JADX 等)无法还原原始逻辑(因为代码已不是标准 DEX 字节码)
  • 攻击者需要先逆向自定义 VM 的指令集,再分析 VM 字节码
  • 每条 VM 指令可以被单独加密,进一步增加分析难度

VMP 的代价是性能:自定义 VM 的指令执行效率通常比原生字节码慢 5-50 倍。因此通常只对关键方法(如 License 验证、签名校验、加密算法)使用 VMP。

五、AOSP 安全相关源码路径

组件 源码路径 说明
APK 签名验证 frameworks/base/core/java/android/content/pm/PackageParser.java 安装时验证 APK 签名
APK 安装 frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java 安装流程,包含签名校验
DEX 加载 art/runtime/class_linker.cc 类加载与链接
DEX 文件操作 art/runtime/dex_file.cc DEX 文件解析
Binder 安全 frameworks/native/libs/binder/IPCThreadState.cpp Binder 调用者的 UID/PID 检查
SELinux system/sepolicy/ 进程安全上下文
dm-verity system/extras/verity/ 系统分区完整性保护

六、常见面试题

Q1: ProGuard/R8 混淆后的代码还能被反编译吗?混淆能提供多少安全保护?

A: 混淆后的代码完全可以被反编译。混淆只是将类名、方法名、变量名替换为无意义的短名称(如 a、b、c),并移除未使用的代码,但它不会改变代码的逻辑结构。JADX 等反编译器仍然可以还原控制流、数据流和调用关系。混淆的本质是增加逆向分析的时间成本和理解难度,而不是阻止逆向。类比来说,混淆相当于把一本书的章节标题和关键词都换成了无意义的代号——内容还在,但理解起来困难得多。真正有效的保护需要结合多种技术:混淆 + 字符串加密 + 控制流平坦化 + 完整性检测 + 反调试 + 服务端验证。

Q2: 为什么不能直接在客户端做 License 校验?即使把校验函数放在 native 层也没用吗?

A: 纯客户端校验在理论上就是不可行的,因为攻击者控制着代码运行的环境。不论校验函数是用 Java 写的还是用 C 写的,不论做了多少混淆和加密,攻击者最终都能通过 Hook 框架(Xposed/Frida)拦截关键的判断点,将返回值修改为 true。放在 native 层只是提高了门槛——攻击者需要掌握 ARM 汇编和 native Hook 技术(如使用 Cydia Substrate 或 Frida 的 Native Hook),但并非不可能。唯一安全的做法是:客户端发起校验请求 → 服务端根据设备指纹、签名、购买记录等做综合判断 → 返回加密的授权结果 → 客户端仅做本地解密和展示。即使攻击者伪造了某个请求的成功响应,服务端的核心逻辑仍然受到保护。

Q3: 什么是”加壳”?壳程序是如何保护原始 DEX 的?

A: 加壳是将原始 APK 的 DEX 文件加密后,嵌入到一个”壳” APK 中。壳程序(Shell DEX)是真正启动时执行的代码,它的职责是:解密原始 DEX → 加载解密后的 DEX → 将控制权交给原始应用。常见的实现方式:(1) 将原始 DEX 加密后放在 assets 目录中,壳程序使用 native 方法解密;(2) 将原始 DEX 附加在壳 DEX 的尾部(dex-tail 技术),壳程序通过解析 ZIP 格式定位并加载;(3) 动态从服务器下载 DEX。壳程序的弱点在于:无论外层保护多强,原始 DEX 最终必须在内存中解密为明文才能被 ART 执行。攻击者可以在 DexClassLoader 的加载点或内存中 dump 出解密后的 DEX。为对抗内存 dump,高级壳会使用分片解密(只解密当前执行的方法,执行完后立即擦除)和反 dump 技术。

Q4: 什么是 Frida?攻击者如何利用 Frida 绕过安全检测?

A: Frida 是一个动态代码注入框架,它可以将 JavaScript 代码注入到目标进程中,从而实现 Hook 函数调用、修改参数和返回值、追踪函数调用栈等操作。攻击者使用 Frida 绕过安全检测的典型手法:(1) Hook isLicensed() 方法,强制返回 true;(2) Hook PackageManager.getPackageInfo(),伪造签名信息;(3) Hook checkSu(),返回 false 隐藏 root 状态;(4) Hook 时间检测函数,返回预期的执行时间;(5) 通过 Interceptor.attach 挂载到 native 函数,修改参数或返回值。防御 Frida 的方法包括:检测 Frida 的特征端口(默认 27042)、检测 Frida 的特征文件(/data/local/tmp/frida-server)、检测 Frida 注入的线程名特征(如 fridagum-js-loop)、使用 inline hook 检测(Frida 修改函数入口时会留下特征指令)等。

Q5: Android 应用的签名验证机制是如何工作的?为什么重新签名后签名一定不同?

A: Android 使用 APK Signature Scheme(v1/v2/v3)来保证 APK 的完整性和来源认证。V1 方案(JAR 签名):计算 APK 中每个文件的 SHA 摘要存储在 MANIFEST.MF 中,然后对 MANIFEST.MF 计算摘要存储在 CERT.SF 中,最后用开发者的私钥对 CERT.SF 签名生成 CERT.RSA。V2 方案(APK Signature Scheme v2):在 ZIP 文件的数据块中插入一个 APK Signing Block,对整个 APK 内容(除 Signing Block 本身外)进行签名。V3 方案增加了密钥轮换支持。重新签名后之所以签名不同,是因为攻击者没有原始开发者的私钥——他们只能用自己的私钥生成新的签名,而证书的 issuer、serial number、公钥等信息都与原始的不同。应用在安装时,PackageManagerService 会验证签名的一致性。但要注意,签名验证只在安装时发生——运行时获取签名的 PackageManager.getPackageInfo() API 返回的是当前 APK 的实际签名,而不是开发者的原始签名。

Q6: 如果我开发的是一个免费应用(不涉及 License 或内购),还需要做防破解吗?

A: 需要,但要基于风险评估来决定投入多少资源。免费应用面临的主要威胁不是 License 绕过,而是:(1) 二次打包——攻击者反编译你的应用,注入恶意代码(窃取用户数据、植入广告、发起 DDoS 攻击),然后通过第三方应用市场分发,损害你的品牌声誉;(2) 广告劫持——攻击者替换你的广告 ID,将你的广告收入转移到他们的账户;(3) 协议破解——如果你的应用与服务端有自定义协议,攻击者可能通过逆向协议实现来编写恶意客户端(刷量、群控)。因此即使是免费应用,也建议至少做基础防护:开启 ProGuard/R8 混淆、对关键字符串使用编码(不直接明文存储)、在 native 层做签名校验、对敏感接口请求添加签名参数。基础防护的投入产出比很高——攻破门槛从”5 分钟”提高到”数小时”,能够阻止大部分没有经验的攻击者。


参考文档:

打赏
  • 微信
  • 支付宝

评论