分析一个 wechat Pad v08 协议的的 dll 文件

Posted by 叉叉敌 on December 15, 2025

网上有部分编译产物,

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 图。

function

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 ""
}

试试? 效果如何?