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.csdn.net/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
[<