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
186 lines
5.6 KiB
Python
186 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""MoWeChat v4 — efficient heap scanner for WeChat messages."""
|
|
|
|
import os, re, sys, json, time, hashlib, logging
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
LOG_DIR = os.path.join(SCRIPT_DIR, "..", "logs")
|
|
os.makedirs(LOG_DIR, exist_ok=True)
|
|
|
|
LOG_FILE = os.path.join(LOG_DIR, "wechat_v4.log")
|
|
SAWN_FILE = os.path.join(LOG_DIR, "wechat_seen_v4.json")
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s",
|
|
handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()])
|
|
log = logging.getLogger("wc4")
|
|
|
|
# Known wxids
|
|
WXID_DAD = "wxid_c0a6izmwd78y22"
|
|
WXID_MOHE = "wxid_7onnerpx2s2l22"
|
|
KNOWN = {WXID_DAD, WXID_MOHE}
|
|
|
|
|
|
def heap_of(pid):
|
|
"""Get heap address range for a PID."""
|
|
with open(f'/proc/{pid}/maps') as f:
|
|
for line in f:
|
|
if '[heap]' in line:
|
|
parts = line.split()
|
|
a, b = parts[0].split('-')
|
|
return int(a, 16), int(b, 16)
|
|
return None, None
|
|
|
|
|
|
def read_heap(pid):
|
|
"""Read the heap memory."""
|
|
start, end = heap_of(pid)
|
|
if not start:
|
|
return None, None, None
|
|
with open(f'/proc/{pid}/mem', 'rb') as f:
|
|
f.seek(start)
|
|
data = f.read(min(end - start, 40 * 1024 * 1024))
|
|
return data, start, end
|
|
|
|
|
|
def find_messages(data, heap_start):
|
|
"""Extract messages from heap data efficiently."""
|
|
msgs = []
|
|
seen = set()
|
|
|
|
# Pattern: known wxid followed by content
|
|
for wxid in KNOWN:
|
|
pattern = wxid.encode() + b'\x00'
|
|
pos = 0
|
|
limit = 0
|
|
while limit < 100:
|
|
idx = data.find(pattern, pos)
|
|
if idx < 0:
|
|
break
|
|
limit += 1
|
|
|
|
after = idx + len(pattern)
|
|
snippet = data[after:after+300]
|
|
|
|
i = 0
|
|
while i < len(snippet):
|
|
if snippet[i] == 0:
|
|
i += 1
|
|
continue
|
|
s = i
|
|
while i < len(snippet) and snippet[i] != 0:
|
|
i += 1
|
|
if i - s < 3:
|
|
i += 1
|
|
continue
|
|
try:
|
|
text = snippet[s:i].decode('utf-8', errors='replace')
|
|
except:
|
|
i += 1
|
|
continue
|
|
|
|
# Score: CJK chars + ASCII letters
|
|
cjk = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
|
alpha = sum(1 for c in text if c.isascii() and c.isalpha())
|
|
score = cjk * 3 + alpha
|
|
|
|
if score < 5:
|
|
i += 1
|
|
continue
|
|
|
|
h = hashlib.md5(text.encode()).hexdigest()
|
|
if h not in seen:
|
|
seen.add(h)
|
|
msgs.append((text, wxid, heap_start + after + s))
|
|
break
|
|
|
|
pos = idx + 1
|
|
|
|
# Also scan for the \xNNcontent pattern directly (faster catch-all)
|
|
# Pattern: [1 byte length/bufsize][readable text]
|
|
i = 0
|
|
limit2 = 0
|
|
while i < len(data) - 10 and limit2 < 500:
|
|
b = data[i]
|
|
# Possible prefix: small values (0x04-0x40 = 4-64, common buffer sizes)
|
|
if 0x04 <= b <= 0x40:
|
|
# Check if what follows looks like text
|
|
j = i + 1
|
|
text_bytes = bytearray()
|
|
while j < len(data) and j - i - 1 < b and data[j] != 0:
|
|
if data[j] >= 0x20:
|
|
text_bytes.append(data[j])
|
|
j += 1
|
|
else:
|
|
break
|
|
if len(text_bytes) >= 5:
|
|
try:
|
|
text = text_bytes.decode('utf-8', errors='replace')
|
|
except:
|
|
i += 1
|
|
continue
|
|
cjk = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
|
alpha = sum(1 for c in text if c.isascii() and c.isalpha())
|
|
if (cjk >= 2 or alpha >= 6) and text[0] not in '&/\\.':
|
|
h = hashlib.md5(('any:' + text).encode()).hexdigest()
|
|
if h not in seen:
|
|
# Find which wxid is near this text (search backward)
|
|
nearby = data[max(0, i-200):i]
|
|
wxid_match = re.search(rb'wxid_[a-zA-Z0-9]{10,28}', nearby)
|
|
owner = wxid_match.group(0).decode() if wxid_match else 'unknown'
|
|
seen.add(h)
|
|
msgs.append((text, owner, heap_start + i))
|
|
limit2 += 1
|
|
i += 1
|
|
|
|
return msgs
|
|
|
|
|
|
def find_wechat():
|
|
"""Find the main wechat process PID."""
|
|
for p in os.listdir('/proc'):
|
|
if not p.isdigit():
|
|
continue
|
|
try:
|
|
with open(f'/proc/{p}/maps') as f:
|
|
if "/opt/wechat/wechat" in f.read():
|
|
return int(p)
|
|
except:
|
|
continue
|
|
return None
|
|
|
|
|
|
def main():
|
|
pid = find_wechat()
|
|
if not pid:
|
|
log.error("WeChat not found")
|
|
return 1
|
|
|
|
log.info(f"Found PID {pid}")
|
|
|
|
data, hs, he = read_heap(pid)
|
|
|
|
if data is None:
|
|
log.error("Cannot read heap")
|
|
return 1
|
|
|
|
log.info(f"Heap: 0x{hs:x}-0x{he:x} ({len(data)} bytes)")
|
|
|
|
msgs = find_messages(data, hs)
|
|
|
|
# Deduplicate
|
|
seen_texts = set()
|
|
for text, wxid, addr in msgs:
|
|
key = text.strip()
|
|
if key not in seen_texts and len(key) >= 2:
|
|
seen_texts.add(key)
|
|
print(f"{wxid}: {text}")
|
|
|
|
if not msgs:
|
|
print("No messages found")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|