本章目标是理解 Android App 中的 Native 层:如何发现
.so,如何定位 JNI 入口,如何用静态工具分析二进制,如何用 Frida 在授权 Demo 上观察 Native 函数行为。
1. 为什么需要 Native 逆向
很多 App 会把关键逻辑放到 Native 层:
| 场景 | 为什么放 Native | 逆向关注点 |
|---|---|---|
| 加密签名 | 增加 Java 层分析难度 | JNI 调用、密钥来源、算法参数 |
| 环境检测 | 检查 root、debug、frida、模拟器 | 系统调用、文件路径、进程信息 |
| 完整性校验 | 校验 APK、DEX、签名、so 自身 | 哈希计算、签名证书摘要 |
| 性能模块 | 图像、音视频、算法、游戏逻辑 | 输入输出、边界条件 |
| 第三方 SDK | 风控、支付、广告、安全 SDK | SDK 行为和数据采集边界 |
Native 层不是绝对安全,只是提高分析门槛。只要逻辑在客户端执行,就能被观察、调试、Hook 或模拟。
2. so 文件识别
2.1 从 APK 中发现 Native 库
unzip-lapp-debug.apk|grep"lib/.*\\.so"mkdir-pcase-reversedemo/04-nativeunzip-papp-debug.apk lib/arm64-v8a/libreversedemo.so>case-reversedemo/04-native/libreversedemo.sofilecase-reversedemo/04-native/libreversedemo.soABI 说明:
| ABI | 说明 |
|---|---|
arm64-v8a | 64 位 ARM,现代真机常见 |
armeabi-v7a | 32 位 ARM,旧设备或兼容包 |
x86/x86_64 | 模拟器常见 |
分析时要确认设备运行的是哪个 ABI 的 so。Hook 地址、寄存器和调用约定会受架构影响。
2.2 基础命令
readelf-hlibreversedemo.so readelf-slibreversedemo.so|grepJava_ readelf-dlibreversedemo.so strings libreversedemo.so|head-100nm-Dlibreversedemo.so|grepJava_检查重点:
| 命令 | 看什么 |
|---|---|
readelf -h | ELF 架构、位数、端序 |
readelf -s | 符号表,是否导出 JNI 函数 |
readelf -d | 依赖库 |
strings | 字符串线索、URL、路径、错误信息 |
nm -D | 动态符号,导出函数 |
3. JNI 定位方法
3.1 静态注册 JNI
Java/Kotlin:
classNativeGuard{externalfunnativeCheck():Booleancompanionobject{init{System.loadLibrary("reversedemo")}}}Native:
extern"C"JNIEXPORT jboolean JNICALLJava_com_example_reversedemo_NativeGuard_nativeCheck(JNIEnv*,jobject){returntrue;}静态注册特点:
- 函数名包含
Java_包名_类名_方法名。 readelf -s或nm -D可能直接看到符号。- jadx 中能从
System.loadLibrary和external fun反向定位。
3.2 动态注册 JNI
Native 代码可能通过RegisterNatives注册:
staticJNINativeMethod methods[]={{"nativeCheck","()Z",(void*)native_check_impl}};动态注册特点:
- 导出符号里可能看不到完整 Java 风格函数名。
- 需要找
JNI_OnLoad、RegisterNatives、方法名字符串。 - Ghidra 中可通过字符串交叉引用定位实现函数。
4. Ghidra 基础分析流程
4.1 导入和自动分析
- 新建 Ghidra Project。
- Import
libreversedemo.so。 - 选择默认分析器,执行 Auto Analyze。
- 在 Symbol Tree 中查看 Exports、Functions、Strings。
- 搜索
nativeCheck、JNI_OnLoad、/system/bin/su、frida、TracerPid。
4.2 重点窗口
| 窗口 | 用途 |
|---|---|
| Listing | 看汇编、跳转、引用 |
| Decompiler | 看近似 C 代码 |
| Symbol Tree | 找函数和导出符号 |
| Defined Strings | 找路径、错误信息、密钥线索 |
| Function Graph | 看控制流 |
| References | 找字符串或函数被谁使用 |
4.3 Demo:分析 root 检测
示例 Native 逻辑:
boolhas_su(){returnaccess("/system/bin/su",F_OK)==0||access("/system/xbin/su",F_OK)==0;}在 Ghidra 中:
- 搜索字符串
/system/bin/su。 - 查看交叉引用。
- 进入引用函数。
- 看是否调用
access、stat、open。 - 重命名函数为
check_su_path。 - 记录返回值含义。
报告写法:
## Native root 检测 - so:`libreversedemo.so` - 字符串:`/system/bin/su`、`/system/xbin/su` - 调用:`access(path, F_OK)` - 逻辑:任一路径存在则认为环境风险较高 - 风险:如果直接阻断业务,可被 Hook `access` 或修改返回值绕过 - 建议:作为风险评分信号,与服务端风控、设备完整性和行为验证结合5. Native Frida Hook
5.1 Hook Java 层 Native 方法
最简单方式是先 Hook Java 声明的方法:
Java.perform(function(){constNativeGuard=Java.use("com.example.reversedemo.NativeGuard");NativeGuard.nativeCheck.implementation=function(){constresult=this.nativeCheck();console.log("[nativeCheck] original="+result);returntrue;};});优点:不需要处理地址和 ABI。缺点:如果 App 在 Native 内部继续调用其他检测函数,Java 层 Hook 可能覆盖不了细节。
5.2 Hook 导出函数
constmoduleName="libreversedemo.so";constsymbol="Java_com_example_reversedemo_NativeGuard_nativeCheck";constaddr=Module.findExportByName(moduleName,symbol);console.log("nativeCheck addr="+addr);Interceptor.attach(addr,{onEnter(args){console.log("[nativeCheck] enter");},onLeave(retval){console.log("[nativeCheck] original retval="+retval);retval.replace(1);}});验证:
| 验证项 | 通过标准 |
|---|---|
| 找到地址 | 控制台打印非空地址 |
| Hook 命中 | 调用时打印 enter |
| 返回值改变 | App 行为发生变化 |
| 证据保存 | 保存脚本和日志 |
5.3 Hook libc 函数观察检测路径
constaccessPtr=Module.findExportByName(null,"access");Interceptor.attach(accessPtr,{onEnter(args){this.path=args[0].readCString();},onLeave(retval){if(this.path&&this.path.indexOf("su")>=0){console.log("[access] "+this.path+" => "+retval);}}});这个脚本用于观察 Demo 是否检查su路径。不要把这类脚本用于未授权绕过第三方 App 风控。
6. Native 防护知识点
6.1 常见检测
| 检测 | Native 实现线索 | 局限 |
|---|---|---|
| root 检测 | access("/system/bin/su")、which su | 路径可隐藏,调用可 Hook |
| 调试检测 | ptrace、TracerPid、prctl | 可被绕过,可能误伤测试环境 |
| Frida 检测 | 端口、线程名、maps、符号 | 对抗成本高,易产生兼容问题 |
| 完整性校验 | 读取 APK/DEX/so 哈希 | 客户端校验仍可被 Patch |
| 签名校验 | 证书摘要比对 | 摘要可被定位和修改 |
6.2 正确防护思路
Native 防护适合提高成本,但不能承担唯一安全结论:
- 客户端检测只作为风险信号。
- 核心权益、支付、提现、订单、身份认证必须服务端校验。
- 检测结果要和账号、设备、行为、接口频率、服务端风控合并判断。
- 防护上线后必须复测误报、兼容性、性能和可维护性。
7. 本章 Demo:Native 分析任务
7.1 任务清单
- 从 APK 提取
libreversedemo.so。 - 用
file、readelf、strings记录 ELF 信息和字符串线索。 - 在 jadx 中找到
System.loadLibrary和external fun nativeCheck()。 - 在 Ghidra 中定位 JNI 函数。
- 找到 root 检测字符串和
access调用。 - 用 Frida Hook Java 层
nativeCheck()。 - 用 Frida Hook Native 导出函数或 libc
access()。 - 输出 Native 分析报告。
7.2 验证矩阵
| 验证项 | 工具 | 通过标准 | 证据 |
|---|---|---|---|
| so 提取 | unzip | 得到目标 ABI 的 so | 01-so-list.txt |
| ELF 信息 | readelf | 记录架构和动态符号 | 02-readelf.txt |
| 字符串线索 | strings | 找到检测路径或错误信息 | 03-strings.txt |
| JNI 定位 | jadx + Ghidra | Java 方法和 Native 函数对应 | 04-jni-map.md |
| Hook Java | Frida | nativeCheck()命中 | 05-hook-java.log |
| Hook Native | Frida | Native 函数或access命中 | 06-hook-native.log |
8. 报告模板
# Native Analysis Report ## so 信息 - 文件: - ABI: - 架构: - 导出符号: - 依赖库: ## JNI 映射 | Java 方法 | Native 函数 | 注册方式 | 证据 | ## 关键字符串 | 字符串 | 引用函数 | 推测用途 | ## 动态验证 | Hook 点 | 参数/返回值 | 行为变化 | ## 风险与建议 - 风险: - 影响: - 局限: - 修复:9. 常见问题
| 问题 | 原因 | 处理 |
|---|---|---|
| 找不到 JNI 符号 | 动态注册或符号被 strip | 找JNI_OnLoad、RegisterNatives、字符串引用 |
| Ghidra 反编译很乱 | 优化、混淆、控制流复杂 | 先从字符串和导入函数入手 |
| Hook 地址为空 | so 未加载或符号未导出 | 等待System.loadLibrary后再查,或按基址加偏移 |
| 返回值替换无效 | Hook 点不是最终判断 | Hook 更下游函数或 Java 调用点 |
| App 闪退 | ABI、返回类型、调用时机错误 | 先只打印,确认调用约定 |
10. 本章交付物
case-reversedemo/ 04-native/ 01-so-list.txt 02-readelf.txt 03-strings.txt 04-jni-map.md 05-ghidra-notes.md 06-hook-java.js 07-hook-java.log 08-hook-native.js 09-hook-native.log 10-native-report.md11. Native 基础补强
11.1 ELF 文件结构
Android.so是 ELF 动态库。逆向时至少要理解:
| 区域 | 作用 | 分析意义 |
|---|---|---|
| ELF Header | 架构、位数、入口、类型 | 判断 arm64/armv7/x86 |
| Program Headers | 加载段 | 理解内存映射 |
| Section Headers | 代码、数据、符号等节 | 静态分析入口 |
.text | 机器指令 | 反汇编主体 |
.rodata | 只读数据和字符串 | 路径、错误信息、常量 |
.dynsym | 动态符号 | 导出函数和导入函数 |
.init_array | 初始化函数 | 反调试和注册逻辑常见位置 |
.got/.plt | 动态链接跳转 | Hook 和导入调用线索 |
命令:
readelf-hlibreversedemo.so readelf-Slibreversedemo.so readelf-llibreversedemo.so readelf-dlibreversedemo.so11.2 ABI 与寄存器基础
| 架构 | 常见设备 | 参数传递 | 返回值 |
|---|---|---|---|
| arm64-v8a | 现代真机 | x0-x7 | x0 |
| armeabi-v7a | 旧设备 | r0-r3 | r0 |
| x86_64 | 模拟器 | System V 风格 | rax |
Frida Hook Native 函数时,args[0]、args[1]对应底层参数。JNI 函数前两个参数通常是:
JNIEnv *jobject或jclass
真实 Java 方法参数从args[2]开始。
12. JNI 深度
12.1 JNI 类型映射
| Java 类型 | JNI 类型 | 签名 |
|---|---|---|
boolean | jboolean | Z |
int | jint | I |
long | jlong | J |
String | jstring | Ljava/lang/String; |
byte[] | jbyteArray | [B |
Object | jobject | Ljava/lang/Object; |
static方法 | jclass第二参数 | 取代jobject |
12.2 JNI 方法签名
示例:
externalfunsign(path:String,body:ByteArray,timestamp:Long):String签名:
(Ljava/lang/String;[BJ)Ljava/lang/String;理解签名有助于:
- 识别
RegisterNatives表。 - 在 Ghidra 中重命名函数。
- Hook Native 参数。
- 对照 Java 层 external 方法。
12.3 RegisterNatives 分析
动态注册常见结构:
staticJNINativeMethod gMethods[]={{"nativeCheck","()Z",(void*)native_check},{"nativeSign","(Ljava/lang/String;)Ljava/lang/String;",(void*)native_sign},};Ghidra 查找步骤:
- 搜索字符串
nativeCheck。 - 找交叉引用。
- 查看附近是否存在方法名、签名、函数指针三元组。
- 找
RegisterNatives调用。 - 追踪函数指针到真实实现。
13. Native 静态分析套路
13.1 字符串优先
优先搜索这些字符串:
| 类型 | 字符串 |
|---|---|
| root | /system/bin/su、magisk、busybox |
| debug | TracerPid、ptrace、gdb、lldb |
| frida | frida、gum-js-loop、27042 |
| xposed | xposed、substrate、edxp |
| 签名 | SHA-256、MD5、signature |
| APK | .apk、classes.dex、base.apk |
| 网络 | https://、api、cert |
| 错误 | invalid、tamper、debugger |
13.2 导入函数优先级
| 函数 | 可能用途 |
|---|---|
access/stat/open | 文件检测、root 检测 |
ptrace | 反调试 |
fopen/read | 读取 maps、status、APK |
dlopen/dlsym | 动态加载和符号查找 |
pthread_create | 后台检测线程 |
strcmp/strstr | 字符串匹配 |
EVP_* | OpenSSL 加密 |
__system_property_get | 读取系统属性 |
13.3 控制流识别
Native 检测函数常见模式:
读取环境 -> 字符串匹配 -> 返回 true/false -> 上报 Java 层 -> 决定页面或接口行为分析时不要只停在“有 root 检测”,要回答:
- 检测结果被谁使用。
- 是否只影响 UI。
- 是否会上报服务端。
- 是否会阻断高风险接口。
- 是否存在误报处理。
14. Ghidra 操作细化
14.1 函数重命名规则
| 发现 | 建议命名 |
|---|---|
引用/system/bin/su | check_su_path |
引用TracerPid | check_tracer_pid |
调用RegisterNatives | register_native_methods |
| 计算 SHA-256 | calc_sha256 |
| 读取 APK | read_base_apk |
| 比较证书摘要 | compare_signature_digest |
| 返回环境状态 | build_risk_flags |
命名后要在报告中说明“这是分析者命名,不一定是原始函数名”。
14.2 交叉引用分析
Ghidra 中对字符串按X查看引用。每个引用要记录:
## 字符串引用记录 - 字符串: - 地址: - 引用函数: - 函数输入: - 函数输出: - 推测用途: - 后续 Hook 点:14.3 Decompiler 结果校验
反编译 C 代码可能不准确。要用以下方式交叉验证:
- 看汇编条件跳转。
- 看字符串引用。
- 看导入函数。
- 动态 Hook 参数和返回值。
- 和 Java 层调用结果对照。
15. Native Hook 深入
15.1 等待 so 加载
如果Module.findExportByName返回 null,可能 so 还没加载。
constdlopen=Module.findExportByName(null,"android_dlopen_ext");Interceptor.attach(dlopen,{onEnter(args){this.path=args[0].readCString();},onLeave(retval){if(this.path&&this.path.indexOf("libreversedemo.so")>=0){console.log("[loaded] "+this.path);}}});15.2 按偏移 Hook
当符号被 strip 时,可用基址加偏移:
constbase=Module.findBaseAddress("libreversedemo.so");consttarget=base.add(0x1234);Interceptor.attach(target,{onEnter(args){console.log("[target] enter");},onLeave(retval){console.log("[target] retval="+retval);}});偏移来源必须记录清楚:
- Ghidra 函数地址。
- so 加载基址。
- 架构 ABI。
- APK 版本和 so hash。
15.3 读取 jstring
JNIjstring不能直接readCString。可通过 Java API 或 Native helper 处理。简单场景建议先 Hook Java 层 external 方法,必要时再深入 Native。
记录原则:
- 如果参数读不出来,不要猜。
- 先打印指针和返回值。
- 再定位 Java 层包装方法。
- 最后处理 JNI 类型转换。
16. Native 风险库
| 编号 | 风险 | Native 证据 | 动态验证 | 修复 |
|---|---|---|---|---|
| N-001 | root 检测单点阻断 | access("/system/bin/su") | Hookaccess | 风险评分 |
| N-002 | 反调试单点阻断 | ptrace、TracerPid | Hook 返回 | 多信号判断 |
| N-003 | 硬编码密钥 | .rodata字符串 | Hook 加密函数 | 服务端密钥 |
| N-004 | 签名摘要硬编码 | SHA-256 常量 | Patch 比较 | 服务端校验 |
| N-005 | JNI 动态注册隐藏逻辑 | RegisterNatives | Hook Java external | 只提高成本 |
| N-006 | frida 检测误伤 | frida字符串 | 兼容性测试 | 分级响应 |
| N-007 | so 完整性只本地校验 | 读取自身 hash | Patch 返回 | 服务端挑战 |
| N-008 | Native 崩溃暴露 | tombstone/logcat | 触发异常 | 错误处理 |
17. Native Demo 作业
17.1 作业一:JNI 映射
任务:
- 找到 Java 层
NativeGuard。 - 找到
System.loadLibrary。 - 提取目标 ABI so。
- 用
readelf找导出符号。 - 用 Ghidra 定位真实函数。
- 输出 Java 方法到 Native 函数的映射表。
17.2 作业二:root 检测分析
任务:
- 搜索
/system/bin/su。 - 找引用函数。
- 识别
access调用。 - Hook
access打印路径。 - Hook Java
nativeCheck()观察返回值。 - 写出该检测的局限。
17.3 作业三:签名函数观察
任务:
- 在 Native 中加入一个
nativeSign(String input)。 - 用 Ghidra 找到输入字符串处理。
- 用 Frida Hook Java external 方法。
- 记录输入、输出和调用时机。
- 判断密钥是否出现在客户端。
18. Native 报告评分
| 项目 | 分值 | 要求 |
|---|---|---|
| so 信息 | 10 | ABI、hash、架构 |
| JNI 映射 | 20 | Java 和 Native 对应 |
| 静态分析 | 20 | 字符串、导入函数、控制流 |
| 动态 Hook | 20 | Java 或 Native 命中 |
| 风险判断 | 20 | 说明影响和局限 |
| 证据归档 | 10 | 命令、截图、脚本、日志 |
19. Native、JNI 与 so
Native 分析要把 Java external 方法、so 符号、JNI 注册、字符串引用、系统调用和动态 Hook 串起来。
ELF 与 so
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| ELF Header | 说明架构、位数、类型和入口信息。 | 执行readelf -h libreversedemo.so。 | ABI 不同仍混用地址。 |
| Section | .text、.rodata、.dynsym分别对应代码、字符串、符号。 | 用readelf -S查看。 | 只看导出符号,不看字符串。 |
| 动态符号 | 未 strip 的 so 可能暴露 JNI 函数。 | 用nm -D或readelf -s。 | 没有符号就停止分析。 |
| 字符串引用 | 路径、错误文案、算法名常在.rodata。 | 搜索/system/bin/su、TracerPid。 | 不追交叉引用。 |
| 导入函数 | 导入函数揭示文件、属性、加密和线程行为。 | 查access、ptrace、dlsym。 | 只看自定义函数名。 |
JNI 知识
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| 静态注册 | 通过Java_包名_类名_方法名绑定。 | 找到nativeCheck导出符号。 | 包名转义规则理解错误。 |
| 动态注册 | 通过RegisterNatives绑定方法名、签名和函数指针。 | 搜索方法名字符串。 | 没有Java_符号就认为无 JNI。 |
| JNI_OnLoad | so 加载时执行,常做注册和检测。 | Ghidra 定位JNI_OnLoad。 | 只分析被 Java 调用的方法。 |
| JNI 签名 | (Ljava/lang/String;[B)Z描述参数和返回值。 | 把 external 方法转成签名。 | Hook 参数时不理解类型。 |
| JNIEnv | JNI 函数前两个参数通常是 JNIEnv 和 jobject/jclass。 | Native Hook 时从args[2]看真实参数。 | 把args[0]当业务参数。 |
Native 防护与 Hook
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| root 检测 | 常检查 su、Magisk、系统目录和属性。 | Hookaccess打印路径。 | 把 root 检测作为唯一安全门。 |
| 反调试 | 常检查 ptrace、TracerPid、调试端口。 | 搜索TracerPid。 | 认为反调试不可绕过。 |
| Frida 检测 | 检查端口、线程名、maps、符号。 | 搜索frida、gum-js-loop。 | 过度检测导致误伤。 |
| 按偏移 Hook | strip 后用基址加偏移定位函数。 | Ghidra 地址转 Frida 偏移。 | 不同版本混用偏移。 |
| libc Hook | Hook 系统函数可观察 Native 行为。 | Hookopen、access、ptrace。 | 只 Hook Java 层看不到底层调用。 |
| Native 报告 | 必须记录 so hash、ABI、地址、函数、证据。 | 输出native-report.md。 | 报告里没有 hash 和地址,无法复现。 |