OpenRasp源码分析-java功能
最近在做关于后台服务器和web的rasp这块工作,需要在OpenRasp的基础上做一个二次研发,为了能弄清其实现的技术细节,阅读了一下源码。记录的内容主要有以下几个点
- RaspIntall.jar的功能
- 启动执行流程
- 核心原理
- 检测规则
- 其他
整个OpenRasp的基本就是上述几个过程。其中重点关注的是启动执行流程,核心原理,检测规则三个点,下面就一个个的分析其实现。在分析的过程中我用了一个一些逆向工程的思路,当然也可以直接读源码,每个人都有自己的思路,能达到目的就可以了。
下面分析主要关注的java–>springboot–>DeserializationHook相关
RaspIntall.jar的功能
在其官方文档中,有提到了在启动服务之前,需要先安装agent,对于SpringBoot框架,使用如下方式安装
java -jar RaspInstall.jar -nodetect -install <spring_boot_folder> -backendurl http://XXX -appsecret XXX -appid XXX
其中backendurl appsecret appid可以通过启动rasp-cloud可以获取到,这里不是重点,先不去管。
这里使用jd-gui.jar查看RaspInstall.jar,根据参数
-nodetect -install <spring_boot_folder>
定位到MANIFEST.MF下的Main-Class值,如下
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: root
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_232
Main-Class: com.baidu.rasp.App
找到关键类com.baidu.rasp.App,经过分析后,来到关键掉一共
public static void operateServer(String[] args) throws RaspError, ParseException, IOException {
showBanner();
argsParser(args);
checkArgs();
if ("install".equals(install)) {
File serverRoot = new File(baseDir);
InstallerFactory factory = newInstallerFactory();
Installer installer = factory.getInstaller(serverRoot, noDetect);
if (installer != null) {
installer.install();
} else {
throw new RaspError(RaspError.E10007);
}
} else if ("uninstall".equals(install)) {
File serverRoot = new File(baseDir);
UninstallerFactory factory = newUninstallerFactory();
Uninstaller uninstaller = factory.getUninstaller(serverRoot);
if (uninstaller != null) {
uninstaller.uninstall();
} else {
throw new RaspError(RaspError.E10007);
}
}
}
进入到install中的调用,经过分析可以知道,程序是将rasp目录下的所有程序复制到指定的springboot_path下的rasp目录下,主要有以下几个文件
.
├── conf
│ └── openrasp.yml
├── logs
│ ├── alarm
│ │ └── alarm.log
│ ├── plugin
│ │ └── plugin.log
│ ├── policy_alarm
│ │ └── policy_alarm.log
│ └── rasp
│ └── rasp.log
├── plugins
├── rasp-engine.jar
└── rasp.jar
log文件是运行后自动生成的,可以忽略。
RaspInstall.jar主要功能:
- 在目标路径创建rasp目录
- 复制rasp.jar,rasp-engine.jar
- 复制conf目录
- 复制plugins目录
- 生成配置信息conf/openrasp.yml文件
启动执行流程
启动openrasp的时候,使用如下命令
java -javaagent:/opt/spring-boot/rasp/rasp.jar -jar xxx.jar
指定javaagent:参数,在java中启动时指定了agent,则可以完成以下功能
javaagent 的主要功能如下:
可以在加载 class 文件之前做拦截,对字节码做修改
可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
还有其他一些小众的功能
获取所有已经加载过的类
获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
获取某个对象的大小
将某个 jar 加入到 bootstrap classpath 里作为高优先级被 bootstrapClassloader 加载
将某个 jar 加入到 classpath 里供 AppClassloard 去加载
设置某些 native 方法的前缀,主要在查找 native 方法的时候做规则匹配
#参考 JVM 源码分析之 javaagent 原理完全解读
https://www.infoq.cn/article/javaagent-illustrated/
根据参考中的分析,可以知道,在启动的时候指定了javaagent之后,java程序会根据META-INF/MANIFEST.MF新找到
Premain-Class:xxxx
指定的类,接着调用premain方法,使用jd-gui.jar查看rasp.jar中的META-INF/MANIFEST.MF,内容如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-it1rIYSE-1587637934982)(./imgs/rasp_manifest_mf.png)]
该类实现了premain方法,另一个是动态加载的方式,暂时不管,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQLZf4WM-1587637934986)(./imgs/init.png)]
进入到init方法

程序首先调用JarFileHelper.addJarToBootstrap(inst);将rasp.jar添加到bootstrap的路径内,这样系统会优先加载这个jar文件到系统路径中,后续的Hook能完成预先的类加载埋点插桩,后面的核心逻辑功能分析有细致分析

接着调用ModuleLoader.load(mode, action, inst);将rasp-engine.jar加载并执行。进入到ModuleLoader.load程序调用了ModuleLoader创建一个loader的实例对象

这里需要注意的是,在ModuleLoader内有一个静态代码块,这里负责初始化当前加载的根目录baseDirectory和系统加载器systemClassLoader

可以看到在获取 systemClassLoader的时候,有一个判断是否是sun.misc.Launcher$ExtClassLoader,不是就获取其父类的classLoader。
默认情况下,我们在写一个main方法内获取到的ClassLoader是
private static void showClassLoader(){
ClassLoader loader =ClassLoader.getSystemClassLoader();
System.out.println("Defaule ClassLoader Name:"+loader.getClass().getCanonicalName()+" \nparant Name: "+loader.getParent().getClass().getCanonicalName()+"\ngrand parent Name:"+loader.getParent().getParent().getClass().getCanonicalName());
}
sun.misc.Launcher.AppClassLoader
对应的`parent`是
sun.misc.Launcher.ExtClassLoader
对应的grand parent:NullPointer
可以看到sun.misc.Launcher.AppClassLoader对应的父类是sun.misc.Launcher.ExtClassLoader,这两个的父类都是URLClassLoader,具体参考(https://blog.youkuaiyun.com/briblue/article/details/54973413)
private static void showClassLoader(){
ClassLoader loader =ClassLoader.getSystemClassLoader();
// System.out.println("Defaule ClassLoader Name:"+loader.getClass().getCanonicalName()+" \nparant Name: "+loader.getParent().getClass().getCanonicalName()+"\ngrand parent Name:"+loader.getParent().getParent().getClass().getCanonicalName());
ClassLoader module = loader.getParent();
if (module instanceof URLClassLoader){
System.out.println("Got it "+module.getClass().getCanonicalName());
}
}
输出的结果
Got it sun.misc.Launcher.ExtClassLoader
初始化完成后,调用ModuleLoader的构造函数,ModuleLoader的构造函数是一个私有的,在构造函数内通过调用ModuleContainer的构造函数获取一个engineContainer对象,接着调用start方法启动程序。在ModuleContainer内部通过解析rasp-engine.jar获取MANIFEST.MF指定的Rasp-Module-Class类,调用反射方法获取到rasp-engine.jar的入口类,接着使用获取到的类加载器指定反射调用newInstance()方法,执行调用模块对应的start方法。

对应的Rasp-Module-Class在rasp-engine.jar的META-INF/MANIFEST.MF下

由于系统加载器不一定就是默认的加载器,所以在加载的时候做了区分,
一个就是判断是否为java.net.URLClassLoader这个加载器,另一个就是调用ModuleLoader.isCustomClassLoader函数判断是否是weblogic或者jdk9、10和11。
源码内对应的注释

如果两个都不是,则这里就提示加载失败,根据前面的分析可以知道,此时调用的是URLClassLoader部分的逻辑,反之就利用反射初始化模块EngineBoot对象,最后调用start方法。由于EngineBoot实现的是Module接口,每个模块都有start release两个方法

在准备完成后,通过engineContainer.start()的方式调用模块对应的start()

启动正常的话,就能看到了如下输出

核心原理
根据前面的启动流程可以知道,程序最后交给了EngineBoot类并执行了start方法。EngineBoot类主要完成以下几个工作
- 加载openrasp_v8.so (这个是暂时不做重点关注)
- 记载config文件 (这个是暂时不做重点关注)
- 初始化rasp编译信息 (这个是暂时不做重点关注)
- 缓存RASP的build信息 (这个是暂时不做重点关注)
- 加载js插件(这个是暂时不做重点关注)
- 加载java层的hook功能模块(重点关注)
- 初始化加载或者回滚类(重点)
加载java层的hook功能模块
在EngineBoot.start()内,在完成一些js和系统信息初始化后,接着来到了检测点的初始化,通过CheckerManager.init();来实现。在其内部通过加载com.baidu.openrasp.plugin.checker.CheckParameter. Typ内的枚举类,包括了如下类型的hook点
// js插件检测
SQL("sql", new V8AttackChecker(), 1),
COMMAND("command", new V8AttackChecker(), 1 << 1),
DIRECTORY("directory", new V8AttackChecker(), 1 << 2),
REQUEST("request", new V8AttackChecker(), 1 << 3),
READFILE("readFile", new V8AttackChecker(), 1 << 5),
WRITEFILE("writeFile", new V8AttackChecker(), 1 << 6),
FILEUPLOAD("fileUpload", new V8AttackChecker(), 1 << 7),
RENAME("rename", new V8AttackChecker(), 1 << 8),
XXE("xxe", new V8AttackChecker(), 1 << 9),
OGNL("ognl", new V8AttackChecker(), 1 << 10),
DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11),
WEBDAV("webdav", new V8AttackChecker(), 1 << 12),
INCLUDE("include", new V8AttackChecker(), 1 << 13),
SSRF("ssrf", new V8AttackChecker(), 1 << 14),
SQL_EXCEPTION("sql_exception", new V8AttackChecker(), 1 << 15),
REQUESTEND("requestEnd", new V8AttackChecker(), 1 << 17),
DELETEFILE("deleteFile", new V8AttackChecker(), 1 << 18),
MONGO("mongodb", new V8AttackChecker(), 1 << 19),
LOADLIBRARY("loadLibrary", new V8AttackChecker(), 1 << 20),
SSRF_REDIRECT("ssrfRedirect", new V8AttackChecker(), 1 << 21),
RESPONSE("response", new V8AttackChecker(false), 1 << 23),
// java本地检测
XSS_USERINPUT("xss_userinput", new XssChecker(), 1 << 16),
SQL_SLOW_QUERY("sqlSlowQuery", new SqlResultChecker(false), 0),
// 安全基线检测
POLICY_LOG("log", new LogChecker(false), 1 << 22),
POLICY_MONGO_CONNECTION("mongoConnection", new MongoConnectionChecker(false), 0),
POLICY_SQL_CONNECTION("sqlConnection", new SqlConnectionChecker(false), 0),
POLICY_SERVER_TOMCAT("tomcatServer", new TomcatSecurityChecker(false), 0),
POLICY_SERVER_JBOSS("jbossServer", new JBossSecurityChecker(false), 0),
POLICY_SERVER_JBOSSEAP("jbossEAPServer", new JBossEAPSecurityChecker(false), 0),
POLICY_SERVER_JETTY("jettyServer", new JettySecurityChecker(false), 0),
POLICY_SERVER_RESIN("resinServer", new ResinSecurityChecker(false), 0),
POLICY_SERVER_WEBSPHERE("websphereServer", new WebsphereSecurityChecker(false), 0),
POLICY_SERVER_WEBLOGIC("weblogicServer", new WeblogicSecurityChecker(false), 0),
POLICY_SERVER_WILDFLY("wildflyServer", new WildflySecurityChecker(false), 0),
POLICY_SERVER_TONGWEB("tongwebServer", new TongwebSecurityChecker(false), 0),
POLICY_SERVER_BES("bes", new BESSecurityChecker(false), 0);
加这些插件添加到一个EnumMap内
/**
* Created by tyy on 17-11-20.
*
* 用于管理 hook 点参数的检测
*/
public class CheckerManager {
private static EnumMap<Type, Checker> checkers = new EnumMap<Type, Checker>(Type.class);
public synchronized static void init() throws Exception {
for (Type type : Type.values()) {
checkers.put(type, type.checker);
}
}
public synchronized static void release() {
checkers = null;
}
public static boolean check(Type type, CheckParameter parameter) {
return checkers.get(type).check(parameter);
}
}
添加的检测类为
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker n

本文详细分析了OpenRasp的Java功能,包括RaspIntall.jar的安装过程,启动执行流程,核心原理如加载java层的hook功能模块和运行时插桩。在启动流程中,重点讲述了如何通过类加载机制实现动态插桩,以及反序列化检测的逻辑。此外,还简要提及了未分析的native实现部分。
最低0.47元/天 解锁文章
1253

被折叠的 条评论
为什么被折叠?



