NDK 初探:基于 C++ 实现参数哈希与签名校验

NDK 是个啥?为什么用它做哈希?

NDK(Native Development Kit,原生开发套件)其实就是一个工具集,允许我们在 Android 应用中直接使用 C 和 C++ 代码。

它的能力范围和核心优势:

  • 极致的性能:比如游戏引擎、音视频处理(FFmpeg)、复杂的物理模拟等,C/C++ 的运行效率远超 Java。
  • 更高的安全性 :Java 编译后的代码比较容易被反编译,而 C/C++ 编译出的 .so 动态链接库(机器码),逆向破解难度要更大。

为什么要用 NDK 做哈希?

MD5 是一种哈希算法,可以将任意数据转换为固定长度的哈希值,常用于身份的快速匹配或数据完整性的校验。如果我们在 Java 层做接口参数的 MD5 签名处理,很容易就被别人反编译拿到签名逻辑和"盐值"。把这部分逻辑下沉到 C++ 层,就能增加接口被破解和伪造的难度。

接下来,我们来完成通过调用 Native 方法对接口参数哈希。

搭建 NDK 开发环境

首先打开 Tools-> SDK Manager,然后安装 SDK Tools 下的 NDK 和 CMake(C/C++外部构建工具)。

环境搭建好后,创建一个 Native C++ 模版项目即可,项目会自动生成 cpp 目录:

这里粘贴一下项目默认生成的代码,首先是 MainActivity,运行效果是界面会显示 "Hello from C++"。

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 调用 Native 方法
        binding.sampleText.text = stringFromJNI()
    }

    /**
     * 声明一个外部 Native 方法,实现在底层的 demo 库中
     */
    external fun stringFromJNI(): String

    companion object {
        // 在应用启动时加载 demo 底层库
        init {
            System.loadLibrary("demo")
        }
    }
}

还有存放原生 C/C++ 代码的 native-lib.cpp:

c++ 复制代码
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk_demo_MainActivity_stringFromJNI(
        JNIEnv* env, jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

最后是原生代码构建脚本 CMakeLists.txt,它指定了模块名是 demo:

python 复制代码
cmake_minimum_required(VERSION 3.22.1)
# 模块名
project("demo")

# 声明一个共享库(SHARED),并指定源文件路径
add_library(${CMAKE_PROJECT_NAME} SHARED
        native-lib.cpp)

# 链接 Android 系统的基础库和日志库
target_link_libraries(${CMAKE_PROJECT_NAME}
        android
        log)

通过 C++ 实现参数哈希

首先创建签名工具类单例对象 SignatureUtils:

kotlin 复制代码
/**
 * 签名工具类
 */
object SignatureUtils {
    // 必须在初始化时加载对应的 so 库,否则调用方法时应用会直接崩溃
    init {
        System.loadLibrary("demo")
    }
    
    /**
     * 对接口参数字符串生成数字签名
     * @return 参数哈希值
     */
    external fun signatureParams(params: String): String // 仿照默认生成的代码,来声明 Native 方法
}

接下来是 C++ 层的实现。我们先来了解一下 JNI(Java Native Interface),它是 Java 和 C/C++ 互相沟通的桥梁。在 C++ 里,我们没法直接使用 Java 中的 String,必须要通过 JNIEnv 这个工具来进行类型转换。

这里我直接把所有实现给出,目的是为了熟悉 NDK 开发流程:

c 复制代码
#include <jni.h>
#include <string>
#include "md5.h"
#include <sstream>
#include <iomanip>

using namespace std;

// 额外的加盐数据,增加 MD5 被彩虹表暴力破解的难度
const string EXTRA_DATA = "Q3ZkYLfxmyn3F4m9jz9j6DJdNfr9sUw2";

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_ndk_demo_SignatureUtils_signatureParams(JNIEnv *env, jobject thiz, jstring raw_params) {
    // 将 Java 的 jstring 转换成 C++ 的字符数组 (char*)
    const char *raw_params_str = env->GetStringUTFChars(raw_params, nullptr);

    // 拼接原始参数和"盐值"
    string salted_params(raw_params_str);
    salted_params.insert(0, EXTRA_DATA);

    // 用完 Java 传过来的字符串后,必须通知 JVM 释放内存,防止内存泄漏
    env->ReleaseStringUTFChars(raw_params, raw_params_str);

    // --------- 下面是标准的 C/C++ MD5 哈希计算流程 ---------
    MD5_CTX ctx;
    MD5Init(&ctx);
    MD5Update(&ctx, (const unsigned char *) salted_params.c_str(), salted_params.length());

    unsigned char md5_bytes[16];
    MD5Final(md5_bytes, &ctx);

    // 将 16 字节的二进制结果格式化为 32 位的十六进制字符串
    stringstream ss;
    for (unsigned char md5_byte : md5_bytes) {
        ss << hex << setw(2) << setfill('0') << (int) md5_byte;
    }
    string md5_hex = ss.str();
    // -----------------------------------------------------

    // 把 C++ 算好的字符串转换为 Java 的 String 并返回
    return env->NewStringUTF(md5_hex.c_str());
}

(注:MD5 的具体算法代码有些长就不贴了,可以自行搜索 "MD5算法 C语言实现")

写完后,需要在 CMakeList.txt 中加上 md5.cpp 的相对路径,不然在链接阶段,链接器找不到方法对应的实现。

python 复制代码
add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        native-lib.cpp md5.cpp)

现在运行,界面就会显示哈希后的结果,比如: "965f905eb3afa122d35e4b0b82165543"。

防止 so 库被盗用:添加签名校验

虽然现在 MD5 的算法逻辑和盐值不会轻易暴露,增加了破解难度,但无法防止别人直接反编译 Apk 获取 .so 文件,然后在他们自己的 App 里调用。

解决办法是进行包名和签名校验 。在 SignatureUtils 中定义一个校验方法:

kotlin 复制代码
/**
 * 签名校验
 */
external fun signatureVerify(context: Context) // 在 Application 初始化时调用

C++ 层获取应用签名的过程,就相当于在写 Java 的反射代码。具体实现如下:

c 复制代码
static int is_verify = 0;

// 我们自己 App 的包名
const string PACKAGE_NAME = "com.example.ndk.demo";
// 我们自己 App 的签名哈希值
const string APP_SIGNATURE = "你的应用签名"; 

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndk_demo_SignatureUtils_signatureVerify(JNIEnv *env, jobject thiz, jobject context) {
    // 1. 获取包名:相当于 Java 中的 context.getPackageName()
    jclass j_clz = env->GetObjectClass(context);
    jmethodID j_method_ID = env->GetMethodID(j_clz, "getPackageName", "()Ljava/lang/String;");
    auto j_package_name = (jstring) env->CallObjectMethod(context, j_method_ID);

    // 2. 比对包名:防止别人把 so 库拷走后,放在别的包名应用下运行
    const char *c_package_name = env->GetStringUTFChars(j_package_name, nullptr);
    if (strcmp(PACKAGE_NAME.c_str(), c_package_name) != 0) {
        env->ReleaseStringUTFChars(j_package_name, c_package_name);
        return; // 包名不一致,直接终止
    }
    env->ReleaseStringUTFChars(j_package_name, c_package_name);

    // 3. 获取签名:相当于 context.getPackageManager().getPackageInfo(...).signatures[0]
    jmethodID j_get_pm_id = env->GetMethodID(j_clz, "getPackageManager",
                                             "()Landroid/content/pm/PackageManager;");
    jobject j_pm = env->CallObjectMethod(context, j_get_pm_id);

    jclass j_pm_clz = env->GetObjectClass(j_pm);
    jmethodID j_get_pi_id = env->GetMethodID(j_pm_clz, "getPackageInfo",
                                             "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jclass j_pm_class = env->FindClass("android/content/pm/PackageManager");
    jfieldID j_get_signatures_id = env->GetStaticFieldID(j_pm_class, "GET_SIGNATURES", "I");
    jint j_flags = env->GetStaticIntField(j_pm_class, j_get_signatures_id);

    jobject j_pi = env->CallObjectMethod(j_pm, j_get_pi_id, j_package_name, j_flags);
    jclass j_pi_clz = env->GetObjectClass(j_pi);
    jfieldID j_signatures_id = env->GetFieldID(j_pi_clz, "signatures",
                                               "[Landroid/content/pm/Signature;");
    auto j_signatures = (jobjectArray) env->GetObjectField(j_pi, j_signatures_id);

    jobject j_signature = env->GetObjectArrayElement(j_signatures, 0);
    jclass j_signature_clz = env->GetObjectClass(j_signature);
    jmethodID j_to_chars_id = env->GetMethodID(j_signature_clz, "toCharsString",
                                               "()Ljava/lang/String;");
    auto j_signature_str = (jstring) env->CallObjectMethod(j_signature, j_to_chars_id);


    // 4. 比对签名:防止别人通过反编译二次打包
    const char *c_signature_str = env->GetStringUTFChars(j_signature_str, nullptr);
    if (strcmp(APP_SIGNATURE.c_str(), c_signature_str) != 0) {
        env->ReleaseStringUTFChars(j_signature_str, c_signature_str);
        return; // 签名不一致,终止
    }
    env->ReleaseStringUTFChars(j_signature_str, c_signature_str);

    // 5. 签名验证成功,改变状态位
    is_verify = 1;
}

最后,在前面的 signatureParams() 方法开头加上校验判断,这样就能避免一般人随意调用方法了:

c 复制代码
if (is_verify == 0) {
    return env->NewStringUTF("Signature verification not passed");
}

总结与进阶思考

现在,我们就完成了基础的参数哈希和签名校验逻辑,不过当前的方案还是有局限性:

  • 因为 Native 方法名并不能像 Java 那样被混淆,所以反编译后方法名一览无余。并且,如果破解者使用了 IDA 等逆向分析工具,还是能看到我们写死在代码里的"盐值"。

  • 即使我们加了 is_verify 标志位,别人依然可以通过 Xposed 等工具对程序进行动态调试,强行把内存里的 is_verify 改成 1 从而绕过校验。面对这种情况,通常需要加入反调试机制(比如轮询检测 transport ID 是否异常,一旦检测到被调试注入就强行退程序)。

当然,现在的我们只需了解即可。

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui