从零开始的Java RASP实现(二)
最近面试有点多,停更了两周,接着上一篇继续写java RASP的实现过程。
2 RASP-demo
通过前面的例子已经可以看到,通过Instrumentation对象添加Transformer,在transform方法中可以做到对加载的类进行动态修改,如果transform方法可以获取到所有系统类的加载,岂不是就可以有针对性的对有风险的类进行修改,在其危险方法执行前后获取参数加以判断,从而实现RASP。但事情不是那么美好的
以前面javaagent使用流程举例,对主要方法进一些修改,看看效果:
package com.bitterz;
import java.io.IOException;
import java.lang.Runtime;
import java.lang.String;
public class Main {
public static class A{
public void t(){System.out.println("Main$A.t()");}
}
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("-------Main.main() start-------");
Runtime.getRuntime().exec("calc");
String a = "a";
System.out.println(a);
A a1 = new A();
a1.t();
System.out.println("-------Main.main() end-------");
}
}
package com.bitterz;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class PreMain {
public static void premain(String agentArgs, Instrumentation inst) throws IOException {
System.out.println("++++++++Premain start++++++++");
System.out.println(ClassLoader.getSystemClassLoader().toString()); // 查看当前代理类是被哪个类加载器加载的
inst.addTransformer(new DefineTransformer(), true);
System.out.println("++++++++Premain end++++++++");
}
public static class DefineTransformer implements ClassFileTransformer {
@Override // 添加override会让transform只接收到appClassLoader加载的类,去掉就可以接收重要的系统类
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className.toString() + " " + loader.toString()); // 类名 和 类加载器
System.out.println("");
return classfileBuffer;
}
}
}
输出结果如下
这里注意两个细节
- premain和main都是被同一个类加载器锁加载的
- main方法要new一个A类对象时,会先进入transform函数,且类加载器和前面一样,然后再执行A类中的方法
除了这两个细节外,我们应该注意到,像String、Runtime这些类,也被调用了,transform函数却获取不到!这里就跟类加载机制有关了!
2.1 类加载机制
双亲委派
首先要理解一下类加载的双亲委派机制,每一个类加载器创建时都要指定一个父加载器,当类加载器A要加载B类时,会先由其父加载器对B类进行加载,如果父加载器无法加载,则由它自己进行加载。看一下ClassLoader的源代码就很好理解了
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); // 查看是不是本身已经加载过的类
if (c == null) {
long t0 = System.nanoTime();
try { // 父类先尝试加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) { // 父类加载失败,自己加载
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
结合源代码也就很好理解。特别需要指出的是Class<?> c = findLoadedClass(name);
这一行可以看出,每个ClassLoader都记录着一个自己加载过的类。
BootStrap ClassLoader
jvm中所有的类都是由类加载器加载的,那类加载器又是谁加载的呢?首先,jvm启动之后,会执行一段机器码,也就是C写好的程序,这段程序被成为Bootstrap ClassLoader,注意它不是可以访问的java对象。由它去加载系统类,以及扩展类加载器(extClassLoader)和应用程序类加载器(AppClassLoader)。
- 其中extClassLoader负责加载
<JAVA_HOME>/lib/ext
目录下或者由系统变量-Djava.ext.dir指定位路径中的类库 - AppClassLoader负责加载系统类路径
java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath路径
他们的关系如下:
启动类加载器,由C++实现,没有父类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
另外需要指出的是,一个Java类,由它的全限定名称和加载它的classloader两者确定。
说了这么多,回到2.1章节最开始的问题,agentmain和main都是同一个appClassLoader加载的,并且我们写好的各种类都是AppClassLoader加载的,那BootstrapClassLoader和extClassLoader加载的类调用我们写好的代理方法,这些类加载器向上委派寻找类时,扩展类加载器和引导类加载器都没有加过,直接违背双亲委派原则!举个例子,因为我们可以在transform函数里面获取到类字节码,并加以修改,如果我们在系统类方法前面插了代理方法,由于这些系统类是被Bootstrap ClassLoader加载的,当BootstrapClassLoader检查这些代理方法是否被加载时,直接就报错了,因为代理类是appClassLoader加载的(见2.1 类加载机制
这个章节上面的图),要解决这个问题,我们就应该想办法把代理类通过BootstrapClassLoader进行加载,从百度的OpenRASP可以学到解决方案:
// localJarPath为代理jar包的绝对路径
inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))
通过appendToBootstrapClassLoaderSearch
方法,可以把一个jar包放到Bootstrap ClassLoader的搜索路径,也就是说,当Bootstrap ClassLoader检查自身加载过的类,发现没有找到目标类时,会在指定的jar文件中搜索,从而避免前面提到的违背双亲委派问题。参考官方文档和翻译文档
2.2 Instrumentation介绍
为了方便用户对JVM进行操作,JDK1.5之后引入了这个Instrumentation特性,通过Instrumentation的实例对象,可以对jvm进行一定的操作,例如修改字节码、插桩等等。它的实现原理是JVMTI(JVM Tool Interface),也就是JVM向用户提供的操作jvm的接口。JVMTI是事件驱动的,当发生一定的处理逻辑时,才会调用回调接口,而这些接口可以让用户扩展一些逻辑。例如前面的transform函数调用,就是JVMTI监听到类加载,就会基于这个事件,回调instrumentation中的所有ClassTransformer.transform函数,进行类转换(Class transform)。所以我们可以理解为获得instrumentation对象,就可以实现对一个jvm的一定操作,获取的这个对象的方法就是前文提到的javaagent和attach方法。
Instrumentation类中常用方法
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)//注册ClassFileTransformer实例,注册多个会按照注册顺序进行调用。所有的类被加载完毕之后会调用ClassFileTransformer实例,相当于它们通过了redefineClasses方法进行重定义。布尔值参数canRetransform决定这里被重定义的类是否能够通过retransformClasses方法进行回滚。
void addTransformer(ClassFileTransformer transformer)//相当于addTransformer(transformer, false),也就是通过ClassFileTransformer实例重定义的类不能进行回滚。
boolean removeTransformer(ClassFileTransformer transformer)//移除(反注册)ClassFileTransformer实例。
void retransformClasses(Class<?>... classes)//已加载类进行重新转换的方法,重新转换的类会被回调到ClassFileTransformer的列表中进行处理。
void appendToBootstrapClassLoaderSearch(JarFile jarfile)//指定 JAR 文件,放到Bootstrap ClassLoader搜索路径
void appendToSystemClassLoaderSearch(JarFile jarfile)//将某个jar加入到Classpath里供AppClassloard去加载。
Class[] getAllLoadedClasses()//返回 JVM 当前加载的所有类的数组
Class[] getInitiatedClasses(ClassLoader loader)//获取所有已经被初始化过了的类。
boolean isModifiableClass(Class<?> theClass)//确定一个类是否可以被 retransformation 或 redefinition 修改
void redefineClasses(ClassDefinition... definitions)//重定义类,也就是对已经加载的类进行重定义,ClassDefinition类型的入参包括了对应的类型Class<?>对象和字节码文件对应的字节数组。
Instrumentation触发流程
摘自https://www.cnblogs.com/rickiyang/p/11368932.html
JVMTIAgent
是一个利用JVMTI
暴露出来的接口提供了代理启动时加载(agent on load)、代理通过attach形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。instrument agent
可以理解为一类JVMTIAgent
动态库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent)
,也就是专门为java语言编写的插桩服务提供支持的代理。
启动时加载instrument agent过程:
- 创建并初始化 JPLISAgent;
- 监听
VMInit
事件,在 JVM 初始化完成之后做下面的事情:- 创建 InstrumentationImpl 对象 ;
- 监听 ClassFileLoadHook 事件 ;
- 调用 InstrumentationImpl 的
loadClassAndCallPremain
方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法 ;
- 解析 javaagent 中 MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容。
运行时加载instrument agent过程:
通过 JVM 的attach机制来请求目标 JVM 加载对应的agent,过程大致如下:
- 创建并初始化JPLISAgent;
- 解析 javaagent 里 MANIFEST.MF 里的参数;
- 创建 InstrumentationImpl 对象;
- 监听 ClassFileLoadHook 事件;
- 调用 InstrumentationImpl 的
loadClassAndCallAgentmain
方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class
类的agentmain
方法。
Instrumentation的局限性
摘自https://www.cnblogs.com/rickiyang/p/11368932.html
- premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
- 类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新类和老类的父类必须相同;
- 新类和老类实现的接口数也要相同,并且是相同的接口;
- 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
- 新类和老类新增或删除的方法必须是private static/final修饰的;
- 可以修改方法体。
除了上面的方式,如果想要重新定义一个类,可以考虑基于类加载器隔离的方式:创建一个新的自定义类加载器去通过新的字节码去定义一个全新的类,不过也存在只能通过反射调用该全新类的局限性。
另外由于JVM维护运行线程和逻辑的安全,禁止对运行时的类新加方法并重新定义该方法,只允许修改方法体中的逻辑,如果多次attach时,显然就会出现重复插桩,造成重复警告等,影响业务线程。
2.3 javassist
前面提到修改字节码以实现在系统类或其它重要类执行前后插入代码,运行时防御恶意攻击,基于javassist容易上手的特点,所以直接用javassist修改字节码。javassist有几个重要的类及其方法:
ClassPool:
- getDefault:返回一个ClassPool对象,ClassPool是单例模式,保存了当前运行环境中创建过的CtClass
- get/getCtClass:根据类名获取该类的CtClass对象,而后进一步操作CtClass对象
CtClass:对class文件的解析,将其描述为一个java对象
detach,将一个class从ClassPool中删除,减小内存消耗
getDeclaredMethod(String arg), 获取一个CtMethod对象
getDeclaredMethods,获取所有的这个类中所有的方法,并返回CtMethod数组
getConstructor(String arg),获取一个指定的构造方法,返回CtConstructor对象
getConstructors,获取所有的构造方法,返回CtConstructor数组
toBytecode,将ctClass对象的字节码转换成字节数组,这个方法在rasp中非常需要
writeFile,将ctClass对象的修改保存到文件中。
CtMethod:对类中的方法的描述,可以通过这个对象,在一个方法前后添加代码
- insertBefore,很明显,在给定的方法前插入一段代码
- insertAfter,也很明显,在给定方法的所有return前,插入一段代码,报错会掠过这些代码
- insertAt,在指定位置插入代码,一般不这么操作,改变系统类中方法的逻辑,可能会引发更多的问题
- setBody,将方法的内容设置为指定的代码,如果是abstrict修饰的方法,该修饰符将被移除。指定的代码用$1,$2代表实际传入的参数
ctMethod.setBody("{$1='a';if ($2==1) return 0;.....}")
问题来了:如何修改jvm中的字节码
根据前面提到的这些方法,我们可以思考这样一个问题,就算用了javassist提供的各种方法,给字节码中添加了各类方法,但也只能保存到class文件中,而这个类jvm在运行时,我们修改class文件并不会影响jvm中的字节码,想要执行插入的代码,只能重新运行class文件,能不能动态修改jvm中的字节码,改变程序运行结果呢??
方法1,通过ClassLoader#defineClass方法覆盖jvm中的字节码
package com.bitterz.assist;
import javassist.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
public class Test {
private final String name="laowang";
@Override
public boolean equals(Object o){
System.out.println(this.name);
return false;
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
// 先测试一次equals方法的输出
Test test1 = new Test();
test1.equals("a");
System.out.println("+++++++++++++++++分割+++++++++++++++++");
// 获取当前class文件位置
Process chdir = Runtime.getRuntime().exec("cmd /c chdir");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(chdir.getInputStream(), "gbk"));
String baseDir = bufferedReader.readLine();
String classDir = baseDir + "/target/classes".replace("/", "\\");
// 添加class路径并找到Test类的字节码
ClassPool classPool = new ClassPool(true);
classPool.appendClassPath(classDir);
CtClass ctClass = classPool.get("com.bitterz.assist.Test");
CtMethod[] methods = ctClass.getMethods();
String src = "System.out.println("+ "\" insert by javassist \"" +");";
// 找到对应的method,并在前后插入代码
for (CtMethod method : methods) {
if (method.getName().contains("equals")){
method.insertBefore(src);
}
}
// 获取字节码
byte[] bytes = ctClass.toBytecode();
// 反射调用defineClass方法,将字节码覆盖到jvm中
ClassLoader classLoader = new ClassLoader() {
};
String name = Test.class.getName();
Method defineClass = Class.forName(ClassLoader.class.getName()).getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ProtectionDomain.class);
defineClass.setAccessible(true);
Class<?> test = (Class<?>) defineClass.invoke(classLoader, name, bytes, 0, bytes.length, null);
// 这里取巧用了equals方法,是因为所有类都继承自Object类,因此能过够编译通过,前面重写了equals方法,所以会调用到重写的equals方法中
test.newInstance().equals("a");
}
}
输出结果如下:
可见,通过javassist修改字节码后,利用ClassLoader#defineClass方法可以覆盖jvm中的字节码,实现对jvm中已加载类的动态修改。
- 不过这种方式受限于类加载机制和jvm对重复类定义的检查,每个类最好用新的类加载器去加载,否则在defineClass时会出现报错
- 并且,受限于jvm的安全机制,无法修改系统类,例如java.lang下的类
方法2,通过ClassFileTransformer#transform方法修改字节码
在前一篇笔记中,记录了通过javaagent机制可以对jvm加载过的类进行类转换(Class Transform),也就是在ClassFileTransformer#transform中返回新的字节码,会覆盖原有的字节码,下面来试试对java.lang.ProcessBuilder的字节码进行修改,实现hook
首先时需要执行的main方法
// 项目名为test,跟后面的agent不在一个项目
package com.bitterz;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.String;
public class Main {
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("main start!");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "chdir");
Process process = processBuilder.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "gbk"));
System.out.println(bufferedReader.readLine());
}
}
test项目对应的pom.xml文件,可以直接打包成jar,并且指定了启动类
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bitterz</groupId>
<artifactId>test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Main-Class>com.bitterz.Main</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
接下来是agent项目
// premain,这个类会在前面那个项目的main方法启动前执行
package com.bitterz;
import com.bitterz.hook.ProcessBuilderHook;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class PreMain {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
// 先测试一次使用ProcessBuilder获取当前路径
System.out.println("\n");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd", "/c", "chdir");
Process process = processBuilder.start();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), "gbk"));
System.out.println(bufferedReader.readLine());
// 添加ClassFileTransformer类
ProcessBuilderHook processBuilderHook = new ProcessBuilderHook(inst);
inst.addTransformer(processBuilderHook, true);
// 获取所有jvm中加载过的类
Class[] allLoadedClasses = inst.getAllLoadedClasses();
for (Class aClass : allLoadedClasses) {
if (inst.isModifiableClass(aClass) && !aClass.getName().startsWith("java.lang.invoke.LambdaForm")){
// 调用instrumentation中所有的ClassFileTransformer#transform方法,实现类字节码修改
inst.retransformClasses(new Class[]{aClass});
}
}
System.out.println("++++++++++++++++++hook finished++++++++++++++++++\n");
}
}
// 在transform中执行类转换的逻辑,插入过滤代码
package com.bitterz.hook;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
public class ProcessBuilderHook implements ClassFileTransformer {
private Instrumentation inst;
private ClassPool classPool;
public ProcessBuilderHook(Instrumentation inst){
this.inst = inst;
this.classPool = new ClassPool(true);
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("java/lang/ProcessBuilder")){
CtClass ctClass = null;
try {
// 找到ProcessBuilder对应的字节码
ctClass = this.classPool.get("java.lang.ProcessBuilder");
// 获取所有method
CtMethod[] methods = ctClass.getMethods();
// $0代表this,这里this = 用户创建的ProcessBuilder实例对象
String src = "if ($0.command.get(0).equals(\"cmd\"))" +
"{System.out.println(\"危险!\");" +
"System.out.println();"+
"return null;}";
for (CtMethod method : methods) {
// 找到start方法,并插入拦截代码
if (method.getName().equals("start")){
method.insertBefore(src);
break;
}
}
classfileBuffer = ctClass.toBytecode();
}
catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
finally {
if (ctClass != null){
ctClass.detach();
}
}
}
return classfileBuffer;
}
}
然后是agent项目对应的pom.xml文件,因为要用javassist包,所以把依赖也打包进去了
premain对应的pom.xml,可以直接用maven打包成jar,并且自动填写了MANIFEST.MF中需要的字段
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bitterz</groupId>
<artifactId>java-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.bitterz.PreMain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
做好前面的准备工作之后,执行一下,看看效果
- 很明显,agent项目中使用ProcessBuilder执行系统命令时,因为还没有插入检测逻辑,所以没有被拦截,成果返回当前路径
- hook完成后,main方法启动,执行start后,触发插入的检测代码,并返回一个null
- Main.java的15行报错,出现了null值,看看15行是啥
InputStream inputStream = process.getInputStream();
,很明显了,hook成功,返回一个null,所以15行这里会报错
2.4 总结
到此java RASP的雏形就出来了,通过javaagent机制,在项目启动前,添加类转换方法,利用javassist、ASM这些修改字节码的工具对关键系统类,在类转换方法中修改字节码,并返回给jvm,这样可以绕过defineClass对系统类的保护,完成对系统类和基础类的字节码修改,实现对这些类的hook。
针对不同类,还需要进一步实现不同的方法的hook以及检测逻辑,考虑到通用性,或许可以用json、Yaml这类规则,以便于在php、python、go等语言中能够通用规则。或者使用OpenRASP的思路,java负责hook方法,把参数交给js来实现检测逻辑,同时也能兼顾php等语言的规则通用。或许,使用机器学习或深度学习模型,也能对参数进行检测,这就涉及到很广阔的思路了。
另外就是各种心跳、云控的设计了,不是RASP的重点,所以没有进一步实现,java RASP的研究就到这里了(好像有点浅尝则止呢?不过结合OpenRASP的源码,很容易就能理解java RASP的实现思路了)
参考
https://www.cnblogs.com/rickiyang/p/11368932.html
https://www.cnblogs.com/kendoziyu/p/maven-auto-build-javaagent-jar.html
appendToBootstrapClassLoaderSearch方法说明
从零开始的Java RASP实现(二)的更多相关文章
- 从零开始的Java RASP实现(一)
目录 0 从零开始的Java RASP实现(一) 1 javaagent 1.1 Main方法启动前 概念介绍: 如何使用 创建agent 创建main 1.2 JVM启动后 attach机制 启动一 ...
- 从零开始学 Java - Spring 集成 Memcached 缓存配置(二)
Memcached 客户端选择 上一篇文章 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)中我们讲到这篇要谈客户端的选择,在 Java 中一般常用的有三个: Memc ...
- 从零开始学 Java - Spring 集成 ActiveMQ 配置(二)
从上一篇开始说起 上一篇从零开始学 Java - Spring 集成 ActiveMQ 配置(一)文章中讲了我关于消息队列的思考过程,现在这一篇会讲到 ActivMQ 与 Spring 框架的整合配置 ...
- 从零开始学 Java - Spring 集成 ActiveMQ 配置(一)
你家小区下面有没有快递柜 近两年来,我们收取快递的方式好像变了,变得我们其实并不需要见到快递小哥也能拿到自己的快递了.对,我说的就是类似快递柜.菜鸟驿站这类的代收点的出现,把我们原来快递小哥必须拿着快 ...
- 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)
硬盘和内存的作用是什么 硬盘的作用毫无疑问我们大家都清楚,不就是用来存储数据文件的么?如照片.视频.各种文档或等等,肯定也有你喜欢的某位岛国老师的动作片,这个时候无论我们电脑是否关机重启它们永远在那里 ...
- 从零开始学 Java - 我放弃了 .NET ?
这不是一篇引起战争的文章 毫无疑问,我之前是一名在微软温暖怀抱下干了近三年的 .NET 开发者,为什么要牛(sha)X一样去搞 Java 呢?因为我喜欢 iOS 阿!哈哈,开个玩笑.其实,开始学 Ja ...
- 从零开始学 Java - log4j 项目中的详细配置
你还会用笔来写字么 我是不怎么会了,有时候老是拿起笔之后不知道这个字怎么写,这时候就会拿起手机去打出来:有时候还会写出来这个字之后越看越不像,这时候就开始怀疑自己的能力了:有时候写出来了一大堆字之后, ...
- 从零开始学 Java - Spring AOP 实现用户权限验证
每个项目都会有权限管理系统 无论你是一个简单的企业站,还是一个复杂到爆的平台级项目,都会涉及到用户登录.权限管理这些必不可少的业务逻辑.有人说,企业站需要什么权限管理阿?那行吧,你那可能叫静态页面,就 ...
- 从零开始学 Java - Spring 一主多从、多主多从 数据库配置
待会苹果要开发布会 我写完这篇文章就准备去看发布会了,因为我买了好几包瓜子和啤酒.由于苹果的保密做的越来越差劲,该曝光的信息差不多全部曝光了,我们这种熬夜看发布会的只不过是让这些信息更加真实,或者说是 ...
随机推荐
- 『学了就忘』Linux基础 — 1、UNIX系统介绍
目录 (一)UNIX系统介绍 1.UNIX系统发展历史 2.UNIX 主要发行版本 (二)GNU计划 1.GNU计划介绍 2.为何Stallman会发起这个GNU计划呢? 3.GNU的通用公共许可证: ...
- DRF之过滤排序分页异常处理
一.过滤 对于列表数据要通过字段来进行过滤,就需要添加 django-filter 模块 使用方法: # 1.注册,在app中注册 settings.py INSTALLED_APPS = [ 'dj ...
- Tomcat:启动tomcat服务报错没有权限
1.在linu上部署好tomcat后,准备启动时报错: Cannot find bin/catalina.sh The file is absent or does not have execute ...
- so层反调试方法以及部分反反调试的方法
1.检测ida远程调试所占的常用端口23946,是否被占用 //检测idaserver是否占用了23946端口 void CheckPort23946ByTcp() { FILE* pfile=NUL ...
- [小工具] chrome上日语翻译工具
rikaikun -> 日语 "理解君" 下载地址: https://chrome.google.com/webstore/detail/rikaikun/jipdnfibh ...
- 跨域解决之JSONP和CORS的详细介绍
JSONP跨域和CORS跨域 什么是跨域? 跨域:指的是浏览器不能执行其它网站的脚本,它是由浏览器的同源策略造成的,是浏览器的安全限制! 同源策略 同源策略:域名.协议.端口均相同. 浏览器执行Jav ...
- [转载] 笑话:Developer and product manager
A man flying in a hot air balloon suddenly realizes he's lost. He reduces height and spots a man dow ...
- git常用命令自己梳理总结
一.新建代码库 # git-init - 创建一个空的 Git 存储库或重新初始化一个现有的存储库 $ git init # 在本地新建一个repo,进入一个项目目录,执行git init,会初始化一 ...
- 开发必备linux命令大全-稳赚不亏
我们的服务一般都是在linux系统运行,因此了解一些关于linux命令是必须.接下来将一一详细介绍一些常用的linux的命令 文件操作 远程登录与操作 磁盘挂载 进程管理 启动和结束 系统性能参数查看 ...
- SLAM十四讲第二版项目代码总结
github地址:https://github.com/gaoxiang12/slambook2/tree/master/ch13 双目视觉里程计 头文件 所有的类都在myslam命名空间中 1.co ...