网上有部分编译产物,
https://github.com/G5t4r/WeProtocol
源代码写了, 只能执行在 linux 和 windows 机器上。
根据提供的 v08.go 和 dynlib/linux.go 代码,我们已经知道了这个库的接口定义,这为分析提供了极大的便利。以下是尝试系统的分析和破解/还原路径
对于 clientsdk/v08/v08.go
func vip08() {
// ... 读取 lib/key 并 base64 解码 ...
fmt.Println(string(decodedMessage))
}
这个 Go 函数只是打印了解码后的 key,并没有把它传给 lib.Call。这暗示了两种可能:
libv08.so 自己会在内部读取文件系统上的 lib/key 文件(静态分析时找 fopen)。
或者,这个 Key 根本不重要,只是个幌子,核心算法不需要 key。
Rqtx 函数: 它只接受一个 md5 字符串。这很可能是一个生成校验 token 的函数。你可以尝试对同一个 MD5 多次调用,看返回值是否固定。
EncodeString / EncodeUInt64: 这些函数带有 constant 和 timestamp。
Constant: 可能是版本号或者 magic number(魔法数)。
Timestamp: 时间戳。这通常用于防止重放攻击。
用 Hopper 反编译文件 v08.dll, 可以 CFG 图。

08 库主要包含三个核心导出函数:
-
encode_int64: 用于加密数字(如 uin)。
-
encode_cstr: 用于加密字符串(如设备信息)。
-
rqtx: 核心逻辑被虚拟化混淆(VMProtect / OLLVM),代码中可以看到大量的 switch 跳转和 0x180003e70 这种解释器入口,无法直接通过反编译还原源码。
但是,encode_int64 和 encode_cstr 的逻辑是清晰的。
package v08
import (
"encoding/binary"
"math/bits"
)
// Helper functions for Rotation
func ROL32(x, n uint32) uint32 { return bits.RotateLeft32(x, int(n)) }
func ROR32(x, n uint32) uint32 { return bits.RotateLeft32(x, -int(n)) }
// 8-bit Rotation wrappers (simulating AL register rotation)
func ROL8(x uint8, n int) uint8 { return x<<n | x>>(8-n) } // Simplified, assumes n < 8 usually
func ROR8(x uint8, n int) uint8 { return x>>n | x<<(8-n) }
// EncodeUInt64 对应 DLL 中的 encode_int64
// 逻辑来源:v08.dll.m 中的 encode_int64 函数
func EncodeUInt64(val uint64, constant uint32, timestamp uint32) uint64 {
low := uint32(val & 0xFFFFFFFF)
high := uint32(val >> 32)
// Part 1: 处理低 32 位
// 汇编逻辑:ROR(low, 31) ^ timestamp ^ constant -> ROR(res, 31)
low = ROR32(low, 31)
low ^= timestamp ^ constant
low = ROR32(low, 31)
// Part 2: 处理高 32 位
// 汇编逻辑:ROR(high, 30) ^ (ROR(timestamp, 1) ^ ROR(constant, 31)) -> ROR(res, 30)
high = ROR32(high, 30)
mask := ROR32(timestamp, 1) ^ ROR32(constant, 31)
high ^= mask
high = ROR32(high, 30)
return uint64(low) | (uint64(high) << 32)
}
// EncodeString 对应 DLL 中的 encode_cstr
// 逻辑来源:sub_1800017e0 (4字节块处理) 和 sub_180001870 (尾部字节处理)
func EncodeString(input string, constant uint32, timestamp uint32) string {
data := []byte(input)
length := len(data)
// 处理 4 字节块 (Block Loop)
blockLen := length / 4
for i := 0; i < blockLen; i++ {
offset := i * 4
val := binary.LittleEndian.Uint32(data[offset:])
idx := uint32(i) // 对应汇编中的 rcx 计数器
// Key 生成: ROL(constant, i) ^ ROR(timestamp, i)
key := ROL32(constant, idx) ^ ROR32(timestamp, idx)
// Value 变换: (val >> (31+i)) | (val << (1+i))
// 注意:位移量在 x86 下是对 32 取模的
// 31+i 相当于 -1+i, 1+i 就是 1+i
// 这实际上是一个变长循环左移 ROL(val, i+1)
shift := int(idx + 1)
valTransformed := bits.RotateLeft32(val, shift)
// 异或混淆
res := valTransformed ^ key
// 结果逆变换: (res >> (31+i)) | (res << (1+i))
// 同样也是 ROL(res, i+1)
final := bits.RotateLeft32(res, shift)
binary.LittleEndian.PutUint32(data[offset:], final)
}
// 处理剩余字节 (Tail Loop)
tailStart := blockLen * 4
tailLen := length % 4
if tailLen > 0 {
for j := 0; j < tailLen; j++ {
pos := tailStart + j
b := data[pos]
// 计数器 j
shift := j + 1 // 对应汇编中的 rbx = r10 + 1
// 字节级变换:
// 1. ROL8(b, j+1)
// 2. XOR key (key 也是基于常量的字节级变换)
// 3. ROR8(res, j+1)
// Key byte calculation derived from:
// r11 = ((rbp & 0xff) >> r10 | ...) -> ROR8(timestamp_byte, j)
// r9 = ... ^ (rsi >> ...) -> XOR ROR8(constant_byte, j)
tsByte := uint8(timestamp & 0xFF)
constByte := uint8(constant & 0xFF)
keyByte := byte(ROR8(tsByte, j) ^ ROR8(constByte, j))
// 变换过程
bRot := byte(bits.RotateLeft8(b, shift)) // ROL
bXor := bRot ^ keyByte
bFinal := byte(bits.RotateLeft8(bXor, -shift)) // ROR
data[pos] = bFinal
}
}
return string(data)
}
rqtx 函数的分析.
int rqtx(int arg0, ...) {
// ...
if (*(int8_t *)byte_18000b478 == 0x0) {
sub_180002ce0(...); // 初始化复杂结构
}
// ...
sub_180003e70(rdi, rdx + 0x8, 0x80400, ...); // 进入虚拟机解释器
}
这里有一个子函数
int sub_180002ce0(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) {
r9 = arg3;
r8 = arg2;
var_8 = rbx;
var_16 = rsi;
stack[-8] = rbp;
stack[-16] = r14;
saved_fp = r15;
rsp = rsp - 0x98;
var_8 = *qword_18000b018 ^ rsp;
var_30 = 0xf;
var_48 = 0x7461642e78747172;
rax = sub_180003c20(&var_28, &var_48, r8, r9, stack[-152], stack[-144], stack[-136], stack[-128]);
rax = sub_180003480(&var_60, rax, r8, r9, stack[-152], stack[-144], stack[-136]);
rdx = var_10;
if (rdx < 0x10) goto loc_180002d87;
其中0x7461642e78747172 是little end 编码, 解码之后的数据就是: “rqtx.dat”
底层的 C++ 代码(v08.dll/libv08.so)在初始化 rqtx 算法时,显式加载了名为 rqtx.dat 的文件。
那 v08.dll 是加壳的。
不回了,只有把读取的输入,传到VM 之前的值拿到,然后返回的值拿到。
我用 AI 给的解释是这样的。
写一个 pesudo 实现 v08 .
package v08
import (
"encoding/binary"
"math/bits"
)
// ---------------- Helper Functions ----------------
func ROL32(x, n uint32) uint32 { return bits.RotateLeft32(x, int(n)) }
func ROR32(x, n uint32) uint32 { return bits.RotateLeft32(x, -int(n)) }
func ROL8(x, n uint8) uint8 { return bits.RotateLeft8(x, int(n)) }
func ROR8(x, n uint8) uint8 { return bits.RotateLeft8(x, -int(n)) }
// ---------------- Core Implementation ----------------
// EncodeUInt64 对应 libv08.so 中的 encode_int64
// 逻辑:基于 ROR 和 XOR 的可逆混淆
func EncodeUInt64Impl(val uint64, constant uint32, timestamp uint32) uint64 {
low := uint32(val & 0xFFFFFFFF)
high := uint32(val >> 32)
// 处理低 32 位
low = ROR32(low, 31)
low ^= timestamp ^ constant
low = ROR32(low, 31)
// 处理高 32 位
high = ROR32(high, 30)
mask := ROR32(timestamp, 1) ^ ROR32(constant, 31)
high ^= mask
high = ROR32(high, 30)
return uint64(low) | (uint64(high) << 32)
}
// EncodeString 对应 libv08.so 中的 encode_cstr
// 逻辑:基于变长循环左移 (ROL) 和 异或 (XOR) 的混淆
func EncodeStringImpl(input string, constant uint32, timestamp uint32) string {
data := []byte(input)
length := len(data)
// 1. 处理 4 字节块 (Block Loop)
blockLen := length / 4
for i := 0; i < blockLen; i++ {
offset := i * 4
val := binary.LittleEndian.Uint32(data[offset:])
idx := uint32(i)
// 生成混淆 Key
key := ROL32(constant, idx) ^ ROR32(timestamp, idx)
// 变换 Value: 变长循环左移
shift := int(idx + 1)
valTransformed := bits.RotateLeft32(val, shift)
// 异或混淆
res := valTransformed ^ key
// 结果再次变换
final := bits.RotateLeft32(res, shift)
binary.LittleEndian.PutUint32(data[offset:], final)
}
// 2. 处理剩余字节 (Tail Loop)
tailStart := blockLen * 4
tailLen := length % 4
if tailLen > 0 {
for j := 0; j < tailLen; j++ {
pos := tailStart + j
b := data[pos]
// 计数器 j (对应汇编中的 rbx = r10 + 1)
shift := j + 1
// 计算 Key Byte
tsByte := uint8(timestamp & 0xFF)
constByte := uint8(constant & 0xFF)
keyByte := ROR8(tsByte, uint8(j)) ^ ROR8(constByte, uint8(j))
// 字节级变换
bRot := ROL8(b, uint8(shift))
bXor := bRot ^ keyByte
bFinal := ROL8(bXor, uint8(8-shift)) // ROR = ROL(8-n)
data[pos] = bFinal
}
}
return string(data)
}
在 macOS 上加一个加载 dll 的。
package v08
import (
"bytes"
"debug/elf"
"encoding/binary"
"fmt"
"io/ioutil"
"os"
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
// 模拟器上下文
type WxEmulator struct {
mu uc.Unicorn
base uint64
heapPtr uint64
stack uint64
}
const (
StackSize = 2 * 1024 * 1024 // 2MB Stack
HeapSize = 8 * 1024 * 1024 // 8MB Heap
BaseAddr = 0x40000000 // 模拟加载基址
RqtxDatFd = 100 // 伪造的文件句柄 ID
)
var rqtxContent []byte // 缓存 rqtx.dat 内容
// 初始化模拟器
func NewWxEmulator(libPath string, datPath string) (*WxEmulator, error) {
// 1. 读取 rqtx.dat
var err error
if rqtxContent == nil {
rqtxContent, err = ioutil.ReadFile(datPath)
if err != nil {
return nil, fmt.Errorf("failed to read rqtx.dat: %v", err)
}
}
// 2. 初始化 Unicorn (x86_64)
mu, err := uc.NewUnicorn(uc.ARCH_X86, uc.MODE_64)
if err != nil {
return nil, err
}
emu := &WxEmulator{mu: mu, base: BaseAddr}
// 3. 加载 ELF 文件到内存
if err := emu.loadELF(libPath); err != nil {
return nil, err
}
// 4. 分配栈和堆
emu.stack = 0x70000000
mu.MemMap(emu.stack, StackSize)
mu.RegWrite(uc.X86_REG_RSP, emu.stack+StackSize-8)
emu.heapPtr = 0x80000000
mu.MemMap(emu.heapPtr, HeapSize)
// 5. Hook 系统调用和外部函数
if err := emu.setupHooks(); err != nil {
return nil, err
}
return emu, nil
}
// 简单的 ELF 加载器
func (e *WxEmulator) loadELF(path string) error {
f, err := elf.Open(path)
if err != nil {
return err
}
defer f.Close()
for _, prog := range f.Progs {
if prog.Type == elf.PT_LOAD {
// 对齐内存页 (4KB)
vaddr := prog.Vaddr + e.base
memSize := (prog.Memsz + 4095) & ^uint64(4095)
// 映射内存
// 注意:这里简单给所有段 RWX 权限,实际应根据 prog.Flags 设置
e.mu.MemMap(vaddr, memSize)
// 读取段内容
data, _ := ioutil.ReadAll(prog.Open())
e.mu.MemWrite(vaddr, data)
}
}
return nil
}
// 设置 Hooks
func (e *WxEmulator) setupHooks() error {
// Hook 内存读取/写入错误,便于调试
e.mu.HookAdd(uc.HOOK_MEM_UNMAPPED, func(mu uc.Unicorn, access int, addr uint64, size int, value int64) bool {
fmt.Printf("[Unicorn] Memory Exception: addr=0x%x, size=%d\n", addr, size)
return false // 停止执行
}, 0, 0)
// Hook 外部函数调用 (这里使用简单的 HLT 指令或其他方式来拦截 PLT 条目是比较复杂的)
// 在此示例中,我们假设主要拦截文件 IO,我们 Hook 整个代码段的中断指令或特定地址
// *注意*: 在真实工程中,你需要解析 GOT/PLT 表来精准 Hook fopen/fread 等 libc 函数。
// 为简化演示,这里只演示原理。你需要找到 libv08.so 中调用 fopen 的地址进行 Hook。
// 这里演示如何处理 rqtx.dat 的逻辑:
// 实际代码中 v08 会调用 fopen("rqtx.dat", "rb")。
// 你需要反编译 libv08.so 找到 fopen 的 PLT 地址,或者在 Unicorn 中注册一个中断处理函数。
return nil
}
// 模拟文件系统操作 (伪代码,需要你根据实际 PLT 地址 Hook)
func (e *WxEmulator) HookFopen(filenamePtr uint64, modePtr uint64) uint64 {
filename, _ := e.readString(filenamePtr)
if filename == "rqtx.dat" {
fmt.Println("[Unicorn] fopen rqtx.dat intercepted")
return RqtxDatFd
}
return 0
}
func (e *WxEmulator) HookFread(ptr uint64, size uint64, nmemb uint64, stream uint64) uint64 {
if stream == RqtxDatFd {
bytesToRead := size * nmemb
if bytesToRead > uint64(len(rqtxContent)) {
bytesToRead = uint64(len(rqtxContent))
}
e.mu.MemWrite(ptr, rqtxContent[:bytesToRead])
return bytesToRead / size
}
return 0
}
// 调用 rqtx 函数
func (e *WxEmulator) CallRqtx(md5 string) (uint32, error) {
// 1. 查找 rqtx 函数地址 (你需要用 `nm` 或 `readelf` 找到它的偏移量)
// 假设 rqtx 偏移量是 0x1234 (请替换为实际值)
rqtxOffset := uint64(0x1234)
entry := e.base + rqtxOffset
// 2. 准备参数
// rqtx(char* md5_str)
// 将字符串写入堆内存
strAddr := e.heapPtr
e.mu.MemWrite(strAddr, []byte(md5+"\x00"))
// 设置参数寄存器 (Linux x64: RDI, RSI, RDX...)
e.mu.RegWrite(uc.X86_REG_RDI, strAddr)
// 3. 开始执行
// 注意:需要设置结束地址,或者在函数返回指令(RET)处停止
// 简单的做法是模拟直到 RET
if err := e.mu.Start(entry, entry+0x1000); err != nil { // 这里的结束地址只是示例
return 0, err
}
// 4. 读取返回值 (RAX)
ret, _ := e.mu.RegRead(uc.X86_REG_RAX)
return uint32(ret), nil
}
func (e *WxEmulator) readString(addr uint64) (string, error) {
var ret []byte
buf := make([]byte, 1)
for {
if err := e.mu.MemRead(addr, buf); err != nil {
return "", err
}
if buf[0] == 0 {
break
}
ret = append(ret, buf[0])
addr++
}
return string(ret), nil
}
修改 修改 clientsdk/v08/v08.go,将原来的 CGO 逻辑替换为我们的混合实现。
package v08
import (
"fmt"
"sync"
// "wechatdll/clientsdk/dynlib" // 删除这个引用
)
// 全局模拟器实例
var (
emulator *WxEmulator
initOnce sync.Once
)
func init() {
// 初始化不再加载 dll,而是准备模拟器或纯算法环境
fmt.Println("v08 init: Running in Pure Go / Unicorn Mode")
}
// EncodeString - 使用纯 Go 实现
func EncodeString(input string, constant uint32, timestamp uint32) string {
// 引用 v08_impl.go 中的实现
return EncodeStringImpl(input, constant, timestamp)
}
// EncodeUInt64 - 使用纯 Go 实现
func EncodeUInt64(input uint64, constant uint32, timestamp uint32) uint64 {
// 引用 v08_impl.go 中的实现
return EncodeUInt64Impl(input, constant, timestamp)
}
// Rqtx - 使用 Unicorn 模拟 (或者你也可以选择在这里保留 Docker 调用的逻辑)
func Rqtx(md5 string) uint32 {
initOnce.Do(func() {
var err error
// 确保 libv08.so 和 rqtx.dat 在工作目录
emulator, err = NewWxEmulator("./lib/libv08.so", "./rqtx.dat")
if err != nil {
fmt.Printf("Error initializing Unicorn emulator: %v\n", err)
// 这里可以降级处理,比如 panic 或者返回 0
}
})
if emulator != nil {
ret, err := emulator.CallRqtx(md5)
if err != nil {
fmt.Printf("Error executing rqtx: %v\n", err)
return 0
}
return ret
}
return 0
}
// 兼容旧代码的占位符
func Si(config string, data string) string {
return ""
}
试试? 效果如何?