皮蛋厂的学习日记 | 2023.3.16 2021级fake_s0u1 Java反射
2023-3-16 16:4:58 Author: 山警网络空间安全实验室(查看原文) 阅读量:13 收藏

文章首发于先知社区 

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

  • 前言

  • 类 函数 方法 对象

  • 反射的定义

  • forName

  • Class.newInstance

  • getMethod

  • invoke

  • 遗留问题

  • 深层解析

  • Ending...

  • 参考文章

前言

第一次迈入Java安全的领域 这种感觉又让我回想起了去年第一次开始学习php的时候 面对全新的语法 语言结构 又需要从零开始慢慢领悟 结合之前学习的经验 在刚刚接触一种语言之初 建立自己好的习惯 往往比解决某一个问题更重要 学习的过程将结合p牛的Java安全漫谈 以及网上的大小文章进行全面的领悟与理解 前路道阻且艰 尚需更加努力

类 函数 方法 对象

在学习Java之前 我们需要先确定一下 这几个概念的定义 虽然各类语言之间互通 但是我们还是需要在学习一门新的语言之前 做好充分的准备

类在java中是用class关键字定义的一个结构,类中可以定义函数以及定义变量

函数与方法

我们首先可以来借鉴一下 在python中的这两种概念的区分

定义函数时,我们是可以不设置形参的。 如果设置了形参,形参和实参是对应的,有多少形参,就需要传多少实参。

定义类的方法时,是必须至少设置一个形参的,并且在调用这个方法时,不需要传实参的。

其实 在Java中 也是差不多的道理 我们可以用实参和形参的存在 来区分二者

对象

Java本身就是一门 面向对象编程的语言 对象自然是其中的主角 一切皆为对象 我们可以将其理解为 在内存中的一段代码块 当我们使用new创建了一个实体类后 java虚拟机就会在内存中开辟出一段内存空间 用来存放这个实体类

eg.

Person person = new Person("Jums",78,187);           

代码中定义了Person 实体类 其别名叫做person 那么此时的person 就代表一个对象

反射的定义

反射的定义是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。

我们说的通俗一点 可以说是获取Class对象然后使用java.lang.reflect里提供的方法操作Class对象,Class与java.lang.reflect构成了java的反射技术。

反射是大多数语言中都必不可少的部分 对象可以通过反射获取他的类 类可以通过反射拿到所有方法 拿到的方法可以调用 总之 通过反射 我们可以将Java 这种静态语言 赋予动态特性

我们从这个定义中 我们就可以看出来 Java可以通过反射变得格外的灵活

反射常用API

获取反射中的Class对象

在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象。

在 Java API 中,获取 Class 类对象有三种方法:

第一种,使用 Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。

Class clz = Class.forName("java.lang.String");

第二种,使用 .class 方法。

这种方法只适合在编译前就知道操作的 Class。

Class clz = String.class;

第三种,使用类对象的 getClass() 方法。

String str = new String("Hello");
Class clz = str.getClass();
通过反射创建类对象

通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法。

第一种:通过 Class 对象的 newInstance() 方法。

Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();

第二种:通过 Constructor 对象的 newInstance() 方法

Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();

通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。

Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.classint.class);
Apple apple = (Apple)constructor.newInstance("红富士"15);
通过反射获取类属性、方法、构造器

我们通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。

Class clz = Apple.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}

输出结果是:

price

而如果使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性:

Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}

输出结果是:

name
price

与获取类属性一样,当我们去获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。

我们可以通过一个简单的实例程序 来说明 反射的作用

public class Apple {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public static void main(String[] args) throws Exception{
        //正常的调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用
        Class clz = Class.forName("com.chenshuyi.api.Apple");
        Method setPriceMethod = clz.getMethod("setPrice"int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

上面的代码的输出结果是

Apple Price:5
Apple Price:14

所以 我们可以简单总结一下 我们通过反射获取一个对象的步骤

1.获取类的Class对象实例

Class clz = Class.forName("com.zhenai.api.Apple");

2.根据Class对象实例获取Constructor对象

Constructor appleConstructor = clz.getConstructor();

3.使用Constructor对象的newInstance方法获取反射类对象

Object appleObj = appleConstructor.newInstance();

而当我们要调用某一个方法 则需要通过下面的步骤

1.获取方法的Method对象

Method setPriceMethod = clz.getMethod("setPrice", int.class);

2.利用invoke方法调用方法

setPriceMethod.invoke(appleObj, 14);

其实 我们不免疑惑 上面的这些Class之类 是从什么地方来的呢 这便要介绍一下 java中一个至关重要的包 java.lang

java.lang

这其中 就包含着我们常用类 比如说Object Class 等等 而当我们使用这个包之下的类时 我们是不需要import导入的 这是一个默认导入的 包

我们简单介绍一下其中的几个类

对象基类Object

Object,是java.lang的根类,也是所有类的超类 超类也就是 当我们的派生关键字 extends出现的时候 被继承的类称为超类 或者是基类 父类

派生出来的类 则为子类

其实 当父类 子类这种字眼出现的时候 我们就很好去理解了 而这里的超类 往往定义的都是公共部分 他们的子类才会具有更加丰富的功能

Class,用来表示类和接口的类型。Class对象在类加载时由JVM调用类加载器中的defineClass方法自动构造。 ClassLoader,负责加载类。 Compiler,作为编译器的占位符,它不做任何事情,仅用来支持Java到本机代码的编译器及相关服务。

对这些知识有了基本的了解之后 我们才可以在理解的时候更加简单

动态特性

那么 什么 叫动态特性呢 这是p牛在安全漫谈中提出的一个概念 p牛对他的解释是 一段代码 改变其中的变量 将会导致这段代码产生功能性的变化

举个例子 在php中 其本身就拥有很多动态特性 所以 我们就可以通过一句话木马 来执行各种功能 Java虽然做不到php那么灵活 但是 也是可以提供一些动态特性的

public void execute(String className, String methodName) throws Exception {
 Class clazz = Class.forName(className);
 clazz.getMethod(methodName).invoke(clazz.newInstance());
}

在上面 包含了几个极为重要的方法

获取类的方法 forName

实例化类对象的方法 newInstance

获取函数的方法 getMethod

执行函数的方法  invoke

我们在下面将分开介绍

forName

forName是Class类中的一个static的成员方法 所有的Class对象都来自于Class类 所以 Class类中 定义的方法将适用于所有Class对象  Class.forName的调用 会返回一个对应类的Class对象 因此 当我们想要去获取一个正在运行的Class对象的相关属性时 我们就可以使用forName方法 获取对Class对象的引用

Class.forName 如果知道某个类的名字 想获取到这个类 就可以使用 forName 来获取

对于大部分人来说 第一次见到class.forName(String className)  的时候 应该是 在使用JDBC连接数据库的时候

import com.mysql.jdbc.Driver;
import java.sql.*;
  
    public class JdbcDemo {
        public static void main(String[] args) throws SQLException, ClassNotFoundException {
        String url = "jdbc:mysql://127.0.0.1:3306/mydb";
        String username = "root";
        String password = "redhat";
        Class.forName("com.mysql.jdbc.Driver"); //这里
        Connection connection = DriverManager.getConnection(url, username, password);
        String sql = "SELECT * FROM msg";
        PreparedStatement prepareStatement = connection.prepareStatement(sql);
        ResultSet resultSet = prepareStatement.executeQuery();
        resultSet.next();
        String address = resultSet.getString("address");
        System.out.println(address);
    }
}

这里通过 Class.forName 传入 com.mysql.jdbc.Driver 之后,就判断了连接的数据库为 mysql 数据库

forName有两个函数重载 (函数重载就是 可以在一个类中存在多个函数 函数名称相同 但是 参数列表不同 eg.假如你是个木工,我要让你制作桌子(返回值)。然后,我给了你紫檀木,你就可以制作一张紫檀桌子;我给了你黄花梨,你可以做黄花梨的桌子。这些不同材质的木料,就是参数。返回的都是桌子,但是具体使用哪个函数,就根据传入的木料不同来决定的。)

Class forName(String name)

Class forName(String name, **boolean** initialize, ClassLoader loader)

第一个 其实就是 我们常见的获取class的方式 可以理解为 第二种方式的一个封装

Class.forName(className)
//等于
Class.forname(className,true,currentLoader)

默认情况下  forName的第一个参数是类名 第二个参数是是否初始化 第三个参数 是ClassLoader

ClassLoader 是什么呢 就是一个加载器 告诉Java虚拟机 如何加载这个类 java默认的ClassLoader就是根据类名来加载类 这个类名是类的完整路径 eg.java.lang.Runtime

在上面的代码中 这里传入的类为com.mysql.jdbc.Driver 我们找到他的源码

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
 
 
    public Driver() throws SQLException {
    // Required for Class.forName().newInstance()
    }
}

这个类被初始化后 静态代码块的内容会被执行 也就是说 我们的 Class.forName 和直接写DriverManager.registerDriver(new Driver())是等价的

获取类对象的其他函数

forName  并不是获取类的唯一途径 我们有如下三种方式 获取一个类 的对象 也就是 java.lang.Class 类的对象 :

obj.getClass() 如果上下文存在某个类的实例obj 我们可以直接通过 obj.getClass() 来获取他的类

MyObject x;
Class c1 = x.getClass();

Test.class  如果 已经加载了某个类 只是想获取到他的java.lang.Class对象 那么直接拿他的class属性即可 这个过程 实际上不属于反射

Class cl1 = Manager.class;
Class cl2 = int.class;
Class cl3 = Double[].class;

最后一个 就是 上面我们讲到的Class.forName了

Class c2=Class.forName("MyObject");

初始化

public class TrainPrint {
 {
 System.out.printf("Empty block initial %s\n"this.getClass());
 }
 static {
 System.out.printf("Static initial %s\n", TrainPrint.class);
 }
 public TrainPrint() {
 System.out.printf("Initial %s\n"this.getClass());
 }
}

在上面是三种初始化的方式 那么 他们之间有什么区别呢

我们这里采用两种方法来输出一下

在类的实例化中 先调用的是static{} 其次是{} 最后是构造函数

类初始化中 仅调用static{} 而{}中的代码会放在构造函数的super()后面 但在 当前构造函数的前面

所以说 forName 中的initialize=true其实就是告诉 Java虚拟机是否实行类初始化

那么 当我们在加上父类的情况下 顺序又应该如何呢

具有父类的类的实例化:父类静态初始块->子类静态初始块->父类初始块->父类构造函数->子类初始块->子类构造函数 具有父类的类的初始化:父类静态初始块->子类静态初始块

而在此处我们 提到了几个概念 在这里直接说明一下

非静态初始化块(构造代码块)

作用就是 给对象进行初始化 对象已建立就运行 且优先于构造函数的运行

与构造函数的区别 非静态初始化块给所有对象进行统一的初始化 而构造函数 只会给对应对象进行初始化

应用:将所有构造函数共性的东西定义在构造代码块中。

  • 对于普通的类而言,可以放在初始化块中的初始化工作其实完全可以放到构造函数中进行,只不过有时会带来些许不便,如有多个构造器,就要在多个地方加上初始化函数完成初始化工作,而如果放到初始化块中的话则只要写一次即可。
  • 初始化块真正体现其独一无二的作用是在匿名内部类中。
  • 由于是匿名内部类,因而无法写构造方法,但是很多时候还是要完成相应的初始化工作,这时就需要用到初始化块了,特别是Android中大量地使用匿名内部类,初始化块的作用就十分突出
静态初始化块

作用 给类进行初始化 随着类的加载而执行 且只能执行一次

与构造代码块的区别

1)构造代码块用于初始化对象,每创建一个对象就会被执行一次;静态代码块用于初始化类,随着类的加载而执行,不管创建几个对象,都只执行一次。 2)静态代码块优先于构造代码块的执行 3)都定义在类中,一个带static关键字,一个不带static

eg.

  static
   {
      Random generator = new Random();
      nextId = generator.nextInt(10000);
   }

在类中 用{}包裹 且前面有static关键词

构造函数

1,构造函数就是一个普通的函数,创建方式和普通函数没有区别,

不同的是构造函数习惯上首字母大写.

2.构造函数和普通函数的区别就是调用方式的不同

普通函数是直接调用,而构造函数需要使用new关键字来调用.

结论

1.类的初始化阶段,先执行最顶层的父类的静态初始化块,然后依次向下执行,直到执行当前类的静态初始化块

2.对象初始化阶段,先执行最顶层父类的初始化块,最顶层父类的构造器,然后依次向下,直到执行当前类的初始化块当前类的构造器。

3.当第二次创建对象时,由于这些类在第一次创建对象时已经被加载过了,因此静态初始化块只会执行一次。

利用

假设存在这样一个函数,并且其中的参数 name 我们可控

public void ref(String name) throws Exception {
 Class.forName(name);
}

那么我们就可以编写一个恶意类,利用初始化来执行我们编写的恶意类中的 static 块中的恶意代码

import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
  static {
   try {
    Runtime rt = Runtime.getRuntime();
    String[] commands = {"touch""/tmp/success"};
    Process pc = rt.exec(commands);
    pc.waitFor();
   } catch (Exception e) {
    // do nothing
   }
  }
}

通过上面我们的研究 我们可以知道 当forName进行初始化的时候 会执行静态代码块中的代码 所以 如果将这个恶意类带入目标中 便可以 造成 恶意命令执行

forName 调用内部类

在正常情况下 除了系统类 如果 我们想要拿到一个类 需要先import才能使用 但是 使用 forName就不需要 这样 对于我们的攻击来说 就十分有利 我们可以加载任意类

另外 我们经常 在一些源码中可以看到 类名中包含美元符号 比如 fastjson在checkAutoType的时候 会将$替换成.

$的作用是查找内部类

Java的普通类c1中支持编写内部类 c2 而在编译的时候 会生成 两个文件 c1.class  和 c1C2")` 我们即可加载这个内部类

获得类之后 我们可以继续通过反射来获取这个类中的属性 方法 同时 也可以实例化这个类 并调用方法

Class.newInstance

当然 这个方法就是 字如其名 比较好理解

在php的反序列化中 我们就常常使用new关键字来创建我们类的实例化 而这里也不例外 形式也十分相近

Object obj = new Object();

但是 与new不同的是 newInstance 并不是一个关键字 而是反射框架中类对象创建新的实例化对象的方法 在这个过程中,是先取了这个类的不带参数的构造方法,然后调用构造方法 也就是无参构造函数 的 newInstance 来创建对象

这个函数 还有一个同名函数 在Constructor类中 也有一个newInstance 但是 二者不同之处在于Class中的这个函数 只能调用无参的构造函数 也就是 默认的构造函数 但是在Constructor中的此函数 可以根据其传入的参数 调用任意的构造函数 前者调用的构造函数必须是可键的(public) 但是后者在setAccessible(true)的条件下 可以调用私有的构造函数

class.newInstance 的作用就是调用这个类的 无参构造函数 不过  我们有时候 在写漏洞利用方法的时候 会发现使用newInstance 总是不成功 这时候原因可能是

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数 是私有的

这两个原因 也就是上面 我们将Class中的newInstance 与 Constructor中的相对比时 的不同之处

最最常见的情况就是java.lang.Runtime 这个类我们构造命令执行payload时很常见 但是我们并不能直接这样来执行命令

在安全漫谈中 p牛举了这样一个例子

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

会导致报错 原因是 Runtime类的构造方法 是私有 的

那么 为什么有的类的构造方法会是私有的呢 这便涉及到一个很常见的设计模式 单例模式

比如 对于一个Web应用来说 数据库连接 只需要建立一次 而并不需要每次用到数据库的时候 都去重新建立一个连接 此时 作为开发者 就可以将数据库连接使用的类的构造函数 设置为私有 然后 编写一个静态方法 来获取

public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

这样 只有类初始化的时候 会执行一次构造函数 后面只能通过getInstance 来获取这个对象 避免建立多个数据库连接

我们这里的Runtime类 就是 单例模式 我们只能通过 Runtime.getRuntime() 来获取到Runtime对象 我们将上述payload修改一下就可以正常执行命令了

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",
String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),
"calc.exe")
;

这里用到getMethodinvoke方法

getMethod

Method Class.getMethod(String name, Class<?>... parameterTypes)

第一个参数name 是要获得的方法的名字 第二个参数 parameterTypes 是按声明顺序标识该方法的形参类型

getMethod 的作用是 通过反射获取一个类的某个特定的公有方法  而且是通过Class实例获取所有Method信息 这些信息包含

  1. getName() 返回方法的名称
  2. getReturnType() 返回方法返回值类型 也是一个 Class实例比如 String.class
  3. getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};
  4. getModifiers():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义

而Java中支持类的重载 我们不能仅通过函数名 来确定一个函数所以在调用getMethod 的时候 我们需要传给他你需要获取的函数的参数类型列表

比如 在Runtime.exec 方法中 有6个重载

我们使用第一个 仅有一个参数 类型是string 所以我们使用getMethod("exec",string.class) 来获取Runtime.exec 方法

invoke

其属于Method类 作用就是对方法进行 调用 也比较好理解

Object invoke(Object obj,Object...args),参数 obj 是实例化后的对象,args 为用于方法调用的参数

invoke 的作用是执行方法,它的第一个参数是:

如果这个方法是一个普通方法,那么第一个参数是类对象

如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了 我们正常执行方法是[1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...)

所以 我们将上述命令执行的payload分解一下就是

Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

遗留问题

在p牛的安全漫谈中 提出了这样的两个问题

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

如果一个方法或构造方法是私有方法,我们是否能执行它呢?

问题一

一个类中 没有无参构造方法 也就无法使用我们上面的newInstance 同时没有静态方法 那么我们应该怎样 通过反射 来实例化该类呢

我们在这里需要用到一个新的反射方法 getConstructor 与前面的getMethod 类似 getConstructor 接收的参数是构造函数列表类型 因为构造函数也支持重载 所以 必须用参数列表类型才能确定唯一的一个构造函数

当我们获取到构造函数之后 我们使用newInstance来执行 比如 我们常用的另一种执行命令的方式 ProcessBuilder 我们使用反射来获取其构造函数 然后调用start()来执行命令

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

ProcessBuilder有两个构造函数

public ProcessBuilder(List<String> command)

public ProcessBuilder(String... command)

通过传入的参数 我们看出在上面就是用到了 第一个形式的构造函数 但是 在前面这个payload用到了Java中的强制类型转换 有时我们利用漏洞的时候 是没有这种语法的 所以在这里 我们仍需一步反射

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

通过getMethod 获取到start方法 然后 通过invoke执行 其第一个参数就是 ProcessBuilder Object了

package com.my.package3;

import java.util.Arrays;
import java.util.List;

public class Test2 {
    public static void main(String[] args) throws Exception{
        Class clazz = Class.forName("java.lang.ProcessBuilder");
        clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
    }
}

我们成功执行了第一个 构造函数 那么第二种构造函数 我们应该如何去执行呢

这又会涉及到Java中的可变长参数(varargs)了 正如其他语言一样 java中也支持可变长参数 什么叫可变长参数呢 就是当你定义函数的时候 不确定参数数量的时候 可以使用...这样的语法来表示 这个函数的参数个数是可变的

对于可变长参数 在其编译的时候 会被编译成一个数组 也就是说 下面的两种写法在底层代码中是等价的

public void hello(String[] names) {}
public void hello(String...names) {}

同时 当我们有一个数组想要传给hello函数 可以直接传参

String[] names = {"hello""world"};
hello(names);

那么对于反射来说 如果要获取的目标函数里包含可变长参数 其实我们认为他是数组即可

所以我们将字符串数组的类String[].class 传给getConstructor 获取ProcessBuilder的第二种构造函数

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

问题二

当一个方法 或者 构造方法 是私有方法的时候 我们是否能执行它呢

这里就涉及到了getDeclared 系列的反射了 这里与普通的getMethod getConstructor 的区别是

getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法

getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor 的具体用法和 getConstructor 类似

深层解析

既然 我们上面研究了这么多Java中的反射 难免会有这样一个疑问 那就是 功能这么强大的反射 是怎样通过源码实现的呢

我们从JDK中的invoke方法 开始看起

class AccessibleObject implements AnnotatedElement {
 boolean override;
 //访问权限
 public boolean isAccessible() {
        return override;
    }
}
//Method.class
public Object invoke(Object obj, Object... args)
       throws IllegalAccessException, IllegalArgumentException,
          InvocationTargetException
   
{
       if (!override) {
           if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
               Class<?> caller = Reflection.getCallerClass();
               checkAccess(caller, clazz, obj, modifiers);
           }
       }
       MethodAccessor ma = methodAccessor;             // read volatile
       if (ma == null) {
           ma = acquireMethodAccessor();
       }
       return ma.invoke(obj, args);
   }

上面 便是invoke 在JDK中的源码

首先 该方法在第一步中 就进行了对于访问权限的判断也就是对于override的判断 并且从下面的代码中 我们可以看出 这也就是对应了其是否能忽略其对访问权限的控制 那么 当可以read volatile的时候 其中的MethodAccessor 又是什么呢

此处的MethodAccessor 是一个接口 定义了方法调用的具体操作 这也是我们进行反射时的关键步骤 而此处MethodAccessor 有三个具体的实现类

  • sun.reflect.DelegatingMethodAccessorImpl
  • sun.reflect.MethodAccessorImpl
  • sun.reflect.NativeMethodAccessorImpl

那么就出现了一个问题 在最后return ma.invoke(obj, args); 中 到底调用的是哪个类中的invoke 方法呢 我们需要看一下 MethodAccessor对象返回的到底是哪一个类对象

接下来 我们分别看一下这三个类 都是怎么样的

sun.reflect.DelegatingMethodAccessorImpl

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate;

    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
        setDelegate(delegate);
    }    

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    
{
        return delegate.invoke(obj, args);
    }

    void setDelegate(MethodAccessorImpl delegate) {
        this.delegate = delegate;
    }
}

sun.reflect.NativeMethodAccessorImpl

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }    

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    
{
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
        
        return invoke0(method, obj, args);
    }

    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }

    private static native Object invoke0(Method m, Object obj, Object[] args);
}

sun.reflect.MethodAccessorImpl

abstract class MethodAccessorImpl extends MagicAccessorImpl
    implements MethodAccessor 
{
    public abstract Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
;
}

这里在翻阅源码文档的时候 产生了一个疑问 就是这每个最后结尾的mpl是什么意思 经过查询之后 得知 其为Mozilla Public License 是一个相当于协议证明之类的概念

回归正题 经过上面三段代码阅读之后 我们再回过头去看ma.invoke()中的acquireMethodAccessor()方法

public MethodAccessor newMethodAccessor(Method paramMethod){
 checkInitted();

    if (noInflation) {
 return new MethodAccessorGenerator().generateMethod(paramMethod.getDeclaringClass(), paramMethod.getName(),paramMethod.getParame
}
 NativeMethodAccessorImpl localNativeMethodAccessorImpl = new NativeMethodAccessorImpl(paramMethod);

    DelegatingMethodAccessorImpl localDelegatingMVethodAccessorImpl = new DelegatingMethodAccessorImpl(localNativeMethodccessorImpl);
                                                        
 localNativeMethodAccessorImpl.setParent(localDelegatingMethodAccessorImpl);
    return localDelegatingMethodAccessorImpl;
}

在这其中 出现了上面的sun.reflect.NativeMethodAccessorImpl和sun.reflect.DelegatingMethodAccessorImpl 其中 是先生成了一个NativeMethodAccessorImpl对象 然后 让其作为参数 调用到DelegatingMethodAccessorImpl类的构造方法 所以 最后我们应该关注DelegatingMethodAccessorImpl类的相关代码

在进入这个类中以后 调用了delegate属性的invoke方法 其又有两个实现类 分别是DelegatingMethodAccessorImpl 和 NativeMethodAccessorImpl 在上面 我们也提到了 NativeMethodAccessorImpl 是参数 所以 我们这块代码最终就决定于NativeMethodAccessorImpl了

而在这个类中的invoke方法中 存在这样一段代码

if (++numInvocations > ReflectionFactory.inflationThreshold())

这个的作用 就是可以判断其调用的次数是否超出了阈值 超过的话就会重新生成一个对象 并将之前的delegate属性 重新指向这个新生成的对象

那么 分析到这里 我们可以得出 这里的MethodAccessor对象就是反射类的入口 也就是我们反射中的源头 可以借用这张图 来更加生动形象的说明

Ending...

本文主要以p牛的Java安全漫谈为基础 结合了sp4c1ous师傅的框架 学习了各位师傅们的大小文章 得到了这篇文章 此文作为进入Java安全学习的第一步 更加主要的是以各种初见的名词进行解释 对一些方法进行理解 望各位师傅们斧正

参考文章

https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html

https://blog.csdn.net/mingyuli/article/details/112103702

http://www.whrizyl819.xyz/2022/03/06/JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B%E5%8F%8D%E5%B0%84%E5%9F%BA%E7%A1%80/

https://blog.csdn.net/qq_33521184/article/details/105212040


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450785379&idx=1&sn=1b4faff825ed22c12803eec456d639a9&chksm=b104f54486737c526bd93efe831c118d64b0428e0a797f5b57969e9cd16320cb085583a3b288#rd
如有侵权请联系:admin#unsafe.sh