Java RMI - when Java methods can be invoked remotely (part 1)


Java RMI là một API của Java cho phép programmer tạo các “distributed Java technology-based” nhằm cung cấp cho các Java technology-based application mà trong đó, các method của remote Java objects có thể được invoke từ một Java virtual machine khác, kể cả khi hai máy ảo này không cùng host.
Một số khái niệm cần biết
Stub và skeleton: trong RMI sử dụng stub và skeleton làm cơ chế cho việc communicate với remote object.
- Stub được dùng ở phía client như là một đại điện (representative) hoặc proxy cho remote object. Có thể hiểu như sau: phía client có 1 stub A, khi user cần call đến method nào đó ở remote server thì sẽ thực hiện call method đó lên stub A. Stub A khi đó thực hiện quá trình lookup đến remote server cho method đó, rồi trả về kết quả thực thi. Stub cần implement một interface chứa các method giống với implement của interface đó trên remote server. Ví dụ về stub:
package example.rmi.server;
import java.rmi.Naming;
public class Client {
public static void main(String[] args) throws Exception {
HelloService svc = (HelloService) Naming.lookup("rmi://localhost:1099/hello");
System.out.println("--- " + svc.echo("Hello server! From NoSpaceAvailable"));
}
}
(Ở đây, svc
là một stub)
Skeleton được dùng ở phía server với mục đích là gửi method call đến object thật sự (actual remote object implementation).
Skeleton được giấu khá kỹ trong implement của RMI nên mình sẽ để ở phần giải thích phía dưới
Trong ngữ cảnh RMI, ngoài serialize và deserialize ra thì người ta còn có khái niệm marshal và unmarshal:
Marshal: là quá trình tương tự như serialization, tuy nhiên nó còn record được cả codebase của object. Bất kì object nào được implement java.io.Serializable hoặc java.rmi.Remote đều có thể được marshal.
Unmarshal: là quá trình ngược lại của marshal nhằm tạo ra một bản copy của object với trạng thái giống như object được marshal. Trong quá trình unmarshal object, class definition sẽ được tự động load từ runtime hoặc là từ codebase.
(với các ngữ cảnh khác, VD như JAXB thì việc marshal một object không bắt buộc object đó phải implement java.io.Serializable hoặc java.rmi.Remote)
Cơ chế hoạt động của Java RMI
Khi client thực hiện remote method call lên stub, stub làm các công việc sau:
Khởi tạo connection đến Java Virtual Machine chứa remote object
Marshal parameters đến remote JVM
Đợi kết quả của việc invoke method
Unmarshal return value (hoặc exception)
Gửi kết quả cho client
Khi skeleton nhận được yêu cầu remote invoke method, skeleton làm các việc sau:
Unmarshal parameters được gửi đến
Thực hiện invoke method tương ứng trên object thực sự
Marshal return value (hoặc exception) đến client
Đi vào chi tiết
Để cho tiện lợi thì mình khuyến khích các bạn sử dụng Intellij IDEA để build và debug
- Simple setup
Ở đây mình chuẩn bị gồm có 4 file, với directory tree như sau:
- ApplicationServer.java:
package example.rmi.server;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ApplicationServer {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.createRegistry(2099);
registry.rebind("hello", new HelloServant());
}
}
RMI server dùng registry làm nơi lưu trữ các object. Khi một object mới được tạo ra thì server sẽ lưu nó vào registry bằng cách gọi đến method rebind (hoặc bind). Mỗi object sẽ có 1 tên riêng biệt, gọi là bind name (trong trường hợp này, object HelloServant có bind name là “hello”).
- Client.java:
package example.rmi.server;
import java.rmi.Naming;
public class Client {
public static void main(String[] args) throws Exception {
HelloService svc = (HelloService) Naming.lookup("rmi://localhost:2099/hello");
System.out.println("--- " + svc.echo("Hello server! From NoSpaceAvailable"));
}
}
Ở phía client lúc này thực hiện lookup đến port 2099 với giao thức rmi://, ở đây ta cần tìm object “hello”. Sau khi tham chiếu đến remote object thành công thì thực hiện typecast, sau đó call method echo từ stub.
- HelloServant.java:
package example.rmi.server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloServant extends UnicastRemoteObject implements HelloService {
protected HelloServant() throws RemoteException {
super();
}
public String echo(String msg) throws RemoteException {
return "From server: " + msg;
}
}
Đây là implement hoàn chỉnh của class HelloServant.
- HelloService.java:
package example.rmi.server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloService extends Remote {
public String echo(String msg) throws RemoteException;
}
Cuối cùng thì là interface HelloService, dùng để làm stub và skeleton. Interface này phải được cả phía server và client biết. Ở phía client chỉ quan tâm đến parameters được gửi đến server và return value mà remote method call đó trả về nên mỗi interface là đủ. Tuy nhiên, ở server thì skeleton ngoài nhận remote method call ra thì còn phải gửi chúng đến cho object thật sự để thực thi, nên bắt buộc phía server cần phải có implementation của interface đó.
Lần lượt chạy server và client, ta thu được kết quả:
Việc client thực hiện request method call đến server và nhận được kết quả trả về chứng tỏ rằng hai bên phải trao đổi dữ liệu gì đó. Thử dùng wireshark bắt packet ở localhost như sau:
Thực hiện khởi động client. Sau khi client nhận được message trả về từ server thì ngừng capture network traffic. Đem file pcapng thu được đi phân tích bằng pyshark với script sau (có thể dùng wireshark để analyze cũng được):
import pyshark
RMI_PORT = "2009" # change if needed
hex_file = open("hex.rmi.dat", "a")
bin_file = open("bin.rmi.dat", "ab")
# test analyze a RMI serialize payload
rmi = pyshark.FileCapture("rmi.pcapng")
l = len([p for p in rmi])
for i in range(l):
try:
if hasattr(rmi[i], "tcp"):
data = ''.join(rmi[i].tcp.payload).replace(':', '')
if "aced" in data: # serialized data will contain this
bin_file.write(bytes.fromhex(data) + b'\n-------------------------------------\n')
hex_file.write(str(i) + ": " + data + "\n-------------------------------------\n")
except Exception as e:
continue
Kết quả thu được khi chạy file python trên:
Dễ dàng thấy được data trao đổi giữa server và client đều ở dạng serialized => phải có quá trình serialize và deserialize data ở đây.
Dựa vào documents và theo quan sát của mình về cách mà RMI server ở trên hoạt động thì ta có thể rút ra được sơ đồ sau:
Khi mà client thực hiện invoke method trên stub, các parameter được truyền vào stub method được đưa vào quá trình marshalling. Ở quá trình này, chúng được đóng gói lại và gửi đến server thông qua method marshalValue():
Với các kiểu dữ liệu nguyên thủy như int, boolean, byte, … thì chúng đều được cast về object tương ứng. Nếu không thì sẽ thực hiện serialize bằng writeObject().
Tương tự như vậy, khi server nhận được data từ client, nó thực hiện quá trình unmarshalling với method unmarshalValue(). Khi unmarshal, với các param kiểu dữ liệu nguyên thủy thì method đơn giản là tái tạo lại giá trị ban đầu bằng cách copy. Tuy nhiên nếu param ở dạng object thì chúng sẽ được deserialize với readObject():
- RMI - JRMP protocol
- JRMP là giao thức được sử dụng trong các RMI server. Cấu trúc của nó có dạng như sau:
Khi client invoke một remote object thông qua RMI, về căn bản stub ở phía client sẽ gửi một byte stream đến skeleton ở phía server. Các giá trị đầu tiên trong bytestream sẽ lần lượt đại diện cho magic bytes, version, protocol, operation, ObjIDm, num, hash. Phía server sẽ đọc các byte này rồi dựa vào đó để thực hiện invoke method được client yêu cầu. Mỗi RMI server sẽ giữ một object UnicastServerRef, map đến một class mà chứa remote method:
Để setup debug thì mình có đặt breakpoint ở các class/method sau trong package sun.rmi.transport
:
class
sun.rmi.transport.ConnectionInputStream
, methodreadID
class
sun.rmi.transport.StreamRemoteCall
, methodexecuteCall
vàreadObject
method callclass
sun.rmi.server.ObjID
, methodread
class
sun.rmi.transport.tcp.TCPTransport
, methodhandleMessages
class
sun.rmi.transport.tcp.TCPConnection
,read
method call
Khi thực hiện xong setup thì bật mode debug ở phía server, còn phía client thì thực hiện method call lên stub. Khi này breakpoint đầu tiên sáng lên. Quan sát debug console ta thấy rằng method handleMessage
được gọi tới đầu tiên. Ở tham số Connection var1, ta có thể đọc được data mà client gửi đến:
Khi thực hiện convert các mã ascii trên, ta được string sau (ignore giá trị âm). Có thể thấy chúng gồm magic bytes của JRMI protocol, địa chỉ IP của server và tên của remote object (hello):
Tiếp theo server đọc object id từ client call. Ở đây object id là 0:
Có 1 điều thú vị là ở dòng code thứ 79, server thực hiện kiểm tra xem object id có bằng 2 hay không. Object id bằng 2 tức là garbage collector, mà garbage collector thì chỉ xuất hiện khi đã thực hiện xong remote call:
Object id được server dựa vào để thực hiện call dispatch lên object. Hash value của object được dùng để tham chiếu đến object cần call:
Tuy nhiên nếu đào sâu hơn thì ta sẽ thấy thêm 1 mã hash nữa, đó là hash của method cần call. Đây chính là mã hash bên trong trường hash của byte stream mà client gửi đi:
Các tham số cần thiết cho remote call được marshal lại. Nhưng trước khi unmarshal, server thực hiện deserialize để lấy các tham số:
Lấy tham số xong thì lại đem đi marshal :D
Sau đó thì tham số được quăng cho remote object xử lí rồi trả lại kết quả cho stub:
Có thể thấy rằng trong quá trình trên thì có gọi readObject, đây chính là sink trong các RMI exploit chain.
Tóm lại:
Client gửi một byte stream theo giao thức JRMI đến server. Giao thức này có 7 fields trong cấu trúc của nó, tuy nhiên quan trọng nhất chính là ObjID, port và method call’s hash.
DGC là garbage collector của server với ObjID và method hash biết trước, dùng để dọn bộ nhớ sau khi thực thi xong remote call của client.
(hết phần 1)
Tham khảo
Subscribe to my newsletter
Read articles from Le Quoc Cuong directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
