Skip to content

Android NDK (C/C++)

本文提供基于纯 C 语言和 ARM64 内联汇编的 Android NDK 接入示例,专为高安全需求场景(防逆向、防 Hook)设计。

概述

本 SDK 示例为 Android 应用提供底层强对抗级别的网络验证支持,核心特性包括:

  • 纯 C 实现:无 C++ 运行时依赖,体积小巧
  • ARM64 内联汇编 Syscall:使用 SVC #0 指令直接发起系统调用(socket/connect/send/recv 等),完全绕过 libc 函数,极大增加 Hook 难度
  • 无网络库依赖:内置轻量级 HTTP 客户端,不依赖 libcurl 等常见开源库
  • 自定义字符串函数_strcmp_strlen 等全部自行实现,避免 libc 字符串函数被拦截
  • 敏感数据保护:XOR 内存保护 + 安全清零(sensitive data 用后即清)

环境要求

  • NDK 版本:r21 及以上
  • 目标架构:仅 arm64-v8a(64 位 ARM)
  • 构建工具ndk-build

下载

📦 下载示例源码

项目结构

text
android-cpp/
├── Android.mk              # ndk-build 构建脚本
├── Application.mk          # ABI 和平台配置
├── main.c                  # 演示入口(卡密/试用登录完整流程)
├── nullverify.h            # 核心 SDK API(登录/心跳/退出)
├── nullverify_config.h     # 开发者配置项(需修改)
├── nullverify_http.h       # 基于 ARM64 Syscall 的 HTTP 客户端
├── nullverify_crypto.h     # MD5 / SHA256 / HMAC-SHA256 / Base64
├── nullverify_device.h     # 底层设备指纹采集
├── nullverify_sign.h       # 请求签名 + 响应签名校验
├── nullverify_svc.h        # ARM64 内联汇编 Syscall 封装
└── cjson/
    ├── cJSON.h             # 轻量级 JSON 解析库(vendored)
    └── cJSON.c

配置说明

打开 nullverify_config.h,修改为你在管理后台获取的应用配置:

c
#define NV_APP_KEY       "your_app_key"      // 应用标识
#define NV_APP_SECRET    "your_app_secret"    // 应用密钥
#define NV_API_BASE      "http://api.nullverify.com"  // API 地址
#define NV_API_HOST      "api.nullverify.com" // HTTP Host 头
#define NV_API_PORT      80                   // 端口
#define NV_SIGN_ALGO     1                    // 1=MD5, 2=HMAC-SHA256

编译与运行

bash
# 编译(在包含 Android.mk 的目录下执行)
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk

# 推送到设备
adb push libs/arm64-v8a/nullverify_demo /data/local/tmp/

# 运行
adb shell chmod +x /data/local/tmp/nullverify_demo
adb shell /data/local/tmp/nullverify_demo

完整示例:卡密登录

以下是卡密登录 → 心跳保活 → 退出的核心流程:

c
#include "nullverify.h"
#include <stdio.h>

void demo_card(const char *device_id) {
    const char *card = "YOUR_CARD_NUMBER";

    // 1. 登录
    nv_card_login_result_t login_res;
    int code = nv_card_login(card, device_id, &login_res);
    if (code != 0) {
        printf("登录失败: code=%d msg=%s\n",
               login_res.meta.code, login_res.meta.message);
        return;
    }

    printf("登录成功!Token: %s\n", login_res.token);
    printf("到期时间: %s\n", login_res.expires);
    printf("心跳间隔: %d\n", login_res.hg);

    // 2. 心跳保活(间隔使用 login_res.hg,不要固定 30 秒)
    struct nv_timespec sleep_ts = { login_res.hg, 0 };
    nv_heartbeat_result_t hb_res;

    while (1) {
        _nanosleep(&sleep_ts, 0);
        code = nv_card_heartbeat(card, login_res.token, &hb_res);
        if (code != 0) {
            printf("心跳失败: %s\n", hb_res.meta.message);
            break;
        }
        printf("心跳正常,到期: %s\n", hb_res.expires);
    }

    // 3. 退出登录
    nv_result_meta_t logout_res;
    nv_card_logout(card, login_res.token, &logout_res);
    printf("已退出登录\n");

    // 清理敏感数据
    nv_secure_bzero(&login_res, sizeof(login_res));
}

int main(void) {
    char device_id[65];
    nv_get_device_fingerprint(device_id);
    demo_card(device_id);
    return 0;
}

试用登录

如果应用后台开启了试用功能,可以进行试用设备的登录和保活:

c
void demo_trial(const char *device_id) {
    // 1. 试用登录
    nv_trial_login_result_t trial_res;
    int code = nv_trial_login(device_id, &trial_res);
    if (code != 0) {
        printf("试用登录失败: %s\n", trial_res.meta.message);
        return;
    }

    printf("试用登录成功!到期: %s\n", trial_res.expires);

    // 2. 试用心跳(仅需 device_id,不需要 token)
    struct nv_timespec sleep_ts = { trial_res.hg, 0 };
    nv_heartbeat_result_t hb_res;

    while (1) {
        _nanosleep(&sleep_ts, 0);
        code = nv_trial_heartbeat(device_id, &hb_res);
        if (code != 0) break;
        printf("试用心跳正常,到期: %s\n", hb_res.expires);
    }
}

试用心跳

试用心跳接口 nv_trial_heartbeat 不需要 token 参数,仅传递 device_id 即可。这与卡密心跳(需要 card + token)不同。

签名算法说明

SDK 内部实现了两种签名算法,通过 NV_SIGN_ALGO 配置项选择:

MD5 签名(默认)

sign = md5(sorted_params + secret)

HMAC-SHA256 签名

sign = hmac_sha256(sorted_params, secret)

其中 secret 作为 HMAC 密钥参与运算,不拼接到原始字符串中。

参数排序规则

  • 所有请求参数(不含 sign)按 key 字典序升序排列
  • 格式为 key1=value1&key2=value2&...
  • 使用参数原始值,不进行 URL 编码
  • timestamp秒级时间戳

设备指纹

nullverify_device.h 通过以下方式生成设备唯一标识:

  1. 读取系统属性:ro.product.brandro.product.modelro.serialno
  2. 通过 Netlink RTM_GETLINK 读取网络接口 MAC 地址
  3. 将所有属性拼接后计算 SHA-256 哈希
device_id = hex(sha256(properties + mac_addresses))

输出为 64 字符的小写十六进制字符串。

响应解密

当服务端启用响应加密后,HTTP 响应体为纯密文字符串(base64 编码),Content-Typetext/plain。SDK 会自动检测并解密响应,对上层 API 调用透明。

配置

nullverify_config.h 中设置解密算法:

c
/* 响应解密算法 */
#define NV_RESP_DEC_NONE    0   /* 不解密(服务端未启用加密) */
#define NV_RESP_DEC_AUTO    1   /* 自动检测(推荐) */
#define NV_RESP_DEC_RC4     2   /* 强制 RC4 */
#define NV_RESP_DEC_AES_CBC 3   /* 强制 AES-256-CBC */
#define NV_RESP_DEC_DES_CBC 4   /* 强制 DES-CBC */

/* AUTO 会根据密文格式自动识别算法 */
#define NV_RESPONSE_DECRYPT_ALGO  NV_RESP_DEC_AUTO

编译期裁剪

设置为具体算法(如 NV_RESP_DEC_RC4)时,未使用的算法代码不会被编译,可减小二进制体积。AUTO 模式会编译所有算法。

密钥来源

独立加密密钥:在 nullverify_config.h 中通过 NV_RESPONSE_ENCRYPT_KEY 配置,该密钥从管理后台"应用详情 → 安全配置"获取,为 64 字符 hex 字符串。此密钥由服务端为每个应用独立生成,APP_SECRET 无关

c
#define NV_RESPONSE_ENCRYPT_KEY "your_encrypt_key_hex_from_dashboard"
算法密钥长度
RC4hex_decode(key)[:16] = 16 字节
AES-CBChex_decode(key)[:32] = 32 字节
DES-CBChex_decode(key)[:8] = 8 字节

纯密文响应格式

启用加密后,服务端响应体为纯密文字符串(非 JSON 包装):

// 加密响应示例(AES-CBC)
dGhpcyBpcyBpdi4=.Y2lwaGVydGV4dC4=

SDK 内部自动判断响应类型:

  • { 开头 → 明文 JSON,直接解析
  • 其他 → 纯密文,先解密再解析 JSON

对上层的 nv_card_login() 等 API 调用无影响。

AUTO 检测逻辑

密文格式判断算法
. 分隔符RC4base64(cipher)
1 个 .,IV 长度 16 字节AES-256-CBCbase64(iv).base64(cipher)
1 个 .,IV 长度 8 字节DES-CBCbase64(iv).base64(cipher)
2+ 个 .RSA(不支持)返回错误

安全建议

  • 推荐使用 NV_RESP_DEC_AUTO 自动检测,兼容服务端随时切换算法
  • 推荐优先选择 aes-cbc,安全性与性能平衡最佳
  • rc4 仅适合低安全需求场景
  • RSA 混合加密暂不支持(需引入大数库,超出 header-only 范围)
  • 所有密钥和明文在使用后会被 nv_secure_bzero() 安全清零

注意事项

架构限制

nullverify_svc.h 中的系统调用号和寄存器约定仅适用于 arm64-v8a(AArch64)。不支持 32 位设备(armeabi-v7a)或 x86/x86_64 模拟器。在不支持的架构上编译运行将导致崩溃。

协议限制

本 SDK 仅支持 HTTP 协议,不包含 TLS 支持,也不支持系统代理。如需 HTTPS 通信,建议:

  • 引入 mbedTLS 库实现 TLS
  • 或通过 JNI 桥接 Java 层的安全网络栈

安全性建议

  1. 代码混淆:建议使用 OLLVM 对源码进行控制流平坦化、虚假控制流、字符串加密处理,尤其是 nullverify_config.h 中的密钥
  2. 避免浅层封装:不要将验证逻辑封装成简单的 JNI 接口(如 boolean checkCard(String card))暴露给 Java 层,这样很容易被 Frida/Xposed 拦截。建议将核心业务逻辑也写在 C/C++ 中,验证成功后再放行

错误处理

建议对所有 API 调用进行错误码判断:

c
nv_card_login_result_t res;
int code = nv_card_login(card, device_id, &res);
switch (code) {
    case 0:     printf("登录成功\n"); break;
    case 10210: printf("卡密已过期\n"); break;
    case 10213: printf("超过多开上限\n"); break;
    case 10218: printf("卡密不存在\n"); break;
    case -1:    printf("网络错误\n"); break;
    default:    printf("错误: %s\n", res.meta.message); break;
}

完整错误码列表请参考 错误码对照表

面向脚本与插件开发者的网络验证系统