wx-cli/find_image_key.c

911 lines
36 KiB
C

/*
* 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 <image_dir> # explicit image directory
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <time.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <unistd.h>
#include <CommonCrypto/CommonCryptor.h>
#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: hex string [0-9a-f]{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]<='f') ||
(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/<wxid>/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;
}