关于 java:6. 反射机制
一、Class 类的获取方式(.class、getClass、forName)
1.1 什么是 Class 类?
在 Java 中,每个类在运行时都会被 JVM 加载,并由一个 java.lang.Class
类型的对象表示。
这个 Class
对象中,包含了类的结构信息(字段、方法、构造、注解等),是反射的核心入口。
1.2 Class 类的三种获取方式
1).class
(编译期静态获取)
语法
Class<?> clazz = String.class;
特点
-
编译时已知类型,静态安全
-
类加载器已经加载该类
-
不依赖对象实例
应用场景
-
注解处理器、框架开发(如 Spring 配置类)
-
类型注册表(如
Map<Class<?>, Handler>
)
示例
Class<?> intClass = int.class;
Class<?> stringClass = String.class;
Class<?> userClass = User.class;
逆向应用
一般不构成动态加载,因此在逆向分析中不属于“隐蔽行为”,较少关注。
2)对象.getClass()
(运行期动态获取)
语法
String str = "Hello";
Class<?> clazz = str.getClass();
特点
-
必须有对象实例才能调用
-
运行时确定具体类型(多态判断)
应用场景
-
动态类型判断(
instanceof
替代) -
泛型内部使用
getClass()
判断对象实际类型
示例
Object obj = new ArrayList<>();
System.out.println(obj.getClass().getName()); // java.util.ArrayList
逆向应用
可能结合 invoke()
构成动态行为,如:
obj.getClass().getDeclaredMethod("run").invoke(obj);
可通过 Hook getClass()
观察某类对象类型。
3)Class.forName("类名字符串")
(反射最常用的方式)
语法
Class<?> clazz = Class.forName("com.example.MyClass");
特点
-
通过类名字符串动态加载类
-
类必须已经在 classpath 中
-
可以加载第三方 Jar、系统类、隐藏类
应用场景
-
插件框架加载外部类
-
动态调用逻辑隐藏真实类名
-
配置文件加载类名再执行(如 JDBC)
示例
Class<?> clazz = Class.forName("java.util.HashMap");
Object obj = clazz.getDeclaredConstructor().newInstance();
异常处理
try {Class<?> clazz = Class.forName("xxx");
} catch (ClassNotFoundException e) {e.printStackTrace();
}
1.3 逆向分析中的重点:Class.forName
识别特征:
-
常在加壳、混淆、加密逻辑中使用
-
字符串构造类名,隐藏调用真实类
-
结合
invoke()
、getMethod()
等形成反射链
示例(逆向看到的代码片段):
String clsName = decode("Y29tLmV4YW1wbGUuU2VjcmV0"); // base64 解码出类名,例如 "com.example.Secret"
Class<?> cls = Class.forName(clsName); // 根据类名加载类对象
Method m = cls.getDeclaredMethod("encrypt", String.class); // 获取名为 "encrypt"、参数类型为 String 的方法对象
String result = (String) m.invoke(null, "123456"); // 调用静态方法 encrypt("123456"),返回加密后的字符串
实战 hook:
使用 Frida Hook Class.forName
Java.perform(function () {var clz = Java.use("java.lang.Class");clz.forName.overload("java.lang.String").implementation = function (name) {console.log("[Class.forName] 动态加载类名: " + name);return this.forName(name);};
});
1.4 对比总结
方式 | 说明 | 动态性 | 是否需要对象 | 逆向关注度 |
---|---|---|---|---|
.class | 编译期已知类 | 否 | 否 | 低 |
obj.getClass() | 从对象获取类 | 是 | 是 | 中 |
Class.forName() | 字符串反射类加载 | 强 | 否 | 高 |
1.5 常见变形 & 防御绕过技巧
在高级逆向中,Class.forName 可能会混淆、编码:
混淆调用方式(需配合字符串解密):
String name = AES.decrypt("加密类名");
Class<?> clz = Class.forName(name);
加载其他包的类(第三方 SDK):
DexClassLoader loader = new DexClassLoader(dexPath, odexPath, null, getClassLoader());
Class<?> clz = loader.loadClass("com.sdk.SecretClass");
1.6 小结
-
.class
→ 编译期静态安全 -
getClass()
→ 多态判断,运行期类型确认 -
Class.forName()
→ 最常用、最关键的反射入口,逆向重点关注 -
逆向分析中常结合
invoke()
、动态构造类名、隐藏逻辑 -
可以使用 Frida / Xposed Hook
forName
捕获动态加载类名
二、反射创建对象
2.1 什么是反射创建对象?
反射机制允许我们在不知道类的具体类型或构造函数参数的情况下,通过 Class
对象在运行时动态创建其实例。这是 Java 框架、逆向分析和自动化攻击中的关键技术。
2.2 创建对象的 3 种反射方式
方式一:无参构造创建对象
Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.getDeclaredConstructor().newInstance();
细节说明:
-
clazz.getDeclaredConstructor()
:获取无参构造方法(Constructor<?>
) -
newInstance()
:调用构造方法实例化对象
注意点:
-
构造方法必须是 public 或 accessible
-
如果构造方法是私有的,要加:
constructor.setAccessible(true);
方式二:指定构造参数创建对象
Class<?> clazz = Class.forName("com.example.MyClass");
Constructor<?> cons = clazz.getDeclaredConstructor(String.class, int.class);
Object obj = cons.newInstance("Tom", 25);
示例类:
public class MyClass {public MyClass(String name, int age) {System.out.println("Name: " + name + ", Age: " + age);}
}
特点:
-
适合重载构造函数的类
-
支持私有构造器(记得
setAccessible(true)
)
方式三:带私有构造器的类(如单例类)
Class<?> clazz = Class.forName("com.example.Singleton");
Constructor<?> cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
Object obj = cons.newInstance();
应用场景:
-
逆向破解单例模式、防反射保护
-
调用隐藏构造器
2.3 完整示例
public class User {private String name;private int age;private User(String name, int age) {this.name = name;this.age = age;System.out.println("私有构造创建对象:" + name + " - " + age);}
}
public class Test {public static void main(String[] args) throws Exception {Class<?> clazz = Class.forName("User");Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);constructor.setAccessible(true); // 绕过私有权限Object obj = constructor.newInstance("Alice", 22);}
}
输出:
私有构造创建对象:Alice - 22
2.4 逆向实战中的应用
使用反射创建隐藏类对象:
String clsName = decode("com.xxx.ObfuscatedClass");
Class<?> clazz = Class.forName(clsName);
Object obj = clazz.getDeclaredConstructor().newInstance();
-
有时在 APK 中不会直接看到
new ObfuscatedClass()
,而是用字符串 + 反射创建 -
混淆类名时结合 Base64/AES 解密等方式
Hook 构造函数创建过程(Frida):
Java.perform(function () {var TargetClass = Java.use("com.xxx.SecretClass");TargetClass.$init.overload().implementation = function () {console.log("反射创建了 SecretClass 的实例!");return this.$init();};
});
2.5 常见异常与处理
异常类型 | 原因 |
---|---|
ClassNotFoundException | 类名错误或类不存在 |
NoSuchMethodException | 构造参数签名不对 |
IllegalAccessException | 构造方法是私有但没设 setAccessible(true) |
InvocationTargetException | 构造方法内部抛出异常 |
InstantiationException | 接口、抽象类不能实例化 |
2.6 加固场景下的注意事项
-
一些加固类或壳类通过反射加载自身核心逻辑类(隐藏在 assets 或 dex 中)
-
可使用
Frida
Hook 构造函数,或者 HookClass.forName()
之后跟踪.newInstance()
-
动态加载类之后立刻实例化 → 是分析恶意代码或加密逻辑的突破口
2.7 小结
方法 | 使用方式 | 是否支持私有 | 说明 |
---|---|---|---|
clazz.getDeclaredConstructor().newInstance() | 常用,推荐 | 是 | Java 9 之后推荐方式 |
clazz.newInstance() | 已过时,不支持私有 | 否 | 旧代码可见,不建议 |
Constructor.newInstance(params) | 强大灵活 | 是 | 可指定参数构造器 |
三、反射调用方法
3.1 反射调用方法的核心流程
调用某个对象的方法(无论是公开、私有、静态),反射的基本流程如下:
1. 获取 Class 对象
2. 获取 Method 对象
3. 设置 accessible(如果是私有方法)
4. 调用 invoke 执行
3.2 基本语法与示例
1)调用 public 方法
示例类:
public class User {public void sayHello(String name) {System.out.println("Hello, " + name);}
}
调用代码:
Class<?> clazz = User.class;
Object obj = clazz.getDeclaredConstructor().newInstance();Method method = clazz.getMethod("sayHello", String.class);
method.invoke(obj, "Alice");
输出:
Hello, Alice
2)调用 private 方法
示例类:
public class Secret {private String decrypt(String msg) {return "解密:" + msg;}
}
调用代码:
Class<?> clazz = Secret.class;
Object obj = clazz.getDeclaredConstructor().newInstance();Method method = clazz.getDeclaredMethod("decrypt", String.class);
method.setAccessible(true); // 私有方法必须设置
String result = (String) method.invoke(obj, "密文123");System.out.println(result);
输出:
解密:密文123
3)调用 静态方法
示例类:
public class Utils {public static int add(int a, int b) {return a + b;}
}
调用代码:
Method method = Utils.class.getMethod("add", int.class, int.class);
int result = (int) method.invoke(null, 5, 7); // 静态方法对象传 null
System.out.println(result); // 输出 12
3.3 Method API 常见函数
方法名 | 含义 |
---|---|
getMethod() | 获取 public 方法,包括父类 |
getDeclaredMethod() | 获取当前类声明的所有方法(包括 private) |
setAccessible(true) | 访问私有方法绕过安全检查 |
invoke(obj, args...) | 运行方法,obj 为实例,静态方法为 null |
3.4 逆向分析中常见反射调用行为
常见套路 1:反射调用隐藏逻辑
String cls = AES.decrypt("xxx");
String methodName = "doCheck";
Class<?> clazz = Class.forName(cls);
Method m = clazz.getDeclaredMethod(methodName);
m.setAccessible(true);
m.invoke(clazz.getDeclaredConstructor().newInstance());
- 常见于:行为验证、加固、License 校验、反调检测中
常见套路 2:反射调用系统 API 绕过限制
Method m = TelephonyManager.class.getDeclaredMethod("getDeviceId");
m.setAccessible(true);
String imei = (String) m.invoke(tm);
- 在高版本 Android 中绕过权限控制或使用隐藏 API
常见套路 3:加载类 + 调用静态注册方法
Class<?> cls = Class.forName("com.xxx.NativeLoader");
Method m = cls.getMethod("init", Context.class);
m.invoke(null, ctx); // 静态初始化
-
常用于 SDK 初始化、隐藏壳解密逻辑
3.5 使用 Frida Hook Method.invoke
在逆向中我们常会想知道 哪个方法被反射调用了,可通过 Hook Method.invoke()
捕捉。
Java.perform(function () {var methodClass = Java.use('java.lang.reflect.Method');methodClass.invoke.overload('java.lang.Object', '[Ljava.lang.Object;').implementation = function (receiver, args) {var name = this.getName();var cls = this.getDeclaringClass().getName();console.log("[反射调用] 类: " + cls + " 方法: " + name);return this.invoke(receiver, args); // 正常执行};
});
3.6 常见异常及解决方式
异常 | 说明 |
---|---|
NoSuchMethodException | 方法名或参数签名写错 |
IllegalAccessException | 没有权限访问方法,缺少 setAccessible(true) |
InvocationTargetException | 方法内部抛出异常 |
NullPointerException | 静态方法 invoke(null, ...) 忘记传 null 或对象为 null |
3.7 小结
场景 | 方法名 | 是否 setAccessible | 调用对象 | 备注 |
---|---|---|---|---|
public 实例方法 | getMethod | 否 | 实例 | 最常见 |
private 实例方法 | getDeclaredMethod | 是 | 实例 | 记得绕过权限 |
静态方法 | getMethod / getDeclaredMethod | 根据修饰 | null | 静态调用传 null |
四、反射访问字段
4.1 反射访问字段的能力
通过 java.lang.reflect.Field
,我们可以在运行时:
-
获取字段值(包括 private)
-
修改字段值
-
操作静态字段
-
绕过泛型擦除、绕过访问控制
4.2 Field 的基本操作流程
1. 获取 Class 对象
2. 获取 Field 对象(getField / getDeclaredField)
3. 设置 Accessible(如果是私有字段)
4. 使用 get() / set() 读取或修改值
4.3 访问字段的完整示例
示例类:
public class User {public String name = "Tom";private int age = 18;public static String TAG = "UserClass";
}
1)访问 public 实例字段
Class<?> clazz = User.class;
Object obj = clazz.getDeclaredConstructor().newInstance();Field nameField = clazz.getField("name");
String name = (String) nameField.get(obj);
System.out.println("name = " + name);nameField.set(obj, "Alice");
System.out.println("name = " + nameField.get(obj)); // Alice
2)访问 private 实例字段
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true); // 私有字段必须设置
int age = ageField.getInt(obj);
System.out.println("age = " + age);ageField.setInt(obj, 25);
System.out.println("new age = " + ageField.getInt(obj)); // 25
3)访问 静态字段
Field tagField = clazz.getField("TAG");
String tag = (String) tagField.get(null); // 静态字段:传 null
System.out.println("TAG = " + tag);tagField.set(null, "NewTag");
System.out.println("new TAG = " + tagField.get(null)); // NewTag
4.4 Field 常用方法整理
方法 | 作用 |
---|---|
get(Field) | 读取字段值 |
set(Field, value) | 修改字段值 |
getInt/getBoolean/getLong | 类型安全读取 |
setAccessible(true) | 绕过 private 限制 |
getModifiers() | 获取修饰符(如 static、final) |
isSynthetic() | 判断是否编译器自动生成字段(如 lambda) |
4.5 逆向实战中常见用途
用例 1:破解授权标志位
Class<?> clazz = Class.forName("com.xxx.LicenseChecker");
Object instance = clazz.getDeclaredConstructor().newInstance();
Field f = clazz.getDeclaredField("isAuthorized");
f.setAccessible(true);
f.setBoolean(instance, true); // 伪造授权
用例 2:访问 SDK 隐藏字段 / 加密参数
Field aesKeyField = clazz.getDeclaredField("aesKey");
aesKeyField.setAccessible(true);
String key = (String) aesKeyField.get(obj);
System.out.println("AES KEY = " + key);
用例 3:修改 final
字段(通常需要特殊处理)
Field field = MyClass.class.getDeclaredField("VERSION");
field.setAccessible(true);// 取消 final 修饰(Java 12+ 无法可靠绕过)
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);field.set(null, "2.0");
4.6 Frida 中 Hook 字段读写
Hook 所有字段值读取
Java.perform(function () {var cls = Java.use("com.xxx.MyClass");cls.someMethod.implementation = function () {console.log("当前字段值:" + this.secretField.value);return this.someMethod();};
});
实时修改字段值(绕过逻辑)
Java.perform(function () {var cls = Java.use("com.xxx.MyClass");cls.$init.implementation = function () {this.flag.value = true; // 伪造已登录 / 已验证return this.$init();};
});
4.7 常见异常与排查
异常类型 | 原因 |
---|---|
NoSuchFieldException | 字段名拼写错误或继承类未声明 |
IllegalAccessException | 没有权限,未 setAccessible(true) |
IllegalArgumentException | set() 参数类型不匹配 |
NullPointerException | 静态字段 get(null) 中传了实例 |
4.8 字段反射操作总结表
类型 | 获取方法 | 是否 Accessible | 对象传递 |
---|---|---|---|
public 字段 | getField | 否 | 实例对象 |
private 字段 | getDeclaredField | 是 | 实例对象 |
静态字段 | getField/getDeclaredField | 静态字段:传 null | null 传入 |
final 字段 | 需修改 modifiers | 是 | 可绕过但受限 |
4.9 字段名动态构造 + 加密
常用于加固、反调、验证中隐藏字段名:
String fieldName = decrypt("ENCODED_FIELD_NAME");
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, true);
五、构造函数反射
5.1 什么是构造函数反射?
通过 java.lang.reflect.Constructor
类,反射可以在运行时:
-
获取指定构造方法(包括私有构造函数)
-
创建类的对象实例(即使构造函数被 private 修饰)
-
分析构造参数(有多少、什么类型)
这对于绕过 SDK 限制、防止 new 创建实例的代码场景特别有用。
5.2 构造函数反射基本流程
1. 获取 Class 对象
2. 获取 Constructor 对象(getConstructor / getDeclaredConstructor)
3. 设置 accessible(如果是 private 构造器)
4. 使用 newInstance() 创建对象
5.3 示例详解
示例类:
public class User {private String name;private int age;public User() {this.name = "Tom";this.age = 18;}private User(String name, int age) {this.name = name;this.age = age;}public void show() {System.out.println("name=" + name + ", age=" + age);}
}
1)获取无参构造并创建对象
Class<?> clazz = User.class;// public 无参构造
Constructor<?> cons = clazz.getConstructor();
Object obj = cons.newInstance();((User) obj).show(); // 输出 name=Tom, age=18
2)获取私有构造并创建对象
Constructor<?> cons2 = clazz.getDeclaredConstructor(String.class, int.class);
cons2.setAccessible(true); // 关键点:绕过 privateUser user = (User) cons2.newInstance("Alice", 22);
user.show(); // 输出 name=Alice, age=22
3)获取所有构造函数列表
Constructor<?>[] allConstructors = clazz.getDeclaredConstructors();
for (Constructor<?> c : allConstructors) {System.out.println("构造函数参数:" + Arrays.toString(c.getParameterTypes()));
}
5.4 Constructor 类常用方法
方法 | 作用 |
---|---|
getConstructor(...) | 获取 public 构造方法 |
getDeclaredConstructor(...) | 获取任意(包括 private)构造方法 |
setAccessible(true) | 允许访问 private 构造器 |
newInstance(...) | 创建对象 |
getParameterTypes() | 获取参数类型数组 |
getModifiers() | 获取构造函数修饰符(如 public/private) |
5.5 逆向分析中的实战用途
1)绕过类限制,强行 new 实例
某些 SDK 使用 private 构造器 + 工厂方法限制外部创建:
public class SDKCore {private SDKCore(Context ctx) { ... }public static SDKCore getInstance(Context ctx) { ... }
}
我们可以反射创建实例:
Constructor<?> cons = SDKCore.class.getDeclaredConstructor(Context.class);
cons.setAccessible(true);
SDKCore sdk = (SDKCore) cons.newInstance(context);
2)解壳类:反射触发构造解密逻辑
某些壳子会把原始逻辑藏在构造方法中:
Class<?> realEntry = Class.forName("com.shell.DecryptEntry");
Constructor<?> cons = realEntry.getDeclaredConstructor();
cons.setAccessible(true);
Object entry = cons.newInstance(); // 构造时自动解壳
3)动态构造器名绕过分析
有些混淆壳子会动态加载构造器参数:
String[] params = {"java.lang.String", "int"};
Class<?>[] paramTypes = new Class<?>[params.length];
for (int i = 0; i < params.length; i++) {paramTypes[i] = Class.forName(params[i]);
}Constructor<?> ctor = clazz.getDeclaredConstructor(paramTypes);
ctor.setAccessible(true);
Object obj = ctor.newInstance("Admin", 99);
5.6 Frida 结合 Hook 构造器分析
Hook 所有构造函数调用
Java.perform(function () {var target = Java.use("com.xxx.User");target.$init.overload('java.lang.String', 'int').implementation = function (name, age) {console.log("构造函数调用: name=" + name + ", age=" + age);return this.$init(name, age);};
});
5.7 常见异常与排查
异常类型 | 原因 |
---|---|
NoSuchMethodException | 构造函数签名不匹配 |
IllegalAccessException | 没有权限,忘了 setAccessible(true) |
InstantiationException | 抽象类或接口不能实例化 |
InvocationTargetException | 构造函数内部抛出异常 |
IllegalArgumentException | 传参类型或数量不对 |
5.8 构造器反射总结表
场景 | 获取方式 | 是否 setAccessible | 是否支持 new |
---|---|---|---|
public 无参构造 | getConstructor() | 否 | 是 |
private 构造 | getDeclaredConstructor(...) | 是 | 是 |
构造参数分析 | getParameterTypes() | 否 | 分析参数 |
六、泛型绕过检查
6.1 Java 泛型是伪泛型(类型擦除机制)
Java 泛型在 编译期有效,运行时被擦除,称为 类型擦除(Type Erasure)。
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // 编译时报错,但运行期无法检查类型
编译后运行时实际上变成:
List list = new ArrayList(); // 原始类型
list.add("hello");
list.add(123); // 实际不会报错
6.2 为什么需要绕过泛型检查?
-
突破泛型限制:例如往
List<String>
里强行加Integer
-
逆向 SDK / 框架:很多库用泛型强约束输入类型
-
反序列化攻击:可伪造泛型类型对象进行攻击(例如 Fastjson)
6.3 使用反射绕过泛型限制
示例:强行往 List<String>
里添加 Integer
List<String> list = new ArrayList<>();
list.add("A"); // 合法Method m = list.getClass().getMethod("add", Object.class);
m.invoke(list, 123); // 加入了 Integer,绕过泛型检查System.out.println(list); // 输出:[A, 123]
原理:运行时 List<String>
实际是 List<Object>
,反射无视泛型!
6.4 应用场景详解
1)伪造字段类型(逆向伪造数据结构)
Field f = SomeClass.class.getDeclaredField("data"); // 获取 SomeClass 类中名为 "data" 的字段(Field)
f.setAccessible(true); // 关闭 Java 访问检查,允许操作私有字段// 实际上声明为 Map<String, User>,你可以 set 成 Map<Integer, String>
Map fakeMap = new HashMap<>(); // 创建一个未指定泛型类型的 HashMap(原始类型)
fakeMap.put(1, "bad value"); // 插入与字段声明不符的键值对(绕过泛型)
f.set(obj, fakeMap); // 将 fakeMap 设置到 obj 的 data 字段,绕过泛型校验
2)绕过方法泛型限制
Method m = clazz.getMethod("setList", List.class); // 获取名为 setList 且参数为 List 的方法(泛型会被类型擦除)
List<Integer> list = Arrays.asList(1, 2, 3); // 创建一个 Integer 类型的 List,实际类型与泛型无关
m.invoke(obj, list); // 调用 obj 的 setList 方法,传入 List<Integer>,即使方法定义是 List<String> 也不会报错(因泛型擦除)
3)获取实际泛型参数类型(复杂,使用 Type
)
Field field = MyClass.class.getDeclaredField("userList"); // 通过反射获取 MyClass 类中名为 "userList" 的字段
Type genericType = field.getGenericType(); // 获取字段的泛型类型,例如 List<User>if (genericType instanceof ParameterizedType) { // 判断该类型是否是带泛型参数的类型Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); // 获取泛型参数列表System.out.println("泛型参数是:" + types[0]); // 打印第一个泛型参数,例如 class User
}
6.5 类型擦除导致的问题(也是逆向的机会)
问题 | 描述 |
---|---|
泛型参数无法在运行时校验 | 所有泛型都变成原始类型 |
反射不会抛出泛型错误 | 传错类型也能 set 成功 |
某些序列化框架依赖泛型 | 利用擦除伪造类型攻击 |
6.6 反序列化攻击中的利用(以 Fastjson 为例)
{"@type": "java.util.ArrayList","value": [{"@type": "com.xxx.Admin","role": "root"}]
}
通过伪造泛型类型,服务端反序列化时执行攻击代码,因为运行时没法校验 ArrayList<Admin>
中类型正确性。
6.7 在逆向中如何用 Frida Hook 泛型数据?
虽然 Java 泛型擦除让我们看不到参数类型,但可以用 Frida 拦截函数传参并打印类型:
Java.perform(function () { // 使用 Java.perform 确保操作在 Java VM 初始化完成后进行var Target = Java.use("com.xxx.MyClass"); // 获取 Java 层的 com.xxx.MyClass 类的引用Target.setList.implementation = function (list) { // Hook 并重写 setList(List) 方法的实现console.log("类型实际是:" + list.get(0).$className); // 输出传入列表中第一个元素的 Java 类名(绕过泛型获取真实类型)return this.setList(list); // 调用原方法,保持原有逻辑};
});
6.8 逆向场景总结:泛型绕过能做什么?
场景 | 应用 |
---|---|
破解 SDK 对 List 的输入限制 | 反射 add 非法类型绕过 |
Hook 泛型函数实际参数类型 | 利用 Frida 打印 list.get(0) 类型 |
攻击 JSON/XML 反序列化框架 | 泛型伪造类型攻击 |
模拟复杂数据结构 | 泛型擦除后可自由构造伪造对象 |
6.9 如何保护自己?
在实际开发中,以下方式可以防止泛型绕过带来的安全隐患:
-
运行时类型强校验(如 instanceof)
-
不要信任传入的泛型参数
-
使用 JSON 反序列化时设置白名单(Fastjson、Jackson)
-
避免在 RPC、远程调用接口中使用泛型集合传参
七、动态代理(JDK 动态代理 vs CGLIB)
7.1 什么是动态代理?
动态代理是一种 在运行时创建代理对象 的机制,它允许你:
-
拦截对目标对象的方法调用
-
动态增强或替换原有逻辑(如添加日志、权限校验)
-
解耦业务代码
7.2 动态代理的两种方式
名称 | 要求 | 基于 | 是否能代理类 |
---|---|---|---|
JDK 动态代理 | 必须基于接口 | java.lang.reflect.Proxy | 只能代理接口 |
CGLIB 动态代理 | 不需要接口 | ASM 字节码技术 | 可以代理普通类 |
7.3 JDK 动态代理详解
1)前提:目标类必须实现接口
public interface HelloService { // 定义一个接口 HelloService,用于声明业务方法void sayHello(String name); // 声明一个方法 sayHello,接收一个字符串参数
}public class HelloServiceImpl implements HelloService { // 实现 HelloService 接口的具体类public void sayHello(String name) { // 实现接口中的 sayHello 方法System.out.println("Hello, " + name); // 输出问候语}
}
2)创建 InvocationHandler(核心)
public class MyHandler implements InvocationHandler { // 实现 InvocationHandler 接口,用于动态代理private Object target; // 被代理的真实对象public MyHandler(Object target) { // 构造函数,传入目标对象this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 所有代理方法最终都会走这个 invoke 方法System.out.println("调用前:" + method.getName()); // 方法调用前打印方法名Object result = method.invoke(target, args); // 通过反射调用目标对象的实际方法System.out.println("调用后:" + method.getName()); // 方法调用后打印方法名return result; // 返回方法的执行结果}
}
3)创建代理对象并使用
HelloService target = new HelloServiceImpl(); // 创建真实对象,实现了 HelloService 接口
HelloService proxy = (HelloService) Proxy.newProxyInstance( // 创建代理对象,类型仍为 HelloServicetarget.getClass().getClassLoader(), // 使用目标类的类加载器target.getClass().getInterfaces(), // 获取目标类实现的所有接口,代理必须基于接口new MyHandler(target) // 传入 InvocationHandler 实例,定义增强逻辑
);proxy.sayHello("Alice"); // 调用代理方法,实际会被 MyHandler 中的 invoke 拦截并增强
7.4 CGLIB 动态代理详解
1)目标类 不需要实现接口
public class Animal {public void speak(String word) {System.out.println("Animal says: " + word);}
}
2)创建 MethodInterceptor(核心)
需要引入 CGLIB:
<!-- Maven 依赖 -->
<dependency> <!-- 定义一个依赖项 --><groupId>cglib</groupId> <!-- CGLIB 所在的组织 ID --><artifactId>cglib</artifactId> <!-- CGLIB 的模块名 --><version>3.3.0</version> <!-- 指定版本号 -->
</dependency>
创建拦截器:
public class MyMethodInterceptor implements MethodInterceptor { // 实现 CGLIB 的 MethodInterceptor 接口@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 所有被代理的方法都会走这个 intercept 方法System.out.println("前置增强:" + method.getName()); // 方法执行前打印方法名Object result = proxy.invokeSuper(obj, args); // 调用父类原始方法,注意不是用 method.invokeSystem.out.println("后置增强:" + method.getName()); // 方法执行后打印方法名return result; // 返回原方法的执行结果}
}
3)生成代理对象
Enhancer enhancer = new Enhancer(); // 创建一个 CGLIB Enhancer 对象,用于生成代理类
enhancer.setSuperclass(Animal.class); // 设置被代理的目标类(必须不是 final)
enhancer.setCallback(new MyMethodInterceptor()); // 设置拦截器,用于增强方法逻辑Animal proxyAnimal = (Animal) enhancer.create(); // 创建代理对象(CGLIB 会生成 Animal 的子类)
proxyAnimal.speak("hello"); // 调用代理方法,会触发 MyMethodInterceptor 的 intercept 方法
7.5 JDK vs CGLIB 对比总结
特性 | JDK 动态代理 | CGLIB |
---|---|---|
是否需要接口 | 是 | 否 |
能否代理类 | 否 | 是 |
实现方式 | 反射生成接口实现类 | ASM 生成子类 |
性能 | 接口方法快 | 类代理稍慢 |
局限性 | 只能代理接口 | 不能代理 final 类/方法 |
常见框架使用 | JDK(Spring AOP 默认) | CGLIB(Spring AOP 自动 fallback) |
7.6 在逆向分析中的用途
1)加壳 App 常用动态代理隐藏真实逻辑
// Proxy.newProxyInstance 拦截 SDK 调用
InterfaceX sdk = (InterfaceX) Proxy.newProxyInstance( // 创建代理对象,类型为 SDK 提供的接口 InterfaceXSDKImpl.class.getClassLoader(), // 使用 SDK 实现类的类加载器new Class[]{InterfaceX.class}, // 指定需要实现的接口(通常是 SDK 暴露的接口)new InvocationHandler() { // 匿名实现 InvocationHandler 接口@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("调用方法:" + method.getName()); // 打印被调用的方法名// 可做参数检查、替换、记录等操作Object result = method.invoke(new SDKImpl(), args); // 调用真实 SDK 实现类System.out.println("返回结果:" + result); // 打印返回结果return result; // 返回真实调用结果}}
);
Hook 接口时其实是在 Hook 一个 Proxy,要进一步跟踪 invoke()
中的 method.invoke(target)
。
2)Hook SDK 真实调用路径(Frida Hook)
Java.perform(function () { // 确保在 Java VM 初始化完成后运行 Hookvar Proxy = Java.use("java.lang.reflect.Proxy"); // 获取 Proxy 类,用于动态代理var Handler = Java.use("java.lang.reflect.InvocationHandler"); // 获取 InvocationHandler 接口引用Proxy.newProxyInstance.overload( // Hook Proxy.newProxyInstance 方法的指定重载版本"java.lang.ClassLoader", // 参数1:类加载器"[Ljava.lang.Class;", // 参数2:接口数组(要实现的接口)"java.lang.reflect.InvocationHandler" // 参数3:处理器(回调)).implementation = function (loader, interfaces, handler) { // 重写该方法的实现逻辑console.log("创建了动态代理,接口:" + interfaces); // 打印创建代理时传入的接口信息return this.newProxyInstance(loader, interfaces, handler); // 调用原始实现,保持功能不变};
});
7.7 Xposed/Frida 等框架原理中动态代理的影子
-
Xposed 模拟 AOP 的做法:类似动态代理,在方法执行前后插入代码
-
Frida 动态替换函数:在运行时 hook 并可自由代理原始函数逻辑
7.8 动态代理反编译 & trace 技巧
-
抓 Proxy 生成的 class 文件(JDK 会自动生成
$Proxy0.class
) -
Frida trace
invoke()
看谁调用了代理对象 -
观察字节码:CGLIB 会生成
$$EnhancerByCGLIB$$
这样的类名 -
使用 JD-GUI 或 Fernflower 分析代理类
7.9 小结
-
JDK 动态代理:接口级别拦截器
-
CGLIB 动态代理:类级别子类增强器
-
对逆向分析来说:它们是隐藏核心逻辑、混淆控制流程的利器
八、Java 逆向分析中的反射识别与 hook
8.1 为什么反射在逆向中这么重要?
反射是绕过编译期限制的一种手段,常被用于:
用途 | 说明 |
---|---|
动态加载类 | 防止直接在代码中暴露类名 |
加密关键函数调用 | 加密函数名+反射执行,隐藏调用路径 |
对抗静态分析 | 减少敏感 API 出现在代码中 |
动态注册组件 | 类似“类名 + 方法名”组合调度体系 |
SDK 加壳 | 代理、加载、转发都通过反射完成 |
8.2 如何识别使用了反射?
1)常见反射 API
方法 | 作用 |
---|---|
Class.forName(String) | 加载类 |
Class.getDeclaredMethod(String, Class...) | 获取方法 |
Method.invoke(Object, Object...) | 执行方法 |
Field.set/get() | 访问字段 |
Constructor.newInstance() | 构造对象 |
2)特征识别技巧(静态分析)
搜索关键词:forName
, getMethod
, invoke
, setAccessible
, newInstance
特征堆栈例子:
java.lang.Class.forName
java.lang.Class.getDeclaredMethod
java.lang.reflect.Method.invoke
Jadx 查看:
-
反射用字符串表示类名/方法名(很多通过加密构造)
-
常和
Base64.decode
、AES.decrypt
一起出现(说明字符串是加密的)
8.3 实战 Hook 案例(Frida)
1)Hook Class.forName
Java.perform(function () { // 等待 Java 虚拟机启动完成后执行var clazz = Java.use("java.lang.Class"); // 获取 java.lang.Class 类的 Java 绑定clazz.forName.overload('java.lang.String').implementation = function (name) { // Hook forName(String) 方法console.log("forName 调用了:" + name); // 打印传入的类名(被反射加载的类名)return this.forName(name); // 调用原始 forName 实现,保持功能不变};
});
2)Hook Method.invoke
Java.perform(function () { // 等待 Java 虚拟机加载完毕,确保可以安全 Hookvar Method = Java.use("java.lang.reflect.Method"); // 获取 Method 类,用于 Hook 反射调用Method.invoke.implementation = function (obj, args) { // 重写 Method.invoke 的实现console.log("[invoke] " + this.getName() + " on " + obj.getClass().getName()); // 打印被调用的方法名和所属类名return this.invoke(obj, args); // 调用原始方法,传入原始参数,保持原行为};
});
8.4 动态反射识别流程(逆向中定位核心函数)
-
静态查找反射调用点(Jadx 搜索
invoke
,forName
) -
查看传入的字符串是固定值还是加密值
-
跟踪解密流程:是否 Base64、AES、RC4
-
使用 Frida Hook 解密函数/反射执行点
-
输出真实调用链,还原调用流程
8.5 Frida 自动输出所有反射调用的方法名
Java.perform(function () { // 确保在 Java 层运行环境加载完成后执行 Hookvar Method = Java.use("java.lang.reflect.Method"); // 获取 java.lang.reflect.Method 类的引用Method.invoke.implementation = function (receiver, args) { // 重写 Method.invoke 方法(即反射调用)console.log("反射调用方法: " + this.getName() + // 打印方法名", 所属类: " + this.getDeclaringClass().getName() + // 打印所属类名", 参数个数: " + (args ? args.length : 0)); // 打印参数个数return this.invoke(receiver, args); // 调用原始的 Method.invoke,保持功能不变};
});
这样可以在运行时打印出每一个被反射调用的方法的真实类名和方法名,这在逆向 SDK、定位加密入口点时非常有用。
8.6 反射 + 动态代理的配合手法
很多 SDK 或加壳框架喜欢:
Object proxy = Proxy.newProxyInstance( // 创建一个动态代理对象..., // 第一个参数:类加载器(通常用目标类的 classLoader)..., // 第二个参数:需要实现的接口数组new InvocationHandler() { // 第三个参数:实现 InvocationHandler 接口的匿名类public Object invoke(Object proxy, Method method, Object[] args) { // 所有接口方法的调用都会走这里// 反射执行真实方法Method real = realClass.getMethod(...); // 根据方法名和参数类型获取真实类中的方法return real.invoke(realObj, args); // 使用反射调用真实对象的方法,并传入参数}}
);
如果只 Hook 代理接口,是没用的!必须跟进 invoke()
内部调用的反射。
8.7 Frida + Java API Trace 一键爆破反射调用
可以使用如下脚本,快速定位所有反射使用行为:
Java.perform(function () { // 等待 Java 虚拟机环境准备好后执行const methods = [ // 定义一个数组,包含要 Hook 的类和方法名['java.lang.Class', 'forName'], // 反射加载类的方法['java.lang.Class', 'getMethod'], // 获取公有方法['java.lang.reflect.Method', 'invoke'], // 反射调用方法['java.lang.Class', 'getDeclaredMethod'], // 获取所有声明的方法(包括私有)['java.lang.reflect.Field', 'set'], // 反射设置字段值['java.lang.reflect.Field', 'get'], // 反射获取字段值];for (let [clazz, method] of methods) { // 遍历每个类和方法try {Java.use(clazz)[method].implementation = function () { // 重写对应方法实现console.log(`[反射调用] ${clazz}.${method}`); // 打印被调用的反射方法return this[method].apply(this, arguments); // 调用原始方法,传入所有参数}} catch (e) {} // 捕获异常,防止找不到方法导致脚本崩溃}
});
8.8 遇到反射如何还原?
1)明文传参
Class.forName("com.example.AESUtils");
直接 Hook + 断点即可定位类
2)加密字符串 + 解密后使用反射
String clazzName = AES.decrypt("ENCRYPTED_STRING");
Class<?> clazz = Class.forName(clazzName);
Hook 解密函数 + 输出明文字符串!
8.9 如何反制:反射检测防护建议
方法 | 说明 |
---|---|
ProGuard/R8 混淆类名 | 让反射类名变得模糊 |
native 层逻辑隐藏 | 将类名、函数名在 C 层构造 |
使用 JNI 实现关键逻辑 | 反射无法 Hook |
检测 Frida/Xposed | 防止脚本 Hook 行为 |
8.10 小结
Java 反射是隐藏真实调用、对抗静态分析的利器,而 Frida/Xposed 是揭开它面具的关键工具。