RMI调用流程分析

RMI调用流程分析

RMI

以下介绍内容来自Java的RMI介绍及使用方法详解 | w3cschool笔记

现在有一个需求,我们需要在一台主机上调用另一台主机上的Java代码,这个时候就要用到RMI(Remote Method Invocation)。

使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。Server 端通常会创建一个对象,并使之可以被远程访问。

这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。

Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。

说到这里,是不是发现使用 RMI 在构建一个分布式应用时十分方便,它和 RPC 一样可以实现分布式应用之间的互相通信,甚至和现在的微服务思想都十分类似。


以下内容参考自:

https://su18.org/post/rmi-attack/

http://devgou.com/article/Java-RMI

那么我们来思考一下RMI的具体流程,首先是一个被远程调用的RMI-Server端,和一个RMI-Client端,但是有一个问题,Server端上每一个要被远程调用的对象都对应了一个RMI端口,并且这个端口是随机的,如果有很多需要被远程调用的对象,那么Client端是无法准确区分具体要调用对象的对应端口,所以我们引入RMI-Register。

RMI-Register是一个注册中心,并且Register的端口是我们可以指定的。Server端会将需要被远程调用的对象绑定到Register中,并且起一个别名。那么这个时候如果有多个需要被远程调用的对象,Client端可以通过先访问Rigister以及想要调用的远程对象的别名,此时Register会自动帮我们找到该别名对应的远程对象在RMI上的端口,之后会通过该端口找到对应的远程对象后调用其中的方法。

但其实Client端调用Server端的远程对象,它们之间并非是直接通信的,而是通过一个代理对象-存根(stub和skeleton),这样做是为了屏蔽网络通信间的复杂性。具体流程如下:

  1. Client端访问Register,拿到Stub,Stub中有调用的Server端远程对象的存根(skeleton)
  2. Client通过调用自己的存根(stub)与服务端存根(skeleton)通信
  3. 服务端存根(skeleton)调用服务SS端远程对象的方法并将结果返回给Client端存根(stub)
  4. Client端存根(stub)将结果返回给Client端

image-20250330151056801

image-20250330151114274


使用代码创建RMI服务端及客户端进行通信(注册中心一般在与Server放在一起)

服务端代码架构如下:

image-20250330151139904

客户端代码架构如下:

image-20250330151156929

1、服务端与客户端共同实现的接口RemoteInterface,该接口方法为需要远程调用的方法,并且需要继承Remote接口:

1
2
3
4
5
6
7
8
9
10
11
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;

public String sayHello(Object name) throws RemoteException;

public String sayGoodbye() throws RemoteException;
}

2、服务端:

要被远程调用的Server端对象RemoteObject,该远程对象需要继承UnicastRemoteObject类并且扩展我们之前写好的RemoteInterface接口,实现其中的方法。继承UnicastRemoteObject类后,客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。如果不想继承UnicastRemoteObject ,后面就需要主动的使用其静态方法 exportObject手动export:

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
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}

@Override
public String sayHello() throws RemoteException {
return "Hello User!";
}

@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}

@Override
public String sayGoodbye() throws RemoteException {
return "Bye User!";
}
}

实现Server及Registry,将上面需要被远程调用的对象绑定到Registry中,其中我们使用LocateRegistry类创建Registry,并且将Registry绑定到了12345端口上,Namning是一个可以对Registry进行操作的静态类,其中包括:查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)。Naming.bind格式为//host:port/alias,前面的rmi:加不加都可以,alias为要远程调用的类在Registry中的别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
RemoteObject remoteObject = new RemoteObject();
LocateRegistry.createRegistry(12345);
Naming.bind("rmi://localhost:12345/Hello",remoteObject);
}
}

3、客户端

客户端通过获取服务端已经开放的Registry后进行调用,通过Naming.lookup()在远程对象注册表Registry中查找指定name的对象,并返回远程对象的引用(一个stub),之后可通过stub调用远程对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Array;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RemoteClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost",12345);
String string = new String();
System.out.printf(Arrays.toString(registry.list()));
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayHello(string));
System.out.println(stub.sayGoodbye());
}


}

这样一次简单的通信就结束了,其中还有一些需要注意的点:

可以看到在上面的实例中我们在Client端调用时向sayHello方法中传递了一个对象,这个对象必须是可序列化的。如果传递的该对象在Server端不存在,那么Server端就会抛出ClassNotFound 的异常,但如果设置了java.rmi.server.codebase,那么就可以通过java.rmi.server.codebase设置的另一个地址中获取该类并加载反序列化。该属性可使用System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:7777/");进行设置,也可以使用启动参数

-Djava.rmi.server.codebase="http://127.0.0.1:7777/"进行指定。

并且我们也要指定安全策略,让rmi在动态加载一个类时能够允许该类进行反序列化。

RemoteServer.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
String policyFile = "/Users/y1zh3e7/web安全/Java安全/Java RMI/RMIServer/src/main/java/rmi.policy";
System.setProperty("java.security.policy", policyFile);
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:7777/");
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
RemoteObject remoteObject = new RemoteObject();
LocateRegistry.createRegistry(12345);
Naming.bind("rmi://localhost:12345/Hello",remoteObject);
}
}

RemoteClient.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;

public class RemoteClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost",12345);
System.out.printf(Arrays.toString(registry.list()));
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayHello(new ClientObject()));
System.out.println(stub.sayGoodbye());
}


}

rmi.policy:

1
2
3
4
5
grant {
permission java.net.SocketPermission "localhost:12345", "listen,resolve";
permission java.security.AllPermission;
};

image-20250330151218075

RMI-Server实现

开始debug:

image-20250330151233965

步入几次后进入到UnicastRemoteObject无参构造,并且调用另外一个构造函数,接收一个port为0,并且调用exportObject方法,继续跟进:

1
2
3
4
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}
1
2
3
4
5
protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

我们发现在这个exportObject中又调用了一个重载的exportObject方法,这个方法可以将我们要远程传输的对象发布到网络上,可以发现如果继承了UnicastRemoteObject这个类就会自动调用这个方法,所以如果没有继承UnicastRemoteObject类就需要在RemoteObject中的构造方法主动调用这个静态方法,其中第二个参数传入的是一个UnicastServerRef对象,我们跟进这个对象:

从这里开始是一系列的封装,UnicastServerRef为最外层

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

UnicastServerRef对象构造方法会调用父类构造方法,并传入一个LiveRef对象:

1
2
3
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

跟进LiveRef这个对象,它的构造方法会调用另一个重载的构造方法,我们发现这个接收两个参数的构造方法中调用了TCPEndpoint.getLocalEndpoint(port)(第一个参数就是给这个LiveRef对象分配一个标识id):

1
2
3
public LiveRef(int port) {
this((new ObjID()), port);
}
1
2
3
public LiveRef(ObjID objID, int port) {
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
}

跟进TCPEndpoint.getLocalEndpoint(port),其实就是在这一步中拿到了我们RMI-Server的ip地址及端口,return回来的ep里面存放了我们的ip和端口(到目前为止传递的端口一直是0):

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 如果当前ep列表中没有东西就创建一个ep放到里面,ep里面就存放了我们RMI-Server的ip和端口 
if (epList == null) {
/*
* Create new endpoint list.
*/
ep = new TCPEndpoint(localHost, port, csf, ssf);
epList = new LinkedList<TCPEndpoint>();
epList.add(ep);
ep.listenPort = port;
ep.transport = new TCPTransport(epList);
localEndpoints.put(endpointKey, epList);

if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF,
"created local endpoint for socket factory " + ssf +
" on port " + port);
}
} else {
synchronized (epList) {
ep = epList.getLast();
String lastHost = ep.host;
int lastPort = ep.port;
TCPTransport lastTransport = ep.transport;
// assert (localHost == null ^ lastHost != null)
if (localHost != null && !localHost.equals(lastHost)) {
/*
* Hostname has been updated; add updated endpoint
* to list.
*/
if (lastPort != 0) {
/*
* Remove outdated endpoints only if the
* port has already been set on those endpoints.
*/
epList.clear();
}
ep = new TCPEndpoint(localHost, lastPort, csf, ssf);
ep.listenPort = port;
ep.transport = lastTransport;
epList.add(ep);
}
}
}
}

return ep;
}

image-20250330151252708

创建好ep后,这个ep对象会被封装到LiveRef中:

1
2
3
4
5
public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
ep = endpoint;
id = objID;
this.isLocal = isLocal;
}

image-20250330151313253

经过一系列的初始化后我们又回到了UnicastRef中,会将我们刚才生成好的LiveRef对象封装到UnicastRef中:

1
2
3
public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

我们回到了这个UnicastRemoteObject的exportObject方法上,封装结束,经过一系列封装

UnicastServerRef内封装LiveRef,LiveRef内封装了Endpoint,EndPoint中封装的是当前要被调用的远程对象的ip和端口,UnicastServerRef从名字上就可以看出来是一个RMI-Server端的引用对象,Server端与其它RMI组件之间的通信集中于这一个类上,不需要在被调用的对象上添加任何的属性和方法。

此时我们继续看exportObject(obj, new UnicastServerRef(port))之后的逻辑

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

可以看到到这里将我们封装好的UnicastServerRef再次封装到了要被远程调用的对象的ref属性上,并且调用了UnicastServerRef的exportobject,继续跟进

1
2
3
4
5
6
7
8
9
10
    private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}
}

跟进后我们发现在这个exportObject方法中要进行一个动态代理的创建,这个动态代理就是我们之前提到的stub:

这里可能会有疑问,我们服务端的代理对象不是skeleton吗,客户端才是stub,为什么会在服务端的逻辑中生成,是因为这个stub在服务端生成后需要客户端将生成好的stub拿过去使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

在这行代码中使用getClientRef获取客户端Ref,我们跟进查看:

1
2
3
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
}

可以发现实例化了一个UnicastRef,刚才我们提到了UnicastServerRef,封装的是服务端引用,那么这个UnicastRef其实封装的就是客户端引用了,传了一个ref过去,这个ref里面就是我们封装好的LiveRef,可以看到这个LiveRef和之前封装在UnicastServerRef中的LiveRef是同一个,所以说这个LiveRef是我们整个通信的核心:

1
2
3
protected RemoteRef getClientRef() {
return new UnicastRef(ref);
}

image-20250330151334941

createProxy方法内部创建好了动态代理对象,并代理了两个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final ClassLoader loader = implClass.getClassLoader();
final Class<?>[] interfaces = getRemoteInterfaces(implClass);
final InvocationHandler handler =
new RemoteObjectInvocationHandler(clientRef);

/* REMIND: private remote interfaces? */

try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});
} catch (IllegalArgumentException e) {
throw new StubNotFoundException("unable to create proxy", e);
}
}

image-20250330151351028

动态代理对象创建完成后这里又进行了一次封装,将RMI通信中所有需要的东西全部封装到这个Target对象当中,并且调用了LiveRef的exportObject方法:

1
2
3
4
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);

跟进LiveRef的exportObject方法,发现调用了TCPEndPoint的exportObject:

1
2
3
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

继续跟进,发现调用了TCPTransport的exportObject:

1
2
3
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

继续跟进,我们终于来到了最重要的一个地方,在TCPTransport的exportObject中调用了listen方法,我们跟进listen方法:

1
2
3
4
5
6
7
8
9
10
public void exportObject(Target target) throws RemoteException {
/*
* Ensure that a server socket is listening, and count this
* export while synchronized to prevent the server socket from
* being closed due to concurrent unexports.
*/
synchronized (this) {
listen();
exportCount++;
}

listen方法开启了一个Socket链接,并且这个tcp链接是单独开辟了一个线程运行的,与我们当前运行代码的线程是两个不同的线程,到此时我们传递的port依然是0:

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
33
34
35
36
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();

if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}

try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
} catch (java.net.BindException e) {
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
throw new ExportException("Listen failed on port: " + port, e);
}

} else {
// otherwise verify security access to existing server socket
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkListen(port);
}
}
}

在newServerSocket()中可以看到使用setDefaultPort方法为这个远程对象分配了一个随机的端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ServerSocket newServerSocket() throws IOException {
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE,
"creating server socket on " + this);
}

RMIServerSocketFactory serverFactory = ssf;
if (serverFactory == null) {
serverFactory = chooseFactory();
}
ServerSocket server = serverFactory.createServerSocket(listenPort);

// if we listened on an anonymous port, set the default port
// (for this socket factory)
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);

return server;
}

从listen中出来后会调用super.exportObject,跟进这个exportObject:

1
2
3
4
5
6
7
8
9
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}

这里面将封装好的Target放到了ObjectTable这个表结构中:

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

最终对于Target的一系列操作完毕,我们回到了这里,将 stub return了出去,到这里Server端的一系列过程结束:

1
2
3
4
5
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;

注册中心创建及绑定

image-20250330151407299

进入有参构造,创建RegistryImpl实例对象:

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

RegistryImpl构造方法中仍然创建了一个LiveRef实例,并传递给UnicastServerRef进行封装,再传递给setup调用,这个LiveRef中封装的就是我们Registry启动的ip和端口,并且我们再刚才远程对象的创建中也经历了这些步骤,说明Registry本质上也是一个特殊的远程对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// grant permission for default port only.
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

跟进到setup中,可以看到这里调用了UnicastServerRef的exportObject方法,和发布远程对象的逻辑相似,我们也要将这个RegistryImpl对象发布出去:

1
2
3
4
5
6
7
8
9
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

跟进之后很熟悉的地方,创建动态代理Stub,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

跟进到Util.createproxy中,我们发现这里与远程对象创建Stub的方式不同,在远程对象的创建中if判断条件没有通过,所以走的是下面的Proxy.newProxyInstance(loader, interfaces,handler);使用jdk原生动态代理创建的,但在Registry这里,我们发现这个判断条件是成立的,跟进这个if判断:

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
try {
remoteClass = getRemoteClass(implClass);
} catch (ClassNotFoundException ex ) {
throw new StubNotFoundException(
"object does not implement a remote interface: " +
implClass.getName());
}

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

final ClassLoader loader = implClass.getClassLoader();
final Class<?>[] interfaces = getRemoteInterfaces(implClass);
final InvocationHandler handler =
new RemoteObjectInvocationHandler(clientRef);

/* REMIND: private remote interfaces? */

try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});

我们发现这个判断条件是可以进去的,其中stubClassExists的判断条件为true,跟进去发现这个方法会使用反射寻找类名后拼接一个_Stub,如果成功找到就返回true,否则就抛出异常后返回false,此时remoteClass.getName()是我们的sun.rmi.registry.RegistryImpl,那么存不存在sun.rmi.registry.RegistryImpl_Stub这个类的,我们在jdk原生的包下是可以找到这个类的,可以发现不光有Stub也有Skel,这两个类就是JDK帮我们生成好的Registry远程对象的Stub和Skeleton:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

image-20250330151420689

回到上面的判断中,返回为true,所以走的是createStub(remoteClass, clientRef)这里,我们继续跟进:

1
2
3
4
5
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

cretaeStub中用反射为我们创建好了sun.rmi.registry.RegistryImpl_Stub这个类:

1
2
3
4
5
6
7
8
9
10
11
12
String stubname = remoteClass.getName() + "_Stub";

/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

回到UnicastServerRef中,继续向下跟进,Stub已经初始化好了,接下来要对Skeleton进行初始化,跟进setSkeleton方法:

1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

发现调用的仍然是Utilz中的createSkeleton,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setSkeleton(Remote impl) throws RemoteException {
if (!withoutSkeletons.containsKey(impl.getClass())) {
try {
skel = Util.createSkeleton(impl);
} catch (SkeletonNotFoundException e) {
/*
* Ignore exception for skeleton class not found, because a
* skeleton class is not necessary with the 1.2 stub protocol.
* Remember that this impl's class does not have a skeleton
* class so we don't waste time searching for it again.
*/
withoutSkeletons.put(impl.getClass(), null);
}
}
}

和创建Stub时一样,反射加载:

1
2
3
4
5
String skelname = cl.getName() + "_Skel";
try {
Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

return (Skeleton)skelcl.newInstance();

Skeleton初始化好后接着走完UnicastServerRef最后一段代码,仍然是将这些要用到的东西封装到了Target对象中去,并且调用LiveRef.exportObject:

1
2
3
4
5
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;

LiveRef.exportObject中逻辑和远程对象创建中基本上是一样的,不过我们可以继续跟进到Transport.exportObject中看一下现在的ObjectTable中都有哪些Target对象:

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

我们在ObjectTable中可以看到当前的objTable有如下三个Target对象:

第一个Target对象中的Stub为DGCImpl_Stub,这个东西我们在后续内容中会提到:
image-20250330151439778

第二个Target对象中的Stub为RegistryImpl_Stub,就是我们初始化好的Registry:

image-20250330151503371

第三个Target对象中的Stub为一个动态代理对象,这个Target就是我们的远程对象了

image-20250330151524744

到这里我们的Registry就初始化好了,最终拿到了一个RegistryImpl对象:

image-20250330151545420

接下来我们看一下远程对象是如何绑定到Registry上的:

我这了是使用了远程绑定的方式:

1
2
3
RemoteObject remoteObject = new RemoteObject();
Registry r = LocateRegistry.createRegistry(12345);
Naming.bind("rmi://localhost:12345/Hello",remoteObject);

不过实际应用中我们都会直接用创建好的Registry来进行绑定,所以我们直接看这种本地的绑定:

1
2
3
RemoteObject remoteObject = new RemoteObject();
Registry r = LocateRegistry.createRegistry(12345);
r.bind("Hello",remoteObject);

跟进到这个bind方法中其实发现就是对Registry这个哈希表结构放进去一对kv:

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

其实对于注册中心的其它操作也是基于对这个HashMap的一些操作。

客户端请求注册中心-客户端行为

我们在前面已经了解到了Registry是客户端和服务端通信的核心,但是又说到Registry它本身也是一个远程对象,所以需要客户端先拿到Registry的Stub再和Registry的Skeleton进行通信,所以我们下面来看一下客户端是如何拿到Registry的Stub的:

image-20250330151559616

跟进之后调用重载的getRegistry:

1
2
3
4
5
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}

跟进这个重载的方法中,可以发现客户端拿到Registry的Stub并不是通过接收Registry的一个反序列化对象实现的,而是直接本地创建了一个LiveRef,然后封装到动态代理当中:

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
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
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);
}

跟到这个createProxy中可以发现这个动态代理的创建和再Registry中创建的那个Stub过程一模一样,所以说Client端拿到Registry的Stub其实是通过在本地创建了一个一模一样的(指其中封装的LiveRef的属性是一模一样的)RegistryImpl_Stub实现的:

image-20250330151619492

image-20250330151639902

拿到了Registry的Stub后,接下来我们就可以通过这个Stub来寻找其它远程对象的代理,也就是lookup操作

image-20250330151659001

调进去之后我们可以看到这个RegistryImpl_Stub是一个Class文件,它是java1.1时候的东西,由于太古老没有源码文件,我们没办法在这里打断点调试,只能静态分析,可以看到这里调用了一个super.ref.newCall,实际上这里super.ref就是一个UniCastRef,进去看一下:

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
33
34
35
36
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

这里会创建一个StreamRemoteCall对象,其中这个opnum代表的是当前的操作,比如2就代表了lookup操作:

image-20250330151718903

我们接着往下看,创建好这个RemoteCall之后lookup方法中会获取这个RemoteCall的输出流,并且写入一个var1,这个var1其实就是我们调用lookup时传入的name,所以其实在这里我们把相关信息以序列化流的方式要发给Registry,那么可以联想到在Registry端应该会有一个反序列化的地方将这些数据进行反序列化处理,这就可能导致一些安全问题:

1
2
3
4
5
6
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

我们接着往下看,有一处地方会调用UnicastRef的invoke方法,我们继续跟进:

1
super.ref.invoke(var2);
1
2
3
4
5
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");

call.executeCall();

跟进call.executeCall,这个excuteCall其实就是我们客户端实现网络请求的地方了,我们主要看这里的一个异常处理,如果是这个类型的异常就会对一个输入流进行一个反序列化,所以如果这里客户端从Registry接收到的流是一个恶意的对象,并且触发了该异常,那么就会在客户端本地进行反序列化,导致客户端被攻击,并且我们说这里是通过调用excuteClaa方法进入的,而excuteCall方法又是通过调用了一个invoke触发的,那么其实这样就非常隐蔽,并且由于所有的Stub处理网络请求都要经过excuteCall,所以这里就会成为一个非常危险的点:

1
2
3
4
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();

我们再回到RegistryImpl_Stub的lookup中,下面还有一个反序列化的点,所以其实很容易看的出来这里存在反序列化漏洞的问题,但在bind方法中虽然没有显式的调用readObject,但是会调用了invoke,所以还是会隐式的存在反序列化漏洞的问题:

1
2
3
4
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
}

那么做完这些动作后客户端就拿到了这个远程对象的引用代理,里面就存储了这个远程对象所在的端口:

image-20250330151734282

客户端请求服务端-客户端行为

刚才我们通过让客户端访问注册中心,拿到了远程对象所在的端口,那么接下来客户端就要通过这个端口来访问及调用远程对象的方法了,我们继续Debug:

image-20250330151749418

步入之后走到了远程对象Stub的InvocationHandler的invoke中:

image-20250330151817102

这里又调了一次UnicastRef的invoke方法,不过这个不是我们刚才说的那个invoke,而是重载的另外一个invoke方法:

1
2
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));

然后我们继续调,发现又走到了这个executeCall方法中,所以我们上面说了不管是jdk给我们创建好的Stub(RegistryImpl_Stub)还是客户端自己接收到的远程对象的Stub,在通过Stub调用网络请求时都会走到这个方法中:

image-20250330151839958

经过这个方法后我们的远程方法就被调用了,然后可以看到下面有一个判断,如果该方法调用的返回值为void,那么直接return出去,但是我们的这个方法会返回一个值,所以会触发这个unmarshalValue方法:

1
2
3
4
5
6
7
8
9
10
11
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

/* StreamRemoteCall.done() does not actually make use
* of conn, therefore it is safe to reuse this
* connection before the dirty call is sent for
* registered refs.
*/
Object returnValue = unmarshalValue(rtype, in);

跟进到这个方法了里面可以看到会判断返回值的类型,然后通过对应的方式拿到返回值,可以发现如果不是基本类型就会通过反序列化拿到其中的值,那么这里又是一个客户端存在反序列化攻击的点:

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
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

image-20250330151856933

image-20250330151916852

程序结束,我们成功的将远程方法的返回值打印了出来:

image-20250330151939545

客户端请求注册中心-注册中心行为

接下来我们看客户端在请求注册中心时注册中心都干了哪些事情,我们在上面提到了客户端是通过拿到Registry的Stub后通过Stub与Registry的Skeleton进行通信,那么我们先来看一下在创建注册中心时走到Skeleton之前注册中心都干了什么,我们回到RegistryImpl的setup方法这里,一直跟进exportObject中,经过好几层exportObjec我们回到了这个最核心的listen方法:

1
2
3
4
5
6
7
8
9
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

image-20250330151959641

在listen中我们之前提到了会开启一个新线程将我们的网络监听放进去,所以我们跟进到这个进程的创建中(AcceptLoop):

1
2
3
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));

我们来看这个线程对象的run方法,其中调用了executeAcceptLoop,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run() {
try {
executeAcceptLoop();
} finally {
try {
/*
* Only one accept loop is started per server
* socket, so after no more connections will be
* accepted, ensure that the server socket is no
* longer listening.
*/
serverSocket.close();
} catch (IOException e) {
}
}
}

可以看到这里又创建了一个线程池并执行,所以我们继续跟进,查看这个线程的run方法:

1
2
3
4
5
6
7
8
try {
connectionThreadPool.execute(
new ConnectionHandler(socket, clientHost));
} catch (RejectedExecutionException e) {
closeSocket(socket);
tcpLog.log(Log.BRIEF,
"rejected connection from " + clientHost);
}

这个run方法中调用了run0,跟进run0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
run0();
return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}

在run0中有一行主要代码handleMessage,这个方法会读取连接传入的信息,我们跟进:

1
handleMessages(conn, false);

可以看到在这个方法中会对接收到的字段进行处理,比如op代表了要执行什么样的操作,这里可以看到第一种case是当请求到来时的默认操作,其中调用了ServiceCall,我们跟入ServiceCall方法看一下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void handleMessages(Connection conn, boolean persistent) {
int port = getEndpoint().getPort();

try {
DataInputStream in = new DataInputStream(conn.getInputStream());
do {
int op = in.read(); // transport op
if (op == -1) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " +
port + ") connection closed");
}
break;
}

if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") op = " + op);
}

switch (op) {
case TransportConstants.Call:
// service incoming RMI call
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
return;
break;

case TransportConstants.Ping:
// send ack for ping
DataOutputStream out =
new DataOutputStream(conn.getOutputStream());
out.writeByte(TransportConstants.PingAck);
conn.releaseOutputStream();
break;

case TransportConstants.DGCAck:
DGCAckHandler.received(UID.read(in));
break;

default:
throw new IOException("unknown transport op " + op);
}
} while (persistent);

} catch (IOException e) {
// exception during processing causes connection to close (below)
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") exception: ", e);
}
} finally {
try {
conn.close();
} catch (IOException ex) {
// eat exception
}
}
}

ServiceCall中会有对Target表取出的操作,我们之前在分析服务端创建时说过最后会讲通信用到的所有东西(尤其是Stub)放到这个Target对象中:

1
2
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));

那么我们此时在这里打断点,开启调试,客户端请求,成功的断到了这个地方,说明我们之前静态分析的过程都是正确的,接下来查看客户端在对注册中心发起一系列操作时注册中心的回应:

image-20250330152014507

我们接着往下看,拿到这个Target后会对这个Target调用getDispatcher,这个方法的返回值disp其实就是我们的UnicastServerRef:

1
final Dispatcher disp = target.getDispatcher();

image-20250330152030596

我们接着往下走,发现对这个disp调用了dispatch方法:

1
2
3
4
5
6
7
8
9
10
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call);
return null;
}
}, acc);
}

跟进发现有一个判断条件,如果skel不为null就会调oldDispatch,我们此时的这个UniCastServer中skel是不为null的,所以会调用这个方法,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}

在oldDispatch方法中会调用到skel.dispatch这个方法:

1
skel.dispatch(obj, call, op, hash);

那么这个时候我们又回到了RegistryImpl_Skel这个类中了,这个类还是不能调试,所以我们静态分析,这个var3其实就是我们在客户端提到的opnum,比如我们现在为case2,代表的就是要执行lookup操作:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

并且可以看到在case2中,会对我们要lookup的远程对象直接进行一次反序列化,从这次反序列化中就可以读到要lookup的对象的名称以及其它等信息,所以这里就是注册中心可能会受到攻击的一个点,在其他的case操作中可以发现也会存在这种readObject的操作,所以客户端攻击注册中心的方法也非常多,那么到这了调用了RegistryImpl的lookup方法后就结束了:

1
2
3
4
5
6
7
8
9
10
11
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

客户端请求服务端-服务端行为

我们把断点断在这里:

image-20250330152042929

开始调试,和注册中心一样,首先会走到ServiceCall当中:

image-20250330152102168

我们看一下当前的target是什么,可以发现里面的Stub是一个动态代理,这个Target就是存放了我们远程对象相关的Target了:

image-20250330152120333

继续往下调,还是走到了dispatch这里跟进:

image-20250330152134405

与注册中心不同的是这里target中的skel为空,所以没有调用oldDispatch,走到了下面,这里就获取到了我们要调用的远程方法:

1
2
3
4
5
Method method = hashToMethod_Map.get(op);
if (method == null) {
throw new UnmarshalException("unrecognized method hash: " +
"method not supported by remote object");
}

之后走到这里,会对要远程调用的方法内的参数进行检测,我们调用的是无参方法,所以不会进入到这里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
} catch (java.io.IOException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} catch (ClassNotFoundException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}

之后走到这里,就执行到了真正的方法调用,可以发现是通过反射来调用的:

1
2
3
4
5
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}

后面又是走到了这里,不过不同的是这次是将方法的值进行序列化,那么客户端就可以接收到这次序列化后的返回值在进行反序列化后使用了:

1
2
3
4
  if (rtype != void.class) {
marshalValue(rtype, result, out);
}
}

image-20250330152148459

服务端响应结束:

image-20250330152208272

客户端请求服务端-DGC

在之前我们查看远程对象的创建时,会发现最终创建好的远程对象会封装到一个Target当中,再通过putTarget方法放入到一张静态表里,并且在调试期间,我们会发现最先生成的一个Target对象,其中的dist是一个叫做DGCImpl的Target,这个DGC其实就是RMI在使用过程中创建出来的分布式垃圾回收,也就是说在RMI中,我们最终的对象回收都是由RMI来实现的。

那么我们就看一下DGC创建时的方式,在调用到putTarget方法时,可以发现这里会走到一个DGCImpl.dgcLog上,这个dgcLog是DGCImpl中的一个静态属性,在Java中当调用一个类的静态属性时会触发该类的初始化,也就是说会调用这个类的静态代码块:

image-20250330152226206

我们跟进到DGCImpl的静态代码块中,发现其中会创建一个DGCImpl的实例,所以其实在上面的if判断条件后,DGCImpl的实例就被创建出来了,并且可以发现静态代码块中也调用了createProxy,说明DGC也是通过创建一个代理对象来给客户端进行使用:

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
33
34
35
36
37
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ClassLoader savedCcl =
Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());

/*
* Put remote collector object in table by hand to prevent
* listen on port. (UnicastServerRef.exportObject would
* cause transport to listen.)
*/
try {
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref);
Remote stub =
Util.createProxy(DGCImpl.class,
new UnicastRef(ref), true);
disp.setSkeleton(dgc);

Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);

Target target = AccessController.doPrivileged(
new PrivilegedAction<Target>() {
public Target run() {
return new Target(dgc, disp, stub, dgcID, true);
}
}, acceptAcc);

ObjectTable.putTarget(target);
}

我们跟进到这个createProxy当中查看,可以发现还是一样的逻辑,继续跟进createStub:

1
2
3
4
5
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

还是判断有没有DGCImpl_Stub类,发现是存在的,在class sun.rmi.transport.DGCImpl,所以直接通过反射实例化出该Stub实例,逻辑上和远程对象创建基本一样,到这里DGC的创建就结束了:

1
2
3
4
5
6
7
8
9
10
11
12
String stubname = remoteClass.getName() + "_Stub";

/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

当客户机创建(序列化)远程引用时,会在服务器端 DGC 上调用 dirty()。当客户机完成远程引用后,它会调用对应的clean() 方法。

针对远程对象的引用由持有该引用的客户机租用一段时间。租期从收到 dirty()调用开始。在此类租约到期之前,客户机必须通过对远程引用额外调用 dirty() 来更新租约。如果客户机不在租约到期前进行续签,那么分布式垃圾收集器会假设客户机不再引用远程对象。

那么我们接着来看一下DGCImpl_Stub中的代码逻辑,发现有一个clean方法和一个dirty方法:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public DGCImpl_Stub(RemoteRef var1) {
super(var1);
}

public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

再看一下DGCImpl_Skel,其中有两个case,其实代表的就是Stub传递过来的clean操作还是dirty操作,可以发现不管是在stub还是skel中都存在很多的反序列化点,所以DGC也是很容易被攻击的一个地方:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package sun.rmi.transport;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.rmi.MarshalException;
import java.rmi.Remote;
import java.rmi.UnmarshalException;
import java.rmi.dgc.Lease;
import java.rmi.dgc.VMID;
import java.rmi.server.ObjID;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.Skeleton;
import java.rmi.server.SkeletonMismatchException;

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 DGCImpl_Skel() {
}

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;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

public Operation[] getOperations() {
return (Operation[])operations.clone();
}
}