作为一个没有进行过JAVA开发的菜鸡,在审计代码的过程中经常会遇到JAVA的一些特性,了解这些特性是必要的.我采用一边学习一边记录的方式,在学习JAVA安全过程中遇到的各种JAVA特性,概念在这里进行统一的学习记录,以方便查看.

这里我把他们分为两个类型,需要详细深入分析(以下简称详细)**的和了解用法原理(以下简称了解)的,**了解指能让我更好的理解读懂代码,详细指对漏洞触发,漏洞原理有直接影响的的核心特性.

目录:

[TOC]

泛型(了解)

泛型,顾名思义广泛的数据类型,如何让一个变量成为任意的数据类型呢?答:让他成为泛型

例子:在JAVA中List就是一个泛型,我们可以往里面存各种数据,string,int,float,object都可以

可能出现的疑问(会在最后解答):为什么List是一个泛型?

1
2
3
4
5
List demo = new ArrayList();
demo.add(123);//Integer
demo.add("123");//String
System.out.println(demo.get(0));
System.out.println(demo.get(1));

demo这个list中存放了两个数据,Integer,String并且我也可以将他们输出出来

现在我们假设这样一个上下文环境,要求这个demo必须是Integer类型的数据,但是通过我们口头约定肯定是不靠谱的,因为demo是一个泛型,能放入任何类型的数据,假如你手瓢了,写了个demo.add(‘haha’),那么就会出现意想不到的问题

为了防止这种问题的出现,我们需要定义这个demo的类型,如下

1
List demo<Integer> = new ArrayList();

这个时候就只能将Integer类型的数据放入demo了,值得注意的是泛型并不会在运行的时候去解析他,因为这个<Integer>只是一个标识符,在编译过程中编译器便会对其进行检查

指定泛型后idea也给我们标红

所以泛型就是一个可以放入任何数据类型的对象,而我们可以用<>标识去定义它的数据类型

同样的泛型类,泛型接口原理都是一样的.下面就是一个泛型类,在类后面加上<T>**(这个T的意思只是一个占位符而已,他就是类似函数中的形参)**告诉这是一个泛型类,我们可以把T理解为函数中的形参,我们在实例化这个类的时候需要将数据类型当作实参传入,用来指定key的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
class Generic<T> { //在类名后增加<T>使其成为泛型类,同样的可以使用<T,E,B,...>定义多个泛型形参
public T data;
public void setData(T data){
this.data = data;
}
public T getData(){
return this.data;
}
}
Generic demo = new Generic<Integer>(6666);//实例化这个泛型类,定义T为Integer
System.out.println(demo.getData());
//输出:6666

泛型接口同理不再赘述

问题解答:到这里你其实应该知道刚刚产生的疑问了,因为List就是一个泛型接口

JAVA类加载机制(详细)

JAVA类加载机制非常多的详解文章,个人认为冗余较多,类加载机制又是极其重要的,所以我尝试精简但又不失详细的分析记录.

类加载

运行java程序时我们写的XXX.java文件被javac编译成XXX.class字节码文件,字节码文件是给JVM虚拟机执行的.但是我们不可能在运行初期就把所有的字节码文件全部读入JVM(因为一个java程序会有很多的.java文件),那样无疑会带来巨量的消耗.JVM通过类加载机制,在需要用到某个类的时候在去加载他,以提高程序执行效率

类加载机制

一张很经典的图说明了加载一个类所经历的过程

.java经过javac编译为字节码文件(.class),如果这个时候这个class需要被使用,那么我们就触发加载流程开始加载这个类

加载:其实就是读取.class文件到JVM中,然后将其转为静态数据结构存放在方法区中,然后在堆中生成一个java.lang.class类型的对象.静态数据结构很唬人,其实就理解为将class文件的元数据(类名称,方法名,….各种数据存起来而已)

**验证:**这个时期会验证class文件格式,元数据是否合法,字节码是否合法,符号引用是否可发,反正就是一些列验证,就是写出来看一眼就忘了,没必要浪费篇幅.

**准备:**主要工作就是分配内存,初始化类变量(static),不会初始化实例变量.这一过程中并不会执行显示赋值,而是会隐式给予他们默认值(如0,null),在初始化阶段在执行显示赋值

**解析:**将符号引用转为直接引用,意思就是将符号替换为符号所代表的真实地址,不了解符号引用的同学自行百度了解

初始化:这个时候就会完成刚刚没有完成的赋值操作,如将类中的变量显示赋值.

在以下情况会触发类的加载:

(1)创建类的实例,也就是new一个对象

(2)访问某个类或接口的静态变量,或者对该静态变量赋值

(3)调用类的静态方法

(4)反射(Class.forName(“com.mengda.test”))

(5)初始化一个类的子类(会首先初始化子类的父类)

(6)JVM启动时标明的启动类,即文件名和类名相同的那个类 只有这6中情况才触发类加载.

类加载器

既然需要动态的去加载我们需要的类(.class字节码文件),那么肯定需要一个加载器来加载他(类加载器),JAVA自带的加载器有三个

1.Bootstrap CLassloder:

启动项加载器,用于加载JVM自身需要的类类库及一些核心类库,需要注意的是这个加载器是由C++实现的,不存在于JAVA代码中

2.Extention ClassLoader:

扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。

3.AppClassLoader:

Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。

双亲委派,类加载器的加载顺序

加载器的加载顺序实际上是Bootstrap CLassloder->Extention ClassLoader->AppClassLoader

为什么是这个顺序呢?实际上除了Bootstrap CLassloder加载器之外的加载器都有父加载器,而这些加载器都是很懒惰的,每次需要加载一个类的时候都会让他们的爸爸去加载,如果他们的爸爸不能加载才会自己加载,如果他们的爸爸还有爸爸那就会继续给他们爸爸的爸爸进行加载,总之整个类加载顺序无论如何就是从最顶层开始往下执行的(双亲委派)

看这样一段代码

输出:sun.misc.Launcher$AppClassLoader@b4aac2

很明显这个test类是由AppClassLoader加载而来的

那么AppClassLoader的父亲是谁呢?

输出:sun.misc.Launcher$ExtClassLoader@16d3586

输出是ExtClassLoader,可见ExtClassLoader是AppClassLoader的父亲

那么ExtClassLoader的父亲是谁呢?

输出:null

why?不是说除了Bootstrap CLassloder之外的加载器都有父亲吗,那为什么ExtClassLoader的父亲是null呢?

这里就要引出此处的”父亲”并不是指的继承关系上的父亲

getParent方法直接就返回了parent,那么这个parent是哪儿来的呢

是直接赋值的ClassLoader Parent,是一个类,也就是说这里的父子关系不是继承意义上的父子关系,而是逻辑上的

为什么为null呢?是因为ExtClassLoader的父亲是Bootstrap CLassloder,而Bootstrap CLassloder是C++开发的,并不存在于java代码中,固然为null了

所以这三个加载器由上至下的关系为

由双亲委派机制可以知道他们的执行顺序是由上至下,及Bootstrap CLassloder->Extention ClassLoader->AppClassLoader

双亲委派流程分析

写了一段示例代码帮助我们分析

让我们来看看java是如何实例化一个类的

首先来到loadclass方法,首先判断需要加载的类名是否还存在层级,此处的var.lastIndexof(46),46是.的ascall码,用来判断加载的类是否是xx.xxx的形式,如果不是直接进入第二个if,第二个if主要是判断if是否存在(猜测),但是这个if也不会进入,直接会进入super.loadClass.

来到了java.lang.ClassLoader的loadClass方法,首先会调用findLoadedClass判断test是否被加载器加载过了,此时类加载器是AppClassLoader

那么test此时肯定是没有被加载过的,此时c=null,404行判断parent是否为空(双亲委派机制触发核心,寻找AppClassLoader的父亲是否存在,存在就让父亲加载),此时很明显AppClassLoader的父亲是存在的,为ExtClassLoader

405行调用ExtClassLoader后依然来到了java.lang.ClassLoader的loadClass方法,和刚刚是同一个方法,因为AppClassLoader和ExtClassLoader都是继承自

URLClassLoader的,注意,这里说的继承关系是真正的类继承关系,URLClassLoader是AppClassLoader和ExtClassLoader的父类,但是AppClassLoader和ExtClassLoader还存在一层逻辑父子关系,在逻辑上Ext是App的父亲.此时this已经变为ExtClassLoader

但是ExtClassLoader此处的父亲为null,无法找到父亲了,直接调用407处的findBootStrapClassOrNull,传递到最终的类加载器Bootstrap CLassloder进行加载

又因为test类是属于AppClassLoader范畴的,在经过BootStrap和ExtClassLoader后发现都不是他们能加载的(ExtClassLoader的findclass方法无法找到对应类),于是就又回到了AppClassLoader,调用AppClassLoader的findClass方法寻找对应类,然后AppClassLoader来加载test类

这就是双亲委派的机制

反射(详细)

为什么会产生反射这个需求?

假设这样一个场景,程序执行过程中需要实例化所有具有toString方法的类,但是这个时候我们并不知道那些类有这个方法,那我们应该怎么办呢?new一个A对象然后去尝试调用?这显然是不太可取而且很麻烦的,这个时候就需要一个第三者监管机制,他好比上帝之眼,能在整个java运行过程中独立出来,置于程序外部去帮我们读取类的各种信息,帮助我们执行程序,这样的机制叫做反射

我画了个图,整个反射调用原理大概是这个样子的

结合上面的图我们来看下面这个例子:

fanshe.java

先用Class.forName(“demo”)加载,获得了demo的class对象democlass,然后通过这个democlass的getMethod获取到了demo类中的method方法,然后调用无参构造函数实例化obj(getConstructor()获取无参构造函数),然后使用invoke反射调用方法.

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class fanshe {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class democlass = Class.forName("demo");
Method m = democlass.getMethod("method");
//getConstructor获取共有无参构造方法
Object obj = democlass.getConstructor().newInstance();
m.invoke(obj);
}
}

demo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class demo {
private String teststring;
public demo(){
this.teststring = "test";
System.out.println("无参构造方法调用");
}
public void method(){
System.out.println("method方法被调用");
}
public demo(String teststring){
this.teststring = teststring;
}

public String getTeststring() {
return teststring;
}

public void setTeststring(String teststring) {
this.teststring = teststring;
}
}

输出:

1
2
无参构造方法调用
method方法被调用