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