在这边文章将讲述:
1.Java语言的基础,从JVM角度讲述java程序运行的模型,如jvm内存模型,在脑海形成java语言编程到运行的概念;
2.类加载与应用:理解作为Java程序的基本元素Class字节码,Class对象,Class对象实例化与反射的应用,这个在java序列化,fastjson等中类加载与反射是基础,是彻底理解Java程序漏洞利用的RCE方式,内存webshell的基础。
3.最后将介绍
javassit
的使用:方便人去理解class字节码与及动态创建class字节码,修改class字节码,一些RASP等产品用到这个技术。
一、Class加载器与Class对象
1.1 ClassLoader
这里引用别人的一张图说明java程序从源码到进程的过程:
其中重点学习的是java字节码加载
- 对于源码文件的编译:了解java编译器的编译由:分析和输入到符号表,注解处理,语义分析和生成class,如Java多态的支持在编译期间进行检查并进行类型擦除。
- 对于java运行系统:了解java方法的调用,如函数的重载,多态的调用是通过指针引用方法区进行调用
对于类加载:
输入是class文件,输出是Class对象
对Class对象的理解:
1.在进程层面上:java程序中每个子类对象都有一个且只有一个对应的Class对象,可以通过object.class 、object.getClass()等方式获取到这个Class对象,通过这个Class对象,利用反射可以获得并调用,修改这个对象的任何属性(private|protect|publice)的方法与属性;
2.在jvm内存模型层面上:每一个Class对象标识一个在方法区中内存实列,给反射等机制提供支持。
引起类加载的行为与不同类加载方式有不同的加载行为:
1.显式类加载:new XXClass(),Class.forName(),ClassLoader.loadClass()等会引起类的加载,在学java-RMI漏洞利用的时候指出,jvm在版本迭代中逐步设置不允许远程codebase加载类,远程URL加载类。
2.隐式类加载:如声明某类的数组等不会导致类加载
类的加载不会导致类的构造函数被调用,因为类的加载只是形成这个类对应的Class对象,但会调用该类定义的static方法区与类变量的赋值。所以在《javasec fastjson漏洞利用》中TemplateImpl的利用链是利用了类加载,并且通过Class.newInstance()实例化对象,所以RCE的逻辑可以放在static方法区也可以放在类的默认无参构造函数。
1.1.1 类加载过程:
加载:
任务:
- 类的全限定名来获取定义此类的二进制字节流。如从
ZIP
包读取、从网络中获取、通过运行时计算生成、由其他文件生成、从数据库中读取等等途径;- 将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义
- 在内存中生成一个代表这个类的
java.lang.Class
对象,它将作为程序访问方法区中的这些类型数据的外部接口验证
是连接阶段的第一步,且工作量在
JVM
类加载子系统中占了相当大的一部分目的:为了确保
Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全由此可见,它能直接决定
JVM
能否承受恶意代码的攻击,因此验证阶段很重要,但由于它对程序运行期没有影响,并不一定必要,可以考虑使用-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间检验过程包括下面四个阶段:
A.文件格式验证:
内容:验证字节流是否符合
Class
文件格式的规范、以及是否能被当前版本的虚拟机处理目的:保证输入的字节流能正确地解析并存储于方法区之内,且格式上符合描述一个
Java
类型信息的要求。只有保证二进制字节流通过了该验证后,它才会进入内存的方法区中进行存储,所以后续三个验证阶段全部是基于方法区而不是字节流了例子:
是否以魔数
0xCAFEBABE
开头主次版本号是否在
JVM
接受范围内索引值是否有指向不存在/不符合类型的常量
……
B.元数据验证:
内容:对字节码描述的信息进行语义分析,以保证其描述的信息符合
Java
语言规范的要求目的:对类的元数据信息进行语义校验,保证不存在不符合
Java
语言规范的元数据信息例子:
类是否有父类(除了
java.lang.Object
之外,所有类都应有父类)父类是否继承了不允许被继承的类(
final
修饰的类)如果该类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
……
C.字节码验证:
是验证过程中最复杂的一个阶段
内容:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
例子:
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现“在操作数栈的数据类型中放置了
int
类型的数据,使用时却按long
类型来载入本地变量表中”保证任何跳转指令都不会跳转到方法体外的字节码指令上
……
D.符号引用验证:
- 内容:对类自身以外(如常量池中的各种符号引用)的信息进行匹配性校验
- 目的:确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个
java.lang.IncompatibleClassChangeError
异常的子类- 注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即『解析』阶段
准备
是连接阶段的第二步,主要的任务是:
- 为类变量(静态变量)分配内存:因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在
Java
堆中- 设置类变量初始值:通常情况下零值
解析
是连接阶段的最后一步,主要的任务是:解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
这是类加载的最后一步,给在准备阶段分配好初始内存的变量赋值,进行静态语句块
使用
卸载
1.1.2 类加载器
每个类加载器,都拥有一个独立的命名空间,它不仅用于加载类,还和这个类本身一起作为在JVM
中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个类加载器加载,即使它们来源于同一个Class
文件且被同一个JVM
加载,只要加载它们的类加载器不同,这两个类就必定不相等
从JVM
的角度,可将类加载器分为两种:
- 启动类加载器
- 由
C++
语言实现,是虚拟机自身的一部分- 负责加载存放在
<JAVA_HOME>\lib
目录中、或被-Xbootclasspath
参数所指定路径中的、且可被虚拟机识别的类库- 无法被
Java
程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用null
代替
- 其他类加载器:由
Java
语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
,可被Java
程序直接引用。常见几种:
扩展类加载器
A.由
sun.misc.Launcher$ExtClassLoader
实现B.负责加载
<JAVA_HOME>\lib\ext
目录中的、或者被java.ext.dirs
系统变量所指定的路径中的所有类库应用程序类加载器
A.是默认的类加载器,是
ClassLoader#getSystemClassLoader()
的返回值,故又称为系统类加载器B.由
sun.misc.Launcher$App-ClassLoader
实现C.负责加载用户类路径上所指定的类库
自定义类加载器:如果以上类加载起不能满足需求,可自定义
类加载器的关系
需要注意的是:虽然数组类不通过类加载器创建而是由
JVM
直接创建的,但仍与类加载器有密切关系,因为数组类的元素类型最终还要靠类加载器去创建
1.1.2 双亲委派模型
类加载的双亲委派模型提供安全保证:
- 定义:表示类加载器之间的层次关系
- 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承关系来实现,而是通过组合关系来复用父加载器的代码
- 工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
- 注意:不是一个强制性的约束模型,而是
Java
设计者推荐给开发者的一种类加载器实现方式 - 优点:
- 类会随着它的类加载器一起具备带有优先级的层次关系,可保证
Java
程序的稳定运作 - 实现简单,所有实现代码都集中在
java.lang.ClassLoader的loadClass()
中
- 类会随着它的类加载器一起具备带有优先级的层次关系,可保证
- 比如,某些类加载器要加载
java.lang.Object
类,最终都会委派给最顶端的启动类加载器去加载,这样Object
类在程序的各种类加载器环境中都是同一个类- 相反,系统中将会出现多个不同的
Object
类,Java
类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱
1.1.2 demo
User.class
public class User {
private int age;
public static String name = "thonsun";
static {
System.out.println("static block call");
}
public User() {
System.out.println("default constructor call");
}
public User(int age) {
System.out.println("constructor call");
this.age = age;
}
public static void methodCall() {
System.out.println("static metod call");
}
}
Class.forName()
private static void testClassforName() {
try {
Class<?> clazz = Class.forName("com.thonsun.sec.User");
Object o = clazz.newInstance();
Field name = clazz.getDeclaredField("name");
System.out.println(name.get(o));
} catch (ClassNotFoundException | NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
static block call
default constructor call
thonsun
可以验证Class.forName进行类加载默认初始化类成员并调用static 代码区,class.forName后面有参数可以控制是否进行类初始化
classloader
private static void testClassLoader() {
MainClass mainClass = new MainClass();
System.out.println(mainClass.getClass().getClassLoader());
try {
Class<?> clazz = mainClass.getClass().getClassLoader().loadClass("com.thonsun.sec.User");
Object o = clazz.newInstance();
Field name = clazz.getDeclaredField("name");
System.out.println(name.get(o));
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
static block call
default constructor call
thonsun
1.2 Class
通过Class对象可以实例化对象,可以利用反射访问指定对象的任意属性成员(方法调用,变量修改)等
private static void testClassusage(){
try {
// Transformer[] transformers = new Transformer[]{
// new ConstantTransformer(Runtime.class),
// new InvokerTransformer("getMethod",
// new Class[]{String.class,Class[].class},
// new Object[]{"getRuntime",new Class[0]}),
// new InvokerTransformer("invoke",
// new Class[]{Object.class,Object[].class},
// new Object[]{null,new Object[0]}),
// new InvokerTransformer("exec",
// new Class[]{String.class},
// new Object[]{"notepad"}),
// new ConstantTransformer(Runtime.class),
// new InvokerTransformer("getMethod",
// new Class[]{String.class,Class[].class},
// new Object[]{"getRuntime",new Class[0]}),
// new InvokerTransformer("invoke",
// new Class[]{Object.class,Object[].class},
// new Object[]{null,new Object[0]}),
// new InvokerTransformer("exec",
// new Class[]{String.class},
// new Object[]{"calc"}),
//
// };
Class<Runtime> clazz = Runtime.class;
Method getRuntime = clazz.getMethod("getRuntime");
Runtime o = (Runtime)getRuntime.invoke(null,null);
Process calc = o.exec("calc");
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
二、jvm基础
想要看详细可以观摩《深入理解JVM虚拟机》一书,在这里只是总结我在学习Java安全中认为在理解Java漏洞利用的一些jvm知识,方便构建完整的知识网络。
2.1 jvm 内存模型
jvm的内存模型,在上面中的Class对象,Class对象实例化、方法函数的多态表现提到了jvm的内存模型,这个是jvm的执行引擎的支持。
2.2 jvm 执行引擎
三、反射
fastjson 有用到unsafe : todo: 反射与unsafe的对比
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//隐式
// User user = new User();
// System.out.println(user.getIntVar());
// System.out.println(user.getClass().getClassLoader().hashCode());
// System.out.println(test.getClass().getClassLoader());
// System.out.println(System.getProperty("java.class.path"));
//显式
// Class<?> c = Class.forName("com.thonsun.sec.User");
// Object o = c.newInstance();
// Method getIntVar = o.getClass().getDeclaredMethod("getIntVar");
// System.out.println(getIntVar.invoke(o));
Test t = new Test();
Class<?> c = t.getClass().getClassLoader().loadClass("com.thonsun.sec.User");
Constructor<?> constructor = c.getConstructor();
User o = (User)constructor.newInstance();
//Object o = c.newInstance();
System.out.println(o.getIntVar());
Method getIntVar = o.getClass().getDeclaredMethod("getIntVar");
System.out.println(getIntVar.invoke(o));
}