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
-116
View File
@@ -1,116 +0,0 @@
//go:build windows
package browserutil
import (
"errors"
"fmt"
"os"
"path/filepath"
"golang.org/x/sys/windows/registry"
)
var ErrExecutableNotFound = errors.New("browser executable not found")
type browserLocation struct {
exeName string
fallbacks []string
}
var browserLocations = map[string]browserLocation{
"chrome": {
exeName: "chrome.exe",
fallbacks: []string{
`%ProgramFiles%\Google\Chrome\Application\chrome.exe`,
`%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe`,
`%LocalAppData%\Google\Chrome\Application\chrome.exe`,
},
},
"chrome-beta": {
exeName: "chrome.exe",
fallbacks: []string{
`%ProgramFiles%\Google\Chrome Beta\Application\chrome.exe`,
`%ProgramFiles(x86)%\Google\Chrome Beta\Application\chrome.exe`,
`%LocalAppData%\Google\Chrome Beta\Application\chrome.exe`,
},
},
"edge": {
exeName: "msedge.exe",
fallbacks: []string{
`%ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe`,
`%ProgramFiles%\Microsoft\Edge\Application\msedge.exe`,
},
},
"brave": {
exeName: "brave.exe",
fallbacks: []string{
`%ProgramFiles%\BraveSoftware\Brave-Browser\Application\brave.exe`,
`%ProgramFiles(x86)%\BraveSoftware\Brave-Browser\Application\brave.exe`,
`%LocalAppData%\BraveSoftware\Brave-Browser\Application\brave.exe`,
},
},
"coccoc": {
exeName: "browser.exe",
fallbacks: []string{
`%ProgramFiles%\CocCoc\Browser\Application\browser.exe`,
`%ProgramFiles(x86)%\CocCoc\Browser\Application\browser.exe`,
`%LocalAppData%\CocCoc\Browser\Application\browser.exe`,
},
},
}
func ExecutablePath(browserKey string) (string, error) {
loc, ok := browserLocations[browserKey]
if !ok {
return "", fmt.Errorf("%w: %q (no lookup entry)", ErrExecutableNotFound, browserKey)
}
if p, err := appPathsLookup(loc.exeName, registry.LOCAL_MACHINE); err == nil {
return p, nil
}
if p, err := appPathsLookup(loc.exeName, registry.CURRENT_USER); err == nil {
return p, nil
}
for _, candidate := range loc.fallbacks {
expanded := os.ExpandEnv(candidate)
if fileExists(expanded) {
return expanded, nil
}
}
return "", fmt.Errorf("%w: %q (registry miss and no fallback match)",
ErrExecutableNotFound, browserKey)
}
func appPathsLookup(exeName string, root registry.Key) (string, error) {
sub := `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\` + exeName
k, err := registry.OpenKey(root, sub, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer k.Close()
v, _, err := k.GetStringValue("")
if err != nil {
return "", err
}
v = unquote(v)
if !fileExists(v) {
return "", fmt.Errorf("registry path does not exist: %s", v)
}
return filepath.Clean(v), nil
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func unquote(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
+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
}
+49
View File
@@ -0,0 +1,49 @@
//go:build windows
package winapi
import (
"fmt"
"unsafe"
)
var (
procCryptUnprotectData = Crypt32.NewProc("CryptUnprotectData")
procLocalFree = Kernel32.NewProc("LocalFree")
)
// dataBlob mirrors the DPAPI DATA_BLOB struct.
type dataBlob struct {
cbData uint32
pbData *byte
}
func newBlob(d []byte) *dataBlob {
if len(d) == 0 {
return &dataBlob{}
}
return &dataBlob{pbData: &d[0], cbData: uint32(len(d))}
}
func (b *dataBlob) bytes() []byte {
d := make([]byte, b.cbData)
copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])
return d
}
// DecryptDPAPI decrypts a DPAPI-protected blob using the current user's
// master key. It is the Windows counterpart to macOS/Linux os_crypt
// fallbacks and is called by crypto.DecryptDPAPI.
func DecryptDPAPI(ciphertext []byte) ([]byte, error) {
var out dataBlob
r, _, err := procCryptUnprotectData.Call(
uintptr(unsafe.Pointer(newBlob(ciphertext))),
0, 0, 0, 0, 0,
uintptr(unsafe.Pointer(&out)),
)
if r == 0 {
return nil, fmt.Errorf("CryptUnprotectData: %w", err)
}
defer procLocalFree.Call(uintptr(unsafe.Pointer(out.pbData)))
return out.bytes(), nil
}
+113
View File
@@ -0,0 +1,113 @@
//go:build windows
package winapi
import (
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
// Call-style procs used by the typed wrappers below.
var (
procVirtualAllocEx = Kernel32.NewProc("VirtualAllocEx")
procCreateRemoteThread = Kernel32.NewProc("CreateRemoteThread")
// K32EnumProcesses is the kernel32-embedded twin of psapi!EnumProcesses
// introduced in Windows 7 — using it lets us skip the psapi.dll handle.
procK32EnumProcesses = Kernel32.NewProc("K32EnumProcesses")
procQueryFullProcessImageName = Kernel32.NewProc("QueryFullProcessImageNameW")
)
// Address-style procs. The injector reads their raw addresses via .Addr()
// and patches them into the reflective loader's DOS stub. We never Call
// these from our own process.
var (
procLoadLibraryA = Kernel32.NewProc("LoadLibraryA")
procGetProcAddress = Kernel32.NewProc("GetProcAddress")
procVirtualAlloc = Kernel32.NewProc("VirtualAlloc")
procVirtualProtect = Kernel32.NewProc("VirtualProtect")
procNtFlushIC = Ntdll.NewProc("NtFlushInstructionCache")
)
// VirtualAllocEx wraps kernel32!VirtualAllocEx. Returns the allocated
// base address in the target process, or an error surfacing Win32
// errno-0 explicitly via CallBoolErr.
func VirtualAllocEx(proc windows.Handle, size uintptr, flAllocType, flProtect uint32) (uintptr, error) {
return CallBoolErr(procVirtualAllocEx,
uintptr(proc), 0, size,
uintptr(flAllocType), uintptr(flProtect),
)
}
// CreateRemoteThread wraps kernel32!CreateRemoteThread. Returns the new
// thread's handle, which the caller must CloseHandle.
func CreateRemoteThread(proc windows.Handle, startAddr, param uintptr) (windows.Handle, error) {
h, err := CallBoolErr(procCreateRemoteThread,
uintptr(proc), 0, 0,
startAddr, param, 0, 0,
)
if err != nil {
return 0, err
}
return windows.Handle(h), nil
}
// Addr* functions expose raw function pointers for the reflective
// loader's DOS-stub patching. KnownDlls + session-consistent ASLR
// guarantees these addresses are valid in every process spawned in
// the same boot session.
func AddrLoadLibraryA() uintptr { return procLoadLibraryA.Addr() }
func AddrGetProcAddress() uintptr { return procGetProcAddress.Addr() }
func AddrVirtualAlloc() uintptr { return procVirtualAlloc.Addr() }
func AddrVirtualProtect() uintptr { return procVirtualProtect.Addr() }
func AddrNtFlushInstructionCache() uintptr { return procNtFlushIC.Addr() }
// EnumProcesses returns all PIDs currently visible to the caller. Backed
// by kernel32!K32EnumProcesses (available on Windows 7+), so we do not
// need a separate psapi.dll handle. The buffer doubles on overflow up to
// a 1M-entry safety cap.
func EnumProcesses() ([]uint32, error) {
size := uint32(1024)
for {
pids := make([]uint32, size)
var bytesReturned uint32
r, _, err := procK32EnumProcesses.Call(
uintptr(unsafe.Pointer(&pids[0])),
uintptr(size*4),
uintptr(unsafe.Pointer(&bytesReturned)),
)
if r == 0 {
return nil, fmt.Errorf("K32EnumProcesses: %w", err)
}
n := int(bytesReturned / 4)
// A completely filled buffer means we may have truncated — grow and retry.
if n < int(size) {
return pids[:n], nil
}
size *= 2
if size > 1<<20 {
return nil, fmt.Errorf("EnumProcesses: PID buffer exceeded 1M entries")
}
}
}
// QueryFullProcessImageName returns the full file-system path of the
// executable backing the given process handle. Open the handle with
// PROCESS_QUERY_LIMITED_INFORMATION (available to non-admin callers).
func QueryFullProcessImageName(h windows.Handle) (string, error) {
buf := make([]uint16, windows.MAX_PATH)
size := uint32(len(buf))
r, _, err := procQueryFullProcessImageName.Call(
uintptr(h),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if r == 0 {
return "", fmt.Errorf("QueryFullProcessImageNameW: %w", err)
}
return windows.UTF16ToString(buf[:size]), nil
}
+215
View File
@@ -0,0 +1,215 @@
//go:build windows
package winapi
import (
"fmt"
"strings"
"unsafe"
"golang.org/x/sys/windows"
)
const (
// systemExtendedHandleInformation is the information class for
// NtQuerySystemInformation that returns SYSTEM_HANDLE_INFORMATION_EX.
// This is the 64-bit safe version (class 64) — UniqueProcessId is
// ULONG_PTR instead of USHORT, avoiding PID truncation on 64-bit Windows.
systemExtendedHandleInformation = 64
statusInfoLengthMismatch = 0xC0000004
fileMapRead = 0x0004
pageReadonly = 0x02
// FileTypeDisk is the GetFileType return value for a normal disk file.
FileTypeDisk uint32 = 0x0001
maxHandleBufferSize = 256 * 1024 * 1024
initialHandleBuffer = 4 * 1024 * 1024
)
var (
procNtQuerySystemInformation = Ntdll.NewProc("NtQuerySystemInformation")
procGetFileType = Kernel32.NewProc("GetFileType")
procGetFinalPathNameByHandleW = Kernel32.NewProc("GetFinalPathNameByHandleW")
procCreateFileMappingW = Kernel32.NewProc("CreateFileMappingW")
procMapViewOfFile = Kernel32.NewProc("MapViewOfFile")
procUnmapViewOfFile = Kernel32.NewProc("UnmapViewOfFile")
procGetFileSizeEx = Kernel32.NewProc("GetFileSizeEx")
)
// SystemHandleEntry mirrors SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, the extended
// entry returned by SystemExtendedHandleInformation (class 64).
//
// Layout on 64-bit Windows (40 bytes):
//
// PVOID Object;
// ULONG_PTR UniqueProcessId;
// ULONG_PTR HandleValue;
// ULONG GrantedAccess;
// USHORT CreatorBackTraceIndex;
// USHORT ObjectTypeIndex;
// ULONG HandleAttributes;
// ULONG Reserved;
type SystemHandleEntry struct {
Object uintptr
UniqueProcessID uintptr
HandleValue uintptr
GrantedAccess uint32
CreatorBackTraceIndex uint16
ObjectTypeIndex uint16
HandleAttributes uint32
Reserved uint32
}
// QuerySystemHandles enumerates all open handles system-wide via
// NtQuerySystemInformation(SystemExtendedHandleInformation). The buffer
// size grows on STATUS_INFO_LENGTH_MISMATCH until it succeeds or exceeds
// the safety cap.
func QuerySystemHandles() ([]SystemHandleEntry, error) {
bufSize := uint32(initialHandleBuffer)
for {
buf := make([]byte, bufSize)
var returnLength uint32
ret, _, _ := procNtQuerySystemInformation.Call(
systemExtendedHandleInformation,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(bufSize),
uintptr(unsafe.Pointer(&returnLength)),
)
if ret == statusInfoLengthMismatch {
bufSize *= 2
if bufSize > maxHandleBufferSize {
return nil, fmt.Errorf("handle info buffer exceeded %d bytes", maxHandleBufferSize)
}
continue
}
if ret != 0 {
return nil, fmt.Errorf("NtQuerySystemInformation returned 0x%x", ret)
}
// Header on 64-bit: NumberOfHandles (ULONG_PTR) + Reserved (ULONG_PTR) = 16 bytes.
numberOfHandles := *(*uintptr)(unsafe.Pointer(&buf[0]))
if numberOfHandles == 0 {
return nil, nil
}
count := int(numberOfHandles)
const headerSize = unsafe.Sizeof(uintptr(0)) * 2
entrySize := unsafe.Sizeof(SystemHandleEntry{})
required := headerSize + uintptr(count)*entrySize
if required > uintptr(len(buf)) {
return nil, fmt.Errorf("buffer too small: need %d, have %d", required, len(buf))
}
entries := make([]SystemHandleEntry, count)
for i := 0; i < count; i++ {
src := unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + headerSize + uintptr(i)*entrySize)
entries[i] = *(*SystemHandleEntry)(src)
}
return entries, nil
}
}
// GetFileType returns the Windows FileType for h (e.g., FileTypeDisk).
func GetFileType(h windows.Handle) uint32 {
t, _, _ := procGetFileType.Call(uintptr(h))
return uint32(t)
}
// GetFileSizeEx returns the size of the file referenced by h.
func GetFileSizeEx(h windows.Handle) (int64, error) {
var sz int64
r, _, err := procGetFileSizeEx.Call(uintptr(h), uintptr(unsafe.Pointer(&sz)))
if r == 0 {
return 0, fmt.Errorf("GetFileSizeEx: %w", err)
}
return sz, nil
}
// ExpandEnvString is the Go-friendly wrapper around
// kernel32!ExpandEnvironmentStringsW. Use it when you need to resolve
// Windows-style %VAR% placeholders — Go's stdlib os.ExpandEnv only
// recognizes Unix-style $VAR / ${VAR} and leaves %VAR% untouched.
func ExpandEnvString(s string) (string, error) {
src, err := windows.UTF16PtrFromString(s)
if err != nil {
return "", fmt.Errorf("ExpandEnvString: %w", err)
}
// 4 KB of UTF-16 easily covers MAX_PATH-bounded install locations.
buf := make([]uint16, 4096)
n, err := windows.ExpandEnvironmentStrings(src, &buf[0], uint32(len(buf)))
if n == 0 {
return "", fmt.Errorf("ExpandEnvironmentStringsW: %w", err)
}
if int(n) > len(buf) {
// Buffer was too small — retry with exact size.
buf = make([]uint16, n)
n, err = windows.ExpandEnvironmentStrings(src, &buf[0], uint32(len(buf)))
if n == 0 {
return "", fmt.Errorf("ExpandEnvironmentStringsW: %w", err)
}
}
return windows.UTF16ToString(buf[:n]), nil
}
// GetFinalPathName returns the normalized file path for h, with the
// \\?\ prefix stripped.
func GetFinalPathName(h windows.Handle) (string, error) {
size := 512
for {
buf := make([]uint16, size)
n, _, err := procGetFinalPathNameByHandleW.Call(
uintptr(h),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)),
0, // FILE_NAME_NORMALIZED
)
if n == 0 {
return "", fmt.Errorf("GetFinalPathNameByHandle: %w", err)
}
if int(n) > len(buf) {
size = int(n)
continue
}
path := windows.UTF16ToString(buf[:n])
return strings.TrimPrefix(path, `\\?\`), nil
}
}
// MapFile creates a read-only file mapping over h, copies the first
// size bytes into a Go-owned slice, and releases the mapping. Reads go
// through the OS kernel's file cache, which includes SQLite WAL data
// that has not yet been checkpointed into the main file.
func MapFile(h windows.Handle, size int) ([]byte, error) {
mapping, _, err := procCreateFileMappingW.Call(
uintptr(h),
0, pageReadonly,
0, 0, 0,
)
if mapping == 0 {
return nil, fmt.Errorf("CreateFileMapping: %w", err)
}
defer windows.CloseHandle(windows.Handle(mapping))
viewPtr, _, err := procMapViewOfFile.Call(
mapping, fileMapRead,
0, 0, 0,
)
if viewPtr == 0 {
return nil, fmt.Errorf("MapViewOfFile: %w", err)
}
defer procUnmapViewOfFile.Call(viewPtr)
// viewPtr is a valid pointer from MapViewOfFile syscall.
// go vet flags this as "possible misuse of unsafe.Pointer" but it's
// correct usage for Windows memory-mapped I/O.
data := make([]byte, size)
copy(data, (*[1 << 30]byte)(unsafe.Pointer(viewPtr))[:size]) //nolint:govet
return data, nil
}
+45
View File
@@ -0,0 +1,45 @@
//go:build windows
// Package winapi centralizes low-level Windows API access used across
// HackBrowserData. It exposes typed wrappers around specific syscalls
// that golang.org/x/sys/windows does not cover, plus shared LazyDLL
// handles and a small error-handling helper.
//
// Callers: utils/injector, filemanager, crypto. Higher-level Windows
// browser utilities live in utils/winutil.
package winapi
import (
"errors"
"fmt"
"syscall"
"golang.org/x/sys/windows"
)
// Package-level LazyDLL handles. Declaring them once here avoids the
// NewLazySystemDLL boilerplate previously spread across injector,
// filemanager, and crypto.
var (
Kernel32 = windows.NewLazySystemDLL("kernel32.dll")
Ntdll = windows.NewLazySystemDLL("ntdll.dll")
Crypt32 = windows.NewLazySystemDLL("crypt32.dll")
)
// 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 a
// misleading "operation completed successfully" message. errors.As is
// used instead of 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
}
+101
View File
@@ -0,0 +1,101 @@
//go:build windows
// Package winutil provides high-level Windows utilities for HackBrowserData,
// built on the low-level syscall wrappers in utils/winapi.
//
// It currently covers:
// - Browser executable resolution via registry App Paths + install-path
// fallbacks (browser_path_windows.go).
// - A single source of truth for Windows-side browser metadata: executable
// name, install fallbacks, and ABE dispatch kind (browser_meta_windows.go).
//
// The C-side counterpart — CLSID / IID / vtable-slot bytes consumed by the
// reflective payload — lives in crypto/windows/abe_native/com_iid.c and
// must stay separate: the payload runs inside the injected browser process
// with no Go runtime.
package winutil
// ABEKind selects the App-Bound Encryption dispatch path used by the injected
// payload for this browser. DPAPI-only browsers (classic v10/v11) use ABENone;
// v20-capable Chromium forks pick a vtable slot based on which IElevator
// flavor their elevation_service exposes.
type ABEKind int
const (
// ABENone means this browser has no ABE path — the key retriever chain
// falls through to DPAPI for v10/v11.
ABENone ABEKind = iota
// ABEChromeBase is IElevator slot 5 (Chrome, Brave, CocCoc).
ABEChromeBase
// ABEEdge is IElevator slot 8 (Edge; prepends 3 extra interface methods).
ABEEdge
// ABEAvast is IElevator slot 13 (Avast; extended IElevator).
ABEAvast
)
// Entry is the per-browser Windows metadata record.
//
// Key must match browser.BrowserConfig.Storage so retrievers and path
// resolvers share a single lookup identifier. CLSID/IID bytes are *not*
// stored here; see the package doc for why.
type Entry struct {
Key string
ExeName string
InstallFallbacks []string
ABE ABEKind
}
// Table is the authoritative Go-side map of Windows browser metadata.
// Adding a new Chromium fork on the Go side is a single-entry edit here.
// The corresponding C-side CLSID/IID table lives in com_iid.c.
var Table = map[string]Entry{
"chrome": {
Key: "chrome",
ExeName: "chrome.exe",
InstallFallbacks: []string{
`%ProgramFiles%\Google\Chrome\Application\chrome.exe`,
`%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe`,
`%LocalAppData%\Google\Chrome\Application\chrome.exe`,
},
ABE: ABEChromeBase,
},
"chrome-beta": {
Key: "chrome-beta",
ExeName: "chrome.exe",
InstallFallbacks: []string{
`%ProgramFiles%\Google\Chrome Beta\Application\chrome.exe`,
`%ProgramFiles(x86)%\Google\Chrome Beta\Application\chrome.exe`,
`%LocalAppData%\Google\Chrome Beta\Application\chrome.exe`,
},
ABE: ABEChromeBase,
},
"edge": {
Key: "edge",
ExeName: "msedge.exe",
InstallFallbacks: []string{
`%ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe`,
`%ProgramFiles%\Microsoft\Edge\Application\msedge.exe`,
},
ABE: ABEEdge,
},
"brave": {
Key: "brave",
ExeName: "brave.exe",
InstallFallbacks: []string{
`%ProgramFiles%\BraveSoftware\Brave-Browser\Application\brave.exe`,
`%ProgramFiles(x86)%\BraveSoftware\Brave-Browser\Application\brave.exe`,
`%LocalAppData%\BraveSoftware\Brave-Browser\Application\brave.exe`,
},
ABE: ABEChromeBase,
},
"coccoc": {
Key: "coccoc",
ExeName: "browser.exe",
InstallFallbacks: []string{
`%ProgramFiles%\CocCoc\Browser\Application\browser.exe`,
`%ProgramFiles(x86)%\CocCoc\Browser\Application\browser.exe`,
`%LocalAppData%\CocCoc\Browser\Application\browser.exe`,
},
ABE: ABEChromeBase,
},
}
+129
View File
@@ -0,0 +1,129 @@
//go:build windows
package winutil
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
"github.com/moond4rk/hackbrowserdata/utils/winapi"
)
// ErrExecutableNotFound is returned when a browser's executable cannot be
// located via registry App Paths or any install-location fallback.
var ErrExecutableNotFound = errors.New("browser executable not found")
// ExecutablePath resolves a browser's .exe with a 4-tier search:
// 1. Registry App Paths in HKLM
// 2. Registry App Paths in HKCU
// 3. Running-process probe — scan EnumProcesses for a match by exe name
// and return the owner's QueryFullProcessImageName. Picks up portable
// builds and non-standard installs that never wrote to App Paths.
// 4. Hard-coded InstallFallbacks from Table (last resort when the browser
// is not running and the registry is missing the entry).
//
// browserKey must match an Entry in Table; keys align with
// browser.BrowserConfig.Storage.
func ExecutablePath(browserKey string) (string, error) {
entry, ok := Table[browserKey]
if !ok {
return "", fmt.Errorf("%w: %q (no lookup entry)", ErrExecutableNotFound, browserKey)
}
if p, err := appPathsLookup(entry.ExeName, registry.LOCAL_MACHINE); err == nil {
return p, nil
}
if p, err := appPathsLookup(entry.ExeName, registry.CURRENT_USER); err == nil {
return p, nil
}
if p := runningProcessPath(entry.ExeName); p != "" {
return p, nil
}
for _, candidate := range entry.InstallFallbacks {
// Use winapi.ExpandEnvString (kernel32!ExpandEnvironmentStringsW)
// rather than os.ExpandEnv: Go stdlib only understands Unix-style
// $VAR / ${VAR} and leaves Windows-style %VAR% untouched, which
// would make every fallback path fail to resolve. Verified on
// Windows 10 19044 + Go 1.20.14.
expanded, err := winapi.ExpandEnvString(candidate)
if err != nil {
continue
}
if fileExists(expanded) {
return expanded, nil
}
}
return "", fmt.Errorf("%w: %q (registry miss, no running process, no fallback match)",
ErrExecutableNotFound, browserKey)
}
// runningProcessPath scans live processes for one whose image filename
// matches exeName (case-insensitive) and returns the full path on the
// first hit. Errors are swallowed — this is a best-effort probe that
// yields to the hard-coded fallbacks if nothing matches.
func runningProcessPath(exeName string) string {
pids, err := winapi.EnumProcesses()
if err != nil {
return ""
}
for _, pid := range pids {
if pid == 0 {
continue
}
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
continue
}
path, err := winapi.QueryFullProcessImageName(h)
_ = windows.CloseHandle(h)
if err != nil || path == "" {
continue
}
// Match the leaf filename only — a substring match against the full
// path would accept "chrome_proxy.exe" when we asked for "chrome.exe".
if strings.EqualFold(filepath.Base(path), exeName) {
return path
}
}
return ""
}
func appPathsLookup(exeName string, root registry.Key) (string, error) {
sub := `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\` + exeName
k, err := registry.OpenKey(root, sub, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer k.Close()
v, _, err := k.GetStringValue("")
if err != nil {
return "", err
}
v = unquote(v)
if !fileExists(v) {
return "", fmt.Errorf("registry path does not exist: %s", v)
}
return filepath.Clean(v), nil
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func unquote(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}