最近在做关于后台服务器和web的rasp这块工作,需要在OpenRasp
的基础上做一个二次研发,为了能弄清其实现的技术细节,阅读了一下源码。记录的内容主要有以下几个点
OpenRasp
的基本就是上述几个过程。其中重点关注的是启动执行流程,核心原理,检测规则
三个点,下面就一个个的分析其实现。在分析的过程中我用了一个一些逆向工程的思路,当然也可以直接读源码,每个人都有自己的思路,能达到目的就可以了。下面分析主要关注的java–>springboot–>DeserializationHook相关
在其官方文档中,有提到了在启动服务之前,需要先安装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主要功能:
启动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
类主要完成以下几个工作
在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 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 name: com.baidu.openrasp.plugin.checker.v8.V8AttackChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.local.XssChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.local.SqlResultChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.LogChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.MongoConnectionChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.SqlConnectionChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.TomcatSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JBossSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JBossEAPSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.JettySecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.ResinSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WebsphereSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WeblogicSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.WildflySecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.TongwebSecurityChecker
[Loopher]: ===> loading checker name: com.baidu.openrasp.plugin.checker.policy.server.BESSecurityChecker
完成插件添加后,接着调用initTransformer(inst);
函数开始执行插桩。
插桩的实现是通过CustomClassTransformer implements ClassFileTransformer
来实现。在分析构造函数前,这里需要关注两个变量,一个是hooks
另一个是serverDetector
hooks
用来保存需要插桩的类,例如DeserializationHook
serverDetector
服务器的探测器,这个类也是需要通过CustomClassTransformer
检测服务器并针对不同的服务器做不同的处理。包括的类型有
private ServerDetectorManager() {
detectors.add(new TomcatDetector());//"org/apache/catalina/Server"
detectors.add(new JBossDetector());//"org/jboss/Main"
detectors.add(new JBossEAPDetector());//"org/jboss/modules/Main"
detectors.add(new JettyDetector());//"org/eclipse/jetty/server/Server"
detectors.add(new WeblogicDetector());//"weblogic/servlet/internal/WebAppServletContext"
detectors.add(new ResinDetector());//"com/caucho/server/resin/Resin"
detectors.add(new WebsphereDetector());// "org/eclipse/core/launcher/Main"
detectors.add(new UndertowDetector());//"io/undertow/server/HttpHandler"
detectors.add(new DubboDetector());//"com/alibaba/dubbo/rpc/filter/GenericFilter"
detectors.add(new SpringbootDetector());//"org/apache/catalina/startup/Bootstrap"
detectors.add(new TongWebDetector());// "com/tongweb/web/thor/Server"
detectors.add(new BESDetector());//"com/bes/enterprise/webtier/Server"
}
构造函数内完成了以下功能
inst.addTransformer(this, true);
addAnnotationHook();//添加hook点
重点关注这个addAnnotationHook();
方法。
private void addAnnotationHook() {
//SCAN_ANNOTATION_PACKAGE="com.baidu.openrasp.hook"
Set<Class> classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class);
for (Class clazz : classesSet) {
try {
Object object = clazz.newInstance();
if (object instanceof AbstractClassHook) {
addHook((AbstractClassHook) object, clazz.getName());
}
} catch (Exception e) {
LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e);
}
}
}
在程序内首先扫描了com.baidu.openrasp.hook下的
带有HookAnnotation
注解的类,这里以DeserializationHook
为例,其他的都一样。如下是DeserializationHook
的源码
/**
* Created by tyy on 6/21/17.
* 反序列化漏洞检测hook
*/
@HookAnnotation
public class DeserializationHook extends AbstractClassHook {
/**
* (none-javadoc)
*
* @see com.baidu.openrasp.hook.AbstractClassHook#getType()
*/
@Override
public String getType() {
return "deserialization";
}
/**
* (none-javadoc)
*
* @see com.baidu.openrasp.hook.AbstractClassHook#isClassMatched(String)
*/
@Override
public boolean isClassMatched(String className) {
return "java/io/ObjectInputStream".equals(className);
}
/**
* (none-javadoc)
*
* @see com.baidu.openrasp.hook.AbstractClassHook#hookMethod(CtClass)
*/
@Override
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
String src = getInvokeStaticSrc(DeserializationHook.class, "checkDeserializationClass",
"$1", ObjectStreamClass.class);
insertBefore(ctClass, "resolveClass", "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;", src);
}
/**
* 反序列化监检测点
*
* @param objectStreamClass 反序列化的类的流对象
*/
public static void checkDeserializationClass(ObjectStreamClass objectStreamClass) {
if (objectStreamClass != null) {
String clazz = objectStreamClass.getName();
if (clazz != null) {
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("clazz", clazz);
HookHandler.doCheck(CheckParameter.Type.DESERIALIZATION, params);
}
}
}
}
可以看到DeserializationHook
存在注解类HookAnnotation
,因此会被扫描到。在获取了所有的hook插入点类之后,开始执行程序插桩过程。
迭代获取获取回来的classesSet
类集合,并将每个类执行newInstance
方法
Object object = clazz.newInstance();
在获取到对应的实例对象值后,判断获取的类是否是AbstractClassHook
实例,从DeserializationHook
内可以看到,这个类继承自AbstractClassHook
,这个类是所有的hook点插桩的基类。判断之后就开始调用addhook
函数执行插桩类的存储,后续使用。
private void addHook(AbstractClassHook hook, String className) {
if (hook.isNecessary()) {
necessaryHookType.add(hook.getType());
}
String[] ignore = Config.getConfig().getIgnoreHooks();
for (String s : ignore) {
if (hook.couldIgnore() && (s.equals("all") || s.equals(hook.getType()))) {
LOGGER.info("ignore hook type " + hook.getType() + ", class " + className);
return;
}
}
//保存需要插桩的类
hooks.add(hook);
}
在CustomClassTransformer
构造函数完成后,下一步就是调用retransform
函数。这里先整理下到目前为止的工作
CustomClassTransformer
到系统内com.baidu.openrasp.hook
带有HookAnnotation
类型注解的类AbstractClassHook
hooks
内,hooks
是一个HashSet
在retransform
内,首先通过Instrumentation
来获取所有加载的类,
然后调用isClassMatched
将获取的类和需要插桩的类比较,如果是复合想要插桩的类,则返回True
否则,就调用serverDetector.isClassMatched
做判断,返回true
的类都是要被插桩或者 特殊处理的类,将会调用inst.retransformClasses(clazz);
来hook或者回滚已经被加载的类,这里主要的功能就是初始化已经被回滚已经被加载的类。
public void retransform() {
LinkedList<Class> retransformClasses = new LinkedList<Class>();
Class[] loadedClasses = inst.getAllLoadedClasses();//获取已经加载的类
for (Class clazz : loadedClasses) {
if (isClassMatched(clazz.getName().replace(".", "/"))) {
if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm")) {
try {
// hook已经加载的类,或者是回滚已经加载的类
inst.retransformClasses(clazz);
} catch (Throwable t) {
LogTool.error(ErrorType.HOOK_ERROR,
"failed to retransform class " + clazz.getName() + ": " + t.getMessage(), t);
}
}
}
}
}
//
public boolean isClassMatched(String className) {
for (final AbstractClassHook hook : getHooks()) {
if (hook.isClassMatched(className)) {
return true;//判断是否在类Hook点内
}
}
//另一种是判断是否在存在服务器类型内
return serverDetector.isClassMatched(className);
}
到这里EngineBoot.start()
方法分析完成了,接着是运行时的修改流程
由于CustomClassTransformer
继承自ClassFileTransformer
,当类被加载的时候,会调用transform
函数,原型如下
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {}
其中classfileBuffer
就是类的字节码,类加载进来后会将字节码保存在这个参数内,重写transform
函数,当类被加载的时候,通过这个函数传递类的字节码进来,在这里执行插桩过程,如下
/**
* 过滤需要hook的类,进行字节码更改
*
* @see ClassFileTransformer#transform(ClassLoader, String, Class, ProtectionDomain, byte[])
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (loader != null) {
DependencyFinder.addJarPath(domain);
}
if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {
jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader));
}
for (final AbstractClassHook hook : hooks) {
if (hook.isClassMatched(className)) {
CtClass ctClass = null;
try {
ClassPool classPool = new ClassPool();
addLoader(classPool, loader);//设置classPool参数,添加到运行环境内
ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));//从字节数组内回复一个完整的类对象
if (loader == null) {
hook.setLoadedByBootstrapLoader(true);//设置是从bootstrapLoader加载的类
}
classfileBuffer = hook.transformClass(ctClass);//修改类字节码,返回一个byte[]修改好的类字节数组
if (classfileBuffer != null) {
checkNecessaryHookType(hook.getType());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
}
}
serverDetector.detectServer(className, loader, domain);
//返回修改后的字节码
return classfileBuffer;//
}
从程序中可以看到,利用hooks
来过滤不需要插桩的类,符合条件的类将进入动态插桩流程,这里和之前的java动态修改字节码技术基础中的内容一致,重点进入到classfileBuffer = hook.transformClass(ctClass);
进入到transformClass
方法后,先调用了hookMethod
执行插桩,接着放回类的字节码
public byte[] transformClass(CtClass ctClass) {
try {
hookMethod(ctClass);
return ctClass.toBytecode();
} catch (Throwable e) {
if (Config.getConfig().isDebugEnabled()) {
LOGGER.info("transform class " + ctClass.getName() + " failed", e);
}
}
return null;
}
继续进入到hookMethod
,发现是一个抽象的方法,则需要在子类内实现,这里关注反序列化的检测,因此查看DeserializationHook
的hookMethod
的实现如下
protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
String src = getInvokeStaticSrc(DeserializationHook.class, "checkDeserializationClass",
"$1", ObjectStreamClass.class);
insertBefore(ctClass, "resolveClass", "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;", src);
}
根据java动态修改字节码
的知识可以知道,getInvokeStaticSrc
返回的是一个方法的字符串,如下
/**
* 获取调用静态方法的代码字符串
*
* @param invokeClass 静态方法所属的类
* @param methodName 静态方法名称
* @param paramString 调用传入的参数字符串,按照javassist格式
* @return 整合之后的代码
*/
public String getInvokeStaticSrc(Class invokeClass, String methodName, String paramString, Class... parameterTypes) {
String src;
String invokeClassName = invokeClass.getName();
String parameterTypesString = "";
if (parameterTypes != null && parameterTypes.length > 0) {
for (Class parameterType : parameterTypes) {
if (parameterType.getName().startsWith("[")) {
parameterTypesString += "Class.forName(\"" + parameterType.getName() + "\"),";
} else {
parameterTypesString += (parameterType.getName() + ".class,");
}
}
parameterTypesString = parameterTypesString.substring(0, parameterTypesString.length() - 1);
}
if (parameterTypesString.equals("")) {
parameterTypesString = null;
} else {
parameterTypesString = "new Class[]{" + parameterTypesString + "}";
}
if (isLoadedByBootstrapLoader) {
//被bootstrap加载的类,插入的代码方式
src = "com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass(\"" + invokeClassName + "\").getMethod(\"" + methodName +
"\"," + parameterTypesString + ").invoke(null";
if (!StringUtils.isEmpty(paramString)) {
src += (",new Object[]{" + paramString + "});");
} else {
src += ",null);";
}
src = "try {" + src + "} catch (Throwable t) {if(t.getCause() != null && t.getCause().getClass()" +
".getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\")){throw t;}}";
} else {
src = invokeClassName + '.' + methodName + "(" + paramString + ");";
src = "try {" + src + "} catch (Throwable t) {if(t.getClass()" +
".getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\")){throw t;}}";
}
return src;
}
假设返回来的字符串src
如下
try{
com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass("DeserializationHook").getMethod("checkDeserializationClass","$1").invoke(null,new Object[]{ObjectStreamClass.class});
或者是
com.baidu.openrasp.ModuleLoader.moduleClassLoader.loadClass("java.io.ObjectInputStream").getMethod("resolveClass","java.io.ObjectStreamClass").invoke(null,null);
}catch(Throwable t) {
if(t.getCause() != null && t.getCause().getClass().getName().equals(\"com.baidu.openrasp.exceptions.SecurityException\"))
{throw t;}
}
}
接着调用插桩方法insertBefore
执行插桩,如下
public void insertBefore(CtClass ctClass, String methodName, String desc, String src)
throws NotFoundException, CannotCompileException {
/**
例如反序列化Hook方法,
ctClass="io.ObjectInputStream"
methodName="resolveClass"
des="(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;" 方法描述
src="checkDeserializationClass"封装好的方法体
//2020-04-20新增注释
**/
LinkedList<CtBehavior> methods = getMethod(ctClass, methodName, desc, null);
if (methods != null && methods.size() > 0) {
insertBefore(methods, src);//核心插桩函数
} else {
if (Config.getConfig().isDebugEnabled()) {
LOGGER.info("can not find method " + methodName + " " + desc + " in class " + ctClass.getName());
}
}
}
最后完成插桩
private void insertBefore(LinkedList<CtBehavior> methods, String src)
throws CannotCompileException {
for (CtBehavior method : methods) {
if (method != null) {
insertBefore(method, src);
}
}
}
/**
* 在目标类的目标方法的入口插入相应的源代码
*
* @param method 目标方法
* @param src 源代码
*/
public void insertBefore(CtBehavior method, String src) throws CannotCompileException {
try {
method.insertBefore(src);
LOGGER.info("insert before method " + method.getLongName());
} catch (CannotCompileException e) {
LogTool.traceError(ErrorType.HOOK_ERROR,
"insert before method " + method.getLongName() + " failed: " + e.getMessage(), e);
throw e;
}
}
这里就完成了对关键类的插桩逻辑分析,当发生序列化检测的时候,程序会先调用到插桩函数,然后在执行原始函数,下面进行逻辑分析。
还是以DeserializationHook
为例,在java/io/ObjectInputStream
类中的resolveClass
方法的入口处加入了
checkDeserializationClass.checkDeserializationClass(ObjectInputStream input);
因此发生java.io.ObjectInputStream.resolveClass
之后,首先调用checkDeserializationClass
,此时检测逻辑将进入到HookHandler.doCheck(CheckParameter.Type.DESERIALIZATION, params);
内。
来到HookHandler
内,需要关注几个变量
ThreadLocal
如果不清除的可以自行了解下ThreadLocal
ThreadLocal
ThreadLocal
类型的,目的就是为了多线程支持,避免造成线程间的请求和响应出现混乱。在清除了几个变量之后,再次进入到doCheck
,首先判断是否开启了hook
,如果开启了,则调用doCheckWithoutRequest
函数,在doCheckWithoutRequest
内经过一些校验检测后,最后来到
doRealCheckWithoutRequest(type, params);
这里的type=DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11) ,param={"clazz":"ObjectStreamClass"}
,说明是反序列化检测类型,调用CheckParameter
构建一个检测参数,然后通过
isBlock = CheckerManager.check(type, parameter);
来调用检测逻辑,继续来到CheckerManager.check
函数
public static boolean check(Type type, CheckParameter parameter) {
return checkers.get(type).check(parameter);
}
最后返回是否决定阻止返回,可以知道这里的checkers
就是在一开始就初始化的类,最后通过一个类型获取对应的检测器去检测。
还有一个问题就是,这里的reqeustCache和responseCache
并没有被DeserializationHook
赋值,而是通过了SpringFormContentHook
类的rebuildRequest
进行赋值的
public static void rebuildRequest(Object requestWrapper) {
if (HookHandler.requestCache.get() != null) {
String requestId = HookHandler.requestCache.get().getRequestId();
HookHandler.requestCache.set(new HttpServletRequest(requestWrapper, requestId));
}
}
这个类也是在CustomClassTransformer
阶段的transform
执行代码插桩。
当isBlock=true
的时候,进入到handleBlock
,这里的处理方式很简单粗暴,直接抛出异常
private static void handleBlock(CheckParameter parameter) {
SecurityException securityException = new SecurityException("Request blocked by OpenRASP");
if (responseCache.get() != null) {
responseCache.get().sendError(parameter);
}
throw securityException;
}
这里就差不多分析完成了整个检测调用流程,当然还有一些其他细节,例如响应头的设置等都在HookHandle
类中,只是每个检测点请求的不同,调用的时机也不一样,有需要可以再细读其他细节。接下来就是检测细节的分析在,这里以反序列化检测为例进行分析。
根据前面的流程分析可以知道,此时程序调用了反序列化的检测器
DESERIALIZATION("deserialization", new V8AttackChecker(), 1 << 11),
调用的了check
阶段,调用位置在com.baidu.openrasp.plugin.checker.AbstractChecker内
public boolean check(CheckParameter checkParameter) {
List<EventInfo> eventInfos = checkParam(checkParameter);
boolean isBlock = false;
if (eventInfos != null) {
for (EventInfo info : eventInfos) {
if (info.isBlock()) {
isBlock = true;
}
dispatchCheckEvent(info);
}
}
isBlock = isBlock && canBlock;
return isBlock;
}
其中的checkParam
的实现如下
public List<EventInfo> checkParam(CheckParameter checkParameter) {
return JS.Check(checkParameter);
}
当返回的eventInfos
不为空并且info.isBlock()==true
,则当前的请求会被墙掉。
继续跟进到JS.Check
内,这里的V8
是在EngineBoot.start()
阶段就初始化了,继续跟进到check
函数内,先获取检测类型,这里是DESERIALIZATION
,调用JsonStream.serialize
函数将请求的参数序列化,然后将请求的参数读取出来保存在out
内。进入到实际判断逻辑阶段。
首先判断检测类型,如果是DIRECTORY READFILE WRITEFILE SQL SSRF
类型的,则统一处理,否则,开始调用V8.Check(..)
函数,参数分别是
public static byte[] Check(String type, byte[] params, int params_size, Context context, boolean new_request,
int timeout);
其中Context
对象是
public class Context extends com.baidu.openrasp.v8.Context {
public AbstractRequest request = null;
public static void setKeys() {
setStringKeys(new String[]{"path", "method", "url", "querystring", "protocol", "remoteAddr", "appBasePath",
"requestId", "appId", "raspId", "hostname", "source", "target", "clientIp"});
setObjectKeys(new String[]{"json", "server", "parameter", "header", "nic"});
setBufferKeys(new String[]{"body"});
}
public Context(AbstractRequest request) {
this.request = request;
}
public String getString(String key) {
if (key.equals("path"))
return getPath();
if (key.equals("method"))
return getMethod();
if (key.equals("url"))
return getUrl();
if (key.equals("querystring"))
return getQuerystring();
if (key.equals("appBasePath"))
return getAppBasePath();
if (key.equals("protocol"))
return getProtocol();
if (key.equals("remoteAddr"))
return getRemoteAddr();
if (key.equals("requestId"))
return getRequestId();
if (key.equals("appId"))
return getAppId();
if (key.equals("raspId"))
return getRaspId();
if (key.equals("hostname"))
return getHostname();
if (key.equals("source"))
return getSource();
if (key.equals("target"))
return getTarget();
if (key.equals("clientIp"))
return getClientIp();
return null;
}
public byte[] getObject(String key) {
if (key.equals("json"))
return getJson();
if (key.equals("header"))
return getHeader();
if (key.equals("parameter"))
return getParameter();
if (key.equals("server"))
return getServer();
if (key.equals("nic"))
return getNic();
return null;
}
public byte[] getBuffer(String key) {
if (key.equals("body"))
return getBody();
return null;
}
public String getPath() {
try {
return request.getRequestURI().toString();
} catch (Exception e) {
return "";
}
}
public String getMethod() {
try {
return request.getMethod().toLowerCase().toString();
} catch (Exception e) {
return "";
}
}
public String getUrl() {
try {
return request.getRequestURL().toString().toString();
} catch (Exception e) {
return "";
}
}
public String getQuerystring() {
try {
return request.getQueryString().toString();
} catch (Exception e) {
return "";
}
}
public String getAppBasePath() {
try {
return request.getAppBasePath().toString();
} catch (Exception e) {
return "";
}
}
public String getProtocol() {
try {
return request.getProtocol().toLowerCase().toString();
} catch (Exception e) {
return "";
}
}
public String getRemoteAddr() {
try {
return request.getRemoteAddr().toString();
} catch (Exception e) {
return "";
}
}
public String getRequestId() {
try {
return request.getRequestId().toString();
} catch (Exception e) {
return "";
}
}
// TODO: update openrasp-v8, accept string body
public byte[] getBody() {
try {
return escape(request.getStringBody());
} catch (Exception e) {
return null;
}
}
public byte[] getJson() {
try {
String contentType = request.getContentType();
if (contentType != null && contentType.contains("application/json")) {
return getBody();
}
return null;
} catch (Exception e) {
return null;
}
}
public byte[] escape(String src) throws UnsupportedEncodingException {
char j;
StringBuilder tmp = new StringBuilder();
for (int i = 0; i < src.length(); i++) {
j = src.charAt(i);
if (j < 256)
tmp.append(j);
else {
tmp.append("\\u");
tmp.append(Integer.toString(j, 16));
}
}
return tmp.toString().getBytes("UTF-8");
}
public byte[] getHeader() {
try {
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames == null || !headerNames.hasMoreElements()) {
return null;
}
HashMap<String, String> headers = new HashMap<String, String>();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
headers.put(key.toLowerCase(), value);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
JsonStream.serialize(headers, out);
out.write(0);
return out.getByteArray();
} catch (Exception e) {
return "{}".getBytes();
}
}
public byte[] getParameter() {
try {
Map<String, String[]> parameters = request.getParameterMap();
if (parameters == null || parameters.isEmpty()) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
JsonStream.serialize(parameters, out);
out.write(0);
return out.getByteArray();
} catch (Exception e) {
return null;
}
}
public byte[] getServer() {
try {
Map<String, String> server = ApplicationModel.getApplicationInfo();
ByteArrayOutputStream out = new ByteArrayOutputStream();
JsonStream.serialize(server, out);
out.write(0);
return out.getByteArray();
} catch (Exception e) {
return "{}".getBytes();
}
}
public String getAppId() {
try {
return Config.getConfig().getCloudAppId();
} catch (Exception e) {
return "";
}
}
public String getRaspId() {
try {
return CloudCacheModel.getInstance().getRaspId() != null ? CloudCacheModel.getInstance().getRaspId() : "";
} catch (Exception e) {
return "";
}
}
public String getHostname() {
try {
return OSUtil.getHostName();
} catch (Exception e) {
return "";
}
}
public byte[] getNic() {
try {
List<NicModel> nic = OSUtil.getIpAddress();
ByteArrayOutputStream out = new ByteArrayOutputStream();
JsonStream.serialize(nic, out);
out.write(0);
return out.getByteArray();
} catch (Exception e) {
return "{}".getBytes();
}
}
public String getSource() {
try {
return request.getRemoteAddr();
} catch (Exception e) {
return "";
}
}
public String getTarget() {
try {
return request.getLocalAddr();
} catch (Exception e) {
return "";
}
}
public String getClientIp() {
try {
return request.getClientIp();
} catch (Exception e) {
return "";
}
}
}
保存的整个请求的完整字段信息。
params
是请求的参数,被序列化为了字节,便于在V8引擎内检测。
程序最终来到V8.Check(String type, byte[] params, int params_size, Context context, boolean new_request, int timeout)
函数,这个是一个Native的,需要分析下对应的实现。
后续在补上这块,因为涉及到V8引擎的执行原理,暂且留着。