外观
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 通过以下方式生成设备唯一标识:
- 读取系统属性:
ro.product.brand、ro.product.model、ro.serialno等 - 通过 Netlink
RTM_GETLINK读取网络接口 MAC 地址 - 将所有属性拼接后计算 SHA-256 哈希
device_id = hex(sha256(properties + mac_addresses))输出为 64 字符的小写十六进制字符串。
响应解密
当服务端启用响应加密后,HTTP 响应体为纯密文字符串(base64 编码),Content-Type 为 text/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"| 算法 | 密钥长度 |
|---|---|
| RC4 | hex_decode(key)[:16] = 16 字节 |
| AES-CBC | hex_decode(key)[:32] = 32 字节 |
| DES-CBC | hex_decode(key)[:8] = 8 字节 |
纯密文响应格式
启用加密后,服务端响应体为纯密文字符串(非 JSON 包装):
// 加密响应示例(AES-CBC)
dGhpcyBpcyBpdi4=.Y2lwaGVydGV4dC4=SDK 内部自动判断响应类型:
- 以
{开头 → 明文 JSON,直接解析 - 其他 → 纯密文,先解密再解析 JSON
对上层的 nv_card_login() 等 API 调用无影响。
AUTO 检测逻辑
| 密文格式 | 判断 | 算法 |
|---|---|---|
无 . 分隔符 | RC4 | base64(cipher) |
1 个 .,IV 长度 16 字节 | AES-256-CBC | base64(iv).base64(cipher) |
1 个 .,IV 长度 8 字节 | DES-CBC | base64(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 层的安全网络栈
安全性建议
- 代码混淆:建议使用 OLLVM 对源码进行控制流平坦化、虚假控制流、字符串加密处理,尤其是
nullverify_config.h中的密钥 - 避免浅层封装:不要将验证逻辑封装成简单的 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;
}完整错误码列表请参考 错误码对照表。