● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.

Offline extractor and decryptor for SCCM/MECM Network Access Account (NAA) credentials and policy secrets from dead-disk WMI repositories and DPAPI master keys.

$ git clone https://shehzade.io/repos/naa-dump-offline.git Copy
Source ZIP (290.0 KB) Prebuilt Binaries
Code 15 Commits 3 Releases 5
naa-dump-offline / pkg / dpapi / masterkey.go
← Back to directory
265 lines · 6.9 KB
Raw
1package dpapi
2
3import (
4 "bytes"
5 "crypto/aes"
6 "crypto/cipher"
7 "crypto/des"
8 "crypto/hmac"
9 "crypto/sha1"
10 "crypto/sha512"
11 "encoding/binary"
12 "fmt"
13 "hash"
14 "os"
15 "path/filepath"
16
17 "github.com/shehzade/naa-dump-offline/pkg/registry"
18)
19
20type MasterKeyFile struct {
21 Version uint32
22 Unk1 uint32
23 Unk2 uint32
24 Guid string
25 Unknown uint32
26 Policy uint32
27 Flags uint32
28 MasterKeyLen uint64
29 BackupKeyLen uint64
30 CredHistLen uint64
31 DomainKeyLen uint64
32 MasterKey *MasterKey
33 BackupKey *MasterKey
34}
35
36type MasterKey struct {
37 Version uint32
38 Salt []byte
39 Rounds uint32
40 HashAlgo uint32
41 CryptAlgo uint32
42 Data []byte
43 DecryptedKey []byte
44}
45
46func ParseMasterKeyFile(raw []byte) (*MasterKeyFile, error) {
47 if len(raw) < 128 {
48 return nil, fmt.Errorf("master key file too short (%d bytes)", len(raw))
49 }
50
51 mkf := &MasterKeyFile{
52 Version: binary.LittleEndian.Uint32(raw[0:4]),
53 Unk1: binary.LittleEndian.Uint32(raw[4:8]),
54 Unk2: binary.LittleEndian.Uint32(raw[8:12]),
55 Guid: NormalizeGUID(DecodeUTF16LE(raw[12:84])),
56 Unknown: binary.LittleEndian.Uint32(raw[84:88]),
57 Policy: binary.LittleEndian.Uint32(raw[88:92]),
58 Flags: binary.LittleEndian.Uint32(raw[92:96]),
59 MasterKeyLen: binary.LittleEndian.Uint64(raw[96:104]),
60 BackupKeyLen: binary.LittleEndian.Uint64(raw[104:112]),
61 CredHistLen: binary.LittleEndian.Uint64(raw[112:120]),
62 DomainKeyLen: binary.LittleEndian.Uint64(raw[120:128]),
63 }
64
65 offset := 128
66 if mkf.MasterKeyLen > 0 && offset+int(mkf.MasterKeyLen) <= len(raw) {
67 mkData := raw[offset : offset+int(mkf.MasterKeyLen)]
68 mkf.MasterKey = parseMasterKey(mkData)
69 offset += int(mkf.MasterKeyLen)
70 }
71
72 if mkf.BackupKeyLen > 0 && offset+int(mkf.BackupKeyLen) <= len(raw) {
73 bkmkData := raw[offset : offset+int(mkf.BackupKeyLen)]
74 mkf.BackupKey = parseMasterKey(bkmkData)
75 }
76
77 return mkf, nil
78}
79
80func parseMasterKey(raw []byte) *MasterKey {
81 if len(raw) < 32 {
82 return nil
83 }
84 return &MasterKey{
85 Version: binary.LittleEndian.Uint32(raw[0:4]),
86 Salt: raw[4:20],
87 Rounds: binary.LittleEndian.Uint32(raw[20:24]),
88 HashAlgo: binary.LittleEndian.Uint32(raw[24:28]),
89 CryptAlgo: binary.LittleEndian.Uint32(raw[28:32]),
90 Data: raw[32:],
91 }
92}
93
94func (mk *MasterKey) Decrypt(key []byte) ([]byte, error) {
95 if mk == nil {
96 return nil, fmt.Errorf("nil master key")
97 }
98
99 var hashFunc func() hash.Hash
100 var hashKeyLen int
101 switch mk.HashAlgo {
102 case CALG_HMAC, CALG_SHA1:
103 hashFunc = sha1.New
104 hashKeyLen = 20
105 case CALG_SHA_512:
106 hashFunc = sha512.New
107 hashKeyLen = 64
108 default:
109 return nil, fmt.Errorf("unsupported hash algo: 0x%x", mk.HashAlgo)
110 }
111
112 var cryptKeyLen, ivLen int
113 switch mk.CryptAlgo {
114 case CALG_AES_256:
115 cryptKeyLen = 32
116 ivLen = 16
117 case CALG_3DES:
118 cryptKeyLen = 24
119 ivLen = 8
120 default:
121 return nil, fmt.Errorf("unsupported crypt algo: 0x%x", mk.CryptAlgo)
122 }
123
124 derivedBlob := DeriveKeyDPAPI(key, mk.Salt, cryptKeyLen+ivLen, int(mk.Rounds), hashFunc)
125 cryptKey := derivedBlob[:cryptKeyLen]
126 iv := derivedBlob[cryptKeyLen : cryptKeyLen+ivLen]
127
128 var cleartext []byte
129 switch mk.CryptAlgo {
130 case CALG_AES_256:
131 block, err := aes.NewCipher(cryptKey)
132 if err != nil {
133 return nil, err
134 }
135 if len(mk.Data)%aes.BlockSize != 0 {
136 return nil, fmt.Errorf("ciphertext length not multiple of AES block size")
137 }
138 mode := cipher.NewCBCDecrypter(block, iv)
139 cleartext = make([]byte, len(mk.Data))
140 mode.CryptBlocks(cleartext, mk.Data)
141 case CALG_3DES:
142 block, err := des.NewTripleDESCipher(cryptKey)
143 if err != nil {
144 return nil, err
145 }
146 if len(mk.Data)%des.BlockSize != 0 {
147 return nil, fmt.Errorf("ciphertext length not multiple of 3DES block size")
148 }
149 mode := cipher.NewCBCDecrypter(block, iv)
150 cleartext = make([]byte, len(mk.Data))
151 mode.CryptBlocks(cleartext, mk.Data)
152 }
153
154 if len(cleartext) < 16+hashKeyLen+64 {
155 return nil, fmt.Errorf("cleartext too short (%d)", len(cleartext))
156 }
157
158 decryptedKey := cleartext[len(cleartext)-64:]
159 hmacSalt := cleartext[:16]
160 hmacExpected := cleartext[16 : 16+hashKeyLen]
161
162 h1 := hmac.New(hashFunc, key)
163 h1.Write(hmacSalt)
164 hmacKey := h1.Sum(nil)
165
166 h2 := hmac.New(hashFunc, hmacKey)
167 h2.Write(decryptedKey)
168 hmacCalculated := h2.Sum(nil)
169
170 if !bytes.Equal(hmacCalculated[:hashKeyLen], hmacExpected) {
171 return nil, fmt.Errorf("HMAC verification failed")
172 }
173
174 mk.DecryptedKey = decryptedKey
175 return decryptedKey, nil
176}
177
178func (mkf *MasterKeyFile) DecryptWithSystemKeys(keys *registry.DPAPISystemKeys) ([]byte, string, error) {
179 if mkf.MasterKey != nil && len(keys.UserKey) > 0 {
180 if dec, err := mkf.MasterKey.Decrypt(keys.UserKey); err == nil {
181 return dec, "UserKey", nil
182 }
183 }
184
185 if mkf.MasterKey != nil && len(keys.MachineKey) > 0 {
186 if dec, err := mkf.MasterKey.Decrypt(keys.MachineKey); err == nil {
187 return dec, "MachineKey", nil
188 }
189 }
190
191 if mkf.BackupKey != nil && len(keys.UserKey) > 0 {
192 if dec, err := mkf.BackupKey.Decrypt(keys.UserKey); err == nil {
193 return dec, "BackupKey (UserKey)", nil
194 }
195 }
196
197 if mkf.BackupKey != nil && len(keys.MachineKey) > 0 {
198 if dec, err := mkf.BackupKey.Decrypt(keys.MachineKey); err == nil {
199 return dec, "BackupKey (MachineKey)", nil
200 }
201 }
202
203 return nil, "", fmt.Errorf("failed to decrypt master key with UserKey or MachineKey")
204}
205
206func LoadAndDecryptMasterKey(pathOrDir string, targetGUID string, keys *registry.DPAPISystemKeys) ([]byte, string, error) {
207 info, err := os.Stat(pathOrDir)
208 if err != nil {
209 return nil, "", fmt.Errorf("cannot access master key path: %w", err)
210 }
211
212 targetGUID = NormalizeGUID(targetGUID)
213
214 if !info.IsDir() {
215 data, err := os.ReadFile(pathOrDir)
216 if err != nil {
217 return nil, "", fmt.Errorf("failed to read master key file: %w", err)
218 }
219 mkf, err := ParseMasterKeyFile(data)
220 if err != nil {
221 return nil, "", fmt.Errorf("failed to parse master key file: %w", err)
222 }
223 decKey, keyType, err := mkf.DecryptWithSystemKeys(keys)
224 if err != nil {
225 return nil, "", fmt.Errorf("failed to decrypt master key %s: %w", pathOrDir, err)
226 }
227 return decKey, fmt.Sprintf("%s (%s)", pathOrDir, keyType), nil
228 }
229
230 entries, err := os.ReadDir(pathOrDir)
231 if err != nil {
232 return nil, "", fmt.Errorf("failed to read master key directory: %w", err)
233 }
234
235 var filesToTry []string
236 for _, e := range entries {
237 if !e.IsDir() {
238 filesToTry = append(filesToTry, filepath.Join(pathOrDir, e.Name()))
239 }
240 }
241
242 for _, filePath := range filesToTry {
243 data, err := os.ReadFile(filePath)
244 if err != nil {
245 continue
246 }
247
248 mkf, err := ParseMasterKeyFile(data)
249 if err != nil {
250 continue
251 }
252
253 fileBase := NormalizeGUID(filepath.Base(filePath))
254 if targetGUID != "" && mkf.Guid != targetGUID && fileBase != targetGUID {
255 continue
256 }
257
258 decKey, keyType, err := mkf.DecryptWithSystemKeys(keys)
259 if err == nil {
260 return decKey, fmt.Sprintf("%s (%s)", filePath, keyType), nil
261 }
262 }
263
264 return nil, "", fmt.Errorf("could not find or decrypt valid master key for GUID %s in %s", targetGUID, pathOrDir)
265}