196 lines
6.1 KiB
Python
196 lines
6.1 KiB
Python
"""把 Dart 发出的明文 HTTP 显示在本机页面。
|
|
|
|
Charles 看不到这些请求:它们不走系统代理,在进系统套接字之前就已经加密。
|
|
这里直接从进程内存里取出加密前的报文。
|
|
"""
|
|
import json
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import frida
|
|
|
|
DEVICE = "emulator-5556"
|
|
PORT = 8791
|
|
APP_NAMES = ("RaptDrama",)
|
|
|
|
JS = r"""
|
|
function extract(addr) {
|
|
var back = addr;
|
|
for (var i = 0; i < 160; i++) {
|
|
var b = 0;
|
|
try { b = back.sub(1).readU8(); } catch (e) { break; }
|
|
if (b !== 0x0a && b !== 0x0d && (b < 0x20 || b > 0x7e)) break;
|
|
back = back.sub(1);
|
|
}
|
|
var s = "";
|
|
for (var j = 0; j < 2500; j++) {
|
|
var c = 0;
|
|
try { c = back.add(j).readU8(); } catch (e) { break; }
|
|
if (c === 0x0d) continue;
|
|
if (c === 0x0a) { s += "\n"; continue; }
|
|
if (c < 0x20 || c > 0x7e) break;
|
|
s += String.fromCharCode(c);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
var seen = {};
|
|
var scanning = false;
|
|
var lastScan = 0;
|
|
|
|
function scan() {
|
|
var now = Date.now();
|
|
if (scanning || now - lastScan < 1200) return;
|
|
scanning = true;
|
|
lastScan = now;
|
|
var ranges = Process.enumerateRanges("rw-");
|
|
var fresh = [];
|
|
for (var i = 0; i < ranges.length; i++) {
|
|
var r = ranges[i];
|
|
if (r.size > 24 * 1024 * 1024) continue;
|
|
var matches;
|
|
try { matches = Memory.scanSync(r.base, r.size, "48 54 54 50 2f 31 2e 31"); }
|
|
catch (e) { continue; }
|
|
for (var j = 0; j < matches.length; j++) {
|
|
var text = extract(matches[j].address);
|
|
if (!text || text.length < 16 || seen[text]) continue;
|
|
if (text.indexOf("package:") === 0) continue;
|
|
seen[text] = true;
|
|
fresh.push(text);
|
|
}
|
|
}
|
|
if (fresh.length) send(JSON.stringify(fresh));
|
|
scanning = false;
|
|
}
|
|
|
|
var writePtr = Process.getModuleByName("libc.so").getExportByName("write");
|
|
Interceptor.attach(writePtr, {
|
|
onEnter: function (args) {
|
|
try {
|
|
if (args[1].readU8() === 0x17) scan();
|
|
} catch (e) {}
|
|
}
|
|
});
|
|
scan();
|
|
send("ready");
|
|
"""
|
|
|
|
FLOWS = []
|
|
LOCK = threading.Lock()
|
|
|
|
|
|
def on_message(message, _data):
|
|
if message.get("type") != "send":
|
|
print(message, flush=True)
|
|
return
|
|
payload = message["payload"]
|
|
if payload == "ready":
|
|
print("已挂上进程,等待请求", flush=True)
|
|
return
|
|
items = json.loads(payload)
|
|
with LOCK:
|
|
for text in items:
|
|
first = text.strip().split("\n", 1)[0][:180]
|
|
FLOWS.append({"id": len(FLOWS) + 1, "summary": first, "text": text})
|
|
print(first, flush=True)
|
|
|
|
|
|
def attach():
|
|
device = frida.get_device(DEVICE)
|
|
pid = None
|
|
for proc in device.enumerate_processes():
|
|
if proc.name in APP_NAMES or "Rapt" in proc.name:
|
|
pid = proc.pid
|
|
break
|
|
if pid is None:
|
|
raise SystemExit("RaptDrama 没在运行。先打开 App,再启动这个页面。")
|
|
print(f"attach {pid}", flush=True)
|
|
session = device.attach(pid)
|
|
script = session.create_script(JS)
|
|
script.on("message", on_message)
|
|
script.load()
|
|
return session, script
|
|
|
|
|
|
PAGE = """<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>明文请求</title>
|
|
<style>
|
|
html, body { height: 100%; margin: 0; background: #161616; color: #eee; font: 14px/1.45 "Segoe UI", sans-serif; }
|
|
header { position: sticky; top: 0; display: flex; justify-content: space-between; align-items: center;
|
|
padding: 10px 16px; background: #111; border-bottom: 1px solid #333; z-index: 2; }
|
|
main { display: flex; height: calc(100% - 49px); }
|
|
#list { width: 42%; overflow: auto; border-right: 1px solid #333; }
|
|
button.row { display: block; width: 100%; text-align: left; padding: 8px 12px; border: 0;
|
|
border-bottom: 1px solid #2a2a2a; background: transparent; color: #ddd; cursor: pointer; }
|
|
button.row.on { background: #2c3a28; }
|
|
#detail { flex: 1; margin: 0; padding: 12px 16px; overflow: auto; white-space: pre-wrap; word-break: break-all; }
|
|
.muted { color: #9aa; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<strong>Dart 明文请求</strong>
|
|
<span id="count" class="muted">0 条</span>
|
|
</header>
|
|
<main>
|
|
<div id="list"></div>
|
|
<pre id="detail">打开 App 并滑动页面,新的请求会出现在左边。</pre>
|
|
</main>
|
|
<script>
|
|
let selected = 0;
|
|
async function refresh() {
|
|
const rows = await (await fetch("/api/flows")).json();
|
|
document.getElementById("count").textContent = rows.length + " 条";
|
|
const list = document.getElementById("list");
|
|
list.innerHTML = "";
|
|
rows.slice().reverse().forEach((row) => {
|
|
const button = document.createElement("button");
|
|
button.className = "row" + (row.id === selected ? " on" : "");
|
|
button.textContent = row.summary;
|
|
button.onclick = () => { selected = row.id; document.getElementById("detail").textContent = row.text; refresh(); };
|
|
list.appendChild(button);
|
|
});
|
|
}
|
|
refresh();
|
|
setInterval(refresh, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path.startswith("/api/flows"):
|
|
with LOCK:
|
|
payload = json.dumps(FLOWS, ensure_ascii=False).encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
return
|
|
body = PAGE.encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, fmt, *args):
|
|
return
|
|
|
|
|
|
def main():
|
|
attach()
|
|
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
|
|
print(f"打开 http://127.0.0.1:{PORT}", flush=True)
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|