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
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
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
-kargument to ncat binary
- Client – Agent which is responsible for receiving the command from the server and executing it on the target machine
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.
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
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.
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.
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
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
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
In this case, we are using
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"
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.
The complete code of the server component can be found in
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
In the setup method, it is connecting to the TCP socket, opening the network stream and pass it to the
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.
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
So if server requests for execution of normal external commands such as
id it can be executed in the else block of the above block. The complete code of the client can be found in