Android 签名校验

---
markmap:
  height: 600
---
- Android 签名校验
  - 系统内置的签名校验机制(普通签名校验)
    - V1(JAR签名)
    - V2(APK Signature Scheme v2)
    - V3(密钥轮替支持)
    - V4(Merkle哈希树签名)
  - 应用层的自定义校验手段
    - JAVA层签名校验
      - PackageManager校验
      - Dex/APK完整性校验
      - 基于类名的 Application 校验
      - 新的API签名校验
      - 签名哈希上传服务器校验
      - 运行时检测 Frida/ 调试器附加 /PMS动态代理
    - Native层签名校验
      - 手搓Context
      - 直接解析APK签名块
      - 分块校验技术
      - ELF节校验
      - 使用 Android NDK 提供的安全 API
      - 反调试
  - 逆向对抗层:混淆
    - 代码混淆(ProGuard/R8)
    - 字符串混淆
    - 代码逻辑混淆
    - 控制流伪装
    - 动态代码生成
    - 反动态调试与反Hook
    - 分层混淆
    - 工具支持

参考:
聊聊Android签名检测7种核心检测方案详解-Android安全
《安卓逆向这档事》——正己
安卓签名校验-探讨-看雪

Android 签名校验

系统内置的签名校验机制(普通签名校验)

  • 对应 Android 系统的 V1/V2/V3/V4 签名方案
  • 校验时机:仅在 应用安装或更新时 进行,非运行时
  • 执行主体由 Android 系统自动完成,无需用户干预
  • 实现机制
    • 安装流程中由 PackageManagerService (PMS) 执行签名验证
    • 调用 collectCertificates 等方法校验 APK 签名
    • 校验失败则安装被拒绝

V1(JAR签名)

原理:基于传统的JAR签名方案,对APK中的每个文件生成哈希摘要(存储在MANIFEST.MF),再使用私钥加密生成签名文件(如CERT.RSA)。
assets/Android-签名校验-By-方北七/file-20250420181836933.png
assets/Android-签名校验-By-方北七/file-20250420181814590.png

V2(APK Signature Scheme v2)

原理:引入全文件签名,对整个APK文件(除签名块外)计算哈希值并加密,存储于APK的签名块中。

在Android 7.0上版本才支持
在安装过程中,如果有 v2 签名块,则必须走 v2 签名机制,不能绕过。否则降级走 v1 签名机制。
v1 和 v2 签名机制是可以同时存在的,其中对于 v1 和 v2 版本同时存在的时候,v1 版本的 META_INF 的 .SF 文件属性当中有一个 X-Android-APK-Signed 属性:

X-Android-APK-Signed: 2

assets/Android-签名校验-By-方北七/file-20250420181332547.png
相比v1签名方案,v2签名方案不再以文件为单位计算摘要,而是以1MB为单位将文件拆分为多个连续的块(chunk),每个分区的最后一个块可能会小于1MB。
计算每个块的摘要,然后再计算这些摘要的签名
assets/Android-签名校验-By-方北七/file-20250420181015773.png

V3(密钥轮替支持)

原理:在V2基础上增加密钥历史链,允许开发者轮换签名密钥而保持应用更新连续性。

V4(Merkle哈希树签名)

原理:基于APK所有字节构建Merkle哈希树,根哈希值与盐值作为签名数据,独立存储于.apk.idsig文件。

应用层的自定义校验手段

虽然系统仅在安装时校验签名,但开发者可通过代码实现额外校验逻辑,增强安全性

JAVA层签名校验

注意,以下方法都是通过某种方法获得签名值后与预设值比对。以下只探讨某种方法来获取签名值,而预设值的获取方法假设都是统一的预定义好的。

PackageManager校验

通过PackageManager获取当前应用签名信息,与预设值比对
代码中的Context是一个 Android 提供的核心类,定义了应用运行时的上下文环境,提供了对应用资源和系统服务的访问接口。

// 获取当前签名SHA1值
public static String getCurrentSignature(Context context) {
    try {
        PackageInfo packageInfo = context.getPackageManager()
                .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
        Signature[] signatures = packageInfo.signatures;
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(signatures[0].toByteArray());
        return bytesToHex(md.digest());
    } catch (Exception e) { return ""; }
}

// 比对预设值(需提前用keytool获取)
public static boolean verifySignature(Context context, String expectedSHA1) {
    String currentSHA1 = getCurrentSignature(context);
    return currentSHA1.equals(expectedSHA1);
}

对此,我们可以通过重写ContextPackageManager的方式来伪造签名。

如何获取Context对象:

// 下面几行代码展示如何任意获取Context对象,在jni中也可以使用这种方式
Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
Method currentApplication =  activityThreadClz.getMethod("currentApplication");
Application application = (Application) currentApplication.invoke(null);

在Java层,PMS实例通过ActivityThread的静态字段sPackageManagerApplicationPackageManagermPM字段暴露。要替换PMS实例,需通过反射访问这些私有字段:

// 示例:Hook ActivityThread中的sPackageManager
var ActivityThread = Java.use("android.app.ActivityThread");
var currentThread = ActivityThread.currentActivityThread();
var originalPMS = currentThread.getPackageManager();
var proxyPMS = createProxy(originalPMS); // 创建代理对象
currentThread.sPackageManager.value = proxyPMS; // 反射修改字段

反射仅适用于Java层的字段和方法替换,但无法直接修改Native代码或系统服务内部逻辑

Dex/APK完整性校验

校验Dex文件的CRC或APK整体哈希,并将其与预设的CRC值或哈希值进行比较
防止重打包后Dex被篡改,但需注意资源文件(如strings.xml)的安全性

// 校验classes.dex的CRC值
public static void checkDexCRC(Context context) {
    try {
        ZipFile zipFile = new ZipFile(context.getPackageCodePath());
        ZipEntry dexEntry = zipFile.getEntry("classes.dex");
        long realCRC = dexEntry.getCrc();
        long expectedCRC = Long.parseLong(context.getString(R.string.expected_crc));
        if (realCRC != expectedCRC) {
            System.exit(0); // 触发退出
        }
    } catch (IOException e) { /* 异常处理 */ }
}

// APK整体SHA-1校验
public final boolean check_Hash(Context context0) {
    String apkPath = this.getApkPath(this);
    try (FileInputStream fis = new FileInputStream(new File(apkPath))) {
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            digest.update(buffer, 0, bytesRead);
        }
        String hash = new BigInteger(1, digest.digest()).toString(16);
        Log.e("zj2595", "hash:" + hash);
        return Intrinsics.areEqual(hash, this.ApkHash);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

可以通过io重定向绕过,本质上是hook open( )、openat( )、fopen( )、fake_syscall( )等读取文件函数,把io后的路径写死成原来修改前的apk文件。

[!MT去除签名校验]

旧版的MT去签名校验的原理是
GitHub - L-JINBIN/ApkSignatureKiller: 一键破解APK签名校验
通过hook PMS来过校验,对那些仅通过 PackageManager.getPackageInfo().signatures 来校验签名的应用有效。

新版MT中的“去除签名校验”在原来的基础上加入了 io重定向原理
GitHub - L-JINBIN/ApkSignatureKillerEx: 新版MT去签及对抗
去除了之后可以看到apk里加入了一个dex文件来实现去除签名校验。
具体的,可以看到有killPM和killOpen两个方法,即hook PMS 和 io重定向 都加上了。

assets/Android-签名校验-By-方北七/file-20250421085357781.png
assets/Android-签名校验-By-方北七/file-20250421090040960.png
assets/Android-签名校验-By-方北七/file-20250421090159247.png

基于类名的 Application 校验

检查当前应用的 Application 类是否为 MainApplication

private final boolean checkApplication() {
	return Intrinsics.areEqual("MainApplication", this.getApplication().getClass().getSimpleName());
}

新的API签名校验

  • 如果 Build.VERSION.SDK_INT < 28(即 Android 版本低于 9.0),使用 PackageInfo.signatures 获取签名。
  • 如果 Build.VERSION.SDK_INT >= 28(即 Android 版本 9.0 及以上),使用 SigningInfo.getApkContentsSigners() 获取签名。
private final boolean useNewAPICheck() {
    String s1 = "";
    try {
        byte[] arr_b;
        if (Build.VERSION.SDK_INT < 28) {
            arr_b = this.getPackageManager().getPackageInfo("com.zj.wuaipojie", PackageManager.GET_SIGNATURES).signatures[0].toByteArray();
        } else {
            arr_b = this.getPackageManager().getPackageInfo("com.zj.wuaipojie", PackageManager.GET_SIGNING_CERTIFICATES).signingInfo.getApkContentsSigners()[0].toByteArray();
        }
        s1 = MD5Utils.INSTANCE.MD5(Base64Utils.INSTANCE.encodeToString(arr_b));
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return "074f64af5821ae6aa1ac1779ad5687ad".equals(s1);
}

签名哈希上传服务器校验

tips:需处理网络延迟或拦截攻击(如中间人伪造响应)

// 客户端发送签名至服务端验证
public static void remoteVerify(Context context) {
    String signature = getCurrentSignature(context);
    ApiService.verifySignature(signature, new Callback() {
        @Override
        public void onSuccess() { /* 正常流程 */ }
        @Override
        public void onFailure() { killProcess(); }
    });
}

运行时检测 Frida/ 调试器附加 /PMS动态代理 / root

有很多检测方法,下面举例几种

// 检测Frida/Xposed存在
public static boolean checkHookFramework() {
    return checkFiles("/data/local/tmp/frida", "/data/local/bin/magisk") 
        || checkPort(27042); // Frida默认端口
}

// 检测调试器附加
public static boolean isDebuggerAttached() {
    return Debug.isDebuggerConnected() || Debug.waitingForDebugger();
}


// 检查PackageManager是否被动态代理,通过反射获取 `PackageManager` 的内部字段 `mPM`,并检查该字段的类型是否为 `android.content.pm.IPackageManager$Stub$Proxy`
private final boolean checkPMProxy() {
	try {
		PackageManager packageManager0 = this.getPackageManager();
		Field field0 = packageManager0.getClass().getDeclaredField("mPM");
		field0.setAccessible(true);
		return Intrinsics.areEqual("android.content.pm.IPackageManager$Stub$Proxy", field0.get(packageManager0).getClass().getName());
	}
	catch(Exception exception0) {
		exception0.printStackTrace();
		return Intrinsics.areEqual("android.content.pm.IPackageManager$Stub$Proxy", "");
	}
}
// `checkRootMethod1()` 方法检查设备的 `build tags` 是否包含 `test-keys`。这通常是用于测试的设备,因此如果检测到这个标记,则可以认为设备已被 root。

// `checkRootMethod2()` 方法检查设备是否存在一些特定的文件,这些文件通常被用于执行 root 操作。如果检测到这些文件,则可以认为设备已被 root。

// `checkRootMethod3()` 方法使用 `Runtime.exec()` 方法来执行 `which su` 命令,然后检查命令的输出是否不为空。如果输出不为空,则可以认为设备已被 root。


fun isDeviceRooted(): Boolean {
    return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}

fun checkRootMethod1(): Boolean {
    val buildTags = android.os.Build.TAGS
    return buildTags != null && buildTags.contains("test-keys")
}

fun checkRootMethod2(): Boolean {
    val paths = arrayOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
            "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su")
    for (path in paths) {
        if (File(path).exists()) return true
    }
    return false
}

fun checkRootMethod3(): Boolean {
    var process: Process? = null
    return try {
        process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
        val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
        bufferedReader.readLine() != null
    } catch (t: Throwable) {
        false
    } finally {
        process?.destroy()
    }
}

反制手段
1.算法助手、对话框取消等插件一键hook
assets/Android-签名校验-By-方北七/file-20250421212715335.png
2.分析具体的检测代码
让它检测root全部返回false
3.利用IO重定向使文件不可读
4.修改Andoird源码,去除常见指纹

Native层签名校验

以上Java层方法大部分都可以迁移到Native层上,但是单独移植 效果并不会得到太大提升。一般都是结合上Native层的反调试反静态手段。

//native-lib.cpp
JNIEXPORT jboolean JNICALL Java_com_example_SignCheck_nativeCheck
  (JNIEnv *env, jobject obj, jobject context) {
    // 获取Context实例(需反射调用ActivityThread)
    jclass activityThreadClass = env->FindClass("android/app/ActivityThread");
    jmethodID currentApplication = env->GetStaticMethodID(activityThreadClass, 
        "currentApplication", "()Landroid/app/Application;");
    jobject app = env->CallStaticObjectMethod(activityThreadClass, currentApplication);
    
    // 获取PackageManager并提取签名
    jclass contextClass = env->GetObjectClass(app);
    jmethodID getPackageManager = env->GetMethodID(contextClass, 
        "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject pm = env->CallObjectMethod(app, getPackageManager);
    
    // 此处省略签名提取与校验逻辑...
    return env->NewStringUTF("预设签名哈希") == calculatedHash;
}

Java层一般是通过以下方法来获取签名的,其实现大部分是通过简单对比来确认签名是否正确:

getPackageManager // 获取 `PackageManager` 实例
getPackageInfo  // 获取指定包名的应用信息
PackageManager.GET_SIGNATURES  // 获取应用的签名信息
PackageManager.GET_SIGNING_CERTIFICATES // 从 Android 9.0(API 28)开始
Signature.toByteArray() // 将签名对象转换为字节数组。
MessageDigest.getInstance("SHA-1"); // 获取指定算法的消息摘要实例,如SHA-1
update(byte[] input) // 更新消息摘要实例,将输入的字节数组加入到摘要计算中。
digest //完成摘要计算,返回最终的哈希值。
MD5Utils.MD5(String input) // 计算输入字符串的 MD5 哈希值
Base64Utils.encodeToString(byte[] input)`// 将字节数组编码为 Base64 字符串
getApplication() // 获取当前应用的 Application 实例。
getClass().getSimpleName() // 获取对象的类名(不包含包名)
getPackageCodePath() // 获取 APK 文件的路径。
ZipFile.ZipFile(String name) // 打开一个 ZIP 文件
ZipFile.getEntry(String name) // 获取 ZIP 文件中的指定条目(如 classes.dex)
ZipEntry.getCrc():获取 ZIP 条目的 CRC32 校验值

手搓Context

通过在Native层中,开发者自己在Native层通过反射获取Context对象,避免传递Context导致伪造签名。
但还是实质上还是通过调用Context的PMS来获取签名信息。所以还是可以通过hook PMS来hook native层的手搓Context获取的PMS。

代码在以下文章中:
Android安全系列之:如何在native层保存关键信息 - dongweiq - 博客园

直接解析APK签名块

直接读取apk的签名块,绕过PackageManager的潜在篡改,直接验证签名证书链。

// 读取APK的META-INF/*.RSA文件
FILE* fp = fopen("/data/app/xxx/base.apk", "rb");
fseek(fp, centralDirOffset, SEEK_SET); // 定位签名块偏移
parsePKCS7SignatureBlock(fp); // 解析PKCS#7结构

assets/Android-签名校验-By-方北七/mugpplvjkhg.png

分块校验技术

每个分块计算SHA-256哈希值,并将这些哈希值组合成一个总的哈希值用于验证完整性。

void verifyDexIntegrity() {
    int fd = open("/data/app/xxx/classes.dex", O_RDONLY);
    struct stat st;
    fstat(fd, &st);
    
    // 分块计算哈希
    for(int i=0; i<st.st_size; i+=4096){
        void* map = mmap(0, 4096, PROT_READ, MAP_PRIVATE, fd, i);
        calculate_sha256(map, 4096); 
        munmap(map, 4096);
    }
    close(fd);
}

assets/Android-签名校验-By-方北七/3tyqr9oili7.png

ELF节校验

tips:好像没看到相关Android实现资料。
通过在应用程序中添加特定的字段,这些字段中在编译期给应用程序添加一些类似校验的信息。这个应用程序在运行前需要先经过一个装载程序的校验,校验成功才继续运行此应用程序。
通过在 ELF 文件中加入额外的节(Section),可以存储:

  1. 应用程序的完整校验信息(如哈希值或签名)。
  2. 应用关键代码段或数据段的校验摘要。
  3. 自定义的元信息。
    在应用启动时或动态库装载(通过动态加载器 dlopen)时,可以对这些节进行完整性校验,确保 ELF 文件没有被篡改。
#include <elf.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

void check_elf_integrity(const char *elf_path) {
    int fd = open(elf_path, O_RDONLY);
    if (fd < 0) {
        printf("Failed to open ELF file: %s\n", elf_path);
        return;
    }
    
    // 示例:假设增加了 .custom_verify 的节并对 ELF 文件的整体内容进行校验
    lseek(fd, 0, SEEK_SET);
    char buffer[2048];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);

    ssize_t bytes_read;
    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
        SHA256_Update(&sha256, buffer, bytes_read);
    }

    unsigned char calculated_hash[SHA256_DIGEST_LENGTH];
    SHA256_Final(calculated_hash, &sha256);

    printf("Calculated ELF Hash: ");
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        printf("%02x", calculated_hash[i]);
    }
    printf("\n");

    // 校验:与 ELF 文件中的特定节哈希值对比(伪代码)
    unsigned char expected_hash[SHA256_DIGEST_LENGTH];
    // 从 ELF 文件中的 .custom_verify 节中读取 expected_hash
    // 假设 find_custom_verify_section() 是找节的函数
    if (memcmp(calculated_hash, expected_hash, SHA256_DIGEST_LENGTH) == 0) {
        printf("ELF verification passed.\n");
    } else {
        printf("ELF verification failed.\n");
    }

    close(fd);
}

使用 Android NDK 提供的安全 API

利用 NDK 提供的加密和哈希相关库,在 Native 层完成签名校验流程。

  1. 使用 OpenSSL 或 BoringSSL 提供的 API 计算签名哈希值。
  2. 通过 Native 层直接加载签名文件,如 CERT.RSA。
  3. 对签名内容的公钥与预期结果进行解密对比。
    示例:
    基于 OpenSSL:
#include <openssl/sha.h>
unsigned char *SHA1Hash(const unsigned char *data, size_t len) {
    unsigned char *hash = (unsigned char *)malloc(SHA_DIGEST_LENGTH);
    SHA1(data, len, hash);
    return hash;
}

反调试

Native层能实现的反调试手段比较多,如使用ptrace、检查TracerPid、断点检测等方法。比如,主动调用ptrace(PTRACE_TRACEME)阻止调试器附加,或者轮询检查/proc/self/status中的TracerPid等

逆向对抗层:混淆

代码混淆是一种常见的逆向工程对抗技术,旨在通过变更代码的结构、符号名称和逻辑路径,让逆向工程师难以理解代码逻辑。安卓应用通常使用以下混淆技术:


代码混淆(ProGuard/R8)

基本思路:
通过改变代码中的类名、方法名、字段名,使代码逻辑变得难以理解。ProGuard是常用的Android代码混淆工具,而R8是其替代版本,具有更高效的混淆能力

特点:

  • 自动混淆类名、方法名和字段名。
  • 删除未使用的代码(优化)以减小APK文件大小。
  • 提供配置文件支持,以便定义混淆规则。

示例:
ProGuard配置文件示例:

-keep class com.example.** { *; }
-keepclassmembers class com.example.** { *; }
-keepnames class com.example.** { *; }

其中:

  • -keep: 保留指定的类和成员名不被混淆。
  • -keepclassmembers: 保留特定类的成员名。
  • -keepnames: 保留类名以避免反射调用失败。

注意事项:

  • 混淆后的类名常呈现类似a.a.a的形式。
  • 反射机制问题:如果应用中大量使用反射调用,混淆后的类名可能会导致运行时调用失败,需要配置-keep规则避免关键类名被混淆。

字符串混淆

基本思路:
对代码中的关键字符串(如 API Key、签名哈希值、敏感路径等)进行加密处理,避免直接暴露给逆向工程师。字符串混淆的方法包括:

  • Base64编码
  • XOR加密
  • AES或DES对称加密

示例:

// 示例:对关键字符串进行XOR加密
public static String decryptString(String encrypted, int key) {
    char[] chars = encrypted.toCharArray();
    for (int i = 0; i < chars.length; i++) {
        chars[i] = (char) (chars[i] ^ key);
    }
    return new String(chars);
}

// 示例使用
String encrypted = encryptString("SensitiveString", 123);
String decrypted = decryptString(encrypted, 123);

通过这种方式,可以有效隐藏代码中的敏感信息。


代码逻辑混淆

基本思路:
使用复杂的逻辑结构或冗余代码伪装真正的应用逻辑,扰乱逆向工程师的分析过程。可以通过中间层调用、多分支逻辑或意图干扰手段来实现。

示例:

// 示例:冗余逻辑与陷阱代码
if (randomCondition()) {
    fakeMethod1();
} else if (anotherRandomCondition()) {
    fakeMethod2();
} else {
    realLogicMethod();
}

public void fakeMethod1() {
    // 无效逻辑
}

public void fakeMethod2() {
    // 陷阱逻辑
}

通过嵌入大量伪逻辑,可以有效干扰自动化分析工具的行为。


控制流伪装

基本思路:
改变代码执行的控制流,使其呈现复杂而非线性的特征。这种方法通常伴随以下技术:

  • 分支混淆:插入大量无关的分支条件。
  • 循环嵌套:嵌套循环以混淆逻辑。
  • 跳跃指令:通过硬编码的跳跃指令扰乱代码结构。

示例:

public int exampleFunction(int input) {
    if (input > 100) {
        return input * 2; // 主逻辑
    } else {
        triggerTrap(); // 陷阱逻辑
    }
}

// 示例:通过跳跃语句扰乱分析
void triggerTrap() {
    for (int i = 0; i < 5; i++) {
        if (i == 3) {
            return; // 扰乱分析工具
        }
    }
}

动态代码生成

基本思路:
在运行时动态生成部分代码以实现关键逻辑,避免静态分析工具提取重要信息。动态代码生成通常依赖于DEX动态加载或反射调用,例如:

  • 使用 ClassLoader 加载动态生成的 DEX 文件。
  • 将关键代码嵌入 Native 模块并通过 JNI 调用。

示例:

DexClassLoader dexLoader = new DexClassLoader("/path/to/dynamic.dex", 
    context.getCacheDir().getAbsolutePath(), 
    null, 
    context.getClassLoader());

Class<?> dynamicClass = dexLoader.loadClass("com.example.dynamic.DynamicCode");
Method method = dynamicClass.getDeclaredMethod("execute");
method.invoke(null);

反动态调试与反Hook

基本思路:
对抗动态调试或Hook框架(如Frida/Xposed)所附加的行为,尽可能干扰逆向工程师的调试操作。以下是常见的反动态调试手段:

  • 轮询检测调试器附加状态
    public static boolean isDebuggerAttached() {
        return Debug.isDebuggerConnected() || Debug.waitingForDebugger();
    }
    
  • 检测Frida或Xposed框架文件是否存在
    public static boolean checkFiles(String... paths) {
        for (String path : paths) {
            if (new File(path).exists()) {
                return true;
            }
        }
        return false;
    }
    
  • 动态检查被Hook的函数是否被代理
    • 通过反射检查被Hook的方法调用是否来自于代理对象(如Xposed的XC_MethodHook)。

分层混淆

基本思路:
将关键代码逻辑分散到多个层级(如Java层、Native层或动态加载模块),并对每个层级进行单独混淆。同时可以采用多种语言(如C/C++/Rust)混编以增加逆向难度。

示例:

  • Java层包含部分代码逻辑,其余核心逻辑存储在Native层。
  • Native层通过自定义的ELF节校验或硬编码逻辑实现校验完整性,确保难以通过反编译获取完整逻辑。

工具支持

以下是常用的混淆与逆向对抗工具:

  • ProGuard: Android项目中的基本混淆工具。
  • R8: Google推荐的ProGuard替代工具,效果更强。
  • DexGuard: 一款付费的混淆工具,提供更多高级混淆特性(如字符串加密、动态字节码操作)。
  • Obfuscator-LLVM: 针对Native层代码的混淆工具,可以改变代码结构、控制流等。
posted @ 2025-05-10 19:30  方北七  阅读(369)  评论(0)    收藏  举报