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