NDK-参数加密和签名校验

一、加密算法介绍

  1. RSA、AES、DES 一种方式做解析,不传明文,只传密文
  2. MD5 一种方式做解析,即传递明文(参数),也传递密文(md5),后台先去校验比对成功返回数据,不成功返回错误

RSA(非对称加密)

  • 密钥:一对公钥(public key)和私钥(private key)
  • 常用于:登录认证、公钥加密敏感信息、服务器通信中传输对称密钥等
  • 优点:公私钥分离,安全性高
  • 缺点:运算速度慢,适合加密小数据量

AES(对称加密-高级加密标准)

  • 多用于:对称加密敏感数据,如缓存、Token、本地数据库等
  • 优点:加密速度快,安全性强
  • 缺点:密钥管理复杂(必须保证密钥安全)

DES(对称加密-数据加密标准,已过时)

  • 曾用于早期数据加密,但现在已被 AES 取代
  • 历史久,曾广泛使用
  • 缺点:安全性差,容易被暴力破解

MD5(消息摘要算法)

  • 类型: 哈希算法(不可逆)
  • 输出长度: 128 位(16 字节,通常表示为 32 个十六进制字符)
  • 安卓中常用于:文件校验、快速对比数据是否一致、加密缓存 key 等
  • 优点:速度快,实现简单
  • 缺点存在碰撞漏洞(可构造两个不同输入产生相同 MD5 值)、不适合做密码或安全验证用途

尽管我们平时用到最多的加密就是 MD5,但其缺点我们也要正视,因为 MD5 使用的是哈希运算,所以不可避免的有概率会产生哈希碰撞,出现异常情况,虽然这个概率可能极低,而且考虑到不是特别敏感的情况下及考虑兼容性情况仍在使用,但其问题仍不可忽视!

那么有没有更完美的算法来代替 MD5 ?有的,拿安卓举例,安卓使用的签名方案之一是v2签名方案 (自Android 7.0 Nougat引入)和v3签名方案 (自Android 9 Pie开始支持),它们都支持使用SHA-256SHA-512等更强的哈希算法。

SHA256 代码示例:

java 复制代码
public static String getSHA256Hash(String data) {
try {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(data.getBytes());
    StringBuilder hexString = new StringBuilder();
    for (byte b : hash) {
        String hex = Integer.toHexString(0xff & b);
        if(hex.length() == 1) hexString.append('0');
        hexString.append(hex);
    }
    return hexString.toString();
} catch (NoSuchAlgorithmException e) {
    throw new RuntimeException(e);
}
}

SHA-256主要用来生成文件或整个APK内容的摘要,确保数据未被篡改。由于其高安全性和抗碰撞特性,SHA-256有效地增强了安卓应用的安全性。

但!没有 一中算法可以被认为是最完美的,没有最好只有更好,SHA-256 加密只是整个签名和校验流程的一部分,最终还要结合对称和非对称多种算法混合加密,进一步确保数据完整性。

二、实战

1. 网络请求参数使用MD5加密

不能直接拿原始数据进行MD5加密,不然没有任何意义,可以在so里对数据动点手脚,比如前面加上几个固定字符,后面去掉几个字符,然后生成MD5值,是不是就有点加密那种感觉了。

cpp 复制代码
static char *EXTRA_SIGNATURE = "XAYE";


extern "C"
JNIEXPORT jstring JNICALL
Java_com_xaye_myjni_jni_SignatureUtils_signatureParams(JNIEnv *env, jclass clazz, jstring params) {

    if (is_verify == 0) {
        return env->NewStringUTF("error_signature");
    }

    const char *paramsStr = env->GetStringUTFChars(params, nullptr);
    // MD5 签名规则,加点料
    // 1.字符串前面加点料
    string signature_str(paramsStr);
    signature_str.insert(0, EXTRA_SIGNATURE);
    // 2.后面去掉两位
    signature_str = signature_str.substr(0, signature_str.length() - 2);

    // 3.进行MD5签名, C++ 和 Java  是一样的,唯一不同的是C++需要自己回收内存
    MD5_CTX *ctx = new MD5_CTX();
    MD5Init(ctx);
    MD5Update(ctx, (unsigned char *) signature_str.c_str(), signature_str.length());
    unsigned char digest[16];
    MD5Final(digest, ctx);

    //  生产32位的字符串, 转换为十六进制字符串
    char md5_str[33]; // 32字符 + '\0' ,存放终止符 \0
    for (int i = 0; i < 16; i++) {
        // 不足的情况下补0, f:0f ,ab:ab

        // snprintf 是标准 C 库函数,用于安全地格式化字符串,相比 sprintf 多了一个缓冲区大小参数,防止缓冲区溢出。
        // str:目标缓冲区地址
        // size:缓冲区剩余可用大小,限制写入长度(3)
        // format:格式化字符串
        // ...:可变参数(要格式化的数据)

        // 每次 snprintf 会写入终止符,但下一次循环会覆盖它。最终通过 md5_str[32] = '\0' 显式添加终止符,确保字符串完整。
        snprintf(md5_str + i*2, 3, "%02x", digest[i]);
    }
    md5_str[32] = '\0';

    // 释放资源
    env->ReleaseStringUTFChars(params, paramsStr);

    return env->NewStringUTF(md5_str);
}

这里使用的是使用C++生成MD5值,我们知道在Java中字符串生成MD5非常简单,几行代码搞定,但在C++中,需要一堆代码... 当然你也可以在C++中调用Java代码,去生成MD5值,这样就简单了些,但是如果你想写的so 也放在ios中可以直接使用,那么这种方式就不那么合适了。

下面就简单介绍下,在C++中如何使用MD5算法👇

使用 JNI 在 C++ 实现 MD5 加密算法

C++ 中实现 MD5 的完整流程,重点分为如下几步:

一、填充数据

MD5算法要求输入消息的长度必须是512位(64字节)的整数倍减去64位(因为最后64位要存储原始长度)

  1. 首先计算原始消息的位长度(原始字节数×8)。
  2. 在消息末尾先添加一个0x80字节(二进制10000000),这相当于在消息末尾添加了一个"1"比特,后面跟着七个"0"比特。
  3. 继续填充0x00字节,直到消息长度满足(长度 % 64 == 56)(即448位模512, 56 字节是 448 位 )。
  4. 最后8字节(64位)以小端序存储原始消息的位长度。

填充完后,信息的长度就为N*512+448(bit);

示例 :原始消息为 "abc",即 3 字节,添加 1 字节 0x80,再加 52 个 0x00,再加 8 字节长度,共 64 字节。

二、追加长度(Length)

如上, MD5 需要在填充后的消息末尾追加 8 个字节(64 位):

表示原始消息的长度(以 bit 为单位)

存储方式是小端序,即低位在前,高位在后。

这64位加在第一步结果的后面,这样信息长度就变为N*512+448+64=(N+1)*512位。 这一步的作用是确保哈希值对不同长度的数据唯一性更强。

三、初始化缓冲区(四个幻数)

MD5 使用四个 32 位的整数作为初始化向量(IV),也被称为标准幻数

kotlin 复制代码
context->state[0] = 0x67452301; // A
context->state[1] = 0xefcdab89; // B
context->state[2] = 0x98badcfe; // C
context->state[3] = 0x10325476; // D

这四个初始值会在每一轮运算中不断被更新,最终组合为结果。

四、分组处理、 四轮主循环计算

将填充后的消息按512位(64字节)分组,对每个分组进行如下处理:

  1. 将当前分组划分为16个32位子块(小端序)
kotlin 复制代码
uint32_t M[16];  // 16个 32 位小端整数

32 位 = 4 个字节,16 x 4 = 64 字节

  1. 定义四个核心函数
kotlin 复制代码
F(X,Y,Z) = (X & Y) | (~X & Z)
G(X,Y,Z) = (X & Z) | (Y & ~Z)
H(X,Y,Z) = X ^ Y ^ Z
I(X,Y,Z) = Y ^ (X | ~Z)

这些函数用于各轮次的非线性变换。

  1. 每轮 16 次操作(共 4 轮,64 次)

Round 1 - 使用 F 函数 + FF 宏:

kotlin 复制代码
/* ROTATE_LEFT rotates x left n bits.
 */
#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))

#define F(x,y,z) (((x) & (y)) | ((~x) & (z)))
#define FF(a, b, c, d, x, s, ac) { \
  (a) += F((b),(c),(d)) + (x) + (UINT4)(ac); \
  (a) = ROTATE_LEFT((a), (s)); \
  (a) += (b); \
}
  • 每轮使用不同函数(F/G/H/I),分别执行 16 步;
  • 每步涉及:
    • 输入块 x[k]
    • 常量 ac = T[i] = 2^32 * abs(sin(i))
    • 左移位数 s = Sij
  • 例如:
kotlin 复制代码
FF (a, b, c, d, x[0], S11, 0xd76aa478); // 第1步
FF (d, a, b, c, x[1], S12, 0xe8c7b756); // 第2步

Round 2 - 使用 G 函数 + GG 宏

Round 3 - 使用 H 函数 + HH 宏

Round 4 - 使用 I 函数 + II 宏

每轮操作都是在不断改变 a,b,c,d 的值,最终加回到 state[] 中。

五、输出摘要

kotlin 复制代码
Encode(digest, context->state, 16);
  • state[0]~state[3] 转为 16 字节小端字节串;
  • 将 A、B、C、D 拼接起来,构成 128 位(16 字节)结果 。
  • 最终的 digest[16] 就是 MD5 的结果,输出唯一的128位哈希值。

看不懂?我也看不懂,毕竟有的算法是别人究其一生才研究出来的,难度还是有的,我们只需要了解即可。

这种方法不是很保险,因为我们无法防止别人反编译我们的 apk,因为 ndk 本身原因无法对方法进行混淆,别人只要反编译你的apk 拿到so库并且匹配好包名和方法名照样在任何地方都可以调用,怎么办?


2. 增加包名及签名校验

为了防止别人拿到你的.so后直接拿去使用,我们可以在应用启动时先进行包名和签名校验,校验通过可以正常使用,不通过则无法使用!

在Java中拿到包名和签名信息很简单,如下:

java 复制代码
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
return signatures[0].toCharsString();

而在JNI中要写辣么多代码,其实C++调用Java代码,和Java中反射很像,类比着去看其实也没那么复杂。

cpp 复制代码
//校验签名
static int is_verify = 0;
static char *PACKAGE_NAME = "com.xaye.myjni";
// debug 包的签名,打正式包  需要将 APP_SIGNATURE 修改为  正式 包的签名!
static char *APP_SIGNATURE = "308202e4308201cc020101300d06092a864886f70d010105050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3234313033303135343530385a180f32303534313032333135343530385a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100a5bfb2191c8fc174b11907f7c1fb2d7809a9543f43a17c2d84b0c4f0c7b71361c0a33f98c0a6e45d54d5eb4229a93dcfb78fd8e62057cbf67e84b09b33a06ef10ddd3576ec77c5207601f0d47a4fd889360dbb1bfb60d19adb35d78cc53c39e4134211f2d2262f9cbc5372c38fb9efe778ddace3642201bab0f2da2b0e4b5cee1c88039c5554f7281cda90f1780967e07cc623c339135fcc7f020b5cda9fbab2829b2863f7f94b694140fa744a05af5700946f81f5fa3a3a8cef125367640946d6c73ad957050eff33a63bc3fd82f1fbc8ba3087a0ad31043d266368897208f6dbe350a7aba822a0e2b0ff1f6e94ea94c187dcc8328980a4bcdae7e30301a60d0203010001300d06092a864886f70d010105050003820101001614fc5f47f814ae0814eff343441e13d56f31fb2ea553de94cf90d37e9da0fe3ed5870fe57afb2bbadec1368258955b84ddf39a445379544286ec74d9c3251ca9f5417eb0b9c750af9b32534f05816440a0bb5a9eded76c507032b39ebb628789e5bdcfd13363b7cdb94a4f4ed2869d8a3604c3617676e101161ffe3ea6aec934b01b093db45fc264a3a29de59753513d74d3d94395cbbc0328c36d5086f414a62ddff736634f0d173141d80a5417ed3a14fc9085b2db223280d354caf661b123ae569d1f284e0eff2cf6972f5bd8b2e53b81256ba55d83df9309fb0c1dc1a8bade54587dfcaaed535768d6cd64f7983f9666ed5502929a298d6b42b19be9cc";

extern "C"
JNIEXPORT jstring JNICALL
Java_com_xaye_myjni_jni_SignatureUtils_signatureVerify(JNIEnv *env, jclass clazz, jobject context) {
    // C 调用 Java 代码,分为以下四步

    // 1. 获取包名
    jclass j_clz = env->GetObjectClass(context);
    jmethodID j_method_id = env->GetMethodID(j_clz, "getPackageName", "()Ljava/lang/String;");
    jstring j_package_name = (jstring)env->CallObjectMethod(context, j_method_id);

    // 2. 比对包名是否一样
    const char *c_package_name = env->GetStringUTFChars(j_package_name, nullptr);
    if (strcmp(PACKAGE_NAME, c_package_name) != 0) {
        return env->NewStringUTF("error_package_name");
    }
    LOGI("包名一致:%s", c_package_name);
    // 3. 获取签名

    // 3.1 获取 PackageManager , 先获取方法id ,用这个id 去对应的对象获取,都是按这个步骤。
    j_method_id = env->GetMethodID(j_clz, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject j_package_manager = env->CallObjectMethod(context, j_method_id);

    // 3.2 获取 PackageInfo
    j_clz = env->GetObjectClass(j_package_manager);
    j_method_id = env->GetMethodID(j_clz, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jobject j_package_info = env->CallObjectMethod(j_package_manager, j_method_id, j_package_name, 0x00000040);

    // 3.3 Signature[] 数组
    j_clz = env->GetObjectClass(j_package_info);
    jfieldID j_field_id = env->GetFieldID(j_clz, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray j_signatures = (jobjectArray)env->GetObjectField(j_package_info, j_field_id);

    // 3.4 获取 signatures[0]
    jobject j_signature_first = env->GetObjectArrayElement(j_signatures, 0);

    // 3.5 获取 signatures[0].toCharsString()
    j_clz = env->GetObjectClass(j_signature_first); // 获取 Signature 类,因为 signatures[0] 是 Signature 类
    j_method_id = env->GetMethodID(j_clz, "toCharsString", "()Ljava/lang/String;");
    jstring j_signature_str = (jstring)env->CallObjectMethod(j_signature_first, j_method_id);
    
    // 4. 签名比对
    const char *c_signature_str = env->GetStringUTFChars(j_signature_str, nullptr);
    if (strcmp(c_signature_str, APP_SIGNATURE) != 0) {
        return env->NewStringUTF("error_signature");
    }
    
    is_verify = 1;
    LOGI("签名校验成功√");
    return env->NewStringUTF("success");
}

那么,做到这一步是不是就真正的安全了,当然,只要你的签名文件没被泄露,还是能够达到90%的安全的,比如你的对手很厉害,能够使用 Xpose 调试,Native 线程轮询 tracep_id ... 那么你就得进一步进行加密了,哈哈。


我学习JNI的代码,和本文源码及C++经md5算法,已经上传GitHub:github.com/wuxaye/JNIS...

相关推荐
giaoho4 小时前
Android 系统架构
android·系统架构
m0_659394007 小时前
常见的cms框架的webshell方法
android
fatiaozhang95278 小时前
中兴云电脑W101D2-晶晨S905L3A-2G+8G-安卓9-线刷固件包
android·网络·电脑·电视盒子·刷机固件·机顶盒刷机
IT乐手9 小时前
java 或 安卓项目中耗时统计工具类
android·java
wang_hao..9 小时前
Day4.AndroidAudio初始化
android·音频
维尔切10 小时前
Linux中ssh远程登录原理与配置
android·linux·ssh
louisgeek11 小时前
Android Media3 PlayerView 监听 SurfaceTextureListener
android
广煜永不挂科11 小时前
Android Studio关于Connection refused: connect报错
android·ide·android studio
林林要一直努力11 小时前
Android Studio安装,SDK、Gradle、模拟器、AS路径改为非C盘(Windows为例)
android·ide·android studio
编程乐学11 小时前
网络资源模板--基于Android Studio 实现的课程管理App
android·android studio·大作业·移动端开发·安卓移动开发·课程管理