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
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user