Background
In .Net, distributed computing can be done in a number of ways.
The most widely used way is to use web services, where a remote client would call a publicly exposed web method on a server where the web service is hosted. When using web services, the code of the service method must be physically on the server where the service is hosted. In this scenario the client talks to the server via a proxy class that represents the web service on the server.
Alternatively, we can use .Net remoting to execute code in a distributed environment. Typically, either TCP or HTTP channel is used to communicate object states between client and server. The main advantage of using remoting is that custom sinks can be utilized to incorporate sophisticated data manipulations before the data is sent and received. The amount of data transferred can also be shrunk to minimal if binary formatters are used and thus resulting in very little channel overhead as compared to SOAP messages that typically used in web services.
Object serialization transfers the state of an object, not the physical object itself however. For serialization to work, the object to be serialized needs to exist on both the client and the server. When the object gets serialized, the state of the object is transferred across the wire. And when the object is deserialized, the object on the server side gets created and then populated with the states from data (the serialized object) transferred from the client.
So, in both scenarios, whether using web services or remoting, the object that needs to be used remotely must reside on both ends (a proxy or the actual object).
Autonomous Remote Code Execution
Can we use just one copy (e.g. the client version of the object) of the object to achieve remote code execution in a distributed environment? As it turns out, we can do this using reflection. For a lack of a better name, I call this approach autonomous remote code execution.
The idea here is to create an object on the client side, and physically ship the object over to the server side and the server will execute whatever code that is send over.
I will demonstrate how this can be done using TCP socket. To make the discussion easier, we will use synchronized socket communication model here, the principals illustrated here apply also to asynchronized sockets.
The Client
The client code first creates a TCP socket and reads in whatever .Net application needs to be executed as binary bytes. Then the content is sent through the TCP socket.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
namespace SocketClient
{
class Program
{
public void Send()
{
TcpClient socketForClient;
try
{
socketForClient = new TcpClient("alpha", 10); //server name
}
catch {
Console.WriteLine("Failed to connect to server");
return;
}
NetworkStream networkStream = socketForClient.GetStream(); //Read in the program to be sent
FileStream fs = new FileStream("PrimeGen.exe", FileMode.Open);
BinaryReader br = new BinaryReader(fs);
byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
fs.Close();
br.Close();
StreamReader streamReader = new StreamReader(networkStream);
BinaryWriter streamWriter = new BinaryWriter(networkStream);
try {
string outputString;
outputString = streamReader.ReadLine();
Console.WriteLine(outputString);
streamWriter.Write(bin); streamWriter.Flush();
}
catch {
Console.WriteLine("Exception reading from Server");
}
networkStream.Close();
}
static void Main(string[] args) {
Program p = new Program();
p.Send();
}
}
}
The Server
On the server side, a socket is created and then is blocked listening for the incoming connections. When the client and the server establish the connection, we read in the data as an array of bytes which represent the actual application transferred from the client side.
We then can get the actual object back using reflection. Finally, we can execute the code by jumping to its entry point. In this particular example, it is a .Net program called PrimeGen.
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.IO;
namespace SocketServer {
class Program {
public void Receive() {
TcpListener tcpListener = new TcpListener(Dns.GetHostEntry("alpha").AddressList[0], 10);
tcpListener.Start(); Socket socketForClient = tcpListener.AcceptSocket();
if (socketForClient.Connected) {
Console.WriteLine("Client connected");
NetworkStream networkStream = new NetworkStream(socketForClient);
StreamWriter streamWriter = new StreamWriter(networkStream);
BinaryReader streamReader = new BinaryReader(networkStream);
string theString = "Sending"; streamWriter.WriteLine(theString);
Console.WriteLine(theString); streamWriter.Flush();
//Note, here I simplified it a bit. Since I know that the
//code is less than 16K, so I can just read everything in
//at once. If the code size can not be predetermined, or it
//is too large for a single transmittion, you will have to
//slice it up and reassemble the data at the server side.
byte[] bin = streamReader.ReadBytes(16384);
//load the assembly sent from the wire
Assembly a = Assembly.Load(bin); MethodInfo method = a.EntryPoint;
if (method != null) {
object[] obj = new object[1] {new string[] {}};
try {
method.Invoke(null, obj);
} catch (Exception e) {
Console.WriteLine(e.Message);
}
}
streamReader.Close();
networkStream.Close();
streamWriter.Close();
}
}
static void Main(string[] args) {
Program p = new Program(); p.Receive();
}
}
}
Client Server in Action
Here is a screen shot of the client and server in action:
Remote Execution Using Windows
Using this autonomous remote execution method, we can easily run the server anywhere by simply copy the server side executable. Just for fun, here is a screen shot of the server running on a FreeBSD server and the client is running Windows XP.
Remote Execution using FreeBSD
Final Thoughts
The autonomous remote code execution method mentioned here can be used in various situations. For instance, when conducting numerical analysis in a distributed fashion, it would typically require setting up the same code on all the computers that participate in the computation. To ensure that the all the computers are setup with the correct version of the code can be quite tedious. Further, whenever the computation task is changed, all the code needs to be re-deployed again across all the machines.