Files
helper/native/filehelper_service.py
2026-10-15 18:27:46 +08:00

228 lines
8.9 KiB
Python

"""微信 4.1.15.13 原生文本服务;测试范围仅为文件传输助手。"""
from __future__ import annotations
import ctypes as c
from ctypes import wintypes as w
import logging
import threading
from pathlib import Path
import pefile
from run_selftest import DLL, K, check, modules, remote_call, run
logger = logging.getLogger("NativeFileHelper")
K.GetExitCodeProcess.argtypes = [w.HANDLE, c.POINTER(w.DWORD)]
class ReceivedMessage(c.Structure):
_fields_ = [("size", c.c_uint32), ("present", c.c_uint32),
("type", c.c_uint32), ("thread_id", c.c_uint32),
("dropped", c.c_uint32), ("reserved", c.c_uint32),
("sequence", c.c_uint64), ("sender", c.c_char * 128),
("receiver", c.c_char * 128), ("text", c.c_char * 1024)]
assert c.sizeof(ReceivedMessage) == 1312
class ReceivedTextMessage(ReceivedMessage):
_fields_ = [("mention_source", c.c_char * 4096)]
assert c.sizeof(ReceivedTextMessage) == 5408
class ReceiveChannel:
def __init__(self, pid, *, dll=DLL, poll_export=b"WxPollFileHelperMessage", message_type=ReceivedMessage):
self.message_type = message_type
self.process = None
self.buffer = None
self.pending = False
loaded_modules = modules(pid)
loaded = loaded_modules.get(dll.name.lower())
if not loaded or Path(loaded[1]).resolve() != dll.resolve():
raise RuntimeError("原生文本桥接模块未加载")
pe = pefile.PE(str(dll))
self.address = loaded[0] + next(item.address for item in pe.DIRECTORY_ENTRY_EXPORT.symbols
if item.name == poll_export)
self.core_base = loaded_modules["weixin.dll"][0]
self.process = check(K.OpenProcess(0x43a, False, pid), "打开接收通道")
try:
self.buffer = check(K.VirtualAllocEx(self.process, None, c.sizeof(self.message_type),
0x3000, 0x04), "分配接收缓冲区")
except Exception:
self.close()
raise
def poll(self):
if self.pending:
raise RuntimeError("上次接收线程超时,不能继续复用缓冲区")
try:
code = remote_call(self.process, self.address, self.buffer)
except TimeoutError:
self.pending = True
raise
if code:
raise RuntimeError(f"接收队列读取失败:{code}")
message = self.message_type()
read = c.c_size_t()
check(K.ReadProcessMemory(self.process, self.buffer, c.byref(message), c.sizeof(message),
c.byref(read)), "读取原生消息副本")
if read.value != c.sizeof(message) or message.size != c.sizeof(message):
raise RuntimeError("接收结构大小不匹配")
if message.dropped:
raise RuntimeError(f"接收队列已溢出,丢弃了 {message.dropped} 条消息")
return message if message.present else None
def read_memory(self, address, size):
buffer = c.create_string_buffer(size)
read = c.c_size_t()
check(K.ReadProcessMemory(self.process, address, buffer, size, c.byref(read)), "读取当前登录账号")
if read.value != size:
raise ValueError("账号字段读取不完整")
return buffer.raw
def close(self):
if self.process:
# 超时线程可能仍持有参数,保留缓冲区直到客户端退出。
if self.buffer and not self.pending:
K.VirtualFreeEx(self.process, self.buffer, 0, 0x8000)
K.CloseHandle(self.process)
self.process = self.buffer = None
def process_exited(self):
if not self.process:
return False
code = w.DWORD()
check(K.GetExitCodeProcess(self.process, c.byref(code)), "检查微信进程状态")
return code.value != 259
class NativeFileHelperService:
"""提供原项目 register_msg_handler / send_text 的基础接口。"""
_dll = DLL
_poll_export = b"WxPollFileHelperMessage"
_message_type = ReceivedMessage
_run = staticmethod(run)
def __init__(self, *, pid=None, preserve_reports=True):
self.pid = pid
self.preserve_reports = preserve_reports
self.msg_handlers = []
self.last_result = None
self.last_error = None
self._channel = None
self._thread = None
self._stop = threading.Event()
self._calls = threading.RLock()
self._owns_bridge = False
def register_msg_handler(self, handler):
self.msg_handlers.append(handler)
def start(self):
if self._owns_bridge:
raise RuntimeError("服务已经启动")
self._stop.clear()
self.last_error = None
armed = self._run("--listen", pid=self.pid)
self.pid = armed["pid"]
self._owns_bridge = True
if armed["code"]:
self.stop()
raise RuntimeError(f"接收 Hook 启动失败:{armed['code']}")
try:
self._channel = ReceiveChannel(self.pid, dll=self._dll, poll_export=self._poll_export,
message_type=self._message_type)
self._prepare_channel()
self._thread = threading.Thread(target=self._receive_loop, name="WxFileHelperReceiver", daemon=True)
self._thread.start()
except Exception:
self.stop()
raise
return True
def _receive_loop(self):
try:
while not self._stop.is_set():
with self._calls:
self._before_poll()
message = self._channel.poll()
if message is None:
self._stop.wait(0.3)
continue
data = self._map_message(message)
if data is None:
continue
for handler in list(self.msg_handlers):
try:
handler(self.pid, 11046, data, self)
except Exception:
logger.exception("Python 消息处理器异常")
except Exception as error:
self.last_error = error
self._stop.set()
logger.exception("原生接收服务停止")
def _prepare_channel(self):
"""子类在开始消息线程前读取必要的当前连接信息。"""
def _before_poll(self):
"""子类检查当前登录状态;旧文件传输助手后端保持原行为。"""
def _map_message(self, message):
sender = message.sender.decode("utf-8")
receiver = message.receiver.decode("utf-8")
if "filehelper" not in (sender, receiver) or message.type != 1:
raise RuntimeError("收到范围外的消息副本")
text = message.text.decode("utf-8")
return {"msg": text, "content": text, "from_wxid": "filehelper", "room_wxid": "",
"native_sender": sender, "native_receiver": receiver,
"sequence": message.sequence, "native_thread_id": message.thread_id}
def send_text(self, to_wxid, content):
if to_wxid != "filehelper":
raise NotImplementedError("当前验证版只开放文件传输助手")
with self._calls:
if not self._owns_bridge or self._stop.is_set():
raise RuntimeError("接收服务没有运行")
self.last_result = self._run("--dispatch", pid=self.pid, text=content, keep_loaded=True,
preserve_report=self.preserve_reports)
return self.last_result["success"]
def send_payload(self, payload):
if payload.get("type") != 11237:
raise NotImplementedError("当前验证版只支持文本发送 11237")
data = payload["data"]
return self.send_text(data["to_wxid"], data["content"])
def stop(self):
self._stop.set()
if self._thread and self._thread is not threading.current_thread():
self._thread.join(timeout=35)
if self._thread.is_alive():
raise RuntimeError("接收线程尚未退出,保留 DLL,稍后再尝试停止")
with self._calls:
process_exited = bool(self._channel and self._channel.process_exited())
if self._channel:
self._channel.close()
self._channel = None
if self._owns_bridge:
if process_exited:
self._owns_bridge = False
return {"code": 0, "pid": self.pid, "process_exited": True, "dll_unloaded": True}
result = self._run("--remove", pid=self.pid)
if result["code"] or not result.get("dll_unloaded"):
raise RuntimeError(f"桥接模块未卸载:{result}")
self._owns_bridge = False
return result
def __enter__(self):
self.start()
return self
def __exit__(self, *_):
self.stop()