Skip to content

Implementing a custom Listener Plugin Shellcode (SC)

When creating command plugin shellcode, you have two options:

  1. Write Native Shellcode: Develop the shellcode directly using assembly language and C.
  2. Convert Executable to Shellcode: Write the code in C/C++ or C# and convert the resulting executable to shellcode using tools like Donut.

In this example, we will use the second option and write the code in C#.

Shellcode main activities

The created shellcode must perform the following actions to facilitate data transfer between the command and the Tuoni server:

  1. Open a Named Pipe Connection: Establish a connection to the core agent using a named pipe. The pipe name should be "QQQWWWEEE." It can be overwritten in by plugin itself with name provided by Tuoni server core but does not have to.
  2. Receive Input TLV: Receive an input TLV (Type-Length-Value) message from the core agent through the named pipe. This is the configuration sent by the plugin part in Tuoni server.
  3. Relay Communication Between Agent and Listener on Tuoni Server: To facilitate secure and efficient relay communication between the agent and the listener operating on the Tuoni server, several methods are employed. These methods include requesting encrypted metadata and upstream data (command results) from the agent, as well as sending downstream data (commands) to the agent. The shellcode must incorporate these methods alongside the actual communication processes with the Tuoni server, which is managed by the server plugin component on the server side.

Info

For parsing TLV messages, we recommend using our simple TLV library, which can be found at our example plugin Github repository https://github.com/shell-dot/tuoni-example-plugins/blob/main/shellcodes/CommandEcho/TLV.cs.

Alternatively, you can implement your own TLV parsing logic.

Examples

Data packets exchanged between shellcode and agents

A connection is established to a named pipe to facilitate communication between the agent and its plugins. The data exchanged follows the specific format:

< 4 BYTE INTEGER >< DATA BLOB >
  • 4 BYTE INTEGER: Specifies the length of the subsequent data blob.
  • DATA BLOB: The actual data being transmitted.

When shellcode starts

Upon establishing the connection, the listener shellcode reads the incoming data and parses it as TLV structure

/**
* Creating connection to the named pipe listener in agent 
*    (QQQWWWEEE can be overwritten in by plugin itself with name provided by Tuoni server core)
**/

NamedPipeClientStream client = new NamedPipeClientStream(".", "QQQWWWEEE",
   PipeDirection.InOut, PipeOptions.Asynchronous);
client.Connect();

//Supporting objects for reading and writing to the pipe
BinaryReader reader = new BinaryReader(client);
BinaryWriter writer = new BinaryWriter(client);

//Lets read the data from the pipe (4 byte size of the data, then the data)
int len = reader.ReadInt32();
byte[] data = reader.ReadBytes(len);

TLV tlv = new TLV();
if (!tlv.load(data))
{
  throw new Exception("Failed to load TLV data");
}

Loaded TLV now contains listener configuration provided by the server plugin

Sequence number on some requests from shellcode to agent

Given that shellcode may request various data from the agent asynchronously, it is crucial to maintain accurate tracking of the agent's current responses. To achieve this, requests for metadata and upstream data include a sequence number. This sequence number is also present in the corresponding responses, enabling association of responses with their respective requests.

Asking metadata

The shellcode requests metadata bytes from the agent, which are received in encrypted form and subsequently decrypted by the server. Please note, this example code is not thread-safe and is provided solely for illustrative purposes.

//Put together TLV
TLV tlv = new TLV(0x21);  //GetMetadata type of request
tlv.addChild(new TLV(0x1, new byte[1] { 0x1 })); //0x1 means that it's a request
int seqNrKeep = seqNr++;  //Some general sequence nr counter so each request has unique sequence number
tlv.addChild(new TLV(0x2, BitConverter.GetBytes(seqNrKeep)));

//Send the data
byte[] data = tlv.getFullBuffer();
writer.Write(data.Length);
writer.Write(data);
writer.Flush();

//Get data back
int len = reader.ReadInt32();
byte[] result = reader.ReadBytes(len);
TLV tlv = new TLV();
if (!tlv.load(result))
{  
  throw new Exception("Failed to load TLV data");
}
if (tlv.type != 0x21)
{  
  throw new Exception("Wrong return type: " + tlv.type);
}
int resultSeqNr = tlv.getChild(0x2).getDataAsInt32();
if(seqNrKeep != resultSeqNr)
{
  throw new Exception("Wrong sequence nr. Should have been " + seqNrKeep + " but was " + resultSeqNr);
}
byte[] metadata = tlv.data;

Asking upstream data to send

The shellcode requests upstream data bytes from the agent. These bytes are received in encrypted form and are decrypted by the server for further processing. Please note, this example code is not thread-safe and is provided solely for illustrative purposes.

//Put together TLV
TLV tlv = new TLV(0x22);  //GetData type of request
tlv.addChild(new TLV(0x1, new byte[1] { 0x1 })); //0x1 means that it's a request
int seqNrKeep = seqNr++;  //Some general sequence nr counter so each request has unique sequence number
tlv.addChild(new TLV(0x2, BitConverter.GetBytes(seqNrKeep)));

//Send the data
byte[] data = tlv.getFullBuffer();
writer.Write(data.Length);
writer.Write(data);
writer.Flush();

//Get data back
int len = reader.ReadInt32();
byte[] result = reader.ReadBytes(len);
TLV tlv = new TLV();
if (!tlv.load(result))
{  
  throw new Exception("Failed to load TLV data");
}
if (tlv.type != 0x22)
{  
  throw new Exception("Wrong return type: " + tlv.type);
}
int resultSeqNr = tlv.getChild(0x2).getDataAsInt32();
if(seqNrKeep != resultSeqNr)
{
  throw new Exception("Wrong sequence nr. Should have been " + seqNrKeep + " but was " + resultSeqNr);
}
byte[] dataToServer = tlv.data;

Relay data from server to agent

The shellcode transmits data received from the server (which is relayed by the server's plugin component) to the agent.

1
2
3
4
5
TLV tlv = new TLV(0x23, dataFromServer);
byte[] data = tlv.getFullBuffer();
writer.Write(data.Length);
writer.Write(data);
writer.Flush();

No response is expected to this request.