mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor(windows): split Windows code into winapi (#575)
This commit is contained in:
@@ -0,0 +1,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
|
||||
}
|
||||
Reference in New Issue
Block a user