From 1f9ca3792add25f01d4cc304c6dde674fef09b2e Mon Sep 17 00:00:00 2001 From: bbingz Date: Thu, 5 Mar 2026 21:49:00 +0800 Subject: [PATCH 1/3] feat: add macOS C memory key scanner Scans WeChat process memory for SQLCipher encryption keys using Mach VM API. Outputs all_keys.json compatible with decrypt_db.py. Build: cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation Usage: sudo ./find_all_keys_macos [pid] --- README.md | 1 + find_all_keys_macos.c | 293 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 find_all_keys_macos.c diff --git a/README.md b/README.md index 7b7582c..96f631f 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ python find_image_key.py | `find_image_key.py` | 从微信进程内存提取图片 AES 密钥 | | `find_image_key_monitor.py` | 持续监控版密钥提取(推荐) | | `latency_test.py` | 延迟测量诊断工具 | +| `find_all_keys_macos.c` | macOS 版内存密钥扫描器 (C, Mach VM API) | ## 技术细节 diff --git a/find_all_keys_macos.c b/find_all_keys_macos.c new file mode 100644 index 0000000..9a0471c --- /dev/null +++ b/find_all_keys_macos.c @@ -0,0 +1,293 @@ +/* + * find_all_keys_macos.c - macOS WeChat memory key scanner + * + * Scans WeChat process memory for SQLCipher encryption keys in the + * x'' format used by WeChat 4.x on macOS. + * + * Prerequisites: + * - WeChat must be ad-hoc signed (or SIP disabled) + * - Must run as root (sudo) + * + * Build: + * cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation + * + * Usage: + * sudo ./find_all_keys_macos [pid] + * If pid is omitted, automatically finds WeChat PID. + * + * Output: JSON file at ./all_keys.json (compatible with decrypt_db.py) + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_KEYS 256 +#define KEY_SIZE 32 +#define SALT_SIZE 16 +#define HEX_PATTERN_LEN 96 /* 64 hex (key) + 32 hex (salt) */ +#define CHUNK_SIZE (2 * 1024 * 1024) + +typedef struct { + char key_hex[65]; + char salt_hex[33]; + char full_pragma[100]; +} key_entry_t; + +static int is_hex_char(unsigned char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static pid_t find_wechat_pid(void) { + FILE *fp = popen("pgrep -x WeChat", "r"); + if (!fp) return -1; + char buf[64]; + pid_t pid = -1; + if (fgets(buf, sizeof(buf), fp)) + pid = atoi(buf); + pclose(fp); + return pid; +} + +/* Read DB salt (first 16 bytes) and return hex string */ +static int read_db_salt(const char *path, char *salt_hex_out) { + FILE *f = fopen(path, "rb"); + if (!f) return -1; + unsigned char header[16]; + if (fread(header, 1, 16, f) != 16) { fclose(f); return -1; } + fclose(f); + /* Check if unencrypted */ + if (memcmp(header, "SQLite format 3", 15) == 0) return -1; + for (int i = 0; i < 16; i++) + sprintf(salt_hex_out + i * 2, "%02x", header[i]); + salt_hex_out[32] = '\0'; + return 0; +} + +int main(int argc, char *argv[]) { + pid_t pid; + if (argc >= 2) + pid = atoi(argv[1]); + else + pid = find_wechat_pid(); + + if (pid <= 0) { + fprintf(stderr, "WeChat not running or invalid PID\n"); + return 1; + } + + printf("============================================================\n"); + printf(" macOS WeChat Memory Key Scanner (C version)\n"); + printf("============================================================\n"); + printf("WeChat PID: %d\n", pid); + + /* Get task port */ + mach_port_t task; + kern_return_t kr = task_for_pid(mach_task_self(), pid, &task); + if (kr != KERN_SUCCESS) { + fprintf(stderr, "task_for_pid failed: %d\n", kr); + fprintf(stderr, "Make sure: (1) running as root, (2) WeChat is ad-hoc signed\n"); + return 1; + } + printf("Got task port: %u\n", task); + + /* Resolve real user's HOME (sudo may change HOME to /var/root) */ + const char *home = getenv("HOME"); + const char *sudo_user = getenv("SUDO_USER"); + if (sudo_user) { + struct passwd *pw = getpwnam(sudo_user); + if (pw && pw->pw_dir) + home = pw->pw_dir; + } + if (!home) home = "/root"; + printf("User home: %s\n", home); + + /* Collect DB salts */ + printf("\nScanning for DB files...\n"); + glob_t g; + char pattern[512]; + snprintf(pattern, sizeof(pattern), + "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/" + "xwechat_files/*/db_storage/**/*.db", + home); + + char db_salts[64][33]; + char db_names[64][256]; /* relative path from db_storage, e.g. "contact/contact.db" */ + int db_count = 0; + + if (glob(pattern, GLOB_NOSORT, NULL, &g) == 0) { + for (size_t i = 0; i < g.gl_pathc && db_count < 64; i++) { + char salt[33]; + if (read_db_salt(g.gl_pathv[i], salt) == 0) { + strcpy(db_salts[db_count], salt); + /* Extract relative path from db_storage/ */ + const char *rel = strstr(g.gl_pathv[i], "db_storage/"); + if (rel) rel += strlen("db_storage/"); + else { + rel = strrchr(g.gl_pathv[i], '/'); + rel = rel ? rel + 1 : g.gl_pathv[i]; + } + strncpy(db_names[db_count], rel, 255); + db_names[db_count][255] = '\0'; + printf(" %s: salt=%s\n", db_names[db_count], salt); + db_count++; + } + } + globfree(&g); + } + printf("Found %d encrypted DBs\n", db_count); + + /* Scan memory for x' patterns */ + printf("\nScanning memory for keys...\n"); + key_entry_t keys[MAX_KEYS]; + int key_count = 0; + size_t total_scanned = 0; + int region_count = 0; + + mach_vm_address_t addr = 0; + while (1) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info; + mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t obj_name; + + kr = mach_vm_region(task, &addr, &size, VM_REGION_BASIC_INFO_64, + (vm_region_info_t)&info, &info_count, &obj_name); + if (kr != KERN_SUCCESS) break; + + if ((info.protection & (VM_PROT_READ | VM_PROT_WRITE)) == + (VM_PROT_READ | VM_PROT_WRITE)) { + region_count++; + + mach_vm_address_t ca = addr; + while (ca < addr + size) { + mach_vm_size_t cs = addr + size - ca; + if (cs > CHUNK_SIZE) cs = CHUNK_SIZE; + + vm_offset_t data; + mach_msg_type_number_t dc; + kr = mach_vm_read(task, ca, cs, &data, &dc); + if (kr == KERN_SUCCESS) { + unsigned char *buf = (unsigned char *)data; + total_scanned += dc; + + for (size_t i = 0; i + HEX_PATTERN_LEN + 3 < dc; i++) { + if (buf[i] == 'x' && buf[i + 1] == '\'') { + /* Check if followed by 96 hex chars and closing ' */ + int valid = 1; + for (int j = 0; j < HEX_PATTERN_LEN; j++) { + if (!is_hex_char(buf[i + 2 + j])) { valid = 0; break; } + } + if (!valid) continue; + if (buf[i + 2 + HEX_PATTERN_LEN] != '\'') continue; + + /* Extract key and salt hex */ + char key_hex[65], salt_hex[33]; + memcpy(key_hex, buf + i + 2, 64); + key_hex[64] = '\0'; + memcpy(salt_hex, buf + i + 2 + 64, 32); + salt_hex[32] = '\0'; + + /* Convert to lowercase for comparison */ + for (int j = 0; key_hex[j]; j++) + if (key_hex[j] >= 'A' && key_hex[j] <= 'F') + key_hex[j] += 32; + for (int j = 0; salt_hex[j]; j++) + if (salt_hex[j] >= 'A' && salt_hex[j] <= 'F') + salt_hex[j] += 32; + + /* Deduplicate */ + int dup = 0; + for (int k = 0; k < key_count; k++) { + if (strcmp(keys[k].key_hex, key_hex) == 0 && + strcmp(keys[k].salt_hex, salt_hex) == 0) { + dup = 1; break; + } + } + if (dup) continue; + + if (key_count < MAX_KEYS) { + strcpy(keys[key_count].key_hex, key_hex); + strcpy(keys[key_count].salt_hex, salt_hex); + snprintf(keys[key_count].full_pragma, sizeof(keys[key_count].full_pragma), + "x'%s%s'", key_hex, salt_hex); + key_count++; + } + } + } + mach_vm_deallocate(mach_task_self(), data, dc); + } + ca += cs; + } + } + addr += size; + } + + printf("\nScan complete: %zuMB scanned, %d regions, %d unique keys\n", + total_scanned / 1024 / 1024, region_count, key_count); + + /* Match keys to DBs */ + printf("\n%-25s %-66s %s\n", "Database", "Key", "Salt"); + printf("%-25s %-66s %s\n", + "-------------------------", + "------------------------------------------------------------------", + "--------------------------------"); + + int matched = 0; + for (int i = 0; i < key_count; i++) { + const char *db = NULL; + for (int j = 0; j < db_count; j++) { + if (strcmp(keys[i].salt_hex, db_salts[j]) == 0) { + db = db_names[j]; + matched++; + break; + } + } + printf("%-25s %-66s %s\n", + db ? db : "(unknown)", + keys[i].key_hex, + keys[i].salt_hex); + } + printf("\nMatched %d/%d keys to known DBs\n", matched, key_count); + + /* Save JSON in decrypt_db.py compatible format: + * { "rel/path.db": { "enc_key": "hex" }, ... } + * Uses backslash separators for Windows compat (decrypt_db.py expects this). + * Also saves all keys (including unmatched) to wechat_keys_raw.json for debugging. + */ + const char *out_path = "all_keys.json"; + FILE *fp = fopen(out_path, "w"); + if (fp) { + fprintf(fp, "{\n"); + int first = 1; + for (int i = 0; i < key_count; i++) { + const char *db = NULL; + for (int j = 0; j < db_count; j++) { + if (strcmp(keys[i].salt_hex, db_salts[j]) == 0) { + db = db_names[j]; + break; + } + } + if (!db) continue; + /* Convert forward slashes to backslashes for decrypt_db.py compat */ + char db_path[256]; + strncpy(db_path, db, sizeof(db_path) - 1); + db_path[sizeof(db_path) - 1] = '\0'; + for (char *p = db_path; *p; p++) + if (*p == '/') *p = '\\'; + fprintf(fp, "%s \"%s\": {\"enc_key\": \"%s\"}", + first ? "" : ",\n", db_path, keys[i].key_hex); + first = 0; + } + fprintf(fp, "\n}\n"); + fclose(fp); + printf("Saved to %s (decrypt_db.py compatible)\n", out_path); + } + + return 0; +} From d38d7ebf9c9f8543c9f1dc835a598de69bd653a7 Mon Sep 17 00:00:00 2001 From: bbingz Date: Thu, 5 Mar 2026 21:52:35 +0800 Subject: [PATCH 2/3] fix: replace glob() with nftw() and add chunk overlap - glob() does not support ** recursive matching on macOS (POSIX). Replace with nftw() + opendir to recursively walk db_storage/. - Add overlap between memory chunks to catch x'...' patterns spanning chunk boundaries. --- README.md | 31 +++++++++++++ find_all_keys_macos.c | 105 +++++++++++++++++++++++++++--------------- 2 files changed, 100 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 96f631f..e0e7371 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,37 @@ V2 文件结构: `[6B signature] [4B aes_size LE] [4B xor_size LE] [1B padding]` - `media_*/media_*.db` - 媒体文件索引 - 其他: head_image, favorite, sns, emoticon 等 +## macOS 数据库密钥扫描 (WeChat 4.x) + +macOS 版微信 4.x 使用 SQLCipher 4 加密本地数据库,密钥格式为 `x'<64hex_key><32hex_salt>'`。C 版扫描器通过 Mach VM API 扫描微信进程内存提取密钥。 + +### 前置条件 + +- macOS (Apple Silicon / Intel) +- WeChat 4.x (macOS 版) +- Xcode Command Line Tools: `xcode-select --install` +- 微信需要 ad-hoc 签名(或安装了防撤回补丁): + `sudo codesign --force --deep --sign - /Applications/WeChat.app` + +### 编译和使用 + +```bash +# 编译 +cc -O2 -o find_all_keys_macos find_all_keys_macos.c -framework Foundation + +# 运行(自动查找微信进程、扫描内存、匹配 DB salt) +sudo ./find_all_keys_macos + +# 或指定 PID +sudo ./find_all_keys_macos +``` + +输出 `all_keys.json`,格式兼容 `decrypt_db.py`,可直接用于解密: + +```bash +python3 decrypt_db.py +``` + ## 免责声明 本工具仅用于学习和研究目的,用于解密**自己的**微信数据。请遵守相关法律法规,不要用于未经授权的数据访问。 diff --git a/find_all_keys_macos.c b/find_all_keys_macos.c index 9a0471c..0da1709 100644 --- a/find_all_keys_macos.c +++ b/find_all_keys_macos.c @@ -22,8 +22,10 @@ #include #include #include -#include +#include +#include #include +#include #include #include @@ -39,6 +41,40 @@ typedef struct { char full_pragma[100]; } key_entry_t; +/* Forward declaration */ +static int read_db_salt(const char *path, char *salt_hex_out); + +/* nftw callback state for collecting DB files */ +#define MAX_DBS 256 +static char g_db_salts[MAX_DBS][33]; +static char g_db_names[MAX_DBS][256]; +static int g_db_count = 0; +static int nftw_collect_db(const char *fpath, const struct stat *sb, + int typeflag, struct FTW *ftwbuf) { + (void)sb; (void)ftwbuf; + if (typeflag != FTW_F) return 0; + size_t len = strlen(fpath); + if (len < 3 || strcmp(fpath + len - 3, ".db") != 0) return 0; + if (g_db_count >= MAX_DBS) return 0; + + char salt[33]; + if (read_db_salt(fpath, salt) != 0) return 0; + + strcpy(g_db_salts[g_db_count], salt); + /* Extract relative path from db_storage/ */ + const char *rel = strstr(fpath, "db_storage/"); + if (rel) rel += strlen("db_storage/"); + else { + rel = strrchr(fpath, '/'); + rel = rel ? rel + 1 : fpath; + } + strncpy(g_db_names[g_db_count], rel, 255); + g_db_names[g_db_count][255] = '\0'; + printf(" %s: salt=%s\n", g_db_names[g_db_count], salt); + g_db_count++; + return 0; +} + static int is_hex_char(unsigned char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } @@ -107,40 +143,32 @@ int main(int argc, char *argv[]) { if (!home) home = "/root"; printf("User home: %s\n", home); - /* Collect DB salts */ + /* Collect DB salts by recursively walking db_storage directories. + * Note: POSIX glob() does not support ** recursive matching on macOS, + * so we use nftw() to walk the directory tree instead. */ printf("\nScanning for DB files...\n"); - glob_t g; - char pattern[512]; - snprintf(pattern, sizeof(pattern), - "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/" - "xwechat_files/*/db_storage/**/*.db", + char db_base_dir[512]; + snprintf(db_base_dir, sizeof(db_base_dir), + "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files", home); - char db_salts[64][33]; - char db_names[64][256]; /* relative path from db_storage, e.g. "contact/contact.db" */ - int db_count = 0; - - if (glob(pattern, GLOB_NOSORT, NULL, &g) == 0) { - for (size_t i = 0; i < g.gl_pathc && db_count < 64; i++) { - char salt[33]; - if (read_db_salt(g.gl_pathv[i], salt) == 0) { - strcpy(db_salts[db_count], salt); - /* Extract relative path from db_storage/ */ - const char *rel = strstr(g.gl_pathv[i], "db_storage/"); - if (rel) rel += strlen("db_storage/"); - else { - rel = strrchr(g.gl_pathv[i], '/'); - rel = rel ? rel + 1 : g.gl_pathv[i]; - } - strncpy(db_names[db_count], rel, 255); - db_names[db_count][255] = '\0'; - printf(" %s: salt=%s\n", db_names[db_count], salt); - db_count++; + /* Walk each account's db_storage directory */ + DIR *xdir = opendir(db_base_dir); + if (xdir) { + struct dirent *ent; + while ((ent = readdir(xdir)) != NULL) { + if (ent->d_name[0] == '.') continue; + char storage_path[768]; + snprintf(storage_path, sizeof(storage_path), + "%s/%s/db_storage", db_base_dir, ent->d_name); + struct stat st; + if (stat(storage_path, &st) == 0 && S_ISDIR(st.st_mode)) { + nftw(storage_path, nftw_collect_db, 20, FTW_PHYS); } } - globfree(&g); + closedir(xdir); } - printf("Found %d encrypted DBs\n", db_count); + printf("Found %d encrypted DBs\n", g_db_count); /* Scan memory for x' patterns */ printf("\nScanning memory for keys...\n"); @@ -222,7 +250,12 @@ int main(int argc, char *argv[]) { } mach_vm_deallocate(mach_task_self(), data, dc); } - ca += cs; + /* Advance with overlap to catch patterns spanning chunk boundaries. + * Pattern is x'<96 hex chars>' = 99 bytes total. */ + if (cs > HEX_PATTERN_LEN + 3) + ca += cs - (HEX_PATTERN_LEN + 3); + else + ca += cs; } } addr += size; @@ -241,9 +274,9 @@ int main(int argc, char *argv[]) { int matched = 0; for (int i = 0; i < key_count; i++) { const char *db = NULL; - for (int j = 0; j < db_count; j++) { - if (strcmp(keys[i].salt_hex, db_salts[j]) == 0) { - db = db_names[j]; + for (int j = 0; j < g_db_count; j++) { + if (strcmp(keys[i].salt_hex, g_db_salts[j]) == 0) { + db = g_db_names[j]; matched++; break; } @@ -267,9 +300,9 @@ int main(int argc, char *argv[]) { int first = 1; for (int i = 0; i < key_count; i++) { const char *db = NULL; - for (int j = 0; j < db_count; j++) { - if (strcmp(keys[i].salt_hex, db_salts[j]) == 0) { - db = db_names[j]; + for (int j = 0; j < g_db_count; j++) { + if (strcmp(keys[i].salt_hex, g_db_salts[j]) == 0) { + db = g_db_names[j]; break; } } From 18ffb2e7fa1a146e78e507f26fba0c343542a9fc Mon Sep 17 00:00:00 2001 From: bbingz Date: Thu, 5 Mar 2026 23:19:22 +0800 Subject: [PATCH 3/3] fix: use forward slashes in JSON output and add size==0 guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove forward-to-backslash conversion in JSON keys — forward slashes are native macOS paths and don't need JSON escaping (backslash paths like \b would be misinterpreted as escape sequences by JSON parsers) - Add size==0 guard after mach_vm_region to prevent infinite loop --- find_all_keys_macos.c | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/find_all_keys_macos.c b/find_all_keys_macos.c index 0da1709..eb6a9e5 100644 --- a/find_all_keys_macos.c +++ b/find_all_keys_macos.c @@ -187,6 +187,7 @@ int main(int argc, char *argv[]) { kr = mach_vm_region(task, &addr, &size, VM_REGION_BASIC_INFO_64, (vm_region_info_t)&info, &info_count, &obj_name); if (kr != KERN_SUCCESS) break; + if (size == 0) { addr++; continue; } /* guard against infinite loop */ if ((info.protection & (VM_PROT_READ | VM_PROT_WRITE)) == (VM_PROT_READ | VM_PROT_WRITE)) { @@ -288,10 +289,8 @@ int main(int argc, char *argv[]) { } printf("\nMatched %d/%d keys to known DBs\n", matched, key_count); - /* Save JSON in decrypt_db.py compatible format: - * { "rel/path.db": { "enc_key": "hex" }, ... } - * Uses backslash separators for Windows compat (decrypt_db.py expects this). - * Also saves all keys (including unmatched) to wechat_keys_raw.json for debugging. + /* Save JSON: { "rel/path.db": { "enc_key": "hex" }, ... } + * Uses forward slashes (native macOS paths, valid JSON without escaping). */ const char *out_path = "all_keys.json"; FILE *fp = fopen(out_path, "w"); @@ -307,19 +306,13 @@ int main(int argc, char *argv[]) { } } if (!db) continue; - /* Convert forward slashes to backslashes for decrypt_db.py compat */ - char db_path[256]; - strncpy(db_path, db, sizeof(db_path) - 1); - db_path[sizeof(db_path) - 1] = '\0'; - for (char *p = db_path; *p; p++) - if (*p == '/') *p = '\\'; fprintf(fp, "%s \"%s\": {\"enc_key\": \"%s\"}", - first ? "" : ",\n", db_path, keys[i].key_hex); + first ? "" : ",\n", db, keys[i].key_hex); first = 0; } fprintf(fp, "\n}\n"); fclose(fp); - printf("Saved to %s (decrypt_db.py compatible)\n", out_path); + printf("Saved to %s\n", out_path); } return 0;