Ghostwire: Công cụ Đo Lường Thời Gian Chạy Tương Tự Frida cho JavaScript Trình Duyệt
Expert Network Defense Engineer
Tóm tắt:
- Ghostwire là công cụ định dạng thời gian chạy cho JavaScript trên trình duyệt, thuộc cùng loại với Frida. Cả hai đều quan sát các ranh giới hàm trực tiếp thay vì đọc mã nguồn và hy vọng. Mục tiêu khác nhau: Frida định dạng các quy trình gốc và di động, Ghostwire định dạng JavaScript bên trong Chrome.
- Giao thức là Giao thức DevTools của Chrome qua một ống hệ điều hành. Ghostwire khởi động Chrome với
--remote-debugging-pipe, vì vậy không có cổng gỡ lỗi nào để kết nối và không có phụ thuộc của bên thứ ba nào cần cài đặt cho lõi. - Các hook là các điểm dừng gỡ lỗi, không phải là các bộ bao bọc. Đối tượng hàm trực tiếp không bao giờ bị thay thế, vì vậy
fn.toString()luôn trả về mã nguồn gốc. - Phần thú vị là oracle. Bạn thu thập các cặp
(đầu vào → đầu ra)thực tế, viết lại một phiên bản ứng dụng và để Ghostwire chứng minh điều đó sai với một ví dụ phản chứng cụ thể. - Một lần chạy trực tiếp đã từ chối một ứng viên khả thi. Mười hai cặp được thu thập đã xác minh hàm đúng 12/12; một phiên bản thiếu một
+1khớp 0/13 và tạo ra đầu vào thất bại chính xác. - Ghostwire là một công cụ chạy trên Chrome cục bộ ngay hôm nay. Nó không kết nối với một điểm cuối CDP WebSocket từ xa, vì vậy hiện tại nó không thể điều khiển phiên làm việc của Scrapeless Scraping Browser.
- Chỉ sử dụng nó nơi bạn được phép. Bắt đầu với một tài khoản miễn phí Scrapeless để thu thập dữ liệu trên trình duyệt thông thường, việc này khác với phân tích thời gian chạy.
Đọc JavaScript đã bị làm mờ cho bạn một giả thuyết. Chạy nó cho bạn một sự thật. Ghostwire tồn tại để biến cái trước thành cái sau: nó quan sát những gì một hàm thực sự trả về trong thời gian chạy, sau đó kiểm tra lại việc triển khai của bạn so với sự thật đó và báo cáo mọi đầu vào mà bạn đã sai.
Ghostwire Là Gì
Ghostwire là một thư viện định dạng Python cho JavaScript chạy bên trong Chrome. Nó gắn kết qua Giao thức DevTools của Chrome, thiết lập các hook trên các đối tượng hàm trực tiếp, ghi lại các tham số và giá trị trả về, và so sánh một triển khai ứng viên với hành vi đã được ghi lại.
Phiên bản được sử dụng ở đây là 0.1.0 tại commit upstream a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8. Nó yêu cầu Python 3.9 hoặc mới hơn và phải có giấy phép MIT, và lõi không có bất kỳ phụ thuộc nào từ bên thứ ba vì giao thức chạy qua một ống hệ điều hành thay vì một thư viện khách WebSocket.
Hướng dẫn này sử dụng một trang cục bộ tổng hợp trong toàn bộ. Định dạng thời gian chạy thuộc cùng loại ủy quyền như bất kỳ công cụ bảo mật nào khác: phân tích các hệ thống bạn sở hữu hoặc có sự cho phép rõ ràng để thử nghiệm.
Ghostwire và Frida: Nơi phép tương tự đúng
Gọi Ghostwire là "Frida cho JavaScript trên trình duyệt" là một cách ngắn gọn hữu ích, và nó chính xác về phương pháp chứ không phải triển khai.
| Mối quan tâm | Frida | Ghostwire 0.1.0 |
|---|---|---|
| Mục tiêu chính | Các quy trình gốc, ứng dụng di động, nhị phân máy tính để bàn | JavaScript chạy trong Chrome |
| Cơ chế gắn kết | Tiêm một tác nhân vào quy trình mục tiêu | Gắn kết một phiên gỡ lỗi qua CDP |
| Cơ chế hook | Bộ chặn trên các địa chỉ hàm gốc | Debugger.setBreakpointOnFunctionCall trên các đối tượng hàm trực tiếp |
| Bề mặt kịch bản | Tác nhân JavaScript bên trong quy trình | Python điều khiển giao thức từ bên ngoài |
| Phụ thuộc lõi | Thời gian chạy Frida và các liên kết | Không; CDP qua một ống hệ điều hành |
API JavaScript của Frida chạy một tác nhân bên trong quy trình mà nó đang kiểm tra. Ghostwire không bao giờ đưa mã vào trang. Sự khác biệt đó là có chủ đích: một điểm dừng trên đối tượng hàm trực tiếp để lại đối tượng không bị thay đổi, vì vậy kiểm tra fn.toString() của trang thấy mã nguồn gốc.
Nơi phép tương tự đúng là mô hình hoạt động. Cả hai thay thế việc đọc tĩnh bằng quan sát trực tiếp, và cả hai đều dành cho phân tích được ủy quyền hơn là thu thập dữ liệu sản xuất.
Điều kiện tiên quyết
- Python 3.9 hoặc mới hơn.
- Google Chrome hoặc Chromium được cài đặt cục bộ.
- Một phiên bản kho lưu trữ Ghostwire đã được checkout ở một commit cố định.
- Chỉ các mục tiêu bạn sở hữu hoặc được phép phân tích.
Cài đặt
Lõi không có gói nào để cài đặt, vì vậy một checkout cố định là toàn bộ thiết lập:
bash
git clone https://github.com/sofianeelhor/ghostwire.git
cd ghostwire
git checkout a3fe4ef1e27804ab1e976e28dfa1801ee53aabf8
Đọc pyproject.toml trước khi chạy bất kỳ thứ gì. Nó tuyên bố version = "0.1.0", requires-python = ">=3.9", và một danh sách dependencies trống, với mcp>=1.0 chỉ như một tùy chọn bổ sung cho máy chủ MCP.
Mục tiêu Tổng hợp
Bộ cố định này được phục vụ từ localhost và tính toán một giá trị đơn giản có chủ ý. Nó không chứa thông tin đăng nhập, không có logic chống bot và không có mã của bên thứ ba:
python
PAGE = b"""<!doctype html><html><body><script>
function transform(name, value) {
return btoa(name + ':' + (value * 7 + 1));
}
window.transform = transform;
let i = 0;
setInterval(function () {
window.lastResult = transform('sample' + i, 10 + i);
i += 1;
}, 120);
</script></body></html>"""
Bộ đếm thời gian rất quan trọng. Ghostwire ghi lại các cuộc gọi khi chúng xảy ra, do đó một ranh giới được thực hiện lặp đi lặp lại tạo ra một tập hợp không có bất kỳ tương tác nào.
Gắn và Kết nối
ghostwire.attach() khởi động Chrome, kết nối các đầu dò mặc định và trả về một Inspector:
python
import ghostwire
with ghostwire.attach("http://localhost:8000/", headless=True) as gw:
gw.wait(1.0)
gw.hook("window.transform", capture_returns=True, label="transform")
gw.wait(1.5)
corpus = gw.corpus("transform")
print("cặp đã ghi lại:", len(corpus))
capture_returns=True ghi lại giá trị trả về cùng với các đối số, đây là điều làm cho tập hợp có thể sử dụng như là sự thật cơ sở. label nhóm những cặp đó để xác minh sau này.
Sẵn sàng để thu thập dữ liệu trang bình thường thay vì phân tích hành vi chạy thời gian? Mở tài khoản Scrapeless miễn phí và sử dụng trình duyệt được quản lý cho công việc đó.
Xác minh một Ứng cử viên So với Thực tế
Bạn cung cấp một triển khai lại, và Ghostwire chạy nó trên tập hợp đã ghi lại trong một trang cô lập không thể nhìn thấy chức năng thực:
python
good = "(name,value)=>btoa(name+':'+(value*7+1))"
wrong = "(name,value)=>btoa(name+':'+(value*7))"
good_result = gw.verify("window.transform", good, label="transform")
wrong_result = gw.verify("window.transform", wrong, label="transform")
Ứng cử viên thứ hai là sự thất bại thực tế: cú pháp thì ổn, nhưng ngữ nghĩa bị sai một. Một mô hình đọc mã nguồn đã nén thường xuyên tạo ra loại sai lầm này, và nó vẫn tồn tại sau khi xem xét thông thường vì đầu ra vẫn trông giống như base64 hợp lệ.
Kịch bản Có thể Chạy Hoàn chỉnh
Container được sử dụng để xác minh cần hai điều chỉnh, cả hai đều liên quan đến môi trường chứ không phải lỗi của Ghostwire: Chrome yêu cầu --no-sandbox khi chạy với quyền root, và việc điều hướng HTTP đầu tiên chậm đến mức vượt quá thời hạn 20 giây của CDP mặc định.
python
import base64
import http.server
import socketserver
import sys
import threading
from pathlib import Path
HERE = Path(__file__).resolve().parent
UPSTREAM = next(
p / "upstream" for p in (HERE, *HERE.parents)
if (p / "upstream" / "ghostwire").is_dir()
)
sys.path.insert(0, str(UPSTREAM))
from ghostwire import Browser, Engine, Tracer
from ghostwire.api import Inspector
from ghostwire.crypto import CryptoLogger
from ghostwire.dataflow import DataflowTracer
from ghostwire.objects import LiveObjects
from ghostwire.oracle import Oracle
from ghostwire.origin import OriginTracer
from ghostwire.probes import NetLog, ScriptWatcher
def attach_local(url, extra_flags):
"""Cùng một cách nối như ghostwire.attach(), với các cờ Chrome an toàn cho container."""
browser = Browser(headless=True, extra_flags=extra_flags)
browser.cdp.default_timeout = 90.0
engine = Engine(browser=browser)
scripts, net, tracer = ScriptWatcher(), NetLog(), Tracer()
for probe in (scripts, net, tracer):
engine.add_probe(probe)
oracle = Oracle(engine, tracer)
engine.start()
engine.navigate(url)
return Inspector(
engine, scripts, net, tracer, oracle,
OriginTracer(engine), DataflowTracer(engine),
LiveObjects(engine), CryptoLogger(engine),
)
PAGE = b"""<!doctype html><html><body><script>
function transform(name, value) {
return btoa(name + ':' + (value * 7 + 1));
}
window.transform = transform;
let i = 0;
setInterval(function () {
window.lastResult = transform('sample' + i, 10 + i);
i += 1;
}, 120);
</script></body></html>"""
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(PAGE)))
self.end_headers()
self.wfile.write(PAGE)
def log_message(self, *args):
pass
class QuietServer(socketserver.TCPServer):
allow_reuse_address = True
def handle_error(self, request, client_address):
pass
server = QuietServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
gw = attach_local(
f"http://localhost:{port}/",
["--no-sandbox", "--disable-dev-shm-usage"],
)
try:
gw.wait(1.0)
gw.hook("window.transform", capture_returns=True, label="transform")
gw.wait(1.5)
corpus = gw.corpus("transform")
good = "(name,value)=>btoa(name+':'+(value*7+1))"
sai = "(name,value)=>btoa(name+':'+(value*7))"
ket_qua_tot = gw.verify("window.transform", tot, label="transform")
ket_qua_sai = gw.verify("window.transform", sai, label="transform")
du_lieu_moi = [["alpha", 1], ["beta", 2], ["", 0]]
ket_qua_du_lieu_moi = gw.verify("window.transform", tot, fresh_inputs=du_lieu_moi)
sai_lech = ket_qua_sai["mismatches"][0]
gia_tri_mong_doi_giai_ma = base64.b64decode(sai_lech["expected"]).decode()
gia_tri_nhan_duoc_giai_ma = base64.b64decode(sai_lech["got"]).decode()
assert len(corpus) >= 5
assert ket_qua_tot["verified"] is True
assert ket_qua_sai["verified"] is False
assert ket_qua_du_lieu_moi["verified"] is True
print("phiên bản ghostwire: 0.1.0")
print("cặp thu thập:", len(corpus))
print("ứng viên đúng:", ket_qua_tot["verified"],
"đã kiểm tra:", ket_qua_tot["tested"], "đã khớp:", ket_qua_tot["matched"])
print("ứng viên sai:", ket_qua_sai["verified"],
"đã kiểm tra:", ket_qua_sai["tested"], "đã khớp:", ket_qua_sai["matched"])
print("đầu vào phản chứng:", sai_lech["input"])
print("phản chứng mong đợi:", gia_tri_mong_doi_giai_ma)
print("phản chứng nhận được:", gia_tri_nhan_duoc_giai_ma)
print("đầu vào mới đã xác minh:", ket_qua_du_lieu_moi["verified"])
print("đầu vào mới đã kiểm tra:", ket_qua_du_lieu_moi["tested"])
finally:
gw.close()
server.shutdown()
Kết quả chạy trả về
text
phiên bản ghostwire: 0.1.0
cặp thu thập: 12
ứng viên đúng: True đã kiểm tra: 12 đã khớp: 12
ứng viên sai: False đã kiểm tra: 13 đã khớp: 0
đầu vào phản chứng: ['sample8', 18]
phản chứng mong đợi: sample8:127
phản chứng nhận được: sample8:126
đầu vào mới đã xác minh: True
đầu vào mới đã kiểm tra: 3
Ứng viên sai đã khớp 0 trong số 13, không có một số nào. Bởi vì việc thiếu +1 làm lệch mọi kết quả, corpus từ chối ở khắp mọi nơi. Một lỗi tinh vi hơn sẽ tạo ra một sự khớp một phần, đó chính là tín hiệu mà corpus rất giỏi trong việc đưa ra.
Phản chứng rất cụ thể: đầu vào ['sample8', 18] nên tạo ra sample8:127 vì 18 * 7 + 1 = 127, và ứng viên đã tạo ra sample8:126. Đó là một sự khác biệt đã được giải mã, có thể hành động thay vì chỉ là một lỗi boolean.
Kiểm tra đầu vào mới đã xác minh 3 đầu vào mà không có corpus trước đó. Ghostwire đã gọi chức năng trực tiếp cho các giá trị mà nó chưa từng quan sát, điều này bao gồm các trường hợp biên mà bộ đếm thời gian chưa bao giờ tạo ra, bao gồm cả chuỗi rỗng.
Giới hạn trung thực: Ghostwire và Trình duyệt Lấy dữ liệu
Hiện tại, Ghostwire không thể điều khiển một phiên làm việc Scrapeless Scraping Browser, và lý do là do cấu trúc thay vì một vấn đề cấu hình.
Browser.__init__ khởi động một quá trình Chrome cục bộ với --remote-debugging-pipe=JSON, sao chép hai mô tả tập tin vào đứa trẻ, và chuyển cho PipeConnection. Mỗi thông điệp giao thức chảy qua các mô tả đó. Trình duyệt Lấy dữ liệu thay vào đó phơi bày một đầu cuối WebSocket từ xa, và Ghostwire không có giao vận WebSocket để nói chuyện với nó.
Engine(browser=...) chấp nhận một đối tượng giống như trình duyệt, điều này là một mối nối mở rộng thực sự - trình chạy của bài viết này tự thân sử dụng nó để truyền các cờ Chrome an toàn cho container. Nhưng để nó đạt được một đầu cuối từ xa sẽ yêu cầu một giao thông WebSocket tương thích với PipeConnection cùng với các thay đổi vòng đời, vì Browser.close() giả định nó sở hữu một quá trình cục bộ mà nó có thể kết thúc. Công việc đó không tồn tại ở amrop, vì vậy hãy coi một cây cầu như một phác thảo thiết kế thay vì một con đường được hỗ trợ.
Phân chia thực tiễn là rõ ràng. Sử dụng Ghostwire cục bộ cho phân tích thời gian thực được ủy quyền. Sử dụng Trình duyệt Lấy dữ liệu cho việc thu thập dữ liệu qua trình duyệt quy mô lớn, đó là điều mà tổng quan về Giao thức DevTools của Chrome đề cập.
Khắc phục sự cố
Chrome thoát ngay lập tức và ống bị hỏng
Một BrokenPipeError từ cdp.py nghĩa là Chrome đã chết trong quá trình khởi động. Chạy với quyền root trong một container là nguyên nhân thông thường; truyền --no-sandbox và --disable-dev-shm-usage thông qua extra_flags.
CDPError: timeout: Page.navigate
Quá trình điều hướng vượt quá thời hạn kết nối. Tăng browser.cdp.default_timeout trước khi tạo Engine. Trong container sử dụng ở đây, quá trình điều hướng đầu tiên đến 127.0.0.1 mất khoảng 15 giây trong khi localhost được giải quyết trong khoảng 0,1 giây, vì vậy tên máy chủ đơn giản đã thay đổi kết quả.
Móc không ghi lại gì
Chức năng chưa được định nghĩa khi móc được thiết lập. Đợi trang khởi tạo, và xác nhận biểu thức được giải quyết bằng cách đánh giá typeof window.yourFunction trước.
Kiểm tra thành công nhưng corpus thì nhỏ
Một vài cặp chứng minh rất ít. Tăng cường biên giới hơn, hoặc cung cấp fresh_inputs bao gồm các cạnh mà lưu lượng quan sát chưa bao giờ đạt đến.
Kết luận
Chức năng hooking là nửa dễ dàng. Nửa thay đổi cách bạn làm việc là oracle, cái kiểm tra việc triển khai lại của bạn với hành vi đã được ghi lại và trả lại các đầu vào nơi nó bị lỗi. Trong lần chạy này, đầu vào đó là ['sample8', 18], và ứng viên đã thất bại vì nó bỏ qua một +1 duy nhất.
Hãy giữ cho phạm vi trung thực. Ghostwire là một công cụ nghiên cứu Chrome cục bộ cho các hệ thống mà bạn được phép phân tích, và hiện tại nó không giao tiếp với các điểm cuối trình duyệt từ xa.
Bắt đầu trên kế hoạch miễn phí Scrapeless để thu thập dữ liệu trình duyệt được quản lý, và xem lại giá Scrapeless khi khối lượng công việc đó tăng lên.
Câu hỏi thường gặp
Q: Ghostwire thực sự có phải là Frida cho trình duyệt không?
Nó đảm nhận cùng một vai trò — công cụ ghi lại thời gian thực thay vì đọc tĩnh — nhưng các triển khai khác nhau. Frida tiêm một đại lý vào một quy trình gốc; Ghostwire gắn một phiên gỡ lỗi vào Chrome và không bao giờ đưa mã vào trang.
Q: Tại sao Ghostwire lại sử dụng một pipe thay vì một WebSocket?
--remote-debugging-pipe tránh việc mở một cổng gỡ lỗi mà JavaScript trên trang có thể phát hiện, và nó loại bỏ nhu cầu về một phụ thuộc WebSocket trong lõi.
Q: Việc hooking một hàm có thay đổi những gì mà trang nhìn thấy không?
Không đối với các kiểm tra phát hiện thông thường. Hooks được đặt với Debugger.setBreakpointOnFunctionCall trên đối tượng trực tiếp, vì vậy đối tượng không bị bọc và fn.toString() vẫn trả về nguồn gốc ban đầu.
Q: Oracle xác minh thực sự chứng minh điều gì?
Rằng ứng viên của bạn đã khớp với hàm thực tế trên mọi đầu vào đã thử nghiệm. Đó là bằng chứng bị ràng buộc bởi độ phủ, không phải là chứng minh sự tương đương, vì vậy một kết quả vượt qua với 12 cặp có nghĩa là ít hơn một với độ phủ trường hợp biên rộng.
Q: Ghostwire có thể kết nối với Scrapeless Scraping Browser không?
Không. Ghostwire 0.1.0 khởi động và sở hữu một Chrome cục bộ qua một pipe OS, trong khi Scraping Browser mở một điểm cuối CDP WebSocket từ xa. Kết nối chúng cần một phương tiện WebSocket và xử lý vòng đời từ xa mà upstream không cung cấp.
Q: Có an toàn khi chạy điều này trên bất kỳ trang web nào không?
Không. Công cụ ghi lại thời gian thực nên được giới hạn cho các hệ thống mà bạn sở hữu hoặc đã có quyền viết để thử nghiệm. Xem xét các điều khoản của mục tiêu và chỉ thị robots, và hãy nhận lời khuyên pháp lý trước khi phân tích mã của bên thứ ba.
Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.



