Skip to content

Implementing a custom Command 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.
  3. Return Result/Error TLV: Send a result or error message TLV back through the named pipe.
  4. Return Success/Failure Status TLV: Send a success or failure status TLV through the named pipe.

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.

Example plugin SC code

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.

Upon establishing the connection, the command shellcode reads the incoming data according to this format.

namespace CommandEcho
{
    internal class Program
    {
        static void Main(string[] args)
        {
            /**
            * 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);

The data received should be a TLV (Type-Length-Value) object with a type value of 5. The following steps are performed to handle the received data:

  1. Parse the TLV Object: Extract the type, length, and value components from the TLV object.
  2. Verify the Type: Ensure the type is 5, as expected.
  3. Read the Content: Extract and read the content of the TLV object. This content is generated by the generateShellCode function of the Java plugin.
                TLV tlv = new TLV();
                if (!tlv.load(data))
                {
                    throw new Exception("Failed to load TLV data");
                }
                if(tlv.type != 5)
                {
                    throw new Exception("TLV type is not 5 but " + (int)tlv.type);
                }
                byte[] messageBytes = tlv.data;

Upon successful verification of the received data, the following actions are performed:

  1. Echo the Data: Return the exact bytes received back to the sender, as this operation functions as an echo command.
  2. Send Status Message: Transmit a status message indicating "successful" to confirm the successful handling of the data.
                //Return same bytes back as data (0x30 is TLV type for result data)
                tlv = new TLV(0x30, messageBytes);
                data = tlv.getFullBuffer();
                writer.Write(data.Length);
                writer.Write(data);
                writer.Flush();

                //Return the "successful" status message (0x33 is TLV type for successful command status)
                tlv = new TLV(0x33, new byte[0]);
                data = tlv.getFullBuffer();
                writer.Write(data.Length);
                writer.Write(data);

If an error occurs during the processing of the received data, the following actions are taken:

  1. Return Error Message: Send an error message similar to the original data received.
  2. Send Status Message: Transmit a status message indicating "error" to notify of the issue encountered.
                //Return exception error data (0x32 is TLV type for error data)
                byte[] exceptionBytes = Encoding.UTF8.GetBytes(e.Message);
                TLV tlv = new TLV(0x32, exceptionBytes);
                data = tlv.getFullBuffer();
                writer.Write(data.Length);
                writer.Write(data);
                writer.Flush();

                //Return the "error" status message(0x34 is TLV type for failed command status)
                tlv = new TLV(0x34, new byte[0]);
                data = tlv.getFullBuffer();
                writer.Write(data.Length);
                writer.Write(data);

Upon completion of the data processing and response handling, the following steps are performed to gracefully terminate the program:

  1. Flush the Stream: Ensure all buffered data is written to the stream.
  2. Wait for Pipe to Drain: Allow time for the named pipe to drain any remaining data.
  3. Additional Wait: Introduce a delay of 4 seconds as a precautionary measure.
  4. End the Program: Proceed to terminate the program after the above steps are completed.
1
2
3
4
5
6
7
8
            //Close stuff up and wait 4sec just in case something is still being processed
            writer.Flush();
            client.WaitForPipeDrain();
            Thread.Sleep(4 * 1000);
            client.Close();
        }
    }
}

The resulting executable is converted into shellcode using the Donut tool. This conversion is performed without applying compression or encryption, allowing the Tuoni server to randomize the pipe name as needed.

donut.exe -z 1 -e 1 -o CommandEcho.shellcode -i CommandEcho.exe