Files
AgentsMeeting/gateway/linux/hooks/gdb_hook_messages.py
T
zhiwei f1630ebb03 feat: WeChat Linux bot via docker-wechatbot-webhook
- Docker container with auto-restart
- systemd webhook receiver on :5804
- Full send/receive loop: WeChat ↔ Docker ↔ Hermes
- Fixed login token for persistence
- Firewall rules for container-host communication
2026-06-24 01:59:44 +08:00

199 lines
6.5 KiB
Python

#!/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()