Files
HackBrowserData/utils/injector/reflective_windows_test.go
T
slimwang 8c89ac72dd fix(windows): harden the ABE payload injection lifecycle (#638)
* fix(windows): harden the ABE payload injection lifecycle

The browser main thread is never resumed, so no window appears and the temp user-data-dir is unnecessary. Payload memory is RW then RX, with an explicit FlushInstructionCache before the remote thread executes it.

* test(windows): use US spelling in offset test

* fix(windows): validate bootstrap parameter encoding
2026-08-25 15:53:57 +08:00

113 lines
3.6 KiB
Go

//go:build windows && amd64
package injector
import (
"encoding/binary"
"testing"
"github.com/moond4rk/hackbrowserdata/crypto/windows/abe_native/bootstrap"
"github.com/moond4rk/hackbrowserdata/utils/winapi"
)
// The C payload reads BootstrapParams by field offset while Go writes it as a flat byte
// slice, so this pins the encoding to the cgo-generated offsets rather than to field order.
func TestEncodeBootstrapParams(t *testing.T) {
const scratchBase = 0xDEADBEEF000
params, err := encodeBootstrapParams(scratchBase)
if err != nil {
t.Fatalf("encodeBootstrapParams: %v", err)
}
if len(params) != bootstrap.ParamsSize {
t.Fatalf("params size = %d, want %d (C sizeof(BootstrapParams))", len(params), bootstrap.ParamsSize)
}
cases := []struct {
name string
offset int
want uintptr
}{
{"scratch_base", bootstrap.ParamScratchBase, scratchBase},
{"LoadLibraryA", bootstrap.ParamLoadLibraryA, winapi.AddrLoadLibraryA()},
{"GetProcAddress", bootstrap.ParamGetProcAddress, winapi.AddrGetProcAddress()},
{"VirtualAlloc", bootstrap.ParamVirtualAlloc, winapi.AddrVirtualAlloc()},
{"VirtualProtect", bootstrap.ParamVirtualProtect, winapi.AddrVirtualProtect()},
{"NtFlushInstructionCache", bootstrap.ParamNtFlushIC, winapi.AddrNtFlushInstructionCache()},
}
for _, tc := range cases {
got := binary.LittleEndian.Uint64(params[tc.offset : tc.offset+8])
if got != uint64(tc.want) {
t.Errorf("%s at offset 0x%x = %#x, want %#x", tc.name, tc.offset, got, tc.want)
}
if tc.want == 0 {
t.Errorf("%s resolved to a null address", tc.name)
}
}
}
func TestPutBootstrapParamBounds(t *testing.T) {
const want = uintptr(0x0123456789ABCDEF)
tests := []struct {
name string
size int
offset int
wantErr bool
}{
{name: "last valid field", size: 16, offset: 8},
{name: "negative offset", size: 8, offset: -1, wantErr: true},
{name: "short block", size: 7, offset: 0, wantErr: true},
{name: "one past end", size: 8, offset: 1, wantErr: true},
{name: "maximum offset", size: 8, offset: int(^uint(0) >> 1), wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := make([]byte, tt.size)
err := putBootstrapParam(params, tt.offset, want)
if tt.wantErr {
if err == nil {
t.Fatal("putBootstrapParam returned nil error")
}
return
}
if err != nil {
t.Fatalf("putBootstrapParam: %v", err)
}
if got := binary.LittleEndian.Uint64(params[tt.offset:]); got != uint64(want) {
t.Errorf("encoded value = %#x, want %#x", got, want)
}
})
}
}
// Every field must land inside the struct and none may overlap — a stale offset constant
// would otherwise corrupt a neighboring pointer instead of failing loudly.
func TestBootstrapParamOffsetsAreDisjoint(t *testing.T) {
offsets := []int{
bootstrap.ParamScratchBase,
bootstrap.ParamLoadLibraryA,
bootstrap.ParamGetProcAddress,
bootstrap.ParamVirtualAlloc,
bootstrap.ParamVirtualProtect,
bootstrap.ParamNtFlushIC,
}
seen := make(map[int]bool, len(offsets))
for _, off := range offsets {
if off%8 != 0 {
t.Errorf("offset 0x%x is not 8-byte aligned", off)
}
if off < 0 || off > bootstrap.ParamsSize-bootstrapParamFieldSize {
t.Errorf("offset 0x%x overruns params block of %d bytes", off, bootstrap.ParamsSize)
}
if seen[off] {
t.Errorf("offset 0x%x used by more than one field", off)
}
seen[off] = true
}
if len(seen)*bootstrapParamFieldSize != bootstrap.ParamsSize {
t.Errorf("%d fields cover %d bytes, but C sizeof(BootstrapParams) is %d",
len(seen), len(seen)*bootstrapParamFieldSize, bootstrap.ParamsSize)
}
}