- javaagent技术,实现提前加载类字节码
- 实现hook,插桩技术
- javassist技术
- ASM字节码技术
- 像加载jar,有两种方式
- premain启动前加载:每次变动jar包内容,都需要进行重启服务器
- 利用java的动态attch加载原理,采用probe技术,实现动态加载jar包:容易重写方法
十分清晰的插桩流程图(使用了javaAgent、插桩技术)
java文件运行原理
OpenRASP是怎么进行插桩操作
- 启动时候进行了插桩操作,当有类被 ClassLoader 加载时候,所以会把该类的字节码先交给自定义的 Transformer 处理
- 自定义 Transformer 会判断该类是否为需要 hook 的类,如果是会将该类交给 javassist 字节码处理框架进行处理,javassist
- javassist 框架会将类的字节码依照事件驱动模型逐步解析每个方法,当触发了我们需要 hook 的方法,我们会在方法的开头或者结尾插入进入检测函数的字节码
- 把 hook 好的字节码返回给 transformer 从而载入虚拟机
个人理解【插桩】
- 无论是哪种插桩方式,都是以类加载后,修改class文件的字节码为主;
- 可以看出来,OpenRasp是借助javassist进行对关注类两头监听,查看里面的行为是否违规;
- 大致流程就是一个关注类被加载了,拿这个类的class文件,对文件的字节码进行修改成自己想要的class,然后在放回去,载入JVM,进行执行,原本那个class文件就直接放弃了;
- 好处就是源代码没有修改,只是修改了class文件。
注意:OpenRASP是启动前加载的,所以所有的类模块加载都是在启动前就已经换成自己想要的字节码了,每当调用关注类时,就不会调用原来的代码,而是调用编写好的代码强化的代码模块
OpenRasp是如何进行请求处理
- 服务器收到一个请求,从而进入了服务器的请求 hook 点
- 服务器发起SQL查询
- 进入 SQLStatementHook 点,我们挂钩了 execute、executeUpdate、executeQuery 等方法,从该方法进入检测流程如下:
- 判断当前线程是否为请求线程(第一步标记的),如果是继续下面检测
- 采集 connection_id(这个字段仅JDBC支持)、SQL 语句以及数据库类型 等信息
- 构建参数信息,调用本地插件和 JS 插件进行安全检测,JS 插件由 Rhino 引擎执行,Rhino 引擎执行是 mozilla 为 java 提供的 JavaScript引擎,该引擎会将 JS 代码编译为 java 的 class 字节码在 JVM 中运行,Rhino 引擎文档
- 根据插件的执行结果决定是拦截请求、放行还是仅打印日志
- 进入 SQLResultSetHook 点,我们挂钩了 resultSet.next 方法
- 调用本地插件检查是否发生拖库行为,默认策略为一次查询结果超过500条就报警
- 若决定拦截攻击
- 输出报警日志到 logs/alarm.log
- 如果header还没有发出,默认使用 302 跳转到拦截页面
- 如果body还没有发出,则重置未发送的body
- 输出自定义拦截页面跳转js脚本
javassist技术
在一个测试jar包执行前,提前执行编写好的permain函数,将原本的测试jar的字节码,替换成自己想要的字节码,然后再放回去,JVM只执行我修改好的class文件,转义后,这个代码就拥有自我监控和拦截功能,就达成了无入侵自我保护。
ASM技术和Javassist技术的差异
先说说相同点
这两个技术都是java字节码修改技术,都可以对未加入JVM的class文件,进行修改。
不同之处
ASM主要是通过字节码进行修改,而javassist是通过java代码进行修改的
直接看下他们之间代码,就懂了
同一份java代码,看下他们二者的区别
// 修改前
public void test(String args) {
System.out.println("rasp Test,正常通过!!");
}
// 修改后
public void test(String var1) {
SqlFilter var3 = new SqlFilter();
if (var3.filter(var1)) {
throw new SQLException("invalid sql because of security check");
} else {
System.out.println("rasp Test,正常通过!!");
}
}
// 添加部分
SqlFilter var3 = new SqlFilter();
if (var3.filter(var1)) {
throw new SQLException("invalid sql because of security check");
} else {
// 原本代码 XXXX
}
看下ASM进行插桩
mv.visitTypeInsn(NEW,"com/wu/javaagent01/filter/SqlFilter");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,"com/wu/javaagent01/filter/SqlFilter","<init>","()V",false);
mv.visitVarInsn(ASTORE,3);
mv.visitVarInsn(ALOAD,3);
mv.visitVarInsn(ALOAD,1);
mv.visitMethodInsn(INVOKEVIRTUAL,"com/wu/javaagent01/filter/SqlFilter", "filter","(Ljava/lang/Object;)Z",false);
mv.visitJumpInsn(IFEQ, l92);
mv.visitTypeInsn(NEW, "java/sql/SQLException");
mv.visitInsn(DUP);
mv.visitLdcInsn("invalid sql because of security check");
mv.visitMethodInsn(INVOKESPECIAL, "java/sql/SQLException", "<init>", "(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(l92);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
再看看javassist对这个代码如何插桩的
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("YourClassName"); // 替换成你的类名
CtMethod ctMethod = ctClass.getDeclaredMethod("checkListFiles");
CtClass exceptionClass = classPool.get("java.sql.SQLException");
// 创建异常抛出代码块
CtConstructor exceptionConstructor = exceptionClass.getDeclaredConstructor(new CtClass[] { classPool.get("java.lang.String") });
CtConstructor ctConstructor = ctClass.getClassInitializer();
ctConstructor.insertAfter("{" +
"if (condition) {" +
" try {" +
" throw new java.sql.SQLException(\"invalid sql because of security check\");" +
" } catch (java.sql.SQLException e) {" +
" e.printStackTrace();" +
" }" +
"}");
// 保存修改后的字节码
ctClass.writeFile();
} catch (Exception e) {
e.printStackTrace();
}