吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9047|回复: 73
收起左侧

[原创] 4.0微信朋友圈媒体解密全解析

  [复制链接]
lyxhhh 发表于 2026-2-18 10:40
本帖最后由 lyxhhh 于 2026-2-18 11:00 编辑

本文记录了 WeFlow) 项目中实现朋友圈图片/视频/实况照片解密的完整过程——从硬搬 DLL 函数来调用,到用纯 TypeScript 独立实现。

0x00 背景:朋友圈的图片为什么打不开?

如果你尝试直接访问朋友圈图片的 CDN 地址(形如
https://mmsns.qpic.cn/sns/...) ,你会发现下载下来的文件根本不是正常的 JPEG 或 PNG——它是 加密过的

微信服务器在返回媒体数据时,会在 HTTP 响应头里带上一个字段:

x-Enc: 1

这个 x-Enc: 1  就是在告诉客户端:"这个数据是加密的,你得解密后才能用。" 而解密需要一个 key(密钥),存储在朋友圈动态的 XML 数据里。

那加密用的是什么算法?图片、视频、实况照片的加密方式一样吗?这就是本文要逐一解答的问题。

0x01 媒体数据是怎么下载下来的?

在谈解密之前,首先要搞清楚:图片/视频的 URL、鉴权令牌和密钥从哪来,怎么请求?

一切数据都在本地数据库里

朋友圈的数据存储在微信本地的 WCDB 数据库 (~\xwechat_files\wxid***\db_storage\sns\sns.db)  中。每条动态都有一段 XML 格式的原始数据,里面包含了媒体信息。不同类型的动态,XML 结构有所不同:

普通图片动态:

<media>

  <url>http://mmsns.qpic.cn/sns/...</url>

  <thumb>http://mmsns.qpic.cn/sns/.../150</thumb>

  <token>xxxxxxxxxxxxxx</token>

  <key>123456</key>

</media>

视频动态(Type 15):

<media>

  <url>http://snsvideodownload.../video...</url>

  <thumb>http://vweixinthumb.../thumbnail...</thumb>

  <token>xxxxxxxxxxxxxx</token>

</media>

<!-- 注意:视频的密钥不在 media 里,而在外层的 enc 标签中 -->

<enc key="2105122989" />

实况照片(Live Photo):

<media>

  <url>http://mmsns.qpic.cn/sns/...</url>     <!-- 静态图片 -->

  <key>123456</key>

  <token>xxx</token>

  <livePhoto>

    <url>http://snsvideodownload.../...</url>  <!-- 视频部分 -->

    <token>xxx</token>

  </livePhoto>

</media>

<enc key="2105122989" /> <!-- 视频的密钥 -->

URL 修正

数据库中存的 URL 不能直接用,需要做几个处理:


// 1. HTTP → HTTPS

let fixedUrl = url.replace('http://', 'https://');

// 2. 图片:把缩略图 /150 改成 /0 获取原图(视频不需要)

if (!isVideo) {

    fixedUrl = fixedUrl.replace(/\/150($|\?)/, '/0$1');

}

// 3. 拼接 token(没有 token 服务器直接 403)

if (isVideo) {

    // 视频:token 必须放在参数最前面

    return `${baseUrl}?token=${token}&idx=1${existingParams}`;

} else {

    // 图片:token 追加到末尾

    return `${fixedUrl}${connector}token=${token}&idx=1`;

}

区分图片和视频 URL

微信用不同的域名/路径来分发图片和视频:

URL 特征 类型 说明
mmsns.qpic.cn/sns/... 图片 朋友圈图片
vweixinthumb... 缩略图 视频封面,本质是图片
snsvideodownload... 视频 视频文件

代码上用一个简单的判断:

const isVideoUrl = (url: string) => {

    if (url.includes('vweixinthumb')) return false;  // 排除视频缩略图

    return url.includes('snsvideodownload') || url.includes('video') || url.includes('.mp4');

};

请求头伪装

微信的 CDN 会检查 User-Agent,普通浏览器 UA 会被拒绝:


headers: {

    'User-Agent': 'MicroMessenger Client',

    'Accept': '*/*',

    'Connection': 'keep-alive'

}

图片请求可以加  Accept-Encoding: gzip, deflate, br  (CDN 可能对图片做 gzip 压缩返回)。但视频请求不要加,视频流通常不压缩,加上可能导致异常。

0x02 第一阶段:逆向小白,直接调 DLL

作为一个逆向小白,我最开始的思路非常朴素:既然微信客户端自己能解密,那我直接调用它的解密函数不就行了?

在 IDA 里找到解密函数

用 IDA Pro 打开 Weixin.dll(约 170MB),基地址 0x180000000。

第一步:搜字符串。 在 IDA 的字符串窗口里搜 x-Enc ,找到它位于地址 0x1885282ac

第二步:交叉引用。 右键 → "跳转到交叉引用",发现有 9 个函数引用了这个字符串。其中 sub_1845B0BC0 是 HTTP 响应头解析函数:


0x1845b1323  lea  rdx, aXEnc        ; "x-Enc"

0x1845b132e  call sub_1826819D0     ; 在响应头中查找

0x1845b1359  mov  [rcx+1428h], al   ; 把结果写到对象 +0x1428 偏移处

第三步:顺藤摸瓜。 在收包处理函数 sub_1845B4360 里,找到检查这个标志的代码:


0x1845b4473  cmp  byte ptr [rax+1428h], 1   ; 是否需要解密?

0x1845b447a  jnz  loc_1845B4525              ; 不需要就跳过

; ...

0x1845b44f3  call sub_182674280              ; 调用解密函数!

就这样,通过 字符串搜索 → 交叉引用 → 追踪调用链,定位到了真正的解密函数:sub_182674280
,位于 DLL 偏移 0x2674280

解密函数的反编译结果

IDA 反编译后,这个函数其实很短(约 0x134 字节),核心逻辑如下:


// sub_182674280 的简化伪代码

// 参数:rcx=输入缓冲, rdx=长度, r8=输出缓冲, r9=seed(密钥)

void decrypt(uint8_t* input, uint64_t length, uint8_t* output, uint64_t seed) {

    uint8_t state[4128];          // 巨大的状态缓冲区

    uint64_t keyblock;

    init_state(state, &seed, 1);  // 用 seed 初始化状态机

    for (int i = 0; i < length; i++) {

        if ((i & 7) == 0) {

            // 每 8 字节取一次新的密钥块

            uint64_t raw = get_next_keyblock(state);

            // 注意这里的字节序转换!

            keyblock = htonl(hi32(raw)) << 32 | htonl(lo32(raw));

        }

        output[i] = input[i] ^ ((uint8_t*)&keyblock)[i & 7];

    }

}

核心就是:用 seed 初始化一个状态机,持续产生 8 字节密钥流,然后逐字节 XOR。 标准的流密码结构。

用 koffi(FFI)直接调用

既然找到了函数地址,最简单的做法是直接调用它。用 koffi(Node.js 的 FFI 库)加载Weixin.dll :


const WEIXIN_DLL_OFFSET = 0x2674280n;

// 加载 DLL,获取内存基地址

const koffi = require('koffi');

const weixinLib = koffi.load(dllPath);

const kernel32 = koffi.load('kernel32.dll');

const getModuleHandleW = kernel32.func(

    'void* __stdcall GetModuleHandleW(str16 lpModuleName)'

);

const modulePtr = getModuleHandleW('Weixin.dll');

const base = koffi.address(modulePtr) as bigint;

// 计算解密函数的绝对地址

const decryptAddr = base + WEIXIN_DLL_OFFSET;

// 解码为可调用的函数指针

const addrBox = new BigUint64Array(1);

addrBox[0] = decryptAddr;

const decryptPtr = koffi.decode(addrBox, 'void *');

const decryptProto = koffi.proto(

    'uint64 __fastcall SnsImageDecrypt(void* src, uint64 len, void* dst, uint64 key)'

);

const decryptFn = koffi.decode(decryptPtr, decryptProto);

// 调用解密

const out = Buffer.allocUnsafe(data.length);

decryptFn(data, BigInt(data.length), out, parsedKey);

// out 就是解密后的数据

这种方式的问题

问题 说明
依赖 Weixin.dll 用户必须本地安装了微信,且路径可找到
版本耦合 微信更新后函数偏移可能变化
平台限制 只能在 Windows 上运行
安全风险 加载 170MB 第三方 DLL 到自己进程
黑盒调用 完全不知道算法原理,出问题无法调试

0x03 第二阶段:搞清楚算法 —— ISAAC-64

关键线索:初始化函数里的魔数

深入看 sub_1845E3860(状态机初始化函数),一进函数就看到一组硬编码常量和特征操作:

// 8 个初始变量都被设为同一个值

a = b = c = d = e = f = g = h = 0x9e3779b97f4a7c15;

// mix 函数中的位移操作

a = (a - e);  f ^= (h >> 9);   h = h + a;

b = (b - f);  g ^= (a << 9);   a = a + b;

c = (c - g);  h ^= (b >> 23);  b = b + c;

d = (d - h);  a ^= (c << 15);  c = c + d;

e = (e - a);  b ^= (d >> 14);  d = d + e;

f = (f - b);  c ^= (e << 20);  e = e + f;

g = (g - c);  d ^= (f >> 17);  f = f + g;

h = (h - d);  e ^= (g << 14);  g = g + h;

0x9e3779b97f4a7c15 是黄金比例 φ 的 64 位定点表示。搜索这个常量加上那组位移量
9, 9, 23, 15, 14, 20, 17, 14,直接命中 —— 这是 ISAAC-64

ISAAC 全称 Indirection, Shift, Accumulate, Add, and Count,是密码学家 Bob Jenkins 在 1996 年发明的 CSPRNG(密码学安全伪随机数生成器)。ISAAC-64 是它的 64 位版本。

DLL 函数与 ISAAC-64 的对应关系

DLL 函数 作用 ISAAC-64 对应
sub_1845E3860 | 初始化(含 mix 操作) | randinit()
sub_1845E3430 生成一轮 256 个随机数 isaac64() 核心循环
sub_1845E33E0 | 从池中取下一个随机数    | rand()— 逆序消费 randrsl[]
sub_1845E36F0 | 拷贝 seed 入缓冲区  | seed()
sub_182674280 XOR 解密循环 应用层解密

核心循环 sub_1845E3430 里最有特征的一段:


// 四步循环,根据 i%4 使用不同位移

case 0: aa = ~(aa ^ (aa << 21));   // 取反 + 左移21

case 1: aa =   aa ^ (aa >> 5);     // 右移5

case 2: aa =   aa ^ (aa << 12);    // 左移12

case 3: aa =   aa ^ (aa >> 33);    // 右移33

对比ISAAC原始 C 实现的位移量完全一致。

一个容易忽略的细节:字节序

sub_182674280 的 XOR 循环中:


v12 = htonl(HIDWORD(v10));    // 高 32 位 → 大端序

v13 = htonl(v11);              // 低 32 位 → 大端序

v15 = (v13 << 32) | v12;      // 重新组合

每个 64 位密钥块在 XOR 之前会被转成大端序(Big-Endian)。这个细节如果搞错,解密出来就是乱码。

0x04 第三阶段:完全独立实现

理解了算法后,就可以彻底摆脱 DLL,用纯 TypeScript 重写。

ISAAC-64 的 TypeScript 实现

完整实现只有约 130 行代码,核心分三部分:

1. 初始化 init()


// 黄金比例 —— ISAAC-64 的签名常量

a = b = c = d = e = f = g = h = 0x9e3779b97f4a7c15n;

const mix = () => {

    a = (a - e) & MASK; f ^= (h >> 9n);  h = (h + a) & MASK;

    b = (b - f) & MASK; g ^= (a << 9n);  a = (a + b) & MASK;

    c = (c - g) & MASK; h ^= (b >> 23n); b = (b + c) & MASK;

    d = (d - h) & MASK; a ^= (c << 15n); c = (c + d) & MASK;

    e = (e - a) & MASK; b ^= (d >> 14n); d = (d + e) & MASK;

    f = (f - b) & MASK; c ^= (e << 20n); e = (e + f) & MASK;

    g = (g - c) & MASK; d ^= (f >> 17n); f = (f + g) & MASK;

    h = (h - d) & MASK; e ^= (g << 14n); g = (g + h) & MASK;

};

for (let i = 0; i < 4; i++) mix();  // 预混 4 轮

// 把种子混入并写入 256 槽的 mm[] 数组

for (let i = 0; i < 256; i += 8) {

    a = (a + randrsl[i]) & MASK;  // randrsl[0] = seed

    // ... b~h 同理

    mix();

    mm[i] = a; mm[i+1] = b; /* ... */ mm[i+7] = h;

}

2. 生成随机数 isaac64()


private isaac64() {

    this.cc = (this.cc + 1n) & MASK;

    this.bb = (this.bb + this.cc) & MASK;

    for (let i = 0; i < 256; i++) {

        let x = this.mm[i];

        switch (i & 3) {

            case 0: this.aa ^= ~(this.aa << 21n); break;

            case 1: this.aa ^=  (this.aa >> 5n);  break;

            case 2: this.aa ^=  (this.aa << 12n); break;

            case 3: this.aa ^=  (this.aa >> 33n); break;

        }

        this.aa = (this.mm[(i + 128) & 255] + this.aa) & MASK;

        const y = (this.mm[Number(x >> 3n) & 255] + this.aa + this.bb) & MASK;

        this.mm[i] = y;

        this.bb = (this.mm[Number(y >> 11n) & 255] + x) & MASK;

        this.randrsl[i] = this.bb;

    }

}

3. 生成大端序密钥流


public generateKeystreamBE(size: number): Buffer {

    const buffer = Buffer.allocUnsafe(size);

    for (let i = 0; i < Math.floor(size / 8); i++) {

        buffer.writeBigUInt64BE(this.getNext(), i * 8);

    }

    return buffer;

}

0x05 图片解密:全文件 XOR

图片的解密最简单——整个文件每个字节都被 XOR 加密

WCDB 数据库
│  解析 XML → 拿到 url, token, key

修正 URL (http→https, /150→/0, 拼 token)


HTTPS GET (UA: MicroMessenger Client, Accept-Encoding: gzip/br)


解压 gzip/br → 拿到加密的原始数据


检查响应头 x-Enc: 1 → 需要解密


ISAAC-64(media.key) → 生成与图片等长的密钥流


encrypted[i] ⊕ keystream[i] → 解密后的 JPEG/PNG/WebP


验证文件头魔数 (FF D8 FF = JPEG, 89 50 4E 47 = PNG, etc.)

核心代码:


const wasmService = WasmService.getInstance();

const keystream = await wasmService.getKeystream(key, raw.length);

const decrypted = Buffer.allocUnsafe(raw.length);

for (let i = 0; i < raw.length; i++) {

    decrypted[i] = raw[i] ^ keystream[i];

}

0x06 视频解密:只加密前 128KB

视频解密和图片有三个关键差异。

差异一:密钥来源不同

图片的密钥在 media[].key字段里,但视频的密钥藏在整条动态 XML 的 <enc key="..." /> 标签中,需要正则提取:


const extractVideoKey = (xml: string): string | undefined => {

    const match = xml.match(/<enc\s+key="(\d+)"/i);

    return match ? match[1] : undefined;

};

差异二:只加密前 128KB

视频文件动辄几十 MB,微信不可能对整个文件做流式 XOR。只加密头部就够了——MP4 的关键元数据(ftypmoov atom)都在前面,头部损坏就无法播放:


// 只生成 128KB 密钥流

const keystream = await wasmService.getKeystream(key, 131072);

// 只解密前 128KB(或文件长度,取较小值)

const decryptLen = Math.min(keystream.length, raw.length);

for (let i = 0; i < decryptLen; i++) {

    raw[i] ^= keystream[i];

}

// 验证:检查 offset 4 处是否为 'ftyp'

const ftyp = raw.subarray(4, 8).toString('ascii');

if (ftyp === 'ftyp') { /* 解密成功 */ }

差异三:下载方式不同

图片小,可以在内存里拼接。视频要先流式写入临时文件,下载完再读出来解密:


// 1. 下载到临时文件

const tmpPath = path.join(os.tmpdir(), `sns_video_${Date.now()}.enc`);

const fileStream = fs.createWriteStream(tmpPath);

res.pipe(fileStream);

fileStream.on('finish', async () => {

    // 2. 读出加密数据

    const raw = await readFile(tmpPath);

    // 3. 解密前 128KB

    const keystream = await wasmService.getKeystream(key, 131072);

    const decryptLen = Math.min(keystream.length, raw.length);

    for (let i = 0; i < decryptLen; i++) {

        raw[i] ^= keystream[i];

    }

    // 4. 写入缓存,删除临时文件

    await writeFile(cachePath, raw);

    await unlink(tmpPath);

});

另外视频请求不要设置 Accept-Encoding: gzip,视频流不走压缩。

0x07 实况照片:图片 + 视频的组合

实况照片本质上是一张静态图片 + 一段短视频的组合,分别用各自的逻辑处理。

数据结构

SnsMedia {
url: "静态图片 CDN"          ← 按图片流程处理
key: "123456"                ← 图片的解密密钥
token: "xxx"
livePhoto: {
url: "视频 CDN"          ← 按视频流程处理
token: "xxx"
key: (可能为空)
}
}
rawXml 中:<enc key="2105122989" /> ← 视频的解密密钥

密钥的优先级


// 图片部分:直接用 media.key

imageKey = m.key;

// 视频部分:优先用 XML 提取的 key,其次用 livePhoto 自带的 key,最后用 media.key

videoKey = xmlEncKey || m.livePhoto.key || m.key;

处理流程

实况照片

├── 静态图片部分
│   └── 走图片解密流程(全文件 XOR,key = media.key)

└── 视频部分
└── 走视频解密流程(前 128KB XOR,key = XML enc key)

0x08 三种媒体类型对比

图片 视频 实况照片
URL 域名 mmsns.qpic.cn   | snsvideodownload 图片 + 视频各一个
密钥来源 media[].key     | XML 中 <enc key>     | 图片用 media.key;视频用 XML key
加密范围 全文件 仅前 128KB 图片全文件;视频前 128KB
下载方式 内存收集 chunks 流式写临时文件 分别处理
gzip 解压 需要 不需要 图片需要;视频不需要
URL token 位置 追加到末尾 放在参数最前面 各按类型
缩略图 → 原图 /150/0 不做替换 图片替换;视频不替换
验证方式 JPEG FF D8 FF   | MP4 ftyp @ offset 4 各自验证
解密算法 ISAAC-64 + XOR ISAAC-64 + XOR ISAAC-64 + XOR

核心解密算法完全一样——都是 ISAAC-64 生成密钥流然后 XOR。区别只在于密钥来源、加密范围和下载方式。

0x09 WASM 加速与 Fallback

BigInt 在 JavaScript 中性能不太好,解密大视频时会比较慢。所以项目中做了三层实现:


let keystream: Buffer;

try {

    // 第一优先:WASM 版本(C 编译的 ISAAC-64,性能最好)

    const wasmService = WasmService.getInstance();

    keystream = await wasmService.getKeystream(key, size);

} catch (wasmErr) {

    // Fallback:纯 TypeScript BigInt 版本

    const isaac = new Isaac64(key);

    keystream = isaac.generateKeystreamBE(size);

}

WASM 版本通过 Emscripten 编译,在 Node.js 中用 vm.createContext沙箱加载,暴露 WxIsaac64类调用。

后记

本人对于逆向基本是一窍不通 所以本文由我和claude opus 4.6在cc老师的研究基础上共同完成 感谢阅读到这里

致谢:
https://github.com/mrexodia/ida-pro-mcp 基于此加上codex5.3对微信朋友圈的加密算法初步了解

本文基于 WeFlow项目的实际开发过程撰写,逆向分析使用 IDA Pro 对 Weixin.dll 进行。

免费评分

参与人数 26威望 +2 吾爱币 +120 热心值 +22 收起 理由
hxl819820 + 1 热心回复!
zht030303 + 1 + 1 牛逼
AIways + 1 + 1 再接再厉!
natural123 + 1 + 1 谢谢@Thanks!
beiwangqing + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
杨村长 + 1 + 1 不明觉厉
congcong40 + 1 + 1 谢谢@Thanks!
brIckZ + 1 热心回复!
生化武器 + 1 + 1 用心讨论,共获提升!
xmozi + 1 用心讨论,共获提升!
江阳小道 + 1 + 1 用心讨论,共获提升!
dingzhichuan + 1 用心讨论,共获提升!
bigzhang + 1 + 1 谢谢@Thanks!
submarine1620 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
LQ789 + 1 + 1 热心回复!
Terrorist + 1 + 1 谢谢@Thanks!
Leaf08 + 1 热心回复!
zjun777 + 1 + 1 用心讨论,共获提升!
cqrg + 1 我很赞同!
WuAiLee + 1 + 1 我很赞同!
willJ + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
BrutusScipio + 1 + 1 用心讨论,共获提升!
梦旅意中人 + 1 谢谢@Thanks!
zhoushengzhi + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
laozhang4201 + 1 + 1 热心回复!
YK4237 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| lyxhhh 发表于 2026-2-22 15:35

最开始我们是因为发现朋友圈图片有加密 单纯想在weflow中显示出来并且导出 后来发现4.0微信还没有人做朋友圈相关的解密 所以在实现的同时给思路公开 给各位开发者适配的同时希望可以让后来者少走弯路
pojie1655 发表于 2026-2-20 13:51
会瘦的胖子 发表于 2026-2-20 19:49
Lalala98 发表于 2026-2-21 12:44

可以用来爬朋友圈
robyyang 发表于 2026-2-21 00:44
太强了。我之前听说过爬朋友圈很困难,是不是有这个技术就方便多了
溯雪 发表于 2026-2-21 06:51
这个真强&#128077;
lesliewindy 发表于 2026-2-21 08:31
受益匪浅
zzyzmy 发表于 2026-2-21 12:54
精华帖。厉害的佬
zyone 发表于 2026-2-21 14:53
厉害了,赞一个!
IcePlume 发表于 2026-2-21 21:28
这个厉害,整治一下这个破微信
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-11 12:03

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表