269 lines
10 KiB
Python
269 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
精准Hook方式找到微信8.0.56的真实消息发送类
|
||
通过监听实际发送行为来捕获正确的类名和方法
|
||
"""
|
||
import frida, time, json
|
||
from datetime import datetime
|
||
|
||
PHONE_IP = "192.168.110.80"
|
||
FRIDA_PORT = 27042
|
||
WECHAT_PID = 16816
|
||
BASE = "/Users/karuo/Documents/开发/2、私域银行/工作手机"
|
||
RESULT_FILE = BASE + "/hook_find_send_20260518.json"
|
||
|
||
# 这个脚本通过Hook SQLite的insert操作来捕获消息发送
|
||
# 当微信发送消息时,会往message表insert一条记录
|
||
HOOK_SCRIPT = """
|
||
'use strict';
|
||
|
||
var _foundClasses = [];
|
||
var _messages = [];
|
||
|
||
// 方法1: Hook SQLite insert,捕获消息写入
|
||
function hookSQLiteInsert() {
|
||
try {
|
||
var SQLiteDatabase = Java.use('net.sqlcipher.database.SQLiteDatabase');
|
||
SQLiteDatabase.insert.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues').implementation = function(table, nullColumnHack, values) {
|
||
if (table === 'message') {
|
||
var content = values.getAsString('content');
|
||
var talker = values.getAsString('talker');
|
||
var isSend = values.getAsInteger('isSend');
|
||
if (isSend && parseInt(isSend) === 1) {
|
||
_messages.push({
|
||
table: table,
|
||
talker: talker,
|
||
content: content ? content.substring(0, 100) : '',
|
||
timestamp: Date.now()
|
||
});
|
||
send({ type: 'message_insert', talker: talker, content: content ? content.substring(0, 50) : '' });
|
||
}
|
||
}
|
||
return this.insert(table, nullColumnHack, values);
|
||
};
|
||
send({ type: 'hook_status', msg: 'SQLite insert hook OK' });
|
||
} catch(e) {
|
||
send({ type: 'hook_status', msg: 'SQLite insert hook failed: ' + e });
|
||
}
|
||
}
|
||
|
||
// 方法2: 检查候选类是否存在(不扫描全量)
|
||
function checkCandidateClasses() {
|
||
var candidates = [
|
||
"com.tencent.mm.modelmulti.h",
|
||
"com.tencent.mm.modelmulti.g",
|
||
"com.tencent.mm.modelmulti.f",
|
||
"com.tencent.mm.modelmulti.e",
|
||
"com.tencent.mm.modelmulti.d",
|
||
"com.tencent.mm.modelmulti.c",
|
||
"com.tencent.mm.modelmulti.b",
|
||
"com.tencent.mm.modelmulti.a",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.k",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.j",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.i",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.h",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.g",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.f",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.e",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.d",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.c",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.b",
|
||
"com.tencent.mm.plugin.messenger.foundation.a.a",
|
||
];
|
||
var results = {};
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
try {
|
||
var cls = Java.use(candidates[i]);
|
||
results[candidates[i]] = 'exists';
|
||
// 获取方法列表
|
||
var methods = cls.class.getDeclaredMethods();
|
||
var methodList = [];
|
||
for (var j = 0; j < methods.length; j++) {
|
||
var m = methods[j];
|
||
var params = m.getParameterTypes().map(function(p) { return p.getName(); });
|
||
methodList.push(m.getName() + '(' + params.join(',') + ')');
|
||
}
|
||
results[candidates[i] + '_methods'] = methodList;
|
||
} catch(e) {
|
||
results[candidates[i]] = 'not_found';
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// 方法3: 通过AccessibilityService方式发送(UIAutomator)
|
||
function sendViaADB(toId, content) {
|
||
try {
|
||
// 用am start打开聊天界面
|
||
var Runtime = Java.use('java.lang.Runtime');
|
||
var cmd = 'am start -n com.tencent.mm/.ui.LauncherUI --es username ' + toId;
|
||
var proc = Runtime.getRuntime().exec(['sh', '-c', cmd]);
|
||
proc.waitFor();
|
||
return { success: true, method: 'am_start', cmd: cmd };
|
||
} catch(e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
}
|
||
|
||
// 方法4: 通过ClipboardManager+UIAutomator发送
|
||
function sendViaClipboard(toId, content) {
|
||
try {
|
||
return Java.performNow(function() {
|
||
var ctx = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
|
||
var ClipboardManager = Java.use('android.content.ClipboardManager');
|
||
var ClipData = Java.use('android.content.ClipData');
|
||
var cm = Java.cast(ctx.getSystemService('clipboard'), ClipboardManager);
|
||
var clip = ClipData.newPlainText('msg', content);
|
||
cm.setPrimaryClip(clip);
|
||
return { success: true, method: 'clipboard', content: content };
|
||
});
|
||
} catch(e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
}
|
||
|
||
Java.perform(function() {
|
||
hookSQLiteInsert();
|
||
});
|
||
|
||
rpc.exports = {
|
||
checkCandidates: function() {
|
||
return Java.performNow(function() {
|
||
return checkCandidateClasses();
|
||
});
|
||
},
|
||
getMessages: function() {
|
||
return { messages: _messages, count: _messages.length };
|
||
},
|
||
sendViaClipboard: function(params) {
|
||
return sendViaClipboard(params.to_id, params.content);
|
||
},
|
||
// 尝试通过微信的ShareToTimeline方式发朋友圈
|
||
testSnsPost: function(content) {
|
||
try {
|
||
return Java.performNow(function() {
|
||
var ctx = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
|
||
var Intent = Java.use('android.content.Intent');
|
||
|
||
// 方式1: 通过SnsUploadUI
|
||
var intent1 = Intent.$new();
|
||
intent1.setClassName('com.tencent.mm', 'com.tencent.mm.plugin.sns.ui.SnsUploadUI');
|
||
intent1.putExtra('Ksnsupload_type', 1);
|
||
intent1.putExtra('Ksnsupload_content', content);
|
||
intent1.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
||
ctx.startActivity(intent1);
|
||
return { success: true, method: 'SnsUploadUI_intent', content: content };
|
||
});
|
||
} catch(e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
// 直接写入message表(绕过发送逻辑)
|
||
insertMessage: function(params) {
|
||
try {
|
||
return Java.performNow(function() {
|
||
var toId = params.to_id;
|
||
var content = params.content;
|
||
|
||
// 找DB路径
|
||
var File = Java.use('java.io.File');
|
||
var base = File.$new('/data/data/com.tencent.mm/MicroMsg/');
|
||
var dirs = base.listFiles();
|
||
var dbPath = null;
|
||
if (dirs) {
|
||
for (var i = 0; i < dirs.length; i++) {
|
||
if (dirs[i].isDirectory() && dirs[i].getName().length === 32) {
|
||
var candidate = dirs[i].getAbsolutePath() + '/EnMicroMsg.db';
|
||
if (File.$new(candidate).exists()) { dbPath = candidate; break; }
|
||
}
|
||
}
|
||
}
|
||
if (!dbPath) return { success: false, error: 'DB not found' };
|
||
|
||
var SQLiteDatabase = Java.use('net.sqlcipher.database.SQLiteDatabase');
|
||
var db = SQLiteDatabase.openDatabase(dbPath, '00000000000000000000000000000000', null, 0);
|
||
|
||
var ContentValues = Java.use('android.content.ContentValues');
|
||
var cv = ContentValues.$new();
|
||
cv.put('msgId', Java.use('java.lang.Long').$new(Date.now()));
|
||
cv.put('type', Java.use('java.lang.Integer').$new(1));
|
||
cv.put('isSend', Java.use('java.lang.Integer').$new(1));
|
||
cv.put('talker', toId);
|
||
cv.put('content', content);
|
||
cv.put('createTime', Java.use('java.lang.Long').$new(Date.now()));
|
||
cv.put('status', Java.use('java.lang.Integer').$new(2));
|
||
|
||
var rowId = db.insert('message', null, cv);
|
||
db.close();
|
||
|
||
return { success: rowId > 0, row_id: rowId.toString(), method: 'direct_db_insert' };
|
||
});
|
||
} catch(e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
}
|
||
};
|
||
"""
|
||
|
||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 连接Frida...")
|
||
dm = frida.get_device_manager()
|
||
device = dm.add_remote_device(f"{PHONE_IP}:{FRIDA_PORT}")
|
||
session = device.attach(WECHAT_PID)
|
||
print(f"[OK] 附加微信 PID={WECHAT_PID}")
|
||
|
||
messages_received = []
|
||
def on_message(m, d):
|
||
if m.get("type") == "send":
|
||
payload = m.get("payload", {})
|
||
print(f" [EVENT] {payload}")
|
||
messages_received.append(payload)
|
||
|
||
script = session.create_script(HOOK_SCRIPT)
|
||
script.on("message", on_message)
|
||
script.load()
|
||
print("[OK] Hook脚本已加载,等待2秒...")
|
||
time.sleep(2)
|
||
rpc = script.exports_sync
|
||
|
||
results = {}
|
||
|
||
# 1. 检查候选类
|
||
print("\n=== 检查候选类 ===")
|
||
candidates_result = rpc.check_candidates()
|
||
for cls, status in candidates_result.items():
|
||
if not cls.endswith('_methods'):
|
||
print(f" {status.upper()} {cls}")
|
||
if status == 'exists' and (cls + '_methods') in candidates_result:
|
||
methods = candidates_result[cls + '_methods']
|
||
print(f" 方法: {methods[:5]}")
|
||
results["candidates"] = candidates_result
|
||
|
||
# 2. 测试直接DB插入
|
||
print("\n=== 测试直接DB插入消息 ===")
|
||
insert_result = rpc.insert_message({"to_id": "filehelper", "content": f"[DB直接插入测试{datetime.now().strftime('%H:%M:%S')}]"})
|
||
print(f" 结果: {json.dumps(insert_result, ensure_ascii=False)}")
|
||
results["db_insert"] = insert_result
|
||
time.sleep(1)
|
||
|
||
# 3. 截图验证DB插入是否在界面显示
|
||
import subprocess
|
||
subprocess.run(["adb", "-s", "192.168.110.80:5555", "shell", "screencap", "-p", "/sdcard/db_insert_test.png"])
|
||
subprocess.run(["adb", "-s", "192.168.110.80:5555", "pull", "/sdcard/db_insert_test.png",
|
||
BASE + "/开发文档/6、测试/live_verify_20260518/screenshots/db_insert_test.png"])
|
||
print(" 截图已保存")
|
||
|
||
# 4. 检查消息是否在DB中
|
||
print("\n=== 验证DB中的消息 ===")
|
||
# 用主Hook脚本查询(需要重新attach)
|
||
print(" (通过rawSql查询最新消息)")
|
||
|
||
# 5. 保存结果
|
||
with open(RESULT_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
|
||
print(f"\n结果已保存: {RESULT_FILE}")
|
||
|
||
script.unload()
|
||
session.detach()
|
||
print("完成!")
|