fix: PR #107 后续清理 (security/正确性/一致性)
针对 4 个 review agent 在 PR #107 (5649 行巨型 PR) 找到的关键问题做最小 侵入修复。已合并代码本身能跑,这次是收紧 security + 消重 + 文档一致性。 ## 安全修复 ### wxwork_keys.json 落盘权限 (find_wxwork_keys.py) 含明文 16-byte raw key 的 keys 文件,之前 default umask 落盘。改成: 1. 写 tmp 文件 2. chmod 0o600 (Unix 严格 owner-only; Windows 上 chmod 控制只读位, 至少避免世界可读最差情况) 3. atomic rename 旧产物自然过期,新生成的都受保护。 ### SNS XXE 防护 (export_sns.py) 朋友圈 XML 来源是不可信输入(他人发的 content),原 `ET.fromstring()` 完全没过滤,可被恶意 entity expansion / 外部实体引用攻击。加跟 `mcp_server._XML_UNSAFE_RE` 同模式的过滤(拒 `<!DOCTYPE>` / `<!ENTITY>`) + 200KB 大小上限。`_parse_timeline_xml` 检查后才进 ET.fromstring。 ## 正确性 / 消重 ### AES 对齐公式统一 (decode_image.py + decrypt_sns.py + export_sns.py) 原本三处各写一份: - decode_image.py: aes_size -= ~(~aes_size % 16) ← bitwise trick - decrypt_sns.py: 同上 - export_sns.py: aes_size + (16 - aes_size%16) if … else aes_size+16 两个公式数学等价(对 0/1/15/16/17/100/1000/12345 全部验证一致),但 bitwise trick 难读且漂移风险高。抽 `aligned_aes_block_size()` 到 decode_image.py 作 canonical 实现, 另两处 import 复用。 ### 32-bit pointer 假设明确化 (find_wxwork_keys.py) reviewer 担心 `_read_u32` 在 64-bit 进程上错位,实测 WXWork.exe 5.0.x 是 **32-bit 进程** (`Program Files (x86)\WXWork\` + PE Machine = x86), 所以 4 字节读指针是对的。加注释明确这个假设,腾讯如果升级到 64-bit 要重做整套逆向, 当前实测全部 17 db 解密通过印证。 ## 一致性 ### main.py show_status() 走 _config_file_path() (main.py) 原硬编码 `config_file = "config.json"` 绕开 PR #107 新引入的 `_config_file_path()`,打包成 exe 后 cwd 不一定是 exe 目录,会读到错 位置。改成 `from config import _config_file_path`。 ### EXE_USAGE.md 输出目录写错 (EXE_USAGE.md) EXE_USAGE 说导出到 `export/`,代码实际 `output_base_dir = wechat_files/ <wxid>/`,联系人下还是 `messages.csv/html/json` 而不是 `message_0.db.csv`。修正成真实结构。 ## 文档 README 加两段: - 安全提示: keys 文件 chmod 0600 + 不要 commit 到 git - 朋友圈 XML XXE 防护说明 ## 测试 185/185 通过 (含已有 wxsqlite3 roundtrip + image v2 + msg types filter + pagination hint + chat export helpers 等)。 aligned_aes_block_size 单独验证跟旧公式等价(0/1/15/16/17/100/1000/12345)。 ## 未跟进 (后续 follow-up issue) - 3 处 V1/V2/XOR 解密代码完全重复(decode_image / decrypt_sns / export_messages 各自实现)——抽出来工作量大,本次先抽 helper 不动 完整解密路径,后续单独 PR - export_messages HTML base64 内联图片可能爆几 GB,应改成可选 flag - SNS / wxwork export / batch_decrypt_images / voice_to_mp3 测试缺位 (0 个 test)
This commit is contained in:
@@ -357,6 +357,11 @@ def _wxwork_page_size_chain(memory_regions, starts, cipher_addr):
|
||||
raw_key = cipher + 0x08
|
||||
aes_ctx = *(cipher + 0x2c)
|
||||
page_size = *(*(*(cipher + 0x30) + 0x04) + 0x24)
|
||||
|
||||
NOTE: WXWork.exe (5.0.x) 是 **32-bit** 进程,所有指针 = 4 字节 (`_read_u32`)。
|
||||
腾讯如果升级到 64-bit (`Program Files\WXWork\`),结构偏移和指针大小都要重做
|
||||
逆向,这套代码会直接失效——届时需要扫描入口加 IsWow64Process 检测并给出
|
||||
友好报错。当前实测 5.0.8.6009 全部 17 个 db 解密成功,该 32-bit 假设有效。
|
||||
"""
|
||||
page_size_holder = _read_u32(memory_regions, starts, cipher_addr + 0x30)
|
||||
if page_size_holder is None or not _valid_ptr(memory_regions, starts, page_size_holder, 8):
|
||||
@@ -500,9 +505,18 @@ def save_wxwork_results(db_files, salt_to_dbs, key_map, db_dir, out_file, print_
|
||||
raise RuntimeError("未能从任何企业微信进程中提取到密钥")
|
||||
|
||||
result["_db_dir"] = db_dir
|
||||
with open(out_file, 'w', encoding='utf-8') as f:
|
||||
# 写文件含明文 raw key,先 atomic write 到 tmp 再 rename,中途 chmod 0600
|
||||
# 限本用户读写。Windows 上 os.chmod 主要控制只读位,严格 ACL 需 win32security
|
||||
# 这里至少避免世界可读的最差情况。
|
||||
tmp_file = out_file + ".tmp"
|
||||
with open(tmp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=2, ensure_ascii=False)
|
||||
print_fn(f"\n密钥保存到: {out_file}")
|
||||
try:
|
||||
os.chmod(tmp_file, 0o600)
|
||||
except OSError:
|
||||
pass # Windows 上某些场景 chmod 可能失败,不阻塞主流程
|
||||
os.replace(tmp_file, out_file)
|
||||
print_fn(f"\n密钥保存到: {out_file} (权限已收紧为 0600)")
|
||||
|
||||
missing = [rel for rel, path, sz, salt_hex, page1 in db_files if salt_hex not in key_map]
|
||||
if missing:
|
||||
|
||||
Reference in New Issue
Block a user