Learning and interpretation of Java RMI

Keywords: Java Maven Spring

Write in front #

Following the previous article, this article mainly looks at the source code of the whole RMI process and makes a simple analysis

RMI source code analysis #

Let's review the RMI process first:

  1. Create a remote object interface (RemoteInterface)
  2. Create a remote object class (RemoteObject) to implement the remote object interface (RemoteInterface) and inherit the UnicastRemoteObject class
  3. Create a Registry & Server side. Generally, Registry and Server are on the same side.
    • Create a Registry LocateRegistry.getRegistry("ip", port);
    • Create Server side: mainly instantiate remote objects
    • Register remote objects: via naming.bind( rmi://ip:port/name ,RemoteObject)   Bind name to a remote object
  4. The remote object interface should exist in all three roles: Client/Registry/Server
  5. Create Client side
    • Get the registry LocateRegistry.getRegistry('ip', prot)
    • Through registry.lookup(name)   Method to find the reference of the remote object according to the alias and return the Stub
  6. Implement RMI (Remote Method Invocation) through Stub

Creating remote interfaces and remote objects #

These three things are mainly done in the process of new RemoteObject

  1. Create a local stub for Client access.
  2. Start the socket and listen to the local port.
  3. Target registration and lookup.

Throw a piece of RemoteInterface and RemoteObject code first

RemoteInterface

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteInterface extends Remote{

    String doSomething(String thing) throws RemoteException;

    String say() throws RemoteException;

    String sayGoodbye() throws RemoteException;

    String sayServerLoadClient(Object name) throws RemoteException;

    Object sayClientLoadServer() throws RemoteException;
}

RemoteObject

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {

    protected RemoteObject() throws RemoteException {
    }

    @Override
    public String doSomething(String thing) throws RemoteException {
        return new String("Doing " + thing);
    }

    @Override
    public String say() throws RemoteException {
        return "This is the say Method";
    }

    @Override
    public String sayGoodbye() throws RemoteException {
        return "GoodBye RMI";
    }

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

    @Override
    public Object sayClientLoadServer() throws RemoteException {
        return new ServerObject();
    }
}

So let's take a look at some of the things we mentioned earlier that must be written in the code

Remote#

Let's first look at the part of creating a Remote object interface. As mentioned in the previous article, this interface needs to inherit the java.rmi.Remote interface, and the methods declared in the interface should throw RemoteException exceptions. It is mentioned in the notes of the Remote interface:

This interface is used to identify whether some interfaces can call methods from non local virtual machines, and remote objects must implement this interface indirectly or directly; We also mentioned the "special remote interface" before. When an interface inherits the java.rmi.Remote interface, the methods declared on the interface can be called remotely.

Personally, it feels a bit like a serialized markup interface, which is used to mark whether the implementation class of this interface can be called remotely.

RemoteException#

Exception class. The note explains that any remote interface that inherits java.rmi.Remote needs throws RemoteException exception for the method in the interface. This exception refers to the communication related exceptions that may occur during the execution of remote method calls.

Process and code analysis #

It is used to export remote object s using JRMP, obtain stubs, and communicate with remote objects through stubs

It mainly refers to the construction method and exportObject(Remote). This point is mentioned in master Longofo's article. When the remote interface is implemented without inheriting the UnicastRemoteObject class, you need to call the UnicastRemoteObject.exportObject(Remote) method to export the remote object.

Construction method

/**
     * Creates and exports a new UnicastRemoteObject object using an
     * anonymous port.
     * @throws RemoteException if failed to export object
     * @since JDK1.1
     */
protected UnicastRemoteObject() throws RemoteException
{
  this(0);
}

exportObject(Remote)

/**
     * Exports the remote object to make it available to receive incoming
     * calls using an anonymous port.
     * @param obj the remote object to be exported
     * @return remote object stub
     * @exception RemoteException if export fails
     * @since JDK1.1
     */
    public static RemoteStub exportObject(Remote obj)
        throws RemoteException
    {
        /*
         * Use UnicastServerRef constructor passing the boolean value true
         * to indicate that only a generated stub class should be used.  A
         * generated stub class must be used instead of a dynamic proxy
         * because the return value of this method is RemoteStub which a
         * dynamic proxy class cannot extend.
         */
        return (RemoteStub) exportObject(obj, new UnicastServerRef(true));
    }

These two methods will eventually move towards the overloaded exportObject(Remote obj, UnicastServerRef sref) method

UnicastServerRef is created during initialization   Object and call its exportObject method

In the method, the RemoteObjectInvocationHandler processor will be created through the createProxy() method to create a dynamic proxy for the RemoteInterface interface

Then go back to the UnicastServerRef#exportObject method and create a new Target object, which encapsulates the relevant information of the remote object, including the stub attribute (a dynamic proxy object that proxies the remote interface we defined)

After that, the exportObject method of liveRef is called.

Then call the sun.rmi.transport.tcp.TCPEndpoint#exportObject method (the call stack is shown in the figure below). Finally, the TCPTransport#exportObject() method is called. In this method, the listening local port is turned on and Transport#exportObject() is called

In this method, the ObjectTable.putTarget() method is invoked to register the Target instance into the ObjectTable object.

In the ObjectTarget class, there are two ways (two overloaded methods of getTarget) to find the registered Target, namely, the object with ObjectEndpoint parameter and the object with Remote parameter

Looking back at the dynamic proxy RemoteObjectInvocationHandler, which inherits RemoteObject and implements InvocationHandler, this is a serializable dynamic proxy class that can be transmitted remotely using RMI. Mainly focus on the invoke method. If the class object of the class or interface represented by the incoming method object is Object.class, go to invokeObjectMethod; otherwise, go to invokeRemoteMethod

In the invokeRemoteMethod method, UnicastRef.invoke method is finally called. UnicastRef's invoke method is a process of establishing a connection, executing the call, reading the results and deserializing. Deserialization in   unmarshalValue calls readObject implementation

The above is the flow of the underlying code running in the process of creating a remote interface and instantiating a remote object (with a little more dynamic agent). Here is a timing chart.

It is recommended that you master also make a breakpoint to follow up. The overall process of instantiating remote objects is relatively clear.

Create registry #

Registry registry = LocateRegistry.createRegistry(1099);

When you click debug, you first instantiate a RegistryImpl object

Enter the parameterized structure, first the new LiveRef object, then the new UnicastServerRef object, and call the setup method as a parameter

The UnicastServerRef#exportObject method is still called in the setup method to export the RegistryImpl object; Unlike last time, this time it will go straight into if to create stub, because the stubClassExists method is invoked in if judgment, which will determine whether the incoming class has xxx_ locally. Stub class.

RegistryImpl obviously exists, so it will go into the createstab method

In this method, we construct a method and instantiate RegistryImple_Stub class to create a proxy class.

Call setSkeleton to create a skeleton

It is also a reflection operation, instantiating RegistryImple_Skel class

Finally assigned to UnicastServerRef.skel property

In UnicastServerRef class, the remote object method is called through the dispatch method, and the results are serialized and transmitted to the Client through the network

public void dispatch(Remote var1, RemoteCall var2) throws IOException {
  try {
    long var4;
    ObjectInput var40;
    try {
      var40 = var2.getInputStream();
      int var3 = var40.readInt();
      if (var3 >= 0) {
        if (this.skel != null) {
          this.oldDispatch(var1, var2, var3);
          return;
        }

        throw new UnmarshalException("skeleton class not found but required for client version");
      }

      var4 = var40.readLong();
    } catch (Exception var36) {
      throw new UnmarshalException("error unmarshalling call header", var36);
    }

    MarshalInputStream var39 = (MarshalInputStream)var40;
    var39.skipDefaultResolveClass();
    Method var8 = (Method)this.hashToMethod_Map.get(var4);
    if (var8 == null) {
      throw new UnmarshalException("unrecognized method hash: method not supported by remote object");
    }

    this.logCall(var1, var8);
    Class[] var9 = var8.getParameterTypes();
    Object[] var10 = new Object[var9.length];

    try {
      this.unmarshalCustomCallData(var40);

      for(int var11 = 0; var11 < var9.length; ++var11) {
        var10[var11] = unmarshalValue(var9[var11], var40);
      }
    } catch (IOException var33) {
      throw new UnmarshalException("error unmarshalling arguments", var33);
    } catch (ClassNotFoundException var34) {
      throw new UnmarshalException("error unmarshalling arguments", var34);
    } finally {
      var2.releaseInputStream();
    }

    Object var41;
    try {
      var41 = var8.invoke(var1, var10);
    } catch (InvocationTargetException var32) {
      throw var32.getTargetException();
    }

    try {
      ObjectOutput var12 = var2.getResultStream(true);
      Class var13 = var8.getReturnType();
      if (var13 != Void.TYPE) {
        marshalValue(var13, var41, var12);
      }
    } catch (IOException var31) {
      throw new MarshalException("error marshalling return", var31);
    }
  } catch (Throwable var37) {
    Object var6 = var37;
    this.logCallException(var37);
    ObjectOutput var7 = var2.getResultStream(false);
    if (var37 instanceof Error) {
      var6 = new ServerError("Error occurred in server thread", (Error)var37);
    } else if (var37 instanceof RemoteException) {
      var6 = new ServerException("RemoteException occurred in server thread", (Exception)var37);
    }

    if (suppressStackTraces) {
      clearStackTraces((Throwable)var6);
    }

    var7.writeObject(var6);
  } finally {
    var2.releaseInputStream();
    var2.releaseOutputStream();
  }

}

Most processes of registration center and remote service object registration are the same, with the differences in:

  • The remote service object uses a dynamic proxy, the invoke method finally calls the invoke method of UnicastRef, and the registry uses RegistryImpl_Stub, and registryimpl is also created_ Skel
  • The default random port for remote objects is 1099 (of course, it can also be specified)

Service registration #

This part is actually naming.bind(“ rmi://127.0.0.1:1099/Zh1z3ven ", remoteObject); Implementation of

It's still the interruption point. Follow in and have a look

get into   java.rmi.Naming#bind()   Method will first parse and process the url we passed in. First call the java.rmi#parseURL(name) method, and then enter the intParseURL(String str) method. In this method, the url we passed in will be( rmi://127.0.0.1:1099/Zh1z3ven )Make some judgments such as whether the protocol is RMI and whether there is a problem with the format, and then do the string processing operation to obtain the host (127.0.0.1), port (1099) and name (zh1z3ven) in the incoming url Field and pass it as a parameter into the parameterized constructor of the built-in class ParsedNamingURL of java.rmi.naming

That is, assign values to the attributes in the built-in class

Then go back to Naming#bind() method, assign the instantiated ParsedNamingURL object to parsed and bring it into the java.rmi.Naming#getRegistry method as a parameter

Finally, enter the getRegistry(String host, int port, RMIClientSocketFactory csf) method. The call stack is as follows. The subsequent operation is still to create a dynamic agent. The dynamic proxy part is similar to the operation when creating remote objects, so it is no longer followed

Take a look at the last step in java.rmi.Naming#bind(), where registryimpl is called_ The stub#bind method binds name to a remote object.

The logic inside the method is also clearer. After obtaining the output stream, serialize it and call the UnicastRef#invoke method.

Generally, the logic of service registration, that is, the binding between name and remote object, is different from that in master su18's article. I enter the second invoke method, while master su18 enters the first invoke method. I'm a little confused here and need to be studied.

summary #

Borrow a picture of su18 master. The java Native deserialization operation is used for the communication between the three roles of Server/Registry/Client. In other words, if one end can be controlled or forged, a malicious serialized data can be directly RCE. That is, the three characters have impassable attack scenarios.

END#

Debugging is very difficult. In fact, there may be many unclear places about the RMI source code I mentioned above.

In fact, as long as you interrupt to debug and follow up, you will be very clear about a workflow of RMI. If you don't just need some points, you don't have to follow them very deeply.

The following is the attack tactics against RMI. The next article is more.

Posted by zoobooboozoo on Sat, 30 Oct 2021 00:52:36 -0700