mirror of https://github.com/jackwener/wx-cli.git
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]feat/daemon-cli
parent
24ae180669
commit
1f9ca3792a
|
|
@ -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) |
|
||||
|
||||
## 技术细节
|
||||
|
||||
|
|
|
|||
|
|
@ -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'<key_hex><salt_hex>' 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <glob.h>
|
||||
#include <pwd.h>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Loading…
Reference in New Issue