本篇简单说一下RMI的调用流程和攻击历史,以及分析RMI的JDK源码,对于RMI攻击的各种反序列化并不做过多的分析。
关于JDK源码,详情请参考:https://github.com/frohoff/jdk8u-jdk/tree/master/src/share/classes。
RMI(Remote Method Invocation,远程方法调用),可以引用远程主机上对象的方法,在分布式领域应用广泛。RMI总的来说可以分为:RMI Client、RMI registry、RMI Server(有的代码中把registry和Server合成了一个)。
(1)首先远程主机(RMI Server
)会向RMI注册表(RMI Registry
)中注册对象,给对象绑定一个名称,例如Hello
代表RemoteA对象
。
创建对象:既然要注册一个对象,那么远程主机Server上首先要有一个对象。写一个接口、一个接口的实现类、类实例化对象。
- public interface IRemoteA extends Remote{
- public String hello() throws RemoteException; // 要远程调用的方法
- }
-
- public class RemoteA extends UnicastRemoteObject implements IRemoteA { // 接口实现类
- protected RemoteA() throws RemoteException {
- super();
- }
- public void hello() throws RemoteException { System.out.println("call from");}
- }
- RemoteA h = new RemoteA(); //类的实例化对象
注册对象:将实例化对象绑定到Registry中,绑定名称即为Hello:
- LocateRegistry.createRegistry(1099);
- Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
(2)客户端向RMI注册表中根据名称Hello
去查找(lookup
)该对象,并获取一个远程对象的stub实例,所谓的stub就是一种代理/引用,包含了远程对象的定位信息(服务器codebase地址等)。
- RemoteA hello = (RemoteA)Naming.lookup("rmi://127.0.0.1:1099/Hello");
- hello.hello();
(3)客户端向codebase地址请求远程对象的class,获取到class的同时会进行实例化,即运行类的构造方法/静态代码块static中的代码。
RMI过程因为涉及对象的存储和传递,所以要依靠反序列化来进行。无论是绑定对象还是查询对象的操作都会进行反序列化。这也是RMI攻击的基础。2016年ysoserial中就集成了关于Registry攻击和DGC攻击的内容。
所谓的Registry攻击,就是攻击注册中心。(1)服务器端->注册中心。服务器端是会向注册中心注册对象的(序列化传输),注册中心RegistryImpl_Skel#dispatch
拦截请求的过程中会对流进行反序列化得到对象,如果本地存在CommonsCollections这种调用链就可以造成攻击,这种攻击方式在ysoserial中名为RMIRegistryExploit
。
反过来讲,客户端会从注册中心查询并获取对象RegistryImpl_Stub#lookup
,然后在本地反序列化得到对象,如果本地存在CommonsCollections等则可能被注册中心反打,这可以理解为注册中心->客户端。(2)客户端->注册中心。按理说客户端是从注册中心拉取对象的,并不能传递恶意对象过去。但是RMI框架采用了DGC(分布式垃圾收集机制)来管理远程对象的生命周期,所以客户端可以采用DGC通信的方式发送恶意对象,在ysoserial中名为exploit.JRMPClient
。注册中心DGCImpl_Skel#dispatch
拦截请求进行反序列化的过程中受到攻击,也就是DGC攻击。
但是这两种都在8u121(还有JDK6u141、JDK7u131)中被修复。修复方式就是引入了JEP290。JEP(JDK Enhancement Proposal)是用于收集JDK增强的提案,290是该此提案在其中的索引编号。它代表的议题是Filter Incoming Serialization Data(过滤传入的序列化数据),官方链接:https://openjdk.java.net/jeps/290。其核心思想是在反序列化(ObjectInputStream)过程中调用过滤器对正在反序列化的类、流中的内容等进行校验,然后返回一个接受(ALLOWED)、拒绝(REJECTED)或未决定(UNDECIDED)的状态。简单来说就是反序列化期间加了白名单。对于上面提到的ysoserial的两种RMI攻击方式来说就失效了,增加的校验方法如下:
- // 注册表
- sun.rmi.registry.RegistryImpl#registryFilter
- // DGC
- sun.rmi.transport.DGCImpl#checkInput
也就是在调用栈运行到RegistryImpl_Skel#dispatch / DGCImpl_Skel#dispatch
后会通过上述两个函数进行校验(这两个函数作用于服务器端),这就限制了Registry攻击和DGC攻击。所以后来就开始研究如何绕过JEP 290带来的白名单限制。RegistryImpl#registryFilter
的方法实现中,会对传入的参数的类型进行判断。
- private static Status registryFilter(FilterInfo var0) {
- ...
- Class var2 = var0.serialClass();
- if (var2 != null) {
- if (!var2.isArray()) {
- return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
- } ...
- }
看一下对var2的判断,发现白名单如下,如果不是白名单中的类就返回REJECTED。
- String.class
- Number.class
- Remote.class
- Proxy.class
- UnicastRef.class
- RMIClientSocketFactory.class
- RMIServerSocketFactory.class
- ActivationID.class
- UID.class
相应地诞生了一些绕过方法,例如利用JRMP。一个利用服务端攻打客户端的JRMP Demo如下:
- // Server
- java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator"
- // Client
- Registry registry = LocateRegistry.getRegistry(1099);
- registry.lookup("hello");
假如我们能够作为服务器端,目标系统作为客户端来反连我们,向其发送恶意对象就可以造成攻击效果。JEP290的限制是对于服务器端的,对于客户端没有限制,这样就能达成绕过。
- JRMP Listener ——registry.bind(name, object)——> Deser Filter—> RMI Registry
- JRMP Listener <——Outgoing JRMP connection(not filtered)—— RMI Registry
- JRMP Listener ——Payload——> RMI Registry
想要让目标系统反连我们,就要通过代码构造,让其在反序列化过程中执行了JRMP连接的代码,如ysoserial中的payload.JRMPClient
调用链。
另外,由于JEP290是针对于RMI注册中心和DGC的,客户端发送数据到服务器端并没有做直接的限制,反序列化时如果接收到的不是基础数据类型(如Integer、Boolean、Byte、Character、Short、Long、Float或Double),就会直接对数据进行反序列化,也就是如果此时传入的是Object、String类型的数据会直接反序列化。假如服务器中应接收的不是Object类型的参数,我们在客户端传入Object参数的话,会无法通过method hash校验。
简单Demo如下:
- public interface Hello extends Remote {
- public String hey(Object msg) throws RemoteException;
- }
- public class HelloImpl extends UnicastRemoteObject implements Hello {
- public void hey(Object msg){
- System.out.println(msg.toString());
- }
- }
- // 服务端绑定
- Hello hello = new HelloImpl();
- LocateRegistry.createRegistry(1234);
- String url = "rmi://127.0.0.1:1234/Hello";
- Naming.bind(url, hello);
-
- // 客户端
- Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1234);
- Hello hello = (Hello)registry.lookup("Hello");
- CommonsCollections1 cc1 = new CommonsCollections1();
- hello.hey(cc1.getObject("calc"));
这也能看出这种利用方式存在的问题:需要目标服务器中存在CommonsCollections等,并且!要知道服务器端起服务的接口和接口中的调用方法,如Hello.hey(Object msg)
,较难利用。
总的来说,攻击主要有如下几种,但使用的前提是,目标服务器中存在CommonsCollections或其他反序列化gadget的组件,后两种连接JRMPListener的还要求目标服务器能够出网。对于JEP290的绕过,Oracle建议用户手动配置过滤器sun.rmi.registry.registryFilter
来防御攻击。
- 1. JDK < 8u121 bind、lookup、DGC对Registry端口进行反序列化攻击
- 2. JDK < 8u231 lookup发送UnicastRef对象,反序列化时连接JRMPListener
- 3. JDK < 8u241 lookup发送UnicastRefRemoteObject对象,反序列化时连接JRMPListener
- 4. 服务器端接口实现包含非基础类型接口,如Object,String(String< < JDK 8u242)
此外,在文章最开始的RMI调用流程图中可以看到codebase是对象的加载地址(如果本地的ClassPath中找不到就从codebase中加载),那么如果这个codebase是可控的,也就意味着会从攻击者的服务器上去拉取对象,如果这个类是恶意类就可能造成RCE,这也是JNDI与RMI攻击结合的利用场景。但是后来官方也对这样的攻击进行了限制。所以这种攻击场景的前提是:首先要安装并配置了SecurityManager,Java版本低于7u21、6u45(或者设置了java.rmi.server.useCodebaseOnly=false
)。
因为Java 7u21、6u45开始有了个默认设置java.rmi.server.useCodebaseOnly=true
,这意味着Java虚拟机只从预设的codebase中加载对象,而不支持从RMI请求中获取。所以在之后的版本进行codebase可控攻击时需要将这个属性改为false。
注意:JDK<7u21、6u45 codebase可控,或设置java.rmi.server.useCodebaseOnly=false,codebase可控。
Client(192.168.135.1)和Server(192.168.135.142:1099)交互过程大致如下:
(1)第一阶段
Client -> Server 发送:JRMI, Version:2, StreamProtocol
Server -> Client 回应:JRMI, ProtocolAck
Client -> Server 发送:JRMI, Call
Server -> Client 回应:JRMI, ReturnData
-> ReturnData中包含了codebase地址
(2)第二阶段
Client(192.168.135.1)和Server(192.168.135.142:33769)交互过程大致如下:
Client -> Server 发送:JRMI, Ping
Sevrer -> Client 回应:JRMI, PingAck
Client -> Server发送: JRMI, DgcAck
客户端和服务端之间是通过Socket通信的,这部分都位于sun.rmi.transport
包中。RMI协议的格式可以参考:https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html。其输出流格式和上述流量可以对应上。
- Magic:
- 0x4a 0x52 0x4d 0x49 Version Protocol -> 此十六进制转换为十进制为1246907721
-
- Version:
- 0x00 0x01 -> version == 2
-
- Protocol:
- StreamProtocol -> 0x4b
- SingleOpProtocol -> 0x4c
- MultiplexProtocol -> 0x4d
-
- Messages: -> 三种:Call、Ping、DgcAck
TransportConstants
类就是上述协议格式中的常量的集合:
- public class TransportConstants {
- public static final int Magic = 0x4a524d49; /** Transport magic number: "JRMI"*/
- public static final short Version = 2; /** Transport version number */
- public static final byte StreamProtocol = 0x4b; /** Connection uses stream protocol */
- public static final byte SingleOpProtocol = 0x4c; /** Protocol for single operation per connection; no ack required */
- public static final byte MultiplexProtocol = 0x4d; /** Connection uses multiplex protocol */
- public static final byte ProtocolAck = 0x4e; /** Ack for transport protocol */
- public static final byte ProtocolNack = 0x4f; /** Negative ack for transport protocol (protocol not supported) */
- public static final byte Call = 0x50;
- public static final byte Return = 0x51;
- public static final byte Ping = 0x52;
- public static final byte PingAck = 0x53;
- public static final byte DGCAck = 0x54;
- public static final byte NormalReturn = 0x01;
- public static final byte ExceptionalReturn = 0x02;
- }
Registry的相关内容都在java.rmi.registry
包中,只有:Registry
接口和LocateRegistry
类。
- public interface Registry extends Remote { // Remote接口:标识可以从非本地虚拟机调用的方法,任何远程对象都需要直接或间接实现此接口
- public static final int REGISTRY_PORT = 1099;
- // 绑定
- public void bind(String name, Remote obj) //绑定的对象也要实现Remote接口
- public void unbind(String name)
- public void rebind(String name, Remote obj)
- // 查询
- public Remote lookup(String name)
- public String[] list()
- }
Registry
代表远程对象注册表接口,提供了向注册表中绑定对象(bind、unbind、rebind
)和查询对象的方法(lookup
、list
)。
LocateRegistry
用于定位注册表。包括获取特定主机(包括本地主机)上注册表的引用getRegistry
(这样才能在本地调用Registry
),或者在特定端口上创建注册表createRegistry
。
- public final class LocateRegistry {
- private LocateRegistry() {}
- public static Registry getRegistry()
- public static Registry getRegistry(int port)
- public static Registry getRegistry(String host)
- public static Registry getRegistry(String host, int port)
- public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf)
- public static Registry createRegistry(int port) throws RemoteException {
- return new RegistryImpl(port); // 创建并导出Registry实例RegistryImpl
- }
- public static Registry createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException{
- return new RegistryImpl(port, csf, ssf);
- }
无论是哪种getRegistry
最终调用的都是如下代码:
- public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf) throws RemoteException
- {
- Registry registry = null;
-
- if (port <= 0)
- port = Registry.REGISTRY_PORT; //Registry类中定义了此变量默认为1099
-
- if (host == null || host.length() == 0) { // 如果host为空,尝试转换成真实的本地主机名
- try {
- host = java.net.InetAddress.getLocalHost().getHostAddress();
- } catch (Exception e) { host = ""; }
- }
-
- LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID), new TCPEndpoint(host, port, csf, null), false);
- RemoteRef ref = (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
- // 给注册表创建一个代理
- return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
- }
如果我们看一下ysoserial payload模块中的JRMPClient,会发现其核心代码与getRegistry的后几行完全一致。这几行代码会向指定的RMI Registry发起请求,也就是实现了JRMP连接的过程,实现了JEP290绕过中的反向连接操作。
这里也对这两行代码中涉及的类简要说明一下(下面提到的六个类,除ObjID外,全是传输层的内容)
(1)ObjID
标识RMI运行时的远程对象(Registry、DGC、自定义远程对象等)。所有的服务都需要通过ObjID来调用,只是Registry和DGC作为特殊的服务其ObjID是已知的REGISTRY_ID = 0 | ACTIVATOR_ID = 1| DGC_ID = 2
,其他的服务其ObjID需要先从Registry中lookup查询。
(2)TCPEndpoint
sum.rmi.transport
包中有个tcp文件夹。TCP协议是面向连接的传输层(Transport
)协议,也就是在使用TCP协议之前必须建立TCP连接。每一条TCP连接(Connection
)只能有两个端点(Endpoint
),即点对点,一对一的关系。当连接流量很大时,一个Connection
就会产生性能瓶颈,需要建立多个Connection
,分摊信道Channel
(缓存连接服务)。这些都是通讯过程中涉及到的一些接口,接口对应的实现类分别为TCPChannel、TCPConnection、TCPEndpoint、TCPTransport。简单画了个图示如下:
- Client | Server
- — — — — — — — — — — — — — — — —
- Stub | Skeleton
- — — — — — — — — — — — — — — — —
- Remote Reference
- — — — — — — — — — — — — — — — —
- Transport(传输层)
- — — — — — — — — — — — — — — — —
- Channel
- |<——————>|
- Connection
- Endpoint1|<——————>|Endpoint2
- |<——————>|
(3)LiveRef:表示对象的连线,将一个对象的ObjID和Endpoint进行连线。可以理解为上层到传输层的映射。
另外,说一下RMI服务被传输层识别的代码:
- // Transport.serviceCall
- Target target = ObjectTable.getTarget(new ObjectEndpoint(id, transport)); //ObjID、TCP Socket
- Dispatcher disp = target.getDispatcher();
- disp.dispatch(impl, call); // UnicastServerRef.dispatch()
(1)ObjectEndpoint:将ObjID和Transport做封装。作为ObjectTable的键,映射一个实例到Target。
(2)ObjectTable:共享对象表,将ObjID映射到远程对象目标Target。维护了两个核心表Map
。类中方法都围绕于Target的存放与获取。
(3)Target:封装了与远程对象有关的信息。包含ObjID、Dispatcher、Remote、Vector
RegistryImpl
位于sun.rmi.registry
,该包中也有三个类:RegistryImpl、RegistryImpl_Skel、RegistryImpl_Stub。
RegistryImpl
,它实现了Registry
接口,也就是对该接口中的五种方法进行了实现。以绑定操作为例看一下源码,尝试从this.bindings
中获取var1名称对应的对象,如果获取到了就表明绑定已经存在,否则就用put方法,绑定名称和对象。那么unbind
就是如果找到了绑定关系,就移除this.bindings.remove(var1);
。rebind
则是不做判断,直接 this.bindings.put(var1, var2);
。bindings是一个Hashtable结构。那么list
操作就是遍历Hashtable。
- private Hashtable<String, Remote> bindings;
-
- public void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException {
- synchronized(this.bindings) {
- Remote var4 = (Remote)this.bindings.get(var1);
- if (var4 != null) {
- throw new AlreadyBoundException(var1);
- } else {
- this.bindings.put(var1, var2);
- }
- }
- }
RegistryImpl构造函数,将端口和ObjID进行映射,调用UnicastServerRef.exportObject
导出对象。UnicastServerRef
位于Server端。RMI的核心在于调用远程的方法,而这些方法都是通过UnicastServerRef
对象来引用的,它映射到一个包含远程方法的类。RegistryImpl是要调用Registry相关的类,所以此处setup创建时var1.exportObject
会映射到的类是RegistryImpl_stub
或RegistryImpl_Skel。
- private static ObjID id = new ObjID(0);
-
- public RegistryImpl(final int var1) throws RemoteException {
- this.bindings = new Hashtable(101);
- if (var1 == 1099 && System.getSecurityManager() != null) {
- try {
- AccessController.doPrivileged(new PrivilegedExceptionAction
() { - public Void run() throws RemoteException {
- LiveRef var1x = new LiveRef(RegistryImpl.id, var1); // 1099端口和ObjID映射
- RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> { // setup
- return RegistryImpl.registryFilter(var0);
- }));
- return null;
- }
- }, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
- } ...
- } else {
- LiveRef var2 = new LiveRef(id, var1);
- this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
- }
- }
-
- private void setup(UnicastServerRef var1) throws RemoteException {
- this.ref = var1;
- var1.exportObject(this, (Object)null, true); // UnicastServerRef.exportObject ->创建RegistryImpl_stub、RegistryImpl_Skel
- }
所以注册中心Registry注册对象:UnicastServerRef.exportObject -> LiveRef.exportObject -> TCPEndpoint.exportObject -> TCPTransport.exportObject
,最终TCPTransport.listen开启监听并创建Socket。注册中心Registry处理对象请求,则与上述过程相反,一旦TCPTransport监听到请求,创建线程进行处理,经过对Connection中的信息进行提取和处理,最终交到UnicastServerRef.dispatch
,进而调用RegistryImpl_Skel.dispatch
。
Server服务端,Skeleton接口如下。该接口的实现类之一就是RegistryImpl_Skel:
- public interface Skeleton {
- // 解组参数,调用实际的远程对象实现,并封装返回值
- void dispatch(Remote obj, RemoteCall theCall, int opnum, long hash) throws Exception;
- Operation[] getOperations(); //返回Skeleton支持的操作
- }
RegistryImpl_Skel
,通过dispatch
方法对var3的判断调用Registry
相关操作,var3和五种方法的映射关系为:0->bind、1->list、2->lookup、3->rebind、4->unbind
。另外DGCImpl_Skel
也实现自Skeleton
,所以同样以dispatch
方法调用0 -> clean、1-> dirty
,后面会提到DGC相关内容。
- public final class RegistryImpl_Skel implements Skeleton {
- private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
- private static final long interfaceHash = 4905912898345647071L;
-
- public RegistryImpl_Skel() {}
-
- public Operation[] getOperations() { return (Operation[])operations.clone();}
-
- public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
- if (var3 < 0) {
- ...// 根据var4的hash值给var3赋值
- } else if (var4 != 4905912898345647071L) {
- throw new SkeletonMismatchException("interface hash mismatch");
- }
-
- RegistryImpl var6 = (RegistryImpl)var1;
- StreamRemoteCall var7 = (StreamRemoteCall)var2;
- String var8;
- ObjectInputStream var9;
- ObjectInputStream var10;
- Remote var81;
- switch(var3) {
- case 0:
- RegistryImpl.checkAccess("Registry.bind");
-
- try {
- var10 = (ObjectInputStream)var7.getInputStream();
- var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10);
- var81 = (Remote)var10.readObject();
- } catch (IOException | ClassNotFoundException | ClassCastException var78) {
- var7.discardPendingRefs();
- throw new UnmarshalException("error unmarshalling arguments", var78);
- } finally {
- var7.releaseInputStream();
- }
-
- var6.bind(var8, var81);
-
- try {
- var7.getResultStream(true);
- break;
- } catch (IOException var77) {
- throw new MarshalException("error marshalling return", var77);
- }
- case 1: ...
- case 2:
- try {
- var9 = (ObjectInputStream)var7.getInputStream();
- var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);
- } catch (IOException | ClassCastException var74) {
- var7.discardPendingRefs();
- throw new UnmarshalException("error unmarshalling arguments", var74);
- } finally {
- var7.releaseInputStream();
- }
-
- var81 = var6.lookup(var8);
-
- try {
- ObjectOutput var83 = var7.getResultStream(true);
- var83.writeObject(var81);
- break;
- } catch (IOException var73) {
- throw new MarshalException("error marshalling return", var73);
- }
- case 3: ...
- case 4: ...
- }
- }
无论是哪种操作,先根据传入的RemoteCall
获取输入流,如果是绑定操作对输入流进行反序列化得到Object(var81)和String(var8)将二者进行绑定。如果是查询操作,如list或lookup,则是如下代码:
- ObjectOutput var83 = var7.getResultStream(true);
- var83.writeObject(var81);
可以看到本文的JDK8_261版本在case0后面有一句权限校验RegistryImpl.checkAccess("Registry.bind");
具体看一下代码,只允许localhost发送请求,也就是只能Server端发起请求。
这个限制是从jdk8u141开始的,无论bind,rebind,unbind都会进行校验,只允许服务端发出,但是lookup和list方法前并没有RegistryImpl.checkAccess("Registry.bind");
这样的校验。
- public static void checkAccess(String var0) throws AccessException {
- try {
- final String var1 = getClientHost();
-
- final InetAddress var2;
- try {
- var2 = (InetAddress)AccessController.doPrivileged(new PrivilegedExceptionAction
() { - public InetAddress run() throws UnknownHostException {
- return InetAddress.getByName(var1); // 根据提供的主机名创建InetAddress
- }
- });
- } catch (PrivilegedActionException var5) {
- ...
- }
在看RegistryImpl_Stub
源码前,先看看它底层的抽象类RemoteStub
,使用远程引用RemoteRef
来执行对远程对象的远程方法调用。RemoteRef
表示远程对象的handler(句柄)。句柄代表对象的id,可以靠这个id访问对象。也可以理解为门把手,通过门把手我们可以控制门,但它不是门本身。所以RemoteRef
包含了调用对象的方法——newCall+invoke
。newCall
第三个参数为opnum
操作数,即代表了操作类型,0代表bind
,2代表lookup
...
- abstract public class RemoteStub extends RemoteObject { // `RemoteObject`,它实现了远程对象的行为
- protected RemoteStub() { super(); } //构造RemoteStub
- protected RemoteStub(RemoteRef ref) { super(ref); } // 根据RemoteRef来构造RemoteStub
- }
RegistryImpl_Stub
有了这些前置知识再来看RegistryImpl_Stub
,它也是实现了五种方法,五种方法的实现比较类似,选取bind
和lookup
的代码进行解析。
- private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
- private static final long interfaceHash = 4905912898345647071L;
-
- public RegistryImpl_Stub() {}
-
- public RegistryImpl_Stub(RemoteRef var1) { super(var1); }
-
- public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
- try {
- StreamRemoteCall var3 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, 4905912898345647071L); // RemoteRef.newCall
-
- try {
- ObjectOutput var4 = var3.getOutputStream();
- var4.writeObject(var1);
- var4.writeObject(var2);
- } catch (IOException var5) {
- throw new MarshalException("error marshalling arguments", var5);
- }
-
- this.ref.invoke(var3);
- this.ref.done(var3);
- } ...
- }
-
- public String[] list() throws AccessException, RemoteException {...}
-
- public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
- try {
- StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);
-
- try {
- ObjectOutput var3 = var2.getOutputStream();
- var3.writeObject(var1);
- } catch (IOException var15) {
- throw new MarshalException("error marshalling arguments", var15);
- }
-
- this.ref.invoke(var2);
-
- Remote var20;
- try {
- ObjectInput var4 = var2.getInputStream();
- var20 = (Remote)var4.readObject();
- } catch (IOException | ClassNotFoundException | ClassCastException var13) {
- var2.discardPendingRefs();
- throw new UnmarshalException("error unmarshalling return", var13);
- } finally {
- this.ref.done(var2); // 代表了一个远程调用的完结
- }
-
- return var20;
- } ...
- }
- public void rebind(String var1, Remote var2) throws AccessException, RemoteException {...}
- public void unbind(String var1) throws AccessException, NotBoundException, RemoteException {...}
上面说到我们实际操作还不是远程对象,而是this.ref
—远程对象的引用RemoteRef
,newCall
则创建了对远程对象引用进行具体操作的对象——RemoteCall
,也叫做"call Object"。RemoteCall
已经被弃用了,但是StreamRemoteCall
的源码还是要看看。其构造函数包含了Transport的头部、调用对象、操作方法和stub/skeleton的hash。
- private ConnectionOutputStream out = null;
- private Connection conn;
-
- public StreamRemoteCall(Connection c, ObjID id, int op, long hash) throws RemoteException{
- try {
- conn = c;
-
- // write out remote call header info...
- // call header, part 1 (read by Transport)
- conn.getOutputStream().write(TransportConstants.Call);
- getOutputStream(); // creates a MarshalOutputStream
- id.write(out); // object id (target of call)
- // call header, part 2 (read by Dispatcher)
- // Dispatcher的实现类为UnicastServerRef/UnicastServerRef2
- out.writeInt(op); // method number (operation index)
- out.writeLong(hash); // stub/skeleton hash
- } catch (IOException e) {
- throw new MarshalException("Error marshaling call header", e);
- }
- }
在RegistryImpl中提到服务器端导出对象方法为UnicastServerRef.exportObject
。在看UnicastServerRef
之前先来看看它的父类UnicastRef
。UnicastRef
实现自RemoteRef
接口。其内部操作的对象都是LiveRef
。Unicast翻译过来就是“单一传播”,即客户端和服务器端之间点对点的通信连接。那么UnicastRef就可以理解为单一传播LiveRef对象。
- public class UnicastRef implements RemoteRef {
- protected LiveRef ref;
-
- public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception {
- ...
- Connection var6 = this.ref.getChannel().newConnection();
- StreamRemoteCall var7 = null;
- boolean var8 = true;
- boolean var9 = false;
-
- Object var11;
- try {
- ...
- var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);
-
- try { // 序列化
- ObjectOutput var10 = var7.getOutputStream();
- this.marshalCustomCallData(var10);
- var11 = var2.getParameterTypes();
-
- for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) {
- marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
- }
- } catch (IOException var39) {...}
-
- var7.executeCall(); // 从流中反序列化transport和Object
-
- try { //反序列化
- Class var46 = var2.getReturnType();
- if (var46 != Void.TYPE) {
- var11 = var7.getInputStream();
- Object var47 = unmarshalValue(var46, (ObjectInput)var11); // 反序列化var11得到对象
- var9 = true;
- clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");
- this.ref.getChannel().free(var6, true);
- Object var13 = var47;
- return var13; // 返回unmarshalValue得到的对象
- }
-
- var11 = null;
- } catch (ClassNotFoundException | IOException var40) {...
- } finally {
- try {
- var7.done();
- } catch (IOException var38) {
- var8 = false;
- }
-
- }
- } ...
- return var11;
- }
invoke方法调用流程如下。invoke生成代理对象时要传入接口名、方法名称、参数类型和参数。
- protected static Object unmarshalValue(Class> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
- if (var0.isPrimitive()) {
- if (var0 == Integer.TYPE) {
- return var1.readInt();
- } ...//Boolean.TYPE |Byte.TYPE | Character.TYPE | Short.TYPE|Long.TYPE|Float.TYPE|Double.TYPE
- } else {
- // //返回var1中反序列化得到的Object
- return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();
- }
- }
-
- protected static void marshalValue(Class> var0, Object var1, ObjectOutput var2) throws IOException {
- if (var0.isPrimitive()) {
- if (var0 == Integer.TYPE) {
- var2.writeInt((Integer)var1);
- } ...// Boolean.TYPE | Byte.TYPE|Character.TYPE|Short.TYPE|Long.TYPE|Float.TYPE|Double.TYPE
- } else {
- var2.writeObject(var1); //序列化写入数据
- }
- }
UnicastRef
的newCall
方法,为此对象上的新远程方法调用创建适当的调用对象。
- public RemoteCall newCall(RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException {
- clientRefLog.log(Log.BRIEF, "get connection");
- Connection var6 = this.ref.getChannel().newConnection();
-
- try {
- StreamRemoteCall var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4);
- try {
- this.marshalCustomCallData(var7.getOutputStream());
- } ...
- return var7;
- } catch (RemoteException var10) {
- this.ref.getChannel().free(var6, false);
- throw var10;
- }
- }
继承自UnicastRef
,为UnicastRef
的远程对象实现了服务器端的行为。核心方法:exportObject、dispatch
。exportObject
导出此对象,创建对应调度的skeleton
和stubs
。dispatch
分配服务器端的远程对象。
这部分的运行流程如下,服务端通过listen启动Socket,创建AcceptLoop线程接收客户端的连接,每一个连接创建一个ConnectionHandler进行处理(BIO模式)。从ObjectTable中获取相应的Target对象,根据opnum确定调用的方法,最后通过反射method.invoke(obj, params)
后将结果返回给客户端。
- public class UnicastServerRef extends UnicastRef implements ServerRef, Dispatcher {
- ...
- private transient Skeleton skel;
- private final transient ObjectInputFilter filter;
- private transient Map
hashToMethod_Map; - private static final WeakClassHashMap
- private static final Map
, ?> withoutSkeletons; -
- public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
- Class var4 = var1.getClass();
-
- Remote var5;
- try {
- // 为远程对象创建代理
- // this.getClientRef:return new UnicastRef(this.ref);
- var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
- } catch (IllegalArgumentException var7) {
- throw new ExportException("remote object implements illegal remote interface", var7);
- }
-
- if (var5 instanceof RemoteStub) {
- this.setSkeleton(var1);
- }
-
- // Target(Remote var1, Dispatcher var2, Remote var3(stub), ObjID var4, boolean var5)
- Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
- this.ref.exportObject(var6); // this.ep.exportObject(var6);
- this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
- return var5;
- }
-
- public void dispatch(Remote var1, RemoteCall var2) throws IOException {
- try {
- int var3;
- ObjectInput var41;
- try {
- var41 = var2.getInputStream();
- var3 = var41.readInt();
- } catch (Exception var38) {
- throw new UnmarshalException("error unmarshalling call header", var38);
- }
-
- if (this.skel != null) {
- this.oldDispatch(var1, var2, var3);
- return;
- }
-
- if (var3 >= 0) {
- throw new UnmarshalException("skeleton class not found but required for client version");
- }
-
- long var4;
- try {
- var4 = var41.readLong();
- } catch (Exception var37) {
- throw new UnmarshalException("error unmarshalling call header", var37);
- }
-
- MarshalInputStream var7 = (MarshalInputStream)var41;
- var7.skipDefaultResolveClass();
- Method var42 = (Method)this.hashToMethod_Map.get(var4);
- if (var42 == null) {
- throw new UnmarshalException("unrecognized method hash: method not supported by remote object"); // 如果远程对象中不存在这样的方法,就会报错
- }
-
- this.logCall(var1, var42);
- Object[] var9 = null;
-
- try {
- this.unmarshalCustomCallData(var41);
- var9 = this.unmarshalParameters(var1, var42, var7); // 反序列化得到参数
- } catch (AccessException var34) {
- ((StreamRemoteCall)var2).discardPendingRefs();
- throw var34;
- } catch (ClassNotFoundException | IOException var35) {
- ((StreamRemoteCall)var2).discardPendingRefs();
- throw new UnmarshalException("error unmarshalling arguments", var35);
- } finally {
- var2.releaseInputStream();
- }
-
- Object var10;
- try {
- var10 = var42.invoke(var1, var9); // 执行远程对象的方法
- } catch (InvocationTargetException var33) {
- throw var33.getTargetException();
- }
-
- try {
- ObjectOutput var11 = var2.getResultStream(true);
- Class var12 = var42.getReturnType();
- if (var12 != Void.TYPE) {
- marshalValue(var12, var10, var11);
- }
- } catch (IOException var32) {
- throw new MarshalException("error marshalling return", var32);
- }
- } catch (Throwable var39) {
- ...
- } finally {
- var2.releaseInputStream();
- var2.releaseOutputStream();
- }
- }
-
- private void oldDispatch(Remote var1, RemoteCall var2, int var3) throws Exception {
- ObjectInput var6 = var2.getInputStream();
-
- try {
- Class var7 = Class.forName("sun.rmi.transport.DGCImpl_Skel");
- if (var7.isAssignableFrom(this.skel.getClass())) {
- ((MarshalInputStream)var6).useCodebaseOnly();
- }
- } ...
-
- long var4;
- try {
- var4 = var6.readLong();
- } catch (Exception var8) {
- throw new UnmarshalException("error unmarshalling call header", var8);
- }
-
- Operation[] var10 = this.skel.getOperations();
- this.logCall(var1, var3 >= 0 && var3 < var10.length ? var10[var3] : "op: " + var3);
- this.unmarshalCustomCallData(var6);
- this.skel.dispatch(var1, var2, var3, var4); //DGCImpl_Skel.dispatch
- }
dispatch方法会从RemoteCall中获取输入流,读取RMI协议的头部、方法,然后调用远程对象的该方法。如果此时skel
不为null
,调用DGCImpl_Skel的dispatch方法。
调用远程方法的代码,构造参数,传递给方法调用,服务器返回内容:
- Method method = hashToMethod_Map.get(op); // op: method hash目标方法的散列
- params = unmarshalParameters(obj, method, marshalStream);
- result = method.invoke(obj, params);
- marshalValue(rtype, result, out);
Transport中还有一些关于DGC的类:DGCImpl、DGCImpl_Skel、DGCImpl_Stub、DGCClient、DGCAckHandler
等。DGC也称为分布式垃圾收集算法。对于本地引用的每个远程对象,垃圾收集器都维护了一个引用列表,这样才能让远程客户机保存对象的引用。java.rmi.dgc.DGC
接口包含两个方法:dirty、clean
。当客户端对远程对象进行unmarshaled
时,会调用dirty
。当客户端不再存在对远程对象的引用,调用clean
。dirty
是有期限的(lease period
),从dirty
被调用开始算期限,如果到期前没有更新dirty
,分布式垃圾收集器就假定远程对象不再被该客户端引用。
- public interface DGC extends Remote {
- Lease dirty(ObjID[] ids, long sequenceNum, Lease lease) throws RemoteException;
- void clean(ObjID[] ids, long sequenceNum, VMID vmid, boolean strong) throws RemoteException;
- }
ObjID
标识了一个关联的对象。sequenceNum
参数是一个序列号,用于垃圾收集器的延迟调用。Lease
包含了客户端的唯一标识符VMID
(VM identifier
)和期限(lease period
)。
- public final class Lease implements java.io.Serializable {
-
- private VMID vmid; // a unique VM identifier 标识客户端
- private long value; // Duration of this lease
-
- public Lease(VMID id, long duration){
- vmid = id;
- value = duration;
- }
-
- public VMID getVMID(){ return vmid; }
- public long getValue(){ return value; }
- }
DGC的一个实现类为DCGImpl
,里面包含了一个内部类LeaseInfo
(其构造方法包含了标识符VMID
和期限expiration
),LeaseInfo
中的两个方法:renew()
更新期限,expired()
判断是否在还有效期内。DGCImpl
类中包含了一个静态代码块,获取ClassLoader,创建了Server相关的对象、注册表的代理等,并将生成的Target放入ObjectTable。
- final class DGCImpl implements DGC {
- ...
- private static final long leaseValue = (Long)AccessController.doPrivileged(new GetLongAction("java.rmi.dgc.leaseValue", 600000L));
- private static final long leaseCheckInterval;
- private static final ScheduledExecutorService scheduler;
- private static DGCImpl dgc;
- private Map
leaseTable; // 维护客户端ID和对应期限的Map结构,并提供注册和销毁VMID的方法:registerTarget、unregisterTarget、checkLeases - private Future> checker;
- ...
-
- static DGCImpl getDGCImpl() {
- return dgc;
- } ...
-
- private DGCImpl() {
- this.leaseTable = new HashMap();
- this.checker = null;
- }
-
- static {
- ...
- AccessController.doPrivileged(new PrivilegedAction
() { - public Void run() {
- ClassLoader var1 = Thread.currentThread().getContextClassLoader();
-
- try {
- Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
-
- try {
- DGCImpl.dgc = new DGCImpl();
- final ObjID var2 = new ObjID(2);
- LiveRef var3 = new LiveRef(var2, 0);
- final UnicastServerRef var4 = new UnicastServerRef(var3, (var0) -> {
- return DGCImpl.checkInput(var0);
- });
- final Remote var5 = Util.createProxy(DGCImpl.class, new UnicastRef(var3), true);
- var4.setSkeleton(DGCImpl.dgc);
- Permissions var6 = new Permissions();
- var6.add(new SocketPermission("*", "accept,resolve"));
- ProtectionDomain[] var7 = new ProtectionDomain[]{new ProtectionDomain((CodeSource)null, var6)};
- AccessControlContext var8 = new AccessControlContext(var7);
- Target var9 = (Target)AccessController.doPrivileged(new PrivilegedAction
() { - public Target run() {
- return new Target(DGCImpl.dgc, var4, var5, var2, true);
- }
- }, var8);
- ObjectTable.putTarget(var9);
- } catch (RemoteException var13) {
- throw new Error("exception initializing server-side DGC", var13);
- }
- } finally {
- Thread.currentThread().setContextClassLoader(var1);
- }
- return null;
- }
- });
- }
DGCImpl的dirty和clean方法,dirty方法的实现是将对象映射到Target,并返回相应的Lease。clean则是解除这种映射:
- public Lease dirty(ObjID[] var1, long var2, Lease var4) {
- VMID var5 = var4.getVMID();
- long var6 = leaseValue;
- ...
- var4 = new Lease(var5, var6);
- synchronized(this.leaseTable) {
- // 如果var5对应的LeaseInfo不为空,就更新它对应的leaseValue。
- // 如果var5对应的LeaseInfo为空,this.leaseTable.put(var5, new DGCImpl.LeaseInfo(var5, var6));
- ...
- }
-
- ObjID[] var14 = var1;
- int var15 = var1.length;
-
- for(int var10 = 0; var10 < var15; ++var10) {
- ObjID var11 = var14[var10];
- ...
- ObjectTable.referenced(var11, var2, var5); //将对象ID映射到Target
- }
-
- return var4; // 返回Lease
- }
-
- // ObjectTable.referenced
- static void referenced(ObjID var0, long var1, VMID var3) {
- synchronized(tableLock) {
- ObjectEndpoint var5 = new ObjectEndpoint(var0, Transport.currentTransport());
- Target var6 = (Target)objTable.get(var5);
- if (var6 != null) {
- var6.referenced(var1, var3); //运行到Target.referenced,执行 DGCImpl.getDGCImpl().registerTarget(var3, this);
- }
- }
- }
-
- public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) {
- ObjID[] var6 = var1;
- int var7 = var1.length;
- for(int var8 = 0; var8 < var7; ++var8) {
- ObjID var9 = var6[var8];
- ...
- ObjectTable.unreferenced(var9, var2, var4, var5);
- }
- }
服务器端对DCG的处理类为DGCImpl_Skel
、客户端的DCG实现类为DGCImpl_Stub
。先来看DGCImpl_Skel
,其operation属性中包含了clean和dirty方法。核心方法为dispatch,根据传入的var3参数,0
调用clean
,1
调用dirty
。实际调用方法的对象还是DGCImpl
。
- public final class DGCImpl_Skel implements Skeleton {
- private static final Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")};
- private static final long interfaceHash = -669196253586618813L;
-
- public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
- if (var4 != -669196253586618813L) {
- throw new SkeletonMismatchException("interface hash mismatch");
- } else {
- DGCImpl var6 = (DGCImpl)var1;
- StreamRemoteCall var7 = (StreamRemoteCall)var2;
- ObjID[] var8;
- long var9;
- switch(var3) {
- case 0:
- VMID var34;
- boolean var36;
- try {
- ObjectInput var37 = var7.getInputStream();
- var8 = (ObjID[])((ObjID[])var37.readObject());
- var9 = var37.readLong();
- var34 = (VMID)var37.readObject();
- var36 = var37.readBoolean();
- } catch (IOException | ClassNotFoundException | ClassCastException var32) {
- var7.discardPendingRefs();
- throw new UnmarshalException("error unmarshalling arguments", var32);
- } finally {
- var7.releaseInputStream();
- }
-
- var6.clean(var8, var9, var34, var36);
-
- try {
- var7.getResultStream(true);
- break;
- } catch (IOException var31) {
- throw new MarshalException("error marshalling return", var31);
- }
- case 1:
- Lease var11;
- try {
- ObjectInput var12 = var7.getInputStream();
- var8 = (ObjID[])((ObjID[])var12.readObject());
- var9 = var12.readLong();
- var11 = (Lease)var12.readObject();
- } catch (IOException | ClassNotFoundException | ClassCastException var29) {
- var7.discardPendingRefs();
- throw new UnmarshalException("error unmarshalling arguments", var29);
- } finally {
- var7.releaseInputStream();
- }
-
- Lease var35 = var6.dirty(var8, var9, var11);
-
- try {
- ObjectOutput var13 = var7.getResultStream(true);
- var13.writeObject(var35);
- break;
- } catch (IOException var28) {
- throw new MarshalException("error marshalling return", var28);
- }...
- }
- }
- }
DGCImpl_Stub
也是实现了dirty和clean,其实现方法与DGCImpl_Skel
的dispatch方法类似。都是先将对象序列化或反序列化,不同在于DGCImpl_Stub
对于对象的操作是通过StreamRemoteCall。
StreamRemoteCall var6 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, -669196253586618813L);
上面源码解析没有提到Server中的一个类MarshalOutputStream
,它扩展了ObjectOutputStream
,以添加远程对象的方法。如果需要序列化远程对象或包含对远程对象的引用的对象,则必须使用MarshalOutputStream
而不是ObjectOutputStream
。MarshalOutputStream
的构造函数会根据传入的protocol version(如1或2等)来生成流。序列化流时要用ObjectOutputStream,其内部有一个方法annotateClass,如果需要在序列化后的数据里写入内容,就需要重写这个方法
MarshalOutputStream
作为它的子类,重写了此方法,指定了序列化要加载类的存放位置。那么在对流进行反序列化时就可以读到这个信息。
- // 序列化要加载类的位置,也就是codebase的位置
- protected void annotateClass(Class> cl) throws IOException {
- writeLocation(java.rmi.server.RMIClassLoader.getClassAnnotation(cl));
- }
-
- protected void annotateProxyClass(Class> cl) throws IOException {
- annotateClass(cl);
- }
-
- // 将类的位置写入流。
- protected void writeLocation(String location) throws IOException {
- writeObject(location);
- }
RMI服务扫描工,请参考:https://github.com/NickstaDB/BaRMIe