学完反射和动态加载字节码那块,随便写点总结一下

# 反射

反射机制主要位于 java.lang.reflect 包中,核心类有 ClassFiledMethodConstructor
感觉大致可以这么理解:

获取 Class 对象 -> 实例化对象 -> 获取类里面的成员变量 -> 调用类的方法

获取 Class 对象 forName ("className")
非单例模式直接 newInstance () 创建实例对象,单例模式则需要尝试类中别的方法
随后就是 getMethod () 获取成员变量,调用方法 invoke ()

在佬讲解 URLDNS 时就需要通过反射来解决序列化时由于默认 hashcode=-1 而导致的发送请求的问题
简略描述一下

URL url = new URL("http://xxx");  
Class c = url.getClass();//getClass 实例化对象  
Field hashcodefile = c.getDeclaredField("hashCode");// 获取成员变量 hashCode  
hashcodefile.setAccessible(true);  
hashcodefile.set(url,1234);/先设为大于-1的数防止在序列化时发送请求  
hashmap.put(url,1);  
hashcodefile.set(url,-1);// 重新设回 - 1,反序列化时发送请求
serialize(hashmap);

此外也给出了反射弹计算机的例子,简单写点注释。由于 Runtime 是单例模式,所以不能直接 newInstance 实例化

package org.example;
import java.lang.reflect.Method;
public class Reflection {
    public static void main(String[] args) throws Exception{
        Class<?> c = Class.forName("java.lang.Runtime");// 加载 Runtime 类,返回 Class 对象 c
        Method method = c.getMethod("exec", String.class);// 获取 exec () 方法
        Method RuntimeMethod = c.getMethod("getRuntime");// 获取 getRuntime () 方法
        Object o1 = RuntimeMethod.invoke(null);// 由于 getRuntime 是静态方法所以 invoke 直接返回 Runtime 实例对象 o1=Runtime.getRuntime ()
        method.invoke(o1, "open -a Calculator");// 通过 Runtime 实例调用 exec 方法 = Runtime.getRuntime.exec (...)
    }
}

反射目前接触的就这些,后面就是去理解类加载器和字节码

# 类加载器

理解类加载器逃不过的一点就是双亲委派机制
这一点其实很好理解,就是先不自己处理,一层层传递上去直到 BOOT,看 BOOT 能不能处理,不能处理的话,作为父类加载器向下传递给子类加载器,子类加载器又作为父类进行判断

浅浅画了张示意图

# 动态加载字节码

字节码是 JVM 的指令集,由 java 源代码编译成.class 文件中的内容。字节码经过 JVM 解释 / 编译之后就是机器码了
而能够动态加载字节码就意味着能够运行本地或者远程的 class 文件
寻找的方式则是根据 sun.boot.class.pathjava.class.path 中列举的基础路径

# URLClassLoader

先来看用 URLClassLoader 这个 APPClassLoader 的父类来加载字节码
新建 Calc.java

package org.example;
import java.io.IOException;
public class Calc {
    static{
        try{
            Runtime.getRuntime().exec("open -a Calculator");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

打包成 jar 包之后,编写 URLClassLoader 的启动类

package org.example;
import java.net.URL;
import java.net.URLClassLoader;
public class FileRce {
    public static  void main(String[] args) throws Exception
    {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///Users/xxx/IdeaProjects/javalearn/java_learn/target/classes")});
        Class<?> c = urlClassLoader.loadClass("org.example.Calc");
        c.newInstance();
    }
}

这里的 "/Users/xxx/IdeaProjects/javalearn/java_learn/target/classes" 就是 Calc.class 所在的地方
这里是成功弹出计算器了,但是当我想效仿白日梦组长一样删掉 Calc.java,然后将 Calc.class 移到别的目录进行加载时却出了问题,一直报错表示找不到 Calc
这里我改那路径改了 n 次但一直不对,直到我尝试这么写

URL url = Paths.get("/Users/xxx/Desktop/").toUri().toURL()
ClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> c = urlClassLoader.loadClass("org.example.Calc");

直接通过函数规范化路径,结果就成功弹出来了,一看 url 的值为 file:/User/xxx/Desktop
按一开始那么写改成这样也过了,再改成 file:/// 又行了...
总之很迷

暂且不管,接着学

URLClassLoader 不仅支持 file 协议,也支持 HTTP 协议和 jar 协议
测试时通过 python 起个 http 服务,位置在包所在目录

python -m http.server 8888

java 代码改为

package org.example;
import java.net.URL;
import java.net.URLClassLoader;
public class FileRce {
    public static  void main(String[] args) throws Exception
    {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:8888/")});
        Class<?> c = urlClassLoader.loadClass("org.example.Calc");
        c.newInstance();
    }
}

也能成功加载到 Calc

jar 协议通常不能单独使用,需要配合其他协议
其协议标准格式是 jar:<url>!/[entry]
将 class 打包成 jar

jar cvf Calc.jar org/example/Calc.class

这里一定要注意,进入 org/example 所在目录,以包路径的形式打包,而不是绝对路径,不然会找不到
上述两种协议配合 jar 的写法如下
file
jar:file:///Users/xxx/Desktop/Calc.jar!/

http
jar:http://localhost:8888/Calc.jar!/

# defineClass()

java 加载字节码都会经历 loadClass->findClass->defineClass 的调用
其中的核心是 defineClass,能够将二进制数组转为 class 对象,像 URLClassLoader,作为 ClassLoader 的子类,最后也是会调用 defineClass
但是在 ClassLoader 中,defineClass 是 protected 方法,采取反射的方法

package org.example;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FileRce {
    public static  void main(String[] args) throws Exception
    {   
        ClassLoader cl = ClassLoader.getSystemClassLoader();// 获取系统类加载器
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);// 反射获取 defineClass 方法
        defineClass.setAccessible(true);// 绕过 protected 的限制
        byte[] code = Files.readAllBytes(Paths.get("/Users/xxx/Desktop/org/example/Calc.class"));// 读取文件字节码
        Class c = (Class) defineClass.invoke(cl,"org.example.Calc",code,0,code.length);// 调用方法
        c.newInstance(); // 创建实例
    }
}

这样的好处是可以不出网加载字节码,但是正常情况下,setAccessible 还是很难用上的

在 Unsafe 中也有一个 defineClass,本质上差不多,但是 public 的单例模式,还是私有,多一步获取 theUnsafe 成员变量

开放的 defineClass 方法也是有的,比如 TemplatesImpl 攻击链,这个单独再水吧~