mirror of https://github.com/jackwener/wx-cli.git
Merge PR #15: feat: macOS 图片密钥扫描器 + 批量解密器 (C)
新增 find_image_key.c 和 decrypt_images.c, 通过 Mach VM API + CommonCrypto 实现 macOS 图片解密。 Co-authored-by: bbingzfeat/daemon-cli
commit
5879b58239
25
README.md
25
README.md
|
|
@ -130,6 +130,29 @@ python find_image_key.py
|
||||||
|
|
||||||
> **注意**: AES 密钥仅在微信查看图片时临时加载到内存中。如果扫描未找到密钥,请先在微信中查看几张图片,然后立即重新运行脚本。
|
> **注意**: AES 密钥仅在微信查看图片时临时加载到内存中。如果扫描未找到密钥,请先在微信中查看几张图片,然后立即重新运行脚本。
|
||||||
|
|
||||||
|
#### macOS 图片解密
|
||||||
|
|
||||||
|
macOS 上使用 C 版工具(通过 Mach VM API + CommonCrypto,性能比 Python 高 100 倍):
|
||||||
|
|
||||||
|
**前置条件:**
|
||||||
|
- Xcode Command Line Tools: `xcode-select --install`
|
||||||
|
- 微信需要 ad-hoc 签名:`sudo codesign --force --deep --sign - /Applications/WeChat.app`
|
||||||
|
- 开发者模式:系统设置 → 隐私与安全 → 开发者模式 → 开启
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
`find_image_key` 会自动发现所有未解密的 V2 图片 pattern,持续扫描微信进程内存。当用户在微信中浏览图片时捕获密钥,保存到 `image_keys.json`。支持 `--deep` 模式进行逐字节深度扫描。
|
||||||
|
|
||||||
## 文件说明
|
## 文件说明
|
||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|
|
@ -146,6 +169,8 @@ python find_image_key.py
|
||||||
| `find_image_key_monitor.py` | 持续监控版密钥提取(推荐) |
|
| `find_image_key_monitor.py` | 持续监控版密钥提取(推荐) |
|
||||||
| `latency_test.py` | 延迟测量诊断工具 |
|
| `latency_test.py` | 延迟测量诊断工具 |
|
||||||
| `find_all_keys_macos.c` | macOS 版内存密钥扫描器 (C, Mach VM API) |
|
| `find_all_keys_macos.c` | macOS 版内存密钥扫描器 (C, Mach VM API) |
|
||||||
|
| `find_image_key.c` | macOS 版图片密钥扫描器 (C, 持续监控模式) |
|
||||||
|
| `decrypt_images.c` | macOS 版批量图片解密器 (C, 多密钥支持) |
|
||||||
|
|
||||||
## 技术细节
|
## 技术细节
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,616 @@
|
||||||
|
/*
|
||||||
|
* 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 <key_hex> <image_dir> <out_dir> # single-key manual
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <CommonCrypto/CommonCryptor.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 and validate header claims fit within it */
|
||||||
|
long cur_pos = ftell(fin);
|
||||||
|
fseek(fin, 0, SEEK_END);
|
||||||
|
long file_size = ftell(fin);
|
||||||
|
fseek(fin, cur_pos, SEEK_SET);
|
||||||
|
|
||||||
|
if ((long)aes_ct_size + (long)xor_size > file_size - HEADER_SIZE) {
|
||||||
|
fclose(fin);
|
||||||
|
return -6; /* header claims more data than file contains */
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* Try multi-key lookup (image_keys.json) first, then fall back to provided key */
|
||||||
|
if (aes_ct_size >= 16) {
|
||||||
|
const unsigned char *mk = find_key_for_ct(aes_ct);
|
||||||
|
if (mk) aes_key = mk;
|
||||||
|
}
|
||||||
|
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 fallback_key — decrypt_v2_file tries
|
||||||
|
* image_keys.json lookup first, falls back to this key if provided */
|
||||||
|
const unsigned char *key = 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,917 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
* WeChat may store the AES key as a hex-encoded ASCII string
|
||||||
|
* in memory (e.g. "cfcd208495d565ef" = 16 ASCII bytes).
|
||||||
|
* We use the raw ASCII bytes directly as the 16-byte AES key,
|
||||||
|
* since the key is arbitrary bytes and the hex representation
|
||||||
|
* itself is 16 bytes for a 64-bit key half. */
|
||||||
|
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);
|
||||||
|
int ctx_len = data_cnt - run_start;
|
||||||
|
if (ctx_len > 32) ctx_len = 32;
|
||||||
|
printf(" ASCII context: %.*s\n",
|
||||||
|
ctx_len, 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue