#!/usr/bin/env gdb """ GDB Hook script for WeChat Linux AppImage. Intercepts incoming messages from WeChat's NewSync_ProcessStashMsgList. Based on Ajax's Blog methodology: https://aajax.top/2026/03/11/GettingLinuxWechatMessages/ Usage: gdb -p $(pidof wechat) -x hooks/gdb_hook_messages.py """ import json import os import sys # ── Configuration ────────────────────────────────────────────── # WeChat binary base address (from /proc/PID/maps, first r--p entry) # Must be recalculated each run due to ASLR WECHAT_PID = None # Breakpoint RVA (relative to binary base) for WeChat 4.1.x # From Ajax's IDA Pro analysis of 4.1.0.16: # NewSync_ProcessStashMsgList -> loop call at 0x4994BEB BP_RVA = 0x4994BEB # Log file LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "logs") LOG_FILE = os.path.join(LOG_DIR, "wechat_messages.log") # Message structure offsets (from Ajax's blog) OFF_TYPE = 0x14 # int: message type OFF_SVRID = 0x50 # unsigned long long: server message ID OFF_HOLDER = 0x20 # void*: holder pointer OFF_INNER = 0x08 # void*: inner pointer (relative to holder) OFF_CONTENT_PTR = 0x00 # char*: content string pointer (from inner+0) OFF_CONTENT_LEN = 0x10 # int: content string length (from inner+0x10) def log(msg): """Write log to file and stdout.""" with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"{msg}\n") gdb.write(f"{msg}\n") class WechatMessageBreakpoint(gdb.Breakpoint): """Breakpoint that fires on each incoming WeChat message.""" def __init__(self, address): super().__init__(f"*{address}") self.suppress = True # Don't print to stdout automatically def stop(self): try: # Read registers msg_ptr = int(gdb.parse_and_eval("$rsi")) if msg_ptr == 0: return False # Read message type raw_type = gdb.selected_inferior().read_memory(msg_ptr + OFF_TYPE, 4) msg_type = int.from_bytes(raw_type, byteorder='little', signed=True) # Read server message ID raw_svrid = gdb.selected_inferior().read_memory(msg_ptr + OFF_SVRID, 8) svrid = int.from_bytes(raw_svrid, byteorder='little') # Read holder pointer raw_holder = gdb.selected_inferior().read_memory(msg_ptr + OFF_HOLDER, 8) holder = int.from_bytes(raw_holder, byteorder='little', signed=False) if holder == 0: return False # Read inner pointer raw_inner = gdb.selected_inferior().read_memory(holder + OFF_INNER, 8) inner = int.from_bytes(raw_inner, byteorder='little', signed=False) if inner == 0: return False # Read content string length raw_len = gdb.selected_inferior().read_memory(inner + OFF_CONTENT_LEN, 4) content_len = int.from_bytes(raw_len, byteorder='little', signed=False) if content_len <= 0 or content_len > 100000: return False # Read content string raw_content_ptr = gdb.selected_inferior().read_memory(inner + OFF_CONTENT_PTR, 8) content_ptr = int.from_bytes(raw_content_ptr, byteorder='little', signed=False) if content_ptr == 0: return False raw_content = gdb.selected_inferior().read_memory(content_ptr, min(content_len * 2, 100000)) # Try to decode as UTF-16LE (WeChat internal encoding) try: content = raw_content.tobytes()[:content_len * 2].decode('utf-16le', errors='replace') except: content = str(raw_content) # Read talker/sender info (different offset structure) # This is more complex — skip for initial test message = { "type": msg_type, "svrid": hex(svrid), "content": content[:500], "content_len": content_len, "holder": hex(holder), "inner": hex(inner), } log(f"[WECHAT_MSG] type={msg_type} svrid={hex(svrid)}") log(f"[WECHAT_MSG] content: {content[:200]}") # Forward to Hermes Gateway try: forward_to_hermes(message) except Exception as e: log(f"[WECHAT_MSG] forward error: {e}") except Exception as e: log(f"[WECHAT_MSG] error: {e}") return False # Don't stop execution def forward_to_hermes(msg): """Forward message to Hermes Gateway.""" import urllib.request payload = json.dumps({ "model": "nova-4", "messages": [ { "role": "system", "content": f"You are a WeChat message handler. A new message arrived: type={msg.get('type')}, content={msg.get('content', '')[:100]}" } ] }).encode('utf-8') req = urllib.request.Request( "http://192.168.1.246:8642/v1/chat/completions", data=payload, headers={ "Content-Type": "application/json", "Authorization": "Bearer hermes123" }, method="POST" ) # Don't wait for response — fire and forget try: urllib.request.urlopen(req, timeout=2) except Exception: pass def detect_wechat_base(): """Detect WeChat binary base address from /proc/PID/maps.""" pid = WECHAT_PID if pid is None: try: pid = gdb.selected_inferior().pid except: pass if pid is None: return None try: with open(f"/proc/{pid}/maps", "r") as f: for line in f: if "/opt/wechat/wechat" in line and "r--p" in line: addr = line.split("-")[0] return int(addr, 16) except Exception as e: log(f"[WECHAT_MSG] Failed to detect base: {e}") return None class HookWechatMessages(gdb.Command): """Install WeChat message hook.""" def __init__(self): super().__init__("hook-wechat-messages", gdb.COMMAND_USER) def invoke(self, arg, from_tty): base = detect_wechat_base() if base is None: log("[WECHAT_MSG] ERROR: Could not detect WeChat base address") return addr = base + BP_RVA WechatMessageBreakpoint(addr) log(f"[WECHAT_MSG] Hook installed: base=0x{base:x} bp=0x{addr:x}") log(f"[WECHAT_MSG] Waiting for messages...") # Register the custom command HookWechatMessages()