f1630ebb03
- 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
199 lines
6.5 KiB
Python
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()
|