diff --git a/README.md b/README.md index 0816046..8b5760d 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,22 @@ python find_image_key.py > **注意**: AES 密钥仅在微信查看图片时临时加载到内存中。如果扫描未找到密钥,请先在微信中查看几张图片,然后立即重新运行脚本。 +#### macOS 图片解密 + +macOS 上使用 C 版工具(通过 Mach VM API + CommonCrypto): + +```bash +# 编译 +cc -O3 -o find_image_key find_image_key.c -framework Security +cc -O3 -o decrypt_images decrypt_images.c -framework Security + +# 1. 持续扫描图片密钥(在微信中浏览图片触发密钥加载) +sudo ./find_image_key + +# 2. 批量解密所有 V2 图片 +./decrypt_images +``` + ## 文件说明 | 文件 | 说明 | @@ -146,6 +162,8 @@ python find_image_key.py | `find_image_key_monitor.py` | 持续监控版密钥提取(推荐) | | `latency_test.py` | 延迟测量诊断工具 | | `find_all_keys_macos.c` | macOS 版内存密钥扫描器 (C, Mach VM API) | +| `find_image_key.c` | macOS 版图片密钥扫描器 (C, 持续监控模式) | +| `decrypt_images.c` | macOS 版批量图片解密器 (C, 多密钥支持) | ## 技术细节 diff --git a/decrypt_images.c b/decrypt_images.c new file mode 100644 index 0000000..5fbee58 --- /dev/null +++ b/decrypt_images.c @@ -0,0 +1,617 @@ +/* + * decrypt_images.c — WeChat V2 image batch decryptor (multi-key) + * + * Decrypts all V2 encrypted .dat files in the WeChat image cache. + * Supports multiple keys via image_keys.json (CT block → AES key mapping). + * + * V2 format: + * [15B header] [AES-128-ECB ciphertext] [XOR encrypted tail] + * Header: \x07\x08V2\x08\x07 (6B) + aes_size:u32LE + xor_size:u32LE + 1B pad + * AES region: ceil(aes_size/16)*16 bytes of AES-128-ECB ciphertext + * XOR tail: xor_size bytes, each XOR'd with a single-byte key + * + * Build: + * cc -O3 -o decrypt_images decrypt_images.c -framework Security + * + * Usage: + * ./decrypt_images # auto from config + image_keys.json + * ./decrypt_images # single-key manual + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PATH 4096 +#define V2_MAGIC "\x07\x08V2\x08\x07" +#define V2_MAGIC_LEN 6 +#define HEADER_SIZE 15 +#define MAX_KEYS 4096 + +/* ---- Key mapping: CT block hex → AES key ---- */ +typedef struct { + unsigned char ct[16]; /* CT block 0 pattern */ + unsigned char key[16]; /* AES key for this pattern */ +} key_map_t; + +static key_map_t key_map[MAX_KEYS]; +static int n_keys = 0; + +/* ---- Utility ---- */ + +static int hex2bytes(const char *hex, unsigned char *out, int maxlen) { + int len = 0; + while (*hex && *(hex + 1) && len < maxlen) { + unsigned int b; + if (sscanf(hex, "%2x", &b) != 1) break; + out[len++] = (unsigned char)b; + hex += 2; + } + return len; +} + +static void bytes2hex(const unsigned char *d, int n, char *out) { + for (int i = 0; i < n; i++) sprintf(out + i*2, "%02x", d[i]); + out[n*2] = '\0'; +} + +/* Minimal JSON string extractor (for simple unescaped string values only). */ +static int json_get_string(const char *json, const char *key, + char *value, int maxlen) { + char pattern[256]; + snprintf(pattern, sizeof(pattern), "\"%s\"", key); + const char *p = strstr(json, pattern); + if (!p) return 0; + p = strchr(p + strlen(pattern), '"'); + if (!p) return 0; + p++; + const char *end = strchr(p, '"'); + if (!end) return 0; + int len = (int)(end - p); + if (len >= maxlen) len = maxlen - 1; + memcpy(value, p, len); + value[len] = '\0'; + return 1; +} + +/* Load image_keys.json: { "ct_hex": "key_hex", ... } */ +static int load_key_map(const char *path) { + FILE *f = fopen(path, "r"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + if (sz <= 0) { fclose(f); return 0; } + fseek(f, 0, SEEK_SET); + char *json = malloc((size_t)sz + 1); + if (!json) { fclose(f); return 0; } + size_t rd = fread(json, 1, (size_t)sz, f); + if (rd != (size_t)sz) { + fclose(f); + free(json); + return 0; + } + json[rd] = '\0'; + fclose(f); + + /* Simple parser: find all "32hex": "32hex" pairs */ + const char *p = json; + int warned_capacity = 0; + while ((p = strchr(p, '"')) != NULL) { + if (n_keys >= MAX_KEYS) { + if (!warned_capacity) { + fprintf(stderr, "Warning: image_keys.json exceeds MAX_KEYS=%d, extra keys ignored\n", + MAX_KEYS); + warned_capacity = 1; + } + break; + } + + p++; + const char *end = strchr(p, '"'); + if (!end) break; + int klen = (int)(end - p); + if (klen != 32) { p = end + 1; continue; } + + char ct_hex[33]; + memcpy(ct_hex, p, 32); + ct_hex[32] = '\0'; + const char *colon = end + 1; + while (*colon == ' ' || *colon == '\t' || *colon == '\r' || *colon == '\n') + colon++; + if (*colon != ':') { p = end + 1; continue; } + p = colon + 1; + + /* Find next quoted string (the value) */ + p = strchr(p, '"'); + if (!p) break; + p++; + end = strchr(p, '"'); + if (!end) break; + int vlen = (int)(end - p); + if (vlen != 32) { p = end + 1; continue; } + + char key_hex[33]; + memcpy(key_hex, p, 32); + key_hex[32] = '\0'; + p = end + 1; + + if (hex2bytes(ct_hex, key_map[n_keys].ct, 16) != 16 || + hex2bytes(key_hex, key_map[n_keys].key, 16) != 16) { + continue; + } + n_keys++; + } + free(json); + return n_keys; +} + +/* Find AES key for a given CT block */ +static const unsigned char *find_key_for_ct(const unsigned char *ct) { + for (int i = 0; i < n_keys; i++) + if (memcmp(key_map[i].ct, ct, 16) == 0) return key_map[i].key; + return NULL; +} + +/* Create directory and parents */ +static void mkdirs(const char *path) { + char tmp[MAX_PATH]; + snprintf(tmp, sizeof(tmp), "%s", path); + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + mkdir(tmp, 0755); +} + +static int has_parent_segment(const char *path) { + if (!path || !path[0]) return 1; + if (path[0] == '/' || path[0] == '\\') return 1; + + const char *p = path; + while (*p) { + while (*p == '/' || *p == '\\') p++; + if (!*p) break; + const char *seg = p; + while (*p && *p != '/' && *p != '\\') p++; + if ((p - seg) == 2 && seg[0] == '.' && seg[1] == '.') return 1; + } + return 0; +} + +/* Detect image type from magic bytes */ +static const char *detect_ext(const unsigned char *data, size_t len) { + if (len < 4) return ".bin"; + if (data[0] == 0xFF && data[1] == 0xD8) return ".jpg"; + if (data[0] == 0x89 && data[1] == 0x50 && + data[2] == 0x4E && data[3] == 0x47) return ".png"; + if (data[0] == 'G' && data[1] == 'I' && + data[2] == 'F' && data[3] == '8') return ".gif"; + if (data[0] == 'R' && data[1] == 'I' && + data[2] == 'F' && data[3] == 'F') return ".webp"; + if (data[0] == 0x00 && data[1] == 0x00 && + data[2] == 0x00 && (data[3] == 0x18 || data[3] == 0x1C || + data[3] == 0x20 || data[3] == 0x14)) return ".mp4"; + return ".bin"; +} + +/* Auto-detect XOR key */ +static unsigned char detect_xor_key(const unsigned char *xor_data, size_t xor_size) { + if (xor_size == 0) return 0; + unsigned char candidates[] = {0x80, 0xDC, 0x00}; + for (int i = 0; i < (int)(sizeof(candidates)/sizeof(candidates[0])); i++) { + /* We want a candidate that doesn't produce a leading NUL byte after XOR. */ + unsigned char test = xor_data[0] ^ candidates[i]; + if (test != 0x00 || candidates[i] == 0x00) + return candidates[i]; + } + return 0x80; +} + +/* ---- Decrypt one V2 file ---- */ + +static int decrypt_v2_file(const char *input_path, const char *output_dir, + const char *rel_path, const unsigned char *aes_key, + unsigned char xor_key, int auto_xor, + int *out_xor_detected) { + FILE *fin = fopen(input_path, "rb"); + if (!fin) return -1; + + unsigned char header[HEADER_SIZE]; + if (fread(header, 1, HEADER_SIZE, fin) != HEADER_SIZE) { + fclose(fin); return -1; + } + if (memcmp(header, V2_MAGIC, V2_MAGIC_LEN) != 0) { + fclose(fin); return -2; + } + + uint32_t aes_size, xor_size; + memcpy(&aes_size, header + 6, 4); + memcpy(&xor_size, header + 10, 4); + + if ((uint64_t)aes_size > 100u * 1024u * 1024u || + (uint64_t)xor_size > 100u * 1024u * 1024u) { + fclose(fin); + return -6; + } + + /* PKCS7: when aes_size is already 16-byte aligned, an extra 16-byte + * padding block is present in the ciphertext */ + size_t aes_ct_size = (aes_size % 16 == 0) + ? (size_t)aes_size + 16 + : ((size_t)aes_size + 15) / 16 * 16; + + /* Get total file size to calculate raw_data segment */ + long cur_pos = ftell(fin); + fseek(fin, 0, SEEK_END); + long file_size = ftell(fin); + fseek(fin, cur_pos, SEEK_SET); + + unsigned char *aes_ct = malloc(aes_ct_size); + if (!aes_ct) { fclose(fin); return -1; } + size_t rd = fread(aes_ct, 1, aes_ct_size, fin); + if (rd != aes_ct_size) { + free(aes_ct); + fclose(fin); + return -8; + } + + /* V2 may have unencrypted raw_data between AES and XOR sections */ + long raw_data_size = file_size - HEADER_SIZE - (long)aes_ct_size - (long)xor_size; + if (raw_data_size < 0) raw_data_size = 0; + + unsigned char *raw_data = NULL; + if (raw_data_size > 0) { + raw_data = malloc((size_t)raw_data_size); + if (!raw_data) { free(aes_ct); fclose(fin); return -1; } + rd = fread(raw_data, 1, (size_t)raw_data_size, fin); + if (rd != (size_t)raw_data_size) { + free(aes_ct); free(raw_data); fclose(fin); return -8; + } + } + + unsigned char *xor_data = NULL; + if (xor_size > 0) { + xor_data = malloc(xor_size); + if (!xor_data) { free(aes_ct); free(raw_data); fclose(fin); return -1; } + rd = fread(xor_data, 1, xor_size, fin); + if (rd != xor_size) { + free(aes_ct); free(raw_data); free(xor_data); + fclose(fin); return -8; + } + } + fclose(fin); + + /* If multi-key mode: look up key by CT block 0 */ + if (!aes_key && aes_ct_size >= 16) { + aes_key = find_key_for_ct(aes_ct); + if (!aes_key) { + free(aes_ct); free(raw_data); free(xor_data); return -5; + } + } + if (!aes_key) { free(aes_ct); free(raw_data); free(xor_data); return -5; } + + unsigned char *aes_pt = malloc(aes_ct_size); + if (!aes_pt) { free(aes_ct); free(raw_data); free(xor_data); return -1; } + + size_t moved = 0; + CCCryptorStatus st = CCCrypt( + kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode, + aes_key, 16, NULL, + aes_ct, aes_ct_size, aes_pt, aes_ct_size, &moved); + free(aes_ct); + + if (st != kCCSuccess) { + free(aes_pt); free(raw_data); free(xor_data); return -3; + } + + if (auto_xor && xor_data && xor_size > 0) { + xor_key = detect_xor_key(xor_data, xor_size); + if (out_xor_detected) *out_xor_detected = xor_key; + } + + if (xor_data && xor_size > 0) { + for (uint32_t i = 0; i < xor_size; i++) + xor_data[i] ^= xor_key; + } + + const char *ext = detect_ext(aes_pt, aes_size); + + /* Skip unrecognized formats — avoids writing garbage .bin files */ + if (strcmp(ext, ".bin") == 0) { + free(aes_pt); free(raw_data); free(xor_data); + return -9; /* unrecognized image type */ + } + + char out_path[MAX_PATH]; + char rel_noext[MAX_PATH]; + snprintf(rel_noext, sizeof(rel_noext), "%s", rel_path); + char *dot = strrchr(rel_noext, '.'); + if (dot) *dot = '\0'; + if (has_parent_segment(rel_noext)) { + free(aes_pt); free(raw_data); free(xor_data); + return -7; + } + snprintf(out_path, sizeof(out_path), "%s/%s%s", output_dir, rel_noext, ext); + + /* Skip if already decrypted */ + struct stat st_out; + if (stat(out_path, &st_out) == 0 && st_out.st_size > 0) { + free(aes_pt); free(raw_data); free(xor_data); + return 1; /* already exists */ + } + + char parent[MAX_PATH]; + snprintf(parent, sizeof(parent), "%s", out_path); + char *last_slash = strrchr(parent, '/'); + if (last_slash) { *last_slash = '\0'; mkdirs(parent); } + + FILE *fout = fopen(out_path, "wb"); + if (!fout) { free(aes_pt); free(raw_data); free(xor_data); return -4; } + + fwrite(aes_pt, 1, aes_size, fout); + if (raw_data && raw_data_size > 0) fwrite(raw_data, 1, (size_t)raw_data_size, fout); + if (xor_data && xor_size > 0) fwrite(xor_data, 1, xor_size, fout); + + fclose(fout); + free(aes_pt); + free(raw_data); + free(xor_data); + return 0; +} + +/* ---- Directory walking ---- */ + +typedef struct { + const unsigned char *fallback_key; /* single key from config.json (or NULL) */ + int multi_key; /* 1 if using image_keys.json */ + unsigned char xor_key; + int auto_xor; + const char *output_dir; + const char *base_dir; + int success; + int skipped; + int existed; /* already decrypted */ + int no_key; /* V2 files with no matching key */ + int failed; +} walk_ctx; + +static void walk_dir(const char *dir, walk_ctx *ctx) { + DIR *d = opendir(dir); + if (!d) return; + + struct dirent *ent; + while ((ent = readdir(d))) { + if (ent->d_name[0] == '.') continue; + + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); + + struct stat st; + if (lstat(path, &st) != 0) continue; + if (S_ISLNK(st.st_mode)) continue; + + if (S_ISDIR(st.st_mode)) { + walk_dir(path, ctx); + } else if (S_ISREG(st.st_mode)) { + size_t nlen = strlen(ent->d_name); + if (nlen < 5 || strcmp(ent->d_name + nlen - 4, ".dat") != 0) + continue; + + const char *rel = path + strlen(ctx->base_dir); + if (*rel == '/') rel++; + + int xor_detected = -1; + /* In multi-key mode, pass NULL as key — decrypt_v2_file looks it up */ + const unsigned char *key = ctx->multi_key ? NULL : ctx->fallback_key; + int ret = decrypt_v2_file(path, ctx->output_dir, rel, + key, ctx->xor_key, + ctx->auto_xor, &xor_detected); + + if (ret == 0) { + ctx->success++; + if (ctx->auto_xor && xor_detected >= 0) { + ctx->xor_key = (unsigned char)xor_detected; + ctx->auto_xor = 0; + printf(" Auto-detected XOR key: 0x%02X\n", ctx->xor_key); + } + if (ctx->success <= 5 || ctx->success % 1000 == 0) { + printf(" [%d] %s\n", ctx->success, rel); + } + } else if (ret == 1) { + ctx->existed++; + } else if (ret == -2) { + ctx->skipped++; + } else if (ret == -5) { + ctx->no_key++; + } else { + ctx->failed++; + if (ctx->failed <= 5) + printf(" FAIL(%d): %s\n", ret, rel); + } + } + } + closedir(d); +} + +/* ---- Main ---- */ + +int main(int argc, char *argv[]) { + unsigned char aes_key[16]; + char image_dir[MAX_PATH] = ""; + char output_dir[MAX_PATH] = ""; + char key_hex[64] = ""; + int have_single_key = 0; + + printf("=== WeChat V2 Image Decryptor ===\n\n"); + + /* Determine exe directory for config file lookup */ + char exe_dir[MAX_PATH] = "."; + const char *last_slash = strrchr(argv[0], '/'); + if (last_slash) { + int len = (int)(last_slash - argv[0]); + snprintf(exe_dir, sizeof(exe_dir), "%.*s", len, argv[0]); + } + + if (argc >= 4) { + /* Manual single-key mode */ + strncpy(key_hex, argv[1], sizeof(key_hex) - 1); + key_hex[sizeof(key_hex) - 1] = '\0'; + strncpy(image_dir, argv[2], sizeof(image_dir) - 1); + image_dir[sizeof(image_dir) - 1] = '\0'; + strncpy(output_dir, argv[3], sizeof(output_dir) - 1); + output_dir[sizeof(output_dir) - 1] = '\0'; + have_single_key = (key_hex[0] != '\0'); + } else { + /* Load image_keys.json first (multi-key) */ + char keys_path[MAX_PATH]; + snprintf(keys_path, sizeof(keys_path), "%s/image_keys.json", exe_dir); + int loaded = load_key_map(keys_path); + if (loaded > 0) + printf("Loaded %d key mappings from %s\n", loaded, keys_path); + + /* Read config.json for paths (and fallback single key) */ + char cfg_path[MAX_PATH]; + snprintf(cfg_path, sizeof(cfg_path), "%s/config.json", exe_dir); + FILE *cf = fopen(cfg_path, "r"); + if (!cf) { + fprintf(stderr, "ERROR: Cannot open %s\n", cfg_path); + return 1; + } + + fseek(cf, 0, SEEK_END); + long sz = ftell(cf); + if (sz <= 0) { fclose(cf); return 1; } + fseek(cf, 0, SEEK_SET); + char *json = malloc((size_t)sz + 1); + if (!json) { fclose(cf); return 1; } + size_t rd = fread(json, 1, (size_t)sz, cf); + if (rd != (size_t)sz) { + free(json); + fclose(cf); + return 1; + } + json[sz] = '\0'; + fclose(cf); + + if (json_get_string(json, "image_key", key_hex, sizeof(key_hex)) && + key_hex[0] != '\0') + have_single_key = 1; + else + have_single_key = 0; + + char db_dir[MAX_PATH] = ""; + json_get_string(json, "db_dir", db_dir, sizeof(db_dir)); + + char out_rel[MAX_PATH] = "decrypted_images"; + json_get_string(json, "decrypted_images_dir", out_rel, sizeof(out_rel)); + if (out_rel[0] == '/') + strncpy(output_dir, out_rel, sizeof(output_dir) - 1); + else + snprintf(output_dir, sizeof(output_dir), "%s/%s", exe_dir, out_rel); + output_dir[sizeof(output_dir) - 1] = '\0'; + + if (db_dir[0]) { + char *s = strrchr(db_dir, '/'); + if (!s) s = strrchr(db_dir, '\\'); + if (s) { + int plen = (int)(s - db_dir); + snprintf(image_dir, sizeof(image_dir), + "%.*s/msg", plen, db_dir); + } + } + free(json); + } + + /* Parse single key if available (used as fallback or sole key) */ + if (have_single_key && key_hex[0]) { + if (hex2bytes(key_hex, aes_key, 16) == 16) { + /* If no image_keys.json loaded, add single key to key_map + * by discovering its CT block at runtime */ + } else { + have_single_key = 0; + } + } + + if (n_keys == 0 && !have_single_key) { + fprintf(stderr, "ERROR: No keys available.\n"); + fprintf(stderr, "Run find_image_key first, or set image_key in config.json\n"); + return 1; + } + + /* Auto-detect: scan ~/Library/Containers/com.tencent.xinWeChat */ + if (image_dir[0] == '\0') { + const char *home = getenv("HOME"); + if (!home) home = "/Users"; + char base[MAX_PATH]; + snprintf(base, sizeof(base), + "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files", + home); + DIR *d = opendir(base); + if (d) { + struct dirent *ent; + while ((ent = readdir(d))) { + if (ent->d_name[0] == '.') continue; + char candidate[MAX_PATH]; + snprintf(candidate, sizeof(candidate), "%s/%s/msg", base, ent->d_name); + struct stat st2; + if (stat(candidate, &st2) == 0 && S_ISDIR(st2.st_mode)) { + strncpy(image_dir, candidate, sizeof(image_dir) - 1); + printf("Auto-detected image directory:\n %s\n\n", image_dir); + break; + } + } + closedir(d); + } + } + + if (image_dir[0] == '\0') { + fprintf(stderr, "ERROR: Cannot determine image directory.\n"); + fprintf(stderr, "Tried: command line, config.json, auto-detect.\n"); + fprintf(stderr, "Set db_dir in config.json or pass image_dir as argument.\n"); + return 1; + } + + printf("Mode: %s\n", n_keys > 0 ? "multi-key" : "single-key"); + if (n_keys > 0) printf("Keys: %d pattern→key mappings\n", n_keys); + if (have_single_key) printf("Fallback: %s\n", key_hex); + printf("Image dir: %s\n", image_dir); + printf("Output: %s\n\n", output_dir); + + mkdirs(output_dir); + + walk_ctx ctx = { + .fallback_key = have_single_key ? aes_key : NULL, + .multi_key = (n_keys > 0), + .xor_key = 0, + .auto_xor = 1, + .output_dir = output_dir, + .base_dir = image_dir, + .success = 0, + .skipped = 0, + .existed = 0, + .no_key = 0, + .failed = 0, + }; + + walk_dir(image_dir, &ctx); + + printf("\n==================================================\n"); + printf("Results:\n"); + printf(" Decrypted: %d\n", ctx.success); + printf(" Existed: %d (already decrypted, skipped)\n", ctx.existed); + printf(" No key: %d (run find_image_key to discover more keys)\n", ctx.no_key); + printf(" Skipped: %d (non-V2)\n", ctx.skipped); + printf(" Failed: %d\n", ctx.failed); + printf("Output: %s\n", output_dir); + printf("==================================================\n"); + + return (ctx.success > 0) ? 0 : 1; +} diff --git a/find_image_key.c b/find_image_key.c new file mode 100644 index 0000000..4b8f1ce --- /dev/null +++ b/find_image_key.c @@ -0,0 +1,910 @@ +/* + * find_image_key.c — WeChat V2 image key continuous scanner (macOS) + * + * Discovers all unique V2 encryption patterns from the image cache, + * then continuously scans WeChat process memory to find AES keys. + * User just keeps browsing images in WeChat — the scanner catches + * keys as they transiently appear in memory. + * + * Uses multi-block CCCrypt: one key setup decrypts ALL unsolved + * patterns in a single call (~1.5 min per full scan with 20 patterns). + * + * Build: + * cc -O3 -o find_image_key find_image_key.c -framework Security + * + * Usage: + * sudo ./find_image_key # auto-discover from config.json + * sudo ./find_image_key # explicit image directory + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PATH 4096 +#define MAX_PATTERNS 8192 +#define V2_MAGIC "\x07\x08V2\x08\x07" +#define V2_MAGIC_LEN 6 +#define REGION_MAX (200 * 1024 * 1024) +#define DEEP_PRIORITY_MAX 10 /* byte-by-byte scan for top N unsolved patterns */ + +/* ---- Strict image magic detection (16 bytes available from decrypted block) ---- */ +static int is_image_magic(const unsigned char *pt) { + if (pt[0] == 0xFF && pt[1] == 0xD8 && pt[2] == 0xFF && + pt[3] >= 0xC0 && pt[3] != 0xFF) { + /* JFIF: verify "JF" at offset 6 */ + if (pt[3] == 0xE0) return (pt[6] == 'J' && pt[7] == 'F'); + /* EXIF: verify "Ex" at offset 6 */ + if (pt[3] == 0xE1) return (pt[6] == 'E' && pt[7] == 'x'); + /* Other markers: verify length field is sane (big-endian, 2..32767) */ + uint16_t len = ((uint16_t)pt[4] << 8) | pt[5]; + return (len >= 2 && len < 0x8000); + } + /* PNG: full 8-byte signature */ + if (pt[0]==0x89 && pt[1]==0x50 && pt[2]==0x4E && pt[3]==0x47 && + pt[4]==0x0D && pt[5]==0x0A && pt[6]==0x1A && pt[7]==0x0A) return 1; + /* GIF: "GIF89a" or "GIF87a" */ + if (pt[0]=='G' && pt[1]=='I' && pt[2]=='F' && pt[3]=='8' && + (pt[4]=='9' || pt[4]=='7') && pt[5]=='a') return 1; + /* WebP: "RIFF....WEBP" */ + if (pt[0]=='R' && pt[1]=='I' && pt[2]=='F' && pt[3]=='F' && + pt[8]=='W' && pt[9]=='E' && pt[10]=='B' && pt[11]=='P') return 1; + return 0; +} + +/* ---- Pattern tracking ---- */ +typedef struct { + unsigned char ct[16]; /* CT block 0 (first 16 encrypted bytes) */ + unsigned char key[16]; /* found AES key */ + int solved; + int file_count; /* how many .dat files use this pattern */ + char sample_path[MAX_PATH]; +} pattern_t; + +static pattern_t patterns[MAX_PATTERNS]; +static int npatterns = 0; +static int total_v2_files = 0; + +/* ---- Rejected key blacklist (false positives) ---- */ +#define MAX_REJECTED 256 +static unsigned char rejected_keys[MAX_REJECTED][16]; +static int n_rejected = 0; + +static int is_rejected(const unsigned char *key) { + for (int i = 0; i < n_rejected; i++) + if (memcmp(rejected_keys[i], key, 16) == 0) return 1; + return 0; +} +static void add_rejected(const unsigned char *key) { + if (n_rejected < MAX_REJECTED && !is_rejected(key)) { + memcpy(rejected_keys[n_rejected], key, 16); + n_rejected++; + } +} + +/* ---- Global scan mode ---- */ +static int g_deep_mode = 0; + +/* ---- Graceful shutdown ---- */ +static volatile sig_atomic_t stop_flag = 0; +static void sigint_handler(int sig) { (void)sig; stop_flag = 1; } + +/* ---- Utility ---- */ +static void bytes2hex(const unsigned char *d, int n, char *out) { + for (int i = 0; i < n; i++) sprintf(out + i*2, "%02x", d[i]); + out[n*2] = '\0'; +} +static int hex2bytes(const char *h, unsigned char *o, int max) { + int n = 0; + while (n < max) { + if (!h[0] || !h[1]) return 0; + if (!((h[0] >= '0' && h[0] <= '9') || (h[0] >= 'a' && h[0] <= 'f') || + (h[0] >= 'A' && h[0] <= 'F'))) return 0; + if (!((h[1] >= '0' && h[1] <= '9') || (h[1] >= 'a' && h[1] <= 'f') || + (h[1] >= 'A' && h[1] <= 'F'))) return 0; + + unsigned int b = 0; + if (sscanf(h, "%2x", &b) != 1) return 0; + o[n++] = (unsigned char)b; h += 2; + } + return n; +} + +/* Minimal JSON string extractor */ +static int json_get_string(const char *json, const char *key, + char *val, int maxlen) { + char pat[256]; + snprintf(pat, sizeof(pat), "\"%s\"", key); + const char *p = strstr(json, pat); + if (!p) return 0; + p = strchr(p + strlen(pat), '"'); + if (!p) return 0; + p++; + const char *end = strchr(p, '"'); + if (!end || (int)(end - p) >= maxlen) return 0; + memcpy(val, p, end - p); + val[end - p] = '\0'; + return 1; +} + +/* ---- Pattern discovery ---- */ +static int find_pattern_index(const unsigned char *ct) { + for (int i = 0; i < npatterns; i++) + if (memcmp(patterns[i].ct, ct, 16) == 0) return i; + return -1; +} + +static void discover_dir(const char *dir) { + DIR *d = opendir(dir); + if (!d) return; + struct dirent *ent; + while ((ent = readdir(d))) { + if (ent->d_name[0] == '.') continue; + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); + struct stat st; + if (lstat(path, &st) != 0) continue; + if (S_ISLNK(st.st_mode)) continue; + if (S_ISDIR(st.st_mode)) { + discover_dir(path); + continue; + } + if (!S_ISREG(st.st_mode)) continue; + size_t nlen = strlen(ent->d_name); + if (nlen < 5 || strcmp(ent->d_name + nlen - 4, ".dat") != 0) continue; + + FILE *f = fopen(path, "rb"); + if (!f) continue; + unsigned char hdr[31]; + size_t rd = fread(hdr, 1, 31, f); + fclose(f); + if (rd < 31 || memcmp(hdr, V2_MAGIC, V2_MAGIC_LEN) != 0) continue; + + unsigned char *ct = hdr + 15; + total_v2_files++; + int idx = find_pattern_index(ct); + if (idx >= 0) { + patterns[idx].file_count++; + } else if (npatterns < MAX_PATTERNS) { + memcpy(patterns[npatterns].ct, ct, 16); + patterns[npatterns].file_count = 1; + patterns[npatterns].solved = 0; + strncpy(patterns[npatterns].sample_path, path, + sizeof(patterns[npatterns].sample_path) - 1); + patterns[npatterns].sample_path[sizeof(patterns[npatterns].sample_path) - 1] = '\0'; + npatterns++; + } + } + closedir(d); +} + +/* Sort patterns by file_count descending */ +static int cmp_patterns(const void *a, const void *b) { + return ((pattern_t*)b)->file_count - ((pattern_t*)a)->file_count; +} + +/* ---- Process discovery ---- */ +static int get_wechat_pids(pid_t *pids, int max) { + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}; + size_t sz = 0; + if (sysctl(mib, 4, NULL, &sz, NULL, 0) != KERN_SUCCESS || sz == 0) + return 0; + + size_t alloc_sz = sz + (sz >> 2); + struct kinfo_proc *procs = malloc(alloc_sz); + if (!procs) return 0; + + if (sysctl(mib, 4, procs, &alloc_sz, NULL, 0) != KERN_SUCCESS) { + free(procs); + return 0; + } + + int n = (int)(alloc_sz / sizeof(struct kinfo_proc)), cnt = 0; + for (int i = 0; i < n && cnt < max; i++) + if (strstr(procs[i].kp_proc.p_comm, "WeChat")) + pids[cnt++] = procs[i].kp_proc.p_pid; + free(procs); + return cnt; +} + +/* ---- Verification: decrypt sample file, validate JPEG marker chain ---- */ + +/* Validate JPEG structure: check marker chain (SOI → markers → SOS/EOI) */ +static int verify_jpeg_chain(const unsigned char *data, size_t len) { + if (len < 4 || data[0] != 0xFF || data[1] != 0xD8) return 0; + size_t pos = 2; + int markers = 0; + while (pos + 4 <= len) { + if (data[pos] != 0xFF) return markers >= 2; + unsigned char m = data[pos + 1]; + /* Skip fill bytes (FF FF...) */ + if (m == 0xFF) { pos++; continue; } + if (m == 0x00) return 0; /* stuffed byte outside scan = invalid */ + if (m == 0xD9) return markers >= 1; /* EOI */ + if (m == 0xDA) return markers >= 1; /* SOS = scan data follows */ + if (m < 0xC0) return 0; + uint16_t mlen = ((uint16_t)data[pos+2] << 8) | data[pos+3]; + if (mlen < 2) return 0; + pos += 2 + mlen; + markers++; + } + /* Ran out of data (first marker spans past AES region): accept if >= 1 valid marker */ + return markers >= 1; +} + +/* Validate PNG: 8-byte sig + IHDR chunk */ +static int verify_png_chain(const unsigned char *data, size_t len) { + static const unsigned char sig[8] = {0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; + if (len < 24 || memcmp(data, sig, 8) != 0) return 0; + /* IHDR chunk at offset 8: length(4) + "IHDR"(4) + data(13) + CRC(4) */ + return (data[12]=='I' && data[13]=='H' && data[14]=='D' && data[15]=='R'); +} + +static int verify_key(int pat_idx) { + pattern_t *p = &patterns[pat_idx]; + FILE *f = fopen(p->sample_path, "rb"); + if (!f) return 1; /* can't verify, assume ok */ + + unsigned char hdr[15]; + if (fread(hdr, 1, 15, f) != 15) { fclose(f); return 1; } + uint32_t aes_size; + memcpy(&aes_size, hdr + 6, 4); + /* PKCS7: extra padding block when aes_size is 16-byte aligned */ + uint32_t ct_size = (aes_size % 16 == 0) + ? aes_size + 16 + : ((aes_size + 15) / 16) * 16; + if (ct_size > 10 * 1024 * 1024) { fclose(f); return 1; } + + unsigned char *ct = malloc(ct_size); + size_t rd = fread(ct, 1, ct_size, f); + fclose(f); + if (rd < ct_size) { free(ct); return 1; } + + unsigned char *pt = malloc(ct_size); + size_t moved; + CCCryptorStatus st = CCCrypt(kCCDecrypt, kCCAlgorithmAES128, + kCCOptionECBMode, p->key, 16, NULL, + ct, ct_size, pt, ct_size, &moved); + free(ct); + + if (st != kCCSuccess || moved < 16) { free(pt); return 0; } + + /* Deep validation based on image type */ + int ok = 0; + if (pt[0] == 0xFF && pt[1] == 0xD8) + ok = verify_jpeg_chain(pt, moved); + else if (pt[0] == 0x89 && pt[1] == 0x50) + ok = verify_png_chain(pt, moved); + else if (pt[0] == 'G' && pt[1] == 'I' && pt[2] == 'F') + ok = (moved >= 6 && pt[3] == '8' && (pt[4]=='9'||pt[4]=='7') && pt[5]=='a'); + else if (pt[0] == 'R' && pt[1] == 'I') + ok = (moved >= 12 && pt[8]=='W' && pt[9]=='E' && pt[10]=='B' && pt[11]=='P'); + + free(pt); + return ok; +} + +/* ---- Memory scanning ---- */ + +/* + * Multi-block scan: for each candidate key, decrypt ALL unsolved + * CT blocks in one CCCrypt call (ECB processes blocks independently). + */ +static int g_task_fail_warned = 0; + +static int scan_pid(pid_t pid) { + mach_port_t task; + kern_return_t kr = task_for_pid(mach_task_self(), pid, &task); + if (kr != KERN_SUCCESS) { + if (!g_task_fail_warned) { + g_task_fail_warned = 1; + fprintf(stderr, + " WARNING: task_for_pid(%d) failed (kr=%d).\n" + " Cannot read WeChat memory. Checklist:\n" + " 1. Run with sudo\n" + " 2. Enable Developer Mode: Settings > Privacy & Security > Developer Mode\n" + " 3. Grant Terminal Full Disk Access: Settings > Privacy & Security > Full Disk Access\n" + " 4. If still failing, try: sudo DevToolsSecurity -enable\n" + " 5. Last resort: disable SIP (boot to Recovery, run: csrutil disable)\n", + pid, kr); + } + return 0; + } + + /* Build batch CT buffer for unsolved patterns */ + int unsolved_idx[MAX_PATTERNS]; + int n_unsolved = 0; + for (int i = 0; i < npatterns; i++) + if (!patterns[i].solved) unsolved_idx[n_unsolved++] = i; + if (n_unsolved == 0) { + mach_port_deallocate(mach_task_self(), task); + return 0; + } + + unsigned char *batch_ct = malloc(n_unsolved * 16); + unsigned char *batch_pt = malloc(n_unsolved * 16); + if (!batch_ct || !batch_pt) { + free(batch_ct); + free(batch_pt); + mach_port_deallocate(mach_task_self(), task); + return 0; + } + for (int i = 0; i < n_unsolved; i++) + memcpy(batch_ct + i*16, patterns[unsolved_idx[i]].ct, 16); + + mach_vm_address_t addr = 0; + mach_vm_size_t rsize; + vm_region_basic_info_data_64_t info; + mach_msg_type_number_t count; + mach_port_t obj = MACH_PORT_NULL; + + long regions = 0, found_this_pid = 0; + long long total_bytes = 0, tests = 0; + + while (!stop_flag) { + count = VM_REGION_BASIC_INFO_COUNT_64; + kr = mach_vm_region(task, &addr, &rsize, VM_REGION_BASIC_INFO_64, + (vm_region_info_t)&info, &count, &obj); + if (kr != KERN_SUCCESS) break; + regions++; + if (obj != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), obj); + obj = MACH_PORT_NULL; + } + + if ((info.protection & VM_PROT_READ) && rsize > 0 && rsize < REGION_MAX) { + vm_offset_t data; + mach_msg_type_number_t data_cnt; + kr = mach_vm_read(task, addr, rsize, &data, &data_cnt); + if (kr == KERN_SUCCESS) { + unsigned char *buf = (unsigned char *)data; + total_bytes += data_cnt; + + /* Method 1: every 16-byte aligned position (raw binary keys) */ + for (mach_msg_type_number_t j = 0; + j + 16 <= data_cnt && !stop_flag; j += 16) { + tests++; + size_t moved; + CCCryptorStatus st = CCCrypt( + kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode, + buf + j, 16, NULL, + batch_ct, n_unsolved * 16, + batch_pt, n_unsolved * 16, &moved); + if (st != kCCSuccess) continue; + + for (int p = 0; p < n_unsolved; p++) { + if (is_image_magic(batch_pt + p*16)) { + if (is_rejected(buf + j)) continue; + int idx = unsolved_idx[p]; + memcpy(patterns[idx].key, buf + j, 16); + patterns[idx].solved = 1; + + char kh[33]; bytes2hex(buf + j, 16, kh); + char ch[33]; bytes2hex(patterns[idx].ct, 16, ch); + printf("\n *** FOUND KEY: %s ***\n", kh); + printf(" Pattern: %s (%d files)\n", + ch, patterns[idx].file_count); + printf(" PID %d, addr=0x%llx+0x%x\n", + pid, addr, j); + + /* Cross-check: does this key solve OTHER patterns? */ + for (int q = 0; q < n_unsolved; q++) { + if (q == p || patterns[unsolved_idx[q]].solved) + continue; + unsigned char tpt[16]; + size_t tm; + CCCrypt(kCCDecrypt, kCCAlgorithmAES128, + kCCOptionECBMode, buf + j, 16, NULL, + patterns[unsolved_idx[q]].ct, 16, + tpt, 16, &tm); + if (is_image_magic(tpt)) { + int qi = unsolved_idx[q]; + memcpy(patterns[qi].key, buf + j, 16); + patterns[qi].solved = 1; + char qch[33]; + bytes2hex(patterns[qi].ct, 16, qch); + printf(" Also solves: %s (%d files)\n", + qch, patterns[qi].file_count); + } + } + + found_this_pid++; + /* Rebuild batch for remaining unsolved */ + n_unsolved = 0; + for (int i = 0; i < npatterns; i++) + if (!patterns[i].solved) + unsolved_idx[n_unsolved++] = i; + for (int i = 0; i < n_unsolved; i++) + memcpy(batch_ct + i*16, + patterns[unsolved_idx[i]].ct, 16); + if (n_unsolved == 0) goto done; + break; /* restart block check with new batch */ + } + } + } + + /* Method 2: ASCII [a-z0-9]{16+} at unaligned positions */ + int run = 0, run_start = 0; + for (mach_msg_type_number_t j = 0; + j <= data_cnt && !stop_flag; j++) { + int is_hex = (j < data_cnt) && + ((buf[j]>='a' && buf[j]<='z') || + (buf[j]>='0' && buf[j]<='9')); + if (is_hex) { + if (!run) run_start = j; + run++; + } else { + if (run >= 16) { + for (int k = run_start; k+16 <= run_start+run; k++) { + if (k % 16 == 0) continue; /* already tested */ + tests++; + size_t moved; + CCCrypt(kCCDecrypt, kCCAlgorithmAES128, + kCCOptionECBMode, buf+k, 16, NULL, + batch_ct, n_unsolved*16, + batch_pt, n_unsolved*16, &moved); + for (int p = 0; p < n_unsolved; p++) { + if (is_image_magic(batch_pt + p*16)) { + if (is_rejected(buf+k)) continue; + int idx = unsolved_idx[p]; + memcpy(patterns[idx].key, buf+k, 16); + patterns[idx].solved = 1; + char kh[33]; bytes2hex(buf+k, 16, kh); + char ch[33]; + bytes2hex(patterns[idx].ct, 16, ch); + printf("\n *** FOUND KEY: %s ***\n", kh); + printf(" Pattern: %s (%d files)\n", + ch, patterns[idx].file_count); + printf(" ASCII context: %.32s\n", + buf + run_start); + found_this_pid++; + /* Rebuild */ + n_unsolved = 0; + for (int i = 0; i < npatterns; i++) + if (!patterns[i].solved) + unsolved_idx[n_unsolved++] = i; + for (int i = 0; i < n_unsolved; i++) + memcpy(batch_ct + i*16, + patterns[unsolved_idx[i]].ct, 16); + if (n_unsolved == 0) goto done; + break; + } + } + } + } + run = 0; + } + } + + /* Method 3 (deep mode): byte-by-byte scan for top priority patterns */ + if (g_deep_mode && n_unsolved > 0) { + /* Build priority batch: top N unsolved by file_count */ + int prio_idx[DEEP_PRIORITY_MAX]; + int n_prio = 0; + for (int i = 0; i < n_unsolved && n_prio < DEEP_PRIORITY_MAX; i++) { + int pi = unsolved_idx[i]; + if (patterns[pi].file_count >= 10) + prio_idx[n_prio++] = pi; + } + if (n_prio > 0) { + unsigned char prio_ct[DEEP_PRIORITY_MAX * 16]; + unsigned char prio_pt[DEEP_PRIORITY_MAX * 16]; + for (int i = 0; i < n_prio; i++) + memcpy(prio_ct + i*16, patterns[prio_idx[i]].ct, 16); + + for (mach_msg_type_number_t j = 0; + j + 16 <= data_cnt && !stop_flag; j++) { + if (j % 16 == 0) continue; /* already tested in Method 1 */ + tests++; + size_t moved; + CCCryptorStatus st = CCCrypt( + kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode, + buf + j, 16, NULL, + prio_ct, n_prio * 16, + prio_pt, n_prio * 16, &moved); + if (st != kCCSuccess) continue; + + for (int p = 0; p < n_prio; p++) { + if (!is_image_magic(prio_pt + p*16)) continue; + if (is_rejected(buf + j)) continue; + int idx = prio_idx[p]; + if (patterns[idx].solved) continue; + memcpy(patterns[idx].key, buf + j, 16); + patterns[idx].solved = 1; + + char kh[33]; bytes2hex(buf + j, 16, kh); + char ch[33]; bytes2hex(patterns[idx].ct, 16, ch); + printf("\n *** FOUND KEY (deep): %s ***\n", kh); + printf(" Pattern: %s (%d files)\n", + ch, patterns[idx].file_count); + printf(" PID %d, addr=0x%llx+0x%x (unaligned)\n", + pid, addr, j); + found_this_pid++; + + /* Cross-check against all unsolved */ + for (int q = 0; q < n_unsolved; q++) { + int qi = unsolved_idx[q]; + if (qi == idx || patterns[qi].solved) continue; + unsigned char tpt[16]; + size_t tm; + CCCrypt(kCCDecrypt, kCCAlgorithmAES128, + kCCOptionECBMode, buf + j, 16, NULL, + patterns[qi].ct, 16, tpt, 16, &tm); + if (is_image_magic(tpt)) { + memcpy(patterns[qi].key, buf + j, 16); + patterns[qi].solved = 1; + char qch[33]; + bytes2hex(patterns[qi].ct, 16, qch); + printf(" Also solves: %s (%d files)\n", + qch, patterns[qi].file_count); + } + } + + /* Rebuild main batch */ + n_unsolved = 0; + for (int i = 0; i < npatterns; i++) + if (!patterns[i].solved) + unsolved_idx[n_unsolved++] = i; + for (int i = 0; i < n_unsolved; i++) + memcpy(batch_ct + i*16, + patterns[unsolved_idx[i]].ct, 16); + /* Rebuild priority batch */ + n_prio = 0; + for (int i = 0; i < n_unsolved && n_prio < DEEP_PRIORITY_MAX; i++) { + int pi2 = unsolved_idx[i]; + if (patterns[pi2].file_count >= 10) + prio_idx[n_prio++] = pi2; + } + for (int i = 0; i < n_prio; i++) + memcpy(prio_ct + i*16, patterns[prio_idx[i]].ct, 16); + if (n_unsolved == 0) goto done; + break; + } + } + } + } + + done: + mach_vm_deallocate(mach_task_self(), data, data_cnt); + if (n_unsolved == 0) break; + } + } + addr += rsize; + if (regions % 500 == 0) { + printf(" [%ld regions, %lld MB, %lld tests]\r", + regions, total_bytes/(1024*1024), tests); + fflush(stdout); + } + } + + printf(" PID %d: %ld regions, %lld MB, %lld tests, %ld keys found \n", + pid, regions, total_bytes/(1024*1024), tests, found_this_pid); + + free(batch_ct); + free(batch_pt); + mach_port_deallocate(mach_task_self(), task); + return (int)found_this_pid; +} + +/* ---- Save results ---- */ +static void save_keys(const char *dir) { + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s/image_keys.json", dir); + + int solved = 0; + for (int i = 0; i < npatterns; i++) + if (patterns[i].solved) solved++; + if (solved == 0) return; + + FILE *f = fopen(path, "w"); + if (!f) { fprintf(stderr, "Cannot write %s\n", path); return; } + + fprintf(f, "{\n"); + int first = 1; + for (int i = 0; i < npatterns; i++) { + if (!patterns[i].solved) continue; + char ct_hex[33], key_hex[33]; + bytes2hex(patterns[i].ct, 16, ct_hex); + bytes2hex(patterns[i].key, 16, key_hex); + fprintf(f, "%s \"%s\": \"%s\"", + first ? "" : ",\n", ct_hex, key_hex); + first = 0; + } + fprintf(f, "\n}\n"); + fclose(f); + printf("\nSaved %d keys to %s\n", solved, path); +} + +/* ---- Load existing keys from image_keys.json ---- */ +static int load_keys(const char *dir) { + char path[MAX_PATH]; + snprintf(path, sizeof(path), "%s/image_keys.json", dir); + FILE *f = fopen(path, "r"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + if (sz <= 0) { fclose(f); return 0; } + fseek(f, 0, SEEK_SET); + char *json = malloc((size_t)sz + 1); + if (!json) { fclose(f); return 0; } + size_t rd = fread(json, 1, (size_t)sz, f); + if (rd != (size_t)sz) { + free(json); + fclose(f); + return 0; + } + fclose(f); + json[rd] = '\0'; + + int loaded = 0; + /* Parse "ct_hex": "key_hex" pairs */ + const char *p = json; + while ((p = strchr(p, '"')) != NULL) { + p++; + const char *ct_end = strchr(p, '"'); + if (!ct_end || ct_end - p != 32) { p = ct_end ? ct_end + 1 : p; continue; } + char ct_str[33]; memcpy(ct_str, p, 32); ct_str[32] = '\0'; + unsigned char ct[16]; + if (hex2bytes(ct_str, ct, 16) != 16) { p = ct_end + 1; continue; } + + p = ct_end + 1; + p = strchr(p, '"'); + if (!p) break; + p++; + const char *key_end = strchr(p, '"'); + if (!key_end || key_end - p != 32) { p = key_end ? key_end + 1 : p; continue; } + char key_str[33]; memcpy(key_str, p, 32); key_str[32] = '\0'; + unsigned char key[16]; + if (hex2bytes(key_str, key, 16) != 16) { p = key_end + 1; continue; } + + /* Match to pattern */ + for (int i = 0; i < npatterns; i++) { + if (!patterns[i].solved && memcmp(patterns[i].ct, ct, 16) == 0) { + memcpy(patterns[i].key, key, 16); + patterns[i].solved = 1; + loaded++; + break; + } + } + p = key_end + 1; + } + free(json); + return loaded; +} + +/* ---- Main ---- */ +int main(int argc, char *argv[]) { + signal(SIGINT, sigint_handler); + + printf("=== WeChat V2 Image Key Scanner ===\n\n"); + if (getuid() != 0) { + fprintf(stderr, "ERROR: Run with sudo!\n"); return 1; + } + + /* Determine image directory */ + char image_dir[MAX_PATH] = ""; + char exe_dir[MAX_PATH] = "."; + int deep_mode = 0; + const char *last_slash = strrchr(argv[0], '/'); + if (last_slash) { + int len = (int)(last_slash - argv[0]); + snprintf(exe_dir, sizeof(exe_dir), "%.*s", len, argv[0]); + } + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--deep") == 0) + deep_mode = 1; + else if (image_dir[0] == '\0') { + strncpy(image_dir, argv[i], sizeof(image_dir) - 1); + image_dir[sizeof(image_dir) - 1] = '\0'; + } + } + + if (image_dir[0] == '\0') { + /* Read config.json */ + char cfg_path[MAX_PATH]; + snprintf(cfg_path, sizeof(cfg_path), "%s/config.json", exe_dir); + FILE *cf = fopen(cfg_path, "r"); + if (cf) { + fseek(cf, 0, SEEK_END); + long sz = ftell(cf); + if (sz <= 0) { fclose(cf); return 1; } + fseek(cf, 0, SEEK_SET); + char *json = malloc((size_t)sz + 1); + if (!json) { fclose(cf); return 1; } + size_t rd = fread(json, 1, (size_t)sz, cf); + if (rd != (size_t)sz) { + free(json); + fclose(cf); + return 1; + } + json[rd] = '\0'; + fclose(cf); + char db_dir[MAX_PATH]; + if (json_get_string(json, "db_dir", db_dir, sizeof(db_dir))) { + char *s = strrchr(db_dir, '/'); + if (!s) s = strrchr(db_dir, '\\'); + if (s) { + int plen = (int)(s - db_dir); + snprintf(image_dir, sizeof(image_dir), + "%.*s/msg", plen, db_dir); + } + } + free(json); + } + } + + /* Auto-detect: scan ~/Library/Containers/com.tencent.xinWeChat */ + if (image_dir[0] == '\0') { + const char *home = getenv("HOME"); + if (!home) home = "/Users"; + char base[MAX_PATH]; + snprintf(base, sizeof(base), + "%s/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files", + home); + DIR *d = opendir(base); + if (d) { + struct dirent *ent; + while ((ent = readdir(d))) { + if (ent->d_name[0] == '.') continue; + char candidate[MAX_PATH]; + snprintf(candidate, sizeof(candidate), "%s/%s/msg", base, ent->d_name); + struct stat st; + if (stat(candidate, &st) == 0 && S_ISDIR(st.st_mode)) { + strncpy(image_dir, candidate, sizeof(image_dir) - 1); + printf("Auto-detected image directory:\n %s\n\n", image_dir); + break; + } + } + closedir(d); + } + } + + if (image_dir[0] == '\0') { + fprintf(stderr, "ERROR: Cannot determine image directory.\n"); + fprintf(stderr, "Tried:\n"); + fprintf(stderr, " 1. Command line argument\n"); + fprintf(stderr, " 2. config.json db_dir\n"); + fprintf(stderr, " 3. Auto-detect ~/Library/Containers/com.tencent.xinWeChat/...\n\n"); + fprintf(stderr, "Usage: sudo %s [--deep] [image_dir]\n", argv[0]); + fprintf(stderr, " image_dir: path to .../xwechat_files//msg\n"); + return 1; + } + + /* Phase 1: Discover patterns */ + printf("Discovering encryption patterns in:\n %s\n\n", image_dir); + discover_dir(image_dir); + if (npatterns == 0) { + fprintf(stderr, "No V2 .dat files found!\n"); return 1; + } + qsort(patterns, npatterns, sizeof(pattern_t), cmp_patterns); + + int total_covered = 0; + printf("Found %d patterns across %d V2 files:\n", npatterns, total_v2_files); + for (int i = 0; i < npatterns; i++) { + char ch[33]; bytes2hex(patterns[i].ct, 16, ch); + printf(" #%-2d %s (%d files)\n", i+1, ch, patterns[i].file_count); + total_covered += patterns[i].file_count; + } + if (total_covered < total_v2_files) + printf(" ... and %d files in overflow patterns\n", + total_v2_files - total_covered); + + /* Load previously found keys */ + int preloaded = load_keys(exe_dir); + if (preloaded > 0) + printf("\nLoaded %d existing keys from image_keys.json\n", preloaded); + + if (deep_mode) { + g_deep_mode = 1; + printf("\n*** DEEP MODE: byte-by-byte scan for top %d unsolved patterns ***\n", + DEEP_PRIORITY_MAX); + } + + /* Phase 2: Continuous scanning */ + printf("\nScanning WeChat memory — keep browsing images! (Ctrl+C to stop)\n"); + int round = 0; + while (!stop_flag) { + int unsolved = 0; + for (int i = 0; i < npatterns; i++) + if (!patterns[i].solved) unsolved++; + if (unsolved == 0) break; + + round++; + pid_t pids[64]; + int npids = get_wechat_pids(pids, 64); + if (npids == 0) { + printf(" No WeChat processes found, waiting...\n"); + sleep(3); + continue; + } + + printf("\n--- Round %d: %d unsolved / %d total, %d PIDs ---\n", + round, unsolved, npatterns, npids); + + int found_round = 0; + for (int i = 0; i < npids && !stop_flag; i++) { + found_round += scan_pid(pids[i]); + } + + unsolved = 0; + int solved_files = 0; + for (int i = 0; i < npatterns; i++) { + if (patterns[i].solved) solved_files += patterns[i].file_count; + else unsolved++; + } + + if (found_round > 0) { + printf("\n Progress: %d/%d patterns solved (%d/%d files)\n", + npatterns - unsolved, npatterns, + solved_files, total_v2_files); + /* Verify newly found keys */ + for (int i = 0; i < npatterns; i++) { + if (patterns[i].solved && !verify_key(i)) { + char kh[33]; bytes2hex(patterns[i].key, 16, kh); + printf(" REJECTED: %s (failed verification)\n", kh); + add_rejected(patterns[i].key); + patterns[i].solved = 0; + memset(patterns[i].key, 0, 16); + } + } + /* Save after each find */ + save_keys(exe_dir); + } + + if (unsolved > 0 && !stop_flag) { + printf(" Keep browsing images in different chats...\n"); + sleep(1); + } + } + + /* Phase 3: Summary */ + save_keys(exe_dir); + + int solved = 0, solved_files = 0; + for (int i = 0; i < npatterns; i++) { + if (patterns[i].solved) { + solved++; + solved_files += patterns[i].file_count; + } + } + + printf("\n==================================================\n"); + if (solved == npatterns) { + printf("ALL %d patterns solved! (%d files)\n", npatterns, total_v2_files); + } else { + printf("%d/%d patterns solved (%d/%d files)\n", + solved, npatterns, solved_files, total_v2_files); + printf("Unsolved:\n"); + for (int i = 0; i < npatterns; i++) { + if (patterns[i].solved) continue; + char ch[33]; bytes2hex(patterns[i].ct, 16, ch); + printf(" %s (%d files)\n", ch, patterns[i].file_count); + } + } + + /* Count unique keys */ + int unique_keys = 0; + for (int i = 0; i < npatterns; i++) { + if (!patterns[i].solved) continue; + int dup = 0; + for (int j = 0; j < i; j++) + if (patterns[j].solved && + memcmp(patterns[i].key, patterns[j].key, 16) == 0) { dup = 1; break; } + if (!dup) unique_keys++; + } + printf("%d unique key(s) found.\n", unique_keys); + printf("==================================================\n"); + + return (solved > 0) ? 0 : 1; +}