rmi代码分析

简介前的简介

额,一直以来貌似养成了不好的习惯,在写完这一篇之后发现,我为什么要写文章啊。。

博客是放笔记的,对吧,写的自己看懂就行了。还指望给别人看呢。

所以这是本博客最后一篇可能有可能别人能看懂的文章。

image.png

简介

在处理rmi的时候发现当时学的比较早导致一些东西没有分析的很清楚,今天把他其中的一些过程重新分析一下,rmi还是很重要的一部分。

对rmi其中的各种流程的代码部分进行简单的调试和分析。

简单写一个rmi本地调用的project即可开始调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.demo02;

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
public class RMIHello extends UnicastRemoteObject implements IHello{

protected RMIHello() throws RemoteException {
super();
}

@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello"+name);
return null;
}
}

private void register() throws Exception{
RMIHello rmiHello=new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
System.out.println("Registry运行中......");
}

public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}

服务端

服务端创建远程对象

rmi中的远程对象都继承了UnicastRemoteObject类,然后实现远程接口,IHello接口也继承了Remote

1
public class RMIHello extends UnicastRemoteObject implements IHello{

所以我们要看的就是他的构造方法


UnicastRemoteObject.exportObject

会先进入父类UnicastRemoteObject的构造方法,在UnicastRemoteObject构造方法中会进入java.rmi.server.UnicastRemoteObject#exportObject(java.rmi.Remote, int)方法

其中传入的默认的port为0

image.png

image.png

所以首先会创建一个UnicastServerRef对象,

image.png

在创建这个对象的时候,首先又会封装一个LiveRef对象进去

LiveRef

下面多次封装同一个LiveRef对象,就是为了客户端和服务端的通信

sun.rmi.transport.LiveRef对象会封装进三个属性,其中包含了本地ip,它实际上是负责网络通信的一个对象

image.png

image.png

UnicastServerRef

UnicastServerRef类继承自UnicastRef类,其实也就是简单的把liveRef对象封装进去。

image.png

当我们封装好了UnicastServerRef对象的时候,再进入java.rmi.server.UnicastRemoteObject#exportObject方法中

封装好的UnicastServerRef对象:

image.png

其实又是一个封装的过程,把UnicastServerRef对象封装到一开始的远程对象中,也就是RMIHello这个远程对象

此时我们的远程对象已经创建好了

image.png

UnicastServerRef.exportObject

接着会进入sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)方法中

先用创建动态代理的方式创建了一个存根stub,他就是RMIHello的动态代理类

image.png

Util.createProxy

稍微跟进一下sun.rmi.server.Util#createProxy方法,他是一个创建动态代理的方法

image.png

getRemoteClass方法会对传入的远程对象进行一下验证然后返回它的class

接着就是创建动态代理,classloader,handler

image.png

创建动态代理

image.png

可以看到返回的Proxy0动态代理对象,里面也封装了LiveRef这个对象

image.png


接着新建一个Target对象,把刚刚的一系列东西都封装进去

image.png

image.png


接下来进到sun.rmi.transport.LiveRef#exportObject方法中,也就是哪里都封装了的LiveRef对象

image.png

这个方法进到sun.rmi.transport.tcp.TCPTransport#exportObject方法,实际上他就是开一个端口,然后把Target对象放上去

image.png

接着下面还会exportObject一次target,是进到sun.rmi.transport.Transport#exportObject方法中

image.png

根据注释理解一下,这个方法就是把远程对象放在服务端,等待客户端来的请求

下面的sun.rmi.transport.ObjectTable#putTarget也就是一个表put的操作,把target装入

image.png


image.png

最后返回的是这个对象,至此远程对象创建完毕

注册中心创建逻辑

image.png

其实就是新建一个RegistryImpl对象,传入的参数值是默认的1099端口

image.png

首先会创建一个LiveRef作用于通讯

又创建一个UnicastServerRef类,在服务端创建的时候我们已经遇到过了一次这个构造方法,再一次进入

可以看到也就只是封装了一个ref对象用于网络通讯

image.png

接着进入setup方法

image.png

在其中我们又看到了熟悉的sun.rmi.server.UnicastServerRef#exportObject(java.rmi.Remote, java.lang.Object, boolean)方法,在上面创建服务端对象的时候也遇到过

不过这里参数有所不同

image.png

这里创建的代理是这个注册中心的代理类,也就是RegistryImpl类

他还会进入setSeleton方法中

image.png

image.png

可以看到createSelection方法接受一个Remote对象

然后加载它后缀加上Skel的class对象来获取Skeleton

image.png

这个方法过后,skeleton类被加载到这个UnicastServerRef中

skeleton和stub就是分别对应服务端和客户端的网络代理

image.png

image.png

注册中心同时提供了skl和stub,因为要和服务端和客户端两端通讯

因此最终createRegistry()的结果就是返回了一个RegistryImpl对象,并且赋值this.skel=RegistryImpl_Skel

image.png

注册中心绑定逻辑

image.png

先获取到注册中心

image.png

然后调用sun.rmi.registry.RegistryImpl_Stub#bind把UnicastServerRef对象绑定上去

image.png

客户端

同样的先获取到注册中心

image.png

返回的是注册中心的stub对象

image.png

然后进到sun.rmi.registry.RegistryImpl_Stub#lookup方法中,也就是我们根据注册的名称来查找类

先会对我们查找的这个远程类名称字符串序列化进去

newCall方法就是一个网络请求的操作,这也和之前我们分析的流量对应了起来

说明注册中心也会有一个反序列化的操作

image.png

然后在下面的invoke方法中实现网络请求

也就是这个executeCall方法

image.png

然后就通过反序列化得到返回来的远程对象代理

image.png

这里有2个反序列化点,都是反序列化注册中心返回来的代理对象,如果我们创建一个恶意注册中心返回恶意stub就可能实现对客户端的反序列化攻击,分别是这两个地方(上面提了一个地方)

UnicastRef.invoke

sun.rmi.server.UnicastRef#invoke(java.rmi.server.RemoteCall)

第一个地方就是在invoke方法中的sun.rmi.transport.StreamRemoteCall#executeCall方法中

image.png

在处理异常的地方里面有一个反序列化的操作

如果是这个异常的话,就会触发反序列化

image.png

并且这个sun.rmi.server.UnicastRef#invoke方法在很多地方都会被调用

RegistryImpl_Stub.lookup

image.png

image.png

最后就会获得之前我们创建的服务端对象的远程代理,可以看到里面封装的LiveRef对象

远程方法调用

因为我们获取hello对象是远程对象的动态代理,所以对他调用任意方法的时候就会进入handler(调用处理器)的invoke方法

image.png

首先会对这个方法是否属于该远程对象进行一个判断

然后这里又会调用sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)这个方法

image.png

接着sun.rmi.server.UnicastRef#marshalValue方法中会把参数值序列化进去

image.png

接下来又会调用到executeCall方法,所有的客户端的请求都会用到这个方法

image.png

这里建立的连接就是和服务端直接建立连接了

image.png

接着就是获取方法调用结果

image.png

和上面的marshaValue对应,unmarshalValue方法中对返回的数据进行了反序列化处理

image.png

image.png

总结

引用自Javasec

RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。