一、APK 中的 Java 层代码分析
1.1 Java 层逆向的整体目标
在逆向一个 APK 的 Java 层时,我们主要关注:
关注点 | 说明 |
---|---|
应用启动流程 | 包括 Application 、Activity 启动时做了哪些初始化 |
加密/签名函数 | 如 getSign() 、encrypt() 、generateToken() 等方法 |
网络请求构造 | URL、Headers、参数构造、是否签名等 |
检测函数 | 是否有反调试、root 检测、环境检查 |
动态加载逻辑 | 是否动态加载 dex、class 文件或反射调用 |
1.2 核心工具说明
1)jadx
开源免费,支持 .apk
、.dex
转 Java 源码,带 GUI。
- 安装:
brew install jadx
# 或直接从 release 下载 jadx-gui
- 使用:
jadx-gui target.apk
功能:
-
类/包结构清晰
-
支持搜索关键字(如 encrypt、sign)
-
可直接查看字符串、十六进制资源等
商业级逆向工具,支持 Java + native(.so)联动分析。
-
功能优势:
-
精准反编译 dex → Java;
-
支持动态分析 obfuscation(混淆);
-
能解析
.so
文件,与 Java 调用关系关联; -
插件丰富,支持 Python 脚本自动化。
-
3)jeb2py(JEB 插件脚本)
用 Python 脚本操控 JEB,提取方法、类、分析报告等。
示例功能:
-
批量导出所有类名、方法名;
-
批量提取网络相关方法;
-
自动标记
native
、encrypt
、sign
等方法; -
输出为 markdown/html 分析文档。
示例脚本(提取类名 + 方法):
from com.pnfsoftware.jeb.core.units.code.android import DexClass
for unit in RuntimeProject.getCurrentProject().getLiveArtifacts()[0].getUnits():
if isinstance(unit, DexClass):
print("[Class]", unit.getName(True))
for method in unit.getMethods():
print(" [Method]", method.getName())
1.3 逆向分析实战流程
步骤一:APK 解包与预处理
apktool d app.apk -o app_decoded
-
提取
AndroidManifest.xml
观察主启动组件和权限; -
提取
classes.dex
用于后续反编译。
步骤二:使用 jadx 查看结构和关键逻辑
打开 jadx GUI,加载 APK 或 .dex
文件:
搜索建议:
搜索内容 | 目的 |
---|---|
sign 、token 、encrypt | 定位加密函数 |
System.loadLibrary | 定位调用 native 代码 |
Retrofit.Builder 、OkHttpClient | 定位网络请求 |
Class.forName 、invoke | 定位反射调用 |
Application 、attachBaseContext | 定位启动初始化 |
示例关键函数定位:
public String getSign(String param) {
String token = md5(param + salt);
return Base64.encodeToString(token.getBytes(), Base64.DEFAULT);
}
步骤三:使用 JEB 交叉分析 Java 与 native
常用视角:
-
Java 方法中带
native
修饰 ➜ 对应.so
中实现; -
查找
RegisterNatives()
➜ 动态注册逻辑; -
使用
xref
查看函数调用关系; -
快速跳转 Java → native → Java 方法。
步骤四:结合 jeb2py 自动标记分析点
-
批量输出关键类、方法(混淆名也可);
-
与自己写的加密函数还原表做匹配;
-
自动提取网络请求类和 JSON 构造函数。
1.4 重点分析目标分类
类/模块 | 作用 | 逆向策略 |
---|---|---|
Application 子类 | 全局初始化 | 看是否注册 SDK、反调试 |
Activity /Fragment | 启动流程 | 看启动参数、页面加载逻辑 |
Utils /Sign 类 | 参数处理 | 搜索加密逻辑、构造流程 |
网络模块 Retrofit/OkHttp | 请求结构 | 分析 headers、参数来源 |
Loader/DexClassLoader | 动态加载 | 搜索 loadClass /dexPath |
反射/隐式调用 | 隐藏调用逻辑 | 搜索 Class.forName , invoke |
检测模块 | root, adb 检测 | 搜索关键词如 isDebuggerConnected , Build.TAGS |
1.5 混淆/壳处理方案
1)混淆命名恢复
-
使用
jeb2py
导出方法,手动或基于签名重命名; -
结合运行时
Frida
获取真实类名; -
部分函数名(如
getSign
,encrypt
)不会混淆; -
构造函数
、系统类继承关系
不可混淆,照样能追到逻辑。
2)代码壳(壳 Dex)
-
使用
dexdump
或dex2jar
分析 classesN.dex; -
分析
attachBaseContext()
➜ 多 dex 加载逻辑; -
查找
loadDex
,DexClassLoader
,BaseDexClassLoader
实例化点; -
动态调试下断点配合 dump Dex(可配合 Frida 或 Xposed 插件如 DexDump)。
1.6 实战技巧
技巧 | 方法 |
---|---|
查字符串使用处 | 右键字符串 → xref → 所在函数 |
网络 headers 分析 | 搜 addHeader , setHeader , put("token", ...) |
dump 混淆类结构 | 配合 jeb2py 或 jadx 导出树结构 |
快速找加密函数 | 搜 StringBuilder , Base64 , Cipher , MessageDigest 连续组合出现的函数 |
1.7 小结
工具 | 用途 |
---|---|
jadx-gui | 快速查看结构,初步分析类、函数 |
JEB | 深度分析、混淆处理、Java-native 联动 |
jeb2py | 自动提取关键函数、混淆函数名,还原逻辑 |
apktool | 解包资源、提取 smali 可修改逻辑 |
Frida | 动态验证分析,后续 Hook 解密流程 |
二、smali ↔ Java 结构对应
2.1 类结构对应
Java 代码
package com.example;
public class Demo extends Object {
private int count;
}
对应 smali
.class public Lcom/example/Demo;
.super Ljava/lang/Object;
.field private count:I
说明:
Java | smali |
---|---|
package | L包名/类名; |
extends Object | .super Ljava/lang/Object; |
int count | .field count:I |
String str | .field str:Ljava/lang/String; |
2.2 方法结构(构造函数、参数、返回)
Java 示例
public Demo(int c) {
this.count = c;
}
对应 smali
.method public constructor <init>(I)V
.registers 2
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput p1, p0, Lcom/example/Demo;->count:I
return-void
.end method
对照表
项目 | Java | smali |
---|---|---|
构造函数 | Demo(int c) | <init>(I)V |
参数 | int → I | p1 为第 1 参数 |
this | 隐式 | p0 代表 this |
赋值 | this.count = c | iput p1, p0, Lxx;->count:I |
2.3 基本操作与运算指令
Java 示例
int a = 2 + 3;
return a;
对应 smali
.const/4 v0, 0x2
.const/4 v1, 0x3
add-int v2, v0, v1
return v2
2.4 方法调用
Java 示例
Helper h = new Helper();
h.work("Hello");
对应 smali
new-instance v0, Lcom/example/Helper;
invoke-direct {v0}, Lcom/example/Helper;-><init>()V
const-string v1, "Hello"
invoke-virtual {v0, v1}, Lcom/example/Helper;->work(Ljava/lang/String;)V
方法调用指令对照表
指令 | 含义 | 示例 |
---|---|---|
invoke-virtual | 实例方法 | obj.method() |
invoke-static | 静态方法 | Class.method() |
invoke-direct | 构造方法或私有方法 | new 、this.private() |
invoke-super | 调用父类方法 | super.method() |
invoke-interface | 接口调用 | List.add() |
move-result | 获取返回值 | move-result v0 |
2.5 控制结构映射(if、while、switch)
if 示例(Java)
if (a > b) {
return a;
} else {
return b;
}
smali
if-gt p1, p2, :greater
move v0, p2
goto :end
:greater
move v0, p1
:end
return v0
对照
Java | smali |
---|---|
if (a > b) | if-gt p1, p2, :label |
goto | 跳转到某标签 |
label: | 跳转位置标签,如 :end 、:greater |
2.6 数组操作
Java 示例
int[] arr = new int[3];
arr[0] = 42;
return arr[0];
smali
const/4 v0, 0x3
new-array v1, v0, [I
const/16 v2, 0x2A # 42
aput v2, v1, 0
aget v3, v1, 0
return v3
指令 | 功能 |
---|---|
new-array | 分配数组 [I 表示 int[] |
aput | 数组赋值 |
aget | 数组读取 |
2.7 异常处理结构
Java
try {
int a = 1 / b;
} catch (Exception e) {
return -1;
}
smali
.try_start :start
const/4 v0, 0x1
div-int v1, v0, p1
goto :end
.try_end :start :end
.catch Ljava/lang/Exception; {:start .. :end} :catch
:catch
const/4 v1, -0x1
:end
return v1
2.8 常见类型描述符表
Java 类型 | smali 描述符 |
---|---|
int | I |
long | J |
float | F |
double | D |
boolean | Z |
byte | B |
short | S |
char | C |
void | V |
String | Ljava/lang/String; |
int[] | [I |
String[] | [Ljava/lang/String; |
2.9 反射映射识别
Java
Method m = SomeClass.class.getDeclaredMethod("sign", String.class);
m.setAccessible(true);
String result = (String) m.invoke(null, "data");
对应 smali(大致逻辑)
const-string v0, "sign"
const-class v1, Ljava/lang/String;
invoke-static {v0, v1}, Ljava/lang/Class;->getDeclaredMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
invoke-virtual {v2, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
2.10 常见情况总结
目标 | 对应技巧 |
---|---|
找构造函数逻辑 | 搜 <init> |
看加密参数 | 搜含有 invoke-static 且有 sign 、token 、md5 的函数 |
看网络请求类 | 搜 Retrofit , OkHttpClient , setHeader , put 等 |
看动态加载 | 搜 DexClassLoader , loadClass |
跳转关系图 | 用 jadx GUI 点右键 ➜ 交叉引用(xref) |
判断 native 注册 | 找 native 关键字或 System.loadLibrary |
三、hook Java 方法
3.1 什么是 Hook?
Hook 是指“劫持/拦截”目标函数的执行过程,插入我们自己的逻辑代码。
适用于以下场景:
场景 | Hook 目的 |
---|---|
登录验证函数 | 劫持参数、返回 true、绕过验证 |
token/sign 生成函数 | 抓加密原始参数 + 结果 |
检测函数 | 劫持 isDebuggerConnected() 返回 false |
防护逻辑 | 拦截“崩溃点”防止关闭应用 |
3.2 工具对比:Xposed vs Frida
项目 | Xposed | Frida |
---|---|---|
是否需要 root | 需要 | 可免 root(推荐) |
修改方式 | 系统级模块注入 | 动态注入,实时修改 |
支持范围 | Java 方法、系统 API | Java、native、JNI、socket 等 |
注入时机 | 重启后持续 Hook | 启动后动态 attach |
3.3 使用 Xposed Hook Java 方法
1)基础写法(Java)
findAndHookMethod("com.xxx.LoginManager", lpparam.classLoader, "checkLogin", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.d("Xposed", "Login 参数: " + param.args[0]);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
param.setResult(true); // 强制返回 true(绕过验证)
}
});
关键点说明:
部位 | 功能 |
---|---|
lpparam.classLoader | 当前 app 的类加载器 |
"com.xxx.LoginManager" | 要 hook 的类 |
"checkLogin" | 要 hook 的方法名 |
param.args | 方法参数(数组) |
param.setResult | 修改方法返回值 |
2)实战定位建议:
-
配合
Log.d()
打印日志,记录参数 / 返回值; -
可以搭配 Xposed 框架中的
LSPosed
使用,更加稳定; -
如果函数名是混淆的,可以通过 jadx 找目标函数名。
3.4 使用 Frida Hook Java 方法
启动方式
frida -U -n com.target.app -l hook.js
或先运行:
frida-trace -U -n com.target.app -m "com.xxx.LoginManager.checkLogin"
1)基础 Hook 写法(Frida)
Java.perform(function () {
var Cls = Java.use("com.xxx.LoginManager");
Cls.checkLogin.implementation = function () {
console.log("[*] checkLogin 被调用");
var result = this.checkLogin();
console.log("[*] 原始返回值: " + result);
return true; // 强制登录通过
};
});
2)Hook 有参数方法
Cls.getSign.overload("java.lang.String", "java.lang.String").implementation = function (str1, str2) {
console.log("[*] getSign 被调用");
console.log("参数1:", str1);
console.log("参数2:", str2);
var ret = this.getSign(str1, str2);
console.log("返回值:", ret);
return ret;
};
使用 .overload(...)
精确指定重载函数参数。
3)动态遍历 & 泛用 Hook 工具
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.indexOf("Sign") !== -1) {
console.log("发现类:", className);
}
},
onComplete: function() {}
});
// Hook 所有方法名为 "encrypt" 的函数
Java.perform(function () {
var classList = ["com.xxx.Security", "com.xxx.TokenUtil"];
classList.forEach(function (clsName) {
var Cls = Java.use(clsName);
Cls.encrypt.overloads.forEach(function (over) {
over.implementation = function () {
console.log("[*] 调用了 " + clsName + ".encrypt");
for (var i = 0; i < arguments.length; i++) {
console.log("参数 " + i + ": " + arguments[i]);
}
var res = over.apply(this, arguments);
console.log("返回值:", res);
return res;
}
});
});
});
3.5 常见 Frida 类型转换技巧
Java 类型 | Frida 传参写法 |
---|---|
int | Java.use("java.lang.Integer").valueOf(123) |
byte[] | Java.array('byte', [0x1, 0x2, 0x3]) |
String | "abc" |
Context | Java.use("android.app.ActivityThread").currentApplication().getApplicationContext() |
3.6 实战典型 Hook 案例总结
目标函数 | Hook 操作 |
---|---|
isDebuggerConnected() | 返回 false ,绕过调试检测 |
getToken(String) | 打印参数、返回值,观察加密行为 |
DexClassLoader.loadClass() | 查看是否动态加载 dex |
invoke() | 识别反射函数调用,打印调用栈 |
Base64.encodeToString() | 查看原始加密数据 |
3.7 Frida 报错处理小技巧
错误信息 | 原因 | 解决方案 |
---|---|---|
JavaScriptException: No such overload | 函数参数不匹配 | 用 .overloads 遍历查看 |
Java is not defined | 未附加 Java VM | 用 Java.perform() 包裹 |
Failed to attach | App 启动中 / 已退出 | 重启 App 再 -n 附加 |
Permission denied | 无权限访问进程 | 开启 root,或用模拟器测试 |
3.8 小结
项目 | Xposed | Frida |
---|---|---|
是否动态 | ✘重启注入 | ✔动态 attach |
是否支持 native Hook | ✘ | ✔(包括 JNI) |
hook 稳定性 | 稳定持久 | 动态易断开 |
推荐场景 | 长期拦截、永久生效 | 快速调试、绕过、数据抓取 |
学习曲线 | 中 | 稍高,但功能强大 |
四、通过 Java 调用 native 层 JNI 分析
4.1 目标与适用场景
在逆向中,很多关键逻辑(如加密签名、反调试、root 检测)会藏在 native 层(.so
文件)中,Java 层只做简单封装。
需要分析:
-
Java 是如何调用
.so
中的方法? -
native 方法名称、参数如何映射?
-
.so
内部具体做了什么(加密逻辑、检测、通信)?
4.2 Java → native 调用的 3 种方式
1)静态注册
Java 声明 native 方法,方法名自动映射到 .so
中的函数名。
Java 示例:
public class SignUtil {
static {
System.loadLibrary("native-lib");
}
public static native String getSign(String data);
}
C 层函数:
JNIEXPORT jstring JNICALL Java_com_example_SignUtil_getSign(JNIEnv *env, jclass clazz, jstring data) {
// native 层处理逻辑
}
函数命名遵循规则:
Java_包名_类名_方法名
2)动态注册
Java 中调用 native
方法,但不通过函数名对应,而通过 RegisterNatives()
映射。
Java 示例(同上):
public static native String getSign(String data);
C 层:
static JNINativeMethod methods[] = {
{"getSign", "(Ljava/lang/String;)Ljava/lang/String;", (void *) native_getSign}
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6);
jclass clazz = (*env)->FindClass(env, "com/example/SignUtil");
(*env)->RegisterNatives(env, clazz, methods, 1);
return JNI_VERSION_1_6;
}
3)反射 + native(少见)
Method m = Class.forName("com.example.SignUtil").getDeclaredMethod("getSign", String.class);
m.setAccessible(true);
String sign = (String) m.invoke(null, "data");
本质还是调用 native 函数,只是方法通过反射调用,防调试手段常这样隐藏。
4.3 如何定位 Java 调用 native 的地方?
Step 1:搜索 native
关键字
用 jadx 搜索:
native
System.loadLibrary
可以快速定位所有 Java 层的 native 方法声明。
Step 2:确认加载了哪些 .so
文件
static {
System.loadLibrary("secure"); // 说明会加载 libsecure.so
}
也可能是动态加载:
Runtime.getRuntime().load("/data/data/xx/lib/libnative.so");
Step 3:寻找调用逻辑
看哪些 native 方法会被:
-
Activity 调用
-
getToken/getSign 加密函数调用
-
网络请求构造中调用
4.4 .so 中 native 函数分析方法
工具准备:
工具 | 用途 |
---|---|
IDA Pro / Ghidra | 静态分析 .so,查看函数名 |
nm / readelf / strings | 查看符号表、导出函数 |
Frida | 动态 Hook native 函数 |
objdump | 反汇编 |
静态分析:使用 IDA 打开 .so 文件
常见函数名格式(静态注册):
Java_com_example_SignUtil_getSign
常见结构:
JNIEXPORT jstring JNICALL Java_com_example_SignUtil_getSign(JNIEnv *env, jclass clazz, jstring data) {
const char *str = (*env)->GetStringUTFChars(env, data, 0);
char result[32];
md5(str, result); // 调用内部加密逻辑
return (*env)->NewStringUTF(env, result);
}
可以:
-
追踪关键函数,如:
md5
,aes
,sha1
,encrypt
,sign
等 -
还原 C 层的加密流程
-
尝试重写逻辑、跳过校验
动态分析:使用 Frida hook native 函数
Hook 所有导出函数:
Module.enumerateExportsSync("libnative.so").forEach(function(exp) {
if (exp.type === 'function') {
console.log("发现导出函数:", exp.name);
}
});
Hook 某 native 函数(如 getSign):
Interceptor.attach(Module.findExportByName("libnative.so", "Java_com_example_SignUtil_getSign"), {
onEnter: function (args) {
console.log("getSign 参数: " + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString());
},
onLeave: function (retval) {
console.log("返回结果:", Java.vm.getEnv().getStringUtfChars(retval, null).readCString());
}
});
4.5 RegisterNatives 动态注册的逆向技巧
1)在 smali 中搜索 RegisterNatives:
grep -rn RegisterNatives smali/
2)使用 Frida Hook RegisterNatives 获取映射表:
Interceptor.attach(Module.findExportByName(null, 'RegisterNatives'), {
onEnter: function (args) {
var env = args[0];
var clazz = args[1];
var methods = args[2];
var count = args[3].toInt32();
console.log("注册了 " + count + " 个方法");
for (var i = 0; i < count; i++) {
var fnPtr = methods.add(i * Process.pointerSize * 3);
var name = fnPtr.readPointer().readCString();
var sig = fnPtr.add(Process.pointerSize).readPointer().readCString();
var addr = fnPtr.add(Process.pointerSize * 2).readPointer();
console.log("name:", name, "sig:", sig, "addr:", addr);
}
}
});
4.6 实战定位 Java → native 的完整流程总结
步骤 | 工具 | 目的 |
---|---|---|
1. 用 jadx 搜索 native 方法 | jadx | 找出 Java 层声明 |
2. 搜 System.loadLibrary | jadx | 找出加载的 .so |
3. 用 IDA 打开 .so 分析函数名 | IDA/Ghidra | 确定静态注册 or 动态注册 |
4. 搜 RegisterNatives | smali / Frida | 动态注册就抓注册表 |
5. 用 Frida Hook native 函数 | Frida | 抓加密过程、返回值 |
6. 如果用 JNI 加密,尝试还原流程 | C 分析 | 找算法、key、流程 |
4.7 小结
项 | 示例 |
---|---|
native 方法声明 | public native String getSign(String s) |
System.loadLibrary("xx") | 加载 libxx.so |
Java_com_xx_yy_method | 静态注册 |
RegisterNatives() | 动态注册 |
JNIEXPORT , JNIEnv *env | C 层函数标志 |
GetStringUTFChars , NewStringUTF | 字符串转换 |
Frida Interceptor.attach | 动态 hook native |
五、动态注册类和反射绕过检查识别
5.1 动态注册类识别(JNI)
背景:开发者为了防止 Java_com_xxx_xxx_func()
被静态工具(如 IDA)直接看到,会使用 RegisterNatives()
在运行时动态绑定 Java 方法和 native 实现,绕过函数名暴露。
Java 代码(示例):
public class NativeUtils {
static {
System.loadLibrary("native-lib");
}
public static native String getToken(String str);
}
C/C++ 动态注册代码:
jstring native_getToken(JNIEnv *env, jobject thiz, jstring str) {
// native 层逻辑
}
JNINativeMethod gMethods[] = {
{"getToken", "(Ljava/lang/String;)Ljava/lang/String;", (void *)native_getToken},
};
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
(*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6);
jclass clazz = (*env)->FindClass(env, "com/example/NativeUtils");
(*env)->RegisterNatives(env, clazz, gMethods, 1);
return JNI_VERSION_1_6;
}
如何识别?
方法 1:grep RegisterNatives
in smali
grep -r RegisterNatives ./smali
如果在 JNI_OnLoad()
附近找到调用 RegisterNatives,就说明启用了动态注册。
方法 2:Frida 动态 Hook RegisterNatives
Interceptor.attach(Module.findExportByName(null, "RegisterNatives"), {
onEnter: function (args) {
var env = args[0];
var clazz = args[1];
var methods = args[2];
var count = args[3].toInt32();
console.log("[*] RegisterNatives 被调用,注册 " + count + " 个方法");
for (var i = 0; i < count; i++) {
var base = methods.add(i * Process.pointerSize * 3);
var name = base.readPointer().readCString();
var sig = base.add(Process.pointerSize).readPointer().readCString();
var fn = base.add(Process.pointerSize * 2).readPointer();
console.log("name:", name, "sig:", sig, "addr:", fn);
}
}
});
这段脚本可输出所有动态绑定的方法名称、签名和地址。
5.2 反射调用方法识别
背景:反射是一种运行时获取类、方法、字段并调用的方式。广泛用于:
-
动态构造类调用链
-
躲避静态分析
-
实现插件化、热修复等框架
Java 反射代码示例
Class clazz = Class.forName("com.example.Encrypt");
Method m = clazz.getDeclaredMethod("getSign", String.class);
m.setAccessible(true);
String result = (String) m.invoke(null, "abc");
这段逻辑在 Java 静态代码中根本无法看到方法调用点,必须动态识别。
如何识别反射调用?
方法 1:jadx 搜索关键词
在 jadx 中全局搜索:
-
Class.forName
-
getDeclaredMethod
-
invoke
-
setAccessible
-
getMethod
常见用法:
Method m = Class.forName("xxx").getMethod("xxx", xxx);
Object res = m.invoke(...);
方法 2:hook invoke()
追踪调用者
Java.perform(function () {
var Method = Java.use("java.lang.reflect.Method");
Method.invoke.overload("java.lang.Object", "[Ljava.lang.Object;").implementation = function (obj, args) {
console.log("\n[+] Method.invoke 被调用");
console.log(" -> method: " + this.getName());
console.log(" -> class: " + this.getDeclaringClass().getName());
console.log(" -> args: " + args);
var res = this.invoke(obj, args);
console.log(" -> result: " + res);
return res;
};
});
该脚本能输出所有反射调用的方法名、所属类、参数和返回值。
5.3 结合动态注册 + 反射 的复杂案例识别
开发者经常混合使用反射 + 动态注册 + 多层封装来加密逻辑。
常见特征:
类型 | 特征行为 |
---|---|
隐藏类名 | Class.forName("com.xx.NativeEntry") |
隐藏方法名 | getDeclaredMethod("getSign") |
动态加载 dex | DexClassLoader(...) |
加载 native 库 | System.loadLibrary("xxx") |
Native 层映射 | RegisterNatives() 注册 native 方法 |
5.4 Frida Hook 所有反射类方法
Java.perform(function () {
var c = Java.use("java.lang.Class");
c.forName.overload('java.lang.String').implementation = function(name) {
console.log("[forName] -> " + name);
return this.forName(name);
};
var m = Java.use("java.lang.reflect.Method");
m.invoke.implementation = function(obj, args) {
console.log("[invoke] method: " + this.getName() + " from class: " + this.getDeclaringClass().getName());
return this.invoke(obj, args);
};
});
5.5 小结
类型 | 静态识别方式 | 动态识别方式 | 工具推荐 |
---|---|---|---|
动态注册 JNI | 搜 RegisterNatives 、查看 JNI_OnLoad | Frida hook RegisterNatives | Frida / IDA |
反射调用 | 搜 Class.forName , getMethod , invoke | hook Method.invoke 、Class.forName | Frida |
native 函数映射 | 查看 C 层绑定函数数组 | Frida dump 映射表 | IDA / Frida |
混淆代码识别 | 函数名极短、参数为字符串方法名 | 仍可通过动态输出方法名识别 | Frida |
六、解密/参数生成函数识别
6.1 常见加密参数场景
场景 | 参数名举例 |
---|---|
登录接口 | sign , token , signature , auth , s , _t |
数据接口 | ts , checksum , encrypt_params , body_sign |
验证码/滑块 | w , payload , verify_token |
SDK安全参数 | X-Security , x-bogus , x-tt-token |
这些参数都不会直接明文暴露,需要识别生成函数,或者还原解密逻辑。
6.2 定位加密函数的常规套路(Java 层)
1)搜索关键词法(静态分析)
在 jadx
或 JEB
中搜索关键词:
-
sign
-
token
-
md5
,sha1
,aes
,rsa
,hmac
-
Base64.encode
,URLEncoder
,cipher.init
-
MessageDigest
,Mac
,SecretKeySpec
示例:
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(input.getBytes());
return md.digest();
2)看网络请求前的构造函数
找出网络请求类(如 Retrofit
, OkHttp
, HttpClient
)前的构造逻辑:
String sign = SignUtil.getSign(map);
request.addHeader("sign", sign);
配合动态调试,就能抓到真实参数。
3)使用调用链追踪(jadx 功能)
在 jadx 中右键 getSign()
→ Show Usage → 追踪调用链→ 追踪到调用点 → 看参数来自哪里(request、map、拼接)
6.3 动态定位(Frida)
1)使用 Frida hook 可疑函数
可以直接 hook 类似 getSign
, signData
, encryptParams
等函数:
Java.perform(function () {
var SignUtil = Java.use("com.xx.SignUtil");
SignUtil.getSign.overload('java.util.Map').implementation = function (m) {
console.log("[*] 调用 getSign()");
console.log("[+] 参数: " + m.toString());
var res = this.getSign(m);
console.log("[+] 返回: " + res);
return res;
};
});
2)Hook 常用加密函数(通杀型)
也可以 Hook Java 标准库加密函数,抓底层真实数据:
Hook MD5 / SHA:
Java.perform(function () {
var md = Java.use("java.security.MessageDigest");
md.digest.overload().implementation = function () {
console.log("调用 digest()");
return this.digest();
};
md.update.overload("[B").implementation = function (b) {
console.log("调用 update() 参数:", Java.use("java.lang.String").$new(b));
this.update(b);
};
});
Hook HMAC:
Java.use("javax.crypto.Mac").doFinal.overload("[B").implementation = function (b) {
console.log("[HMAC] doFinal 参数: ", Java.use("java.lang.String").$new(b));
return this.doFinal(b);
};
3)Hook 所有 encode/base64 类函数(抓加密前后数据)
Java.use("android.util.Base64").encodeToString.overload('[B', 'int').implementation = function (b, flag) {
var str = Java.use("java.lang.String").$new(b);
console.log("Base64.encode 参数:", str);
var res = this.encodeToString(b, flag);
console.log("Base64.encode 返回:", res);
return res;
};
6.4 native 层加密识别方法
有时 Java 层只是简单调用 native 方法,真正的参数处理都藏在 .so 里。
1)搜索 native
方法定义(如:getToken
, getSign
)
public native String getToken(String data);
2)Frida Hook JNI native 函数
Interceptor.attach(Module.findExportByName("libnative-lib.so", "Java_com_example_SignUtil_getToken"), {
onEnter: function (args) {
var data = Java.vm.getEnv().getStringUtfChars(args[2], null).readCString();
console.log("[JNI] getToken 参数:", data);
},
onLeave: function (retval) {
console.log("[JNI] 返回值:", Java.vm.getEnv().getStringUtfChars(retval, null).readCString());
}
});
6.5 特征总结:如何判断一个函数是加密/参数生成函数?
特征 | 说明 |
---|---|
函数名含有 sign , token , encrypt , encode | 常见命名模式 |
函数调用后立即参与请求 | 多用于 header/body/sign 生成 |
参数是 Map , String , JSONObject | 多为动态拼接参数 |
函数内部用到了 digest , Mac , AES , RSA | 常见加密组件 |
函数被反射调用 | 故意混淆处理逻辑 |
函数跳转到 native 层 | 利用 JNI 隐藏逻辑 |
6.6 自动追踪推荐
-
打开 jadx,定位网络请求代码 → 找参数拼接位置
-
标记所有调用
getSign
/getToken
的地方 -
用 Frida 写 hook 脚本打印这些函数参数与返回值
-
如果找不到 Java 加密逻辑,找
native
方法 -
若为 native → 用 IDA/Ghidra 分析加密逻辑
-
若为动态注册 JNI → 用 Frida hook RegisterNatives 提取映射表
6.7 Frida 参数追踪推荐模板
Java.perform(function () {
var classes = ["com.xxx.SignUtils", "com.xxx.Encrypt", "com.xxx.Security"];
classes.forEach(function (name) {
try {
var Cls = Java.use(name);
Cls.getSign.overloads.forEach(function (overload) {
overload.implementation = function () {
console.log("[*] Hooked " + name + ".getSign");
for (var i = 0; i < arguments.length; i++) {
console.log("arg[" + i + "]:", arguments[i]);
}
var result = overload.apply(this, arguments);
console.log("result:", result);
return result;
};
});
} catch (e) {
// 忽略未找到类
}
});
});
6.8 小结
方法 | 工具 | 用法 |
---|---|---|
搜函数名关键词 | jadx / jeb | getSign / encrypt / token |
找网络请求前参数构造 | jadx | 请求构造处看 header/body |
动态 hook 方法 | Frida | Hook sign/token 函数输出参数 |
Hook 加密类 | Frida | MessageDigest / Mac / AES |
native 层分析 | IDA / Frida | 找 native 函数名 / 参数分析 |