解决非法字节码
接下来我遇到了一个比较大的坑,通过sa-jdi
库dump
下来的字节码是非法的
在对ApplicationFilterChain
类分析的时候,会报如下的错
起初我怀疑是自己用了最新版ASM
框架:9.2
于是逐渐降级,发现降级到7.0后不再报错,但ClassReader
不报错,在分析时候会报错
经过对比,发现是以下的情况
不报错版本
稍微分析了下,发现是ApplicationFilterChain
类包含了LAMBDA
不止这个类,不少的类都有可能会包含LAMBDA
发现通过sa-jdi
获取的字节码在存在LAMBDA
的情况下是非法字节码,无法进行分析
这时候如果还想进行分析,只有两个选择:
- 自己解析CLASS文件做分析(本末倒置)
- 改写ASM源码使跳过
LAMBDA
根据Java基础知识可以得知:LAMBDA
和INVOKEDYNAMIC
指令相关,于是我改了ASM
的代码
(这里不解释为什么这么改了,是经过多次调试确定的)
org/objectweb/asm/ClassReader#274
bootstrapMethodOffsets = null;
org/objectweb/asm/ClassReader#2456
case Opcodes.INVOKEDYNAMIC: { return; }
改了源码后,就可以正常对非法字节码进行分析了。目前来看没有什么大问题,可以正常分析,但不确定这样的修改是否会存在一些隐患和BUG。总之目前能继续了
分析字节码
分析字节码并不需要太深入做,因为大部分可能出现的内存马都是Runtime.exec
或冰蝎反射调ClassLoader.defineClass
实现的,针对于这两种情况做分析,足以应对绝大多数情况
以下代码是读取dump
的字节码并针对两种情况对所有方法分析
List<Result> results = new ArrayList<>(); int api = Opcodes.ASM9; int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; for (String fileName : files) { byte[] bytes = Files.readAllBytes(Paths.get(fileName)); if (bytes.length == 0) { continue; } ClassReader cr; ClassVisitor cv; try { // runtime exec analysis cr = new ClassReader(bytes); cv = new ShellClassVisitor(api, results); cr.accept(cv, parsingOptions); // classloader defineClass analysis cr = new ClassReader(bytes); cv = new DefineClassVisitor(api, results); cr.accept(cv, parsingOptions); } catch (Exception ignored) { } } for (Result r : results) { logger.info(r.getKey() + " -> " + r.getTypeWord()); }
对于Runtime.exec
型的分析最为简单,仅判断已dump
的字节码中所有方法中是否存在该方法的调用即可(理论上会存在误报,但黑名单类不可能存在该方法,关键字类本身就是可疑的,所以这样做并无不妥)
@Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { boolean runtimeCondition = owner.equals("java/lang/Runtime") && name.equals("exec") && descriptor.equals("(Ljava/lang/String;)Ljava/lang/Process;"); if (runtimeCondition) { Result result = new Result(); result.setKey(this.owner); result.setType(Result.RUNTIME_EXEC_TIME); results.add(result); } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); }
但这种情况不适用于冰蝎反射调ClassLoader.defineClass
代码不长,但对应的字节码较复杂
Method m = ClassLoader.class.getDeclaredMethod("defineClass", String.class, ByteBuffer.class, ProtectionDomain.class); m.invoke(null);
对应字节码
LDC Ljava/lang/ClassLoader;.class // 重点关注 LDC "defineClass" // 重点关注 ICONST_3 ANEWARRAY java/lang/Class DUP ICONST_0 LDC Ljava/lang/String;.class AASTORE DUP ICONST_1 LDC Ljava/nio/ByteBuffer;.class AASTORE DUP ICONST_2 LDC Ljava/security/ProtectionDomain;.class AASTORE INVOKEVIRTUAL java/lang/Class.getDeclaredMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; // 重点关注 ASTORE 1 L1 LINENUMBER 11 L1 ALOAD 1 ACONST_NULL ICONST_0 ANEWARRAY java/lang/Object INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; // 重点关注 POP
这种操作需要多个步骤,并不是简单的一个INVOKE
那么简单,不特殊处理的话,由于反射和ClassLoader
相关操作都算是比较常见的,有一定的误报可能
于是继续掏出栈帧分析大法,具体不再介绍,之前文章 已有详细解释
根据字节码,在defineClass
和Ljava/lang/ClassLoader;
通过LDC
指令入栈之前,应该认为这是恶意操作,模拟JVM指令执行后应该在栈顶设置污点
@Override public void visitLdcInsn(Object value) { if (value instanceof String) { if (value.equals("defineClass")) { super.visitLdcInsn(value); this.operandStack.set(0, "LDC_STRING"); return; } } else { if (value.equals(Type.getType("Ljava/lang/ClassLoader;"))) { super.visitLdcInsn(value); this.operandStack.set(0, "LDC_CL"); return; } } super.visitLdcInsn(value); }
后续主要是对于两个INVOKE
进行分析
- 如果
getDeclaredMethod
传入的是上文LDC
处设置的污点,认为方法返回值也是污点,给栈顶的返回值设置REFLECTION_METHOD
标志 - 如果
Method.invoke
方法中的Method
被标记了REFLECTION_METHOD
则可以确定这是内存马 - 开头一部分代码主要是根据方法参数的实际情况对参数在操作数栈中的索引位置进行确定,是一种动态和自动的确认方式,而不是直接根据经验或者调试写死索引,算是优雅写法
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { Type[] argTypes = Type.getArgumentTypes(descriptor); if (opcode != Opcodes.INVOKESTATIC) { Type[] extendedArgTypes = new Type[argTypes.length + 1]; System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length); extendedArgTypes[0] = Type.getObjectType(owner); argTypes = extendedArgTypes; } boolean reflectionMethod = owner.equals("java/lang/Class") && opcode == Opcodes.INVOKEVIRTUAL && name.equals("getDeclaredMethod"); boolean methodInvoke = owner.equals("java/lang/reflect/Method") && opcode == Opcodes.INVOKEVIRTUAL && name.equals("invoke"); if (reflectionMethod) { int targetIndex = 0; for (int i = 0; i < argTypes.length; i++) { if (argTypes[i].getClassName().equals("java.lang.String")) { targetIndex = i; break; } } if (operandStack.get(argTypes.length - targetIndex - 1).contains("LDC_STRING")) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); operandStack.set(TOP, "REFLECTION_METHOD"); return; } } if (methodInvoke) { int targetIndex = 0; for (int i = 0; i < argTypes.length; i++) { if (argTypes[i].getClassName().equals("java.lang.reflect.Method")) { targetIndex = i; break; } } if (operandStack.get(argTypes.length - targetIndex - 1).contains("REFLECTION_METHOD")) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); Result result = new Result(); result.setKey(owner); result.setType(Result.CLASSLOADER_DEFINE); results.add(result); return; } } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); }