From c2f668b9dba9c61c64c35b4a3dc7f93d709a6e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=9F=E4=B8=8D=E6=87=82=E5=93=8E?= <1797499294@qq.com> Date: Mon, 4 May 2026 23:03:28 +0800 Subject: [PATCH] v1.0.8 --- README.md | 14 + gui.py | 328 ++++++++++++++++-- gui_config.json | 6 +- pyproject.toml | 16 + src/__pycache__/__init__.cpython-310.pyc | Bin 145 -> 145 bytes src/__pycache__/cli.cpython-310.pyc | Bin 2562 -> 2562 bytes src/__pycache__/cloud_audit.cpython-310.pyc | Bin 7811 -> 7811 bytes src/__pycache__/codex.cpython-310.pyc | Bin 6446 -> 6446 bytes src/__pycache__/constants.cpython-310.pyc | Bin 3129 -> 3129 bytes src/__pycache__/engine.cpython-310.pyc | Bin 17702 -> 17702 bytes src/__pycache__/extractor.cpython-310.pyc | Bin 38901 -> 38903 bytes src/__pycache__/logger.cpython-310.pyc | Bin 1941 -> 1941 bytes src/__pycache__/navigator.cpython-310.pyc | Bin 6556 -> 6556 bytes src/__pycache__/userscript.cpython-310.pyc | Bin 3586 -> 3586 bytes src/__pycache__/wxapkg.cpython-310.pyc | Bin 6112 -> 6112 bytes src/extractor.py | 2 +- .../__pycache__/__init__.cpython-310.pyc | Bin 157 -> 157 bytes .../wmpf_debug_pb2.cpython-310.pyc | Bin 10382 -> 10382 bytes uv.lock | 235 +++++++++++++ 启动.bat | 131 +++++-- 启动.sh | 162 +++++++-- 21 files changed, 788 insertions(+), 106 deletions(-) create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/README.md b/README.md index e5cbdeb..e7530a9 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,20 @@ devtools://devtools/bundled/inspector.html?ws=127.0.0.1:62000 --- +## 服务号调试操作 + +1. 打开一个小程序页面作为占位(保持不关闭) +2. 访问需要调试的服务号内容(如 H5 网页) + + ![PixPin_2026-05-04_22-48-42](https://s1.galgame.fun/imgb/u55/20260504_69f8b299a7d85.png) + +3. 在工具栏「服务」目标列表中找到目标网页,双击后切换至调试视图即可开始调试 + + ![PixPin_2026-05-04_22-51-14](https://s1.galgame.fun/imgb/u55/20260504_69f8b299a7d94.png) + ![PixPin_2026-05-04_22-51-39](https://s1.galgame.fun/imgb/u55/20260504_69f8b299a804b.png) + +--- + ## macOS 注意事项 如果 Frida 注入报错,需要解除系统对进程附加的限制,任选其一: diff --git a/gui.py b/gui.py index 790781a..2f81e23 100644 --- a/gui.py +++ b/gui.py @@ -51,12 +51,18 @@ _L = dict( success="#16a34a", error="#dc2626", warning="#ca8a04", ) _TH = {"dark": _D, "light": _L} -_FN = "Microsoft YaHei UI" -_FM = "Consolas" +# Platform-aware default fonts (overridable via gui_config.json: font_ui / font_mono) +if sys.platform == "darwin": + _FN = "PingFang SC" + _FM = "Menlo" +else: + _FN = "Microsoft YaHei UI" + _FM = "Consolas" _MENU = [ ("control", "◉", "控制台"), ("navigator", "⬡", "路由导航"), ("hook", "◈", "Hook"), + ("targets", "◎", "服务目标"), ("cloud", "☁", "云扫描"), ("extract", "◆", "敏感信息提取"), ("vconsole", "◇", "调试开关"), @@ -90,6 +96,49 @@ def _save_cfg(data): pass +# Apply font overrides from config. +# Editable fields in gui_config.json: +# font_ui — UI font family name +# font_ui_size — UI base font size (int, default 9) +# font_mono — monospace font family name +# font_mono_size — monospace font size (int, default 9) +_early_cfg = _load_cfg() +if _early_cfg.get("font_ui"): + _FN = _early_cfg["font_ui"] +if _early_cfg.get("font_mono"): + _FM = _early_cfg["font_mono"] +_FN_SIZE: int = int(_early_cfg.get("font_ui_size", 9)) +_FM_SIZE: int = int(_early_cfg.get("font_mono_size", 9)) + +# Write defaults back so the fields appear in gui_config.json on first launch +_init_cfg = dict(_early_cfg) +_cfg_dirty = False +for _k, _v in [("font_ui", _FN), ("font_ui_size", _FN_SIZE), + ("font_mono", _FM), ("font_mono_size", _FM_SIZE)]: + if _k not in _init_cfg: + _init_cfg[_k] = _v + _cfg_dirty = True +if _cfg_dirty: + _save_cfg(_init_cfg) +del _init_cfg, _cfg_dirty + + +def _qfn(offset: int = 0, weight=None): + """Return QFont for UI font with size relative to _FN_SIZE.""" + f = QFont(_FN, max(6, _FN_SIZE + offset)) + if weight is not None: + f.setWeight(weight) + return f + + +def _qfm(offset: int = 0, weight=None): + """Return QFont for mono font with size relative to _FM_SIZE.""" + f = QFont(_FM, max(6, _FM_SIZE + offset)) + if weight is not None: + f.setWeight(weight) + return f + + # ══════════════════════════════════════════ # QSS 主题 # ══════════════════════════════════════════ @@ -614,7 +663,7 @@ def _make_label(text, bold=False, muted=False, mono=False): elif muted: l.setProperty("class", "muted") if mono: - l.setFont(QFont(_FM, 10)) + l.setFont(_qfm(1)) return l @@ -646,8 +695,8 @@ class App(QMainWindow): _ico = os.path.join(_BASE_DIR, "icon.png") if os.path.exists(_ico): self.setWindowIcon(QIcon(_ico)) - self.resize(960, 620) - self.setMinimumSize(780, 500) + self.resize(1060, 660) + self.setMinimumSize(900, 540) self._cfg = _load_cfg() self._tn = self._cfg.get("theme", "dark") @@ -675,6 +724,7 @@ class App(QMainWindow): self._sts_q = queue.Queue() self._rte_q = queue.Queue() self._cld_q = queue.Queue() + self._tgt_q = queue.Queue() self._nav_route_idx = -1 self._sb_items = {} @@ -761,10 +811,10 @@ class App(QMainWindow): row_lay.setSpacing(6) ic = QLabel(icon) ic.setProperty("class", "sb_icon") - ic.setFont(QFont(_FN, 13)) + ic.setFont(_qfn(4)) nm = QLabel(name) nm.setProperty("class", "sb_name") - nm.setFont(QFont(_FN, 10)) + nm.setFont(_qfn(1)) row_lay.addWidget(ic) row_lay.addWidget(nm, 1) sb_nav_lay.addWidget(row) @@ -783,13 +833,13 @@ class App(QMainWindow): sb_app_card_lay.setSpacing(1) self._sb_app_name = QLabel("未连接") self._sb_app_name.setAlignment(Qt.AlignCenter) - self._sb_app_name.setFont(QFont(_FN, 8)) + self._sb_app_name.setFont(_qfn(-1)) self._sb_app_name.setStyleSheet("color: #5c5c6c;") self._sb_app_name.setWordWrap(True) sb_app_card_lay.addWidget(self._sb_app_name) self._sb_app_id = QLabel("") self._sb_app_id.setAlignment(Qt.AlignCenter) - self._sb_app_id.setFont(QFont(_FN, 8)) + self._sb_app_id.setFont(_qfn(-1)) self._sb_app_id.setStyleSheet("color: #9e9ea8;") self._sb_app_id.setVisible(False) self._sb_app_id.setWordWrap(True) @@ -801,28 +851,28 @@ class App(QMainWindow): self._sb_theme.setObjectName("sb_theme") self._sb_theme.setAlignment(Qt.AlignCenter) self._sb_theme.setCursor(Qt.PointingHandCursor) - self._sb_theme.setFont(QFont(_FN, 9)) + self._sb_theme.setFont(_qfn()) self._sb_theme.mousePressEvent = lambda e: self._toggle_theme() sb_lay.addWidget(self._sb_theme) sb_author = QLabel("by vs-olitus") sb_author.setObjectName("sb_theme") sb_author.setAlignment(Qt.AlignCenter) - sb_author.setFont(QFont(_FN, 8)) + sb_author.setFont(_qfn(-1)) sb_lay.addWidget(sb_author) sb_gh = QLabel("github.com/Spade-sec/First") sb_gh.setObjectName("sb_theme") sb_gh.setAlignment(Qt.AlignCenter) - sb_gh.setFont(QFont(_FN, 7)) + sb_gh.setFont(_qfn(-2)) sb_gh.setCursor(Qt.PointingHandCursor) sb_gh.mousePressEvent = lambda e: ( QDesktopServices.openUrl(QUrl("https://github.com/Spade-sec/First")), self._log_add("info", "[gui] 已打开 GitHub 页面")) sb_lay.addWidget(sb_gh) - sb_ver = QLabel("v1.0.7") + sb_ver = QLabel("v1.0.8") sb_ver.setObjectName("sb_theme") sb_ver.setAlignment(Qt.AlignCenter) - sb_ver.setFont(QFont(_FN, 7)) + sb_ver.setFont(_qfn(-2)) sb_lay.addWidget(sb_ver) sb_lay.addSpacing(12) self._update_theme_label() @@ -862,6 +912,7 @@ class App(QMainWindow): self._build_control() self._build_navigator() self._build_hook() + self._build_targets() self._build_cloud() self._build_extract() self._build_vconsole() @@ -897,10 +948,10 @@ class App(QMainWindow): # Action row ar = QHBoxLayout() self._btn_start = _make_btn("▶ 启动调试", self._do_start) - self._btn_start.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_start.setFont(_qfn(1, QFont.Bold)) ar.addWidget(self._btn_start) self._btn_stop = _make_btn("■ 停止", self._do_stop) - self._btn_stop.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_stop.setFont(_qfn(1, QFont.Bold)) self._btn_stop.setEnabled(False) ar.addWidget(self._btn_stop) ar.addStretch() @@ -910,13 +961,13 @@ class App(QMainWindow): dt_row = QHBoxLayout() self._devtools_lbl = QLabel("") self._devtools_lbl.setProperty("class", "accent") - self._devtools_lbl.setFont(QFont(_FM, 8)) + self._devtools_lbl.setFont(_qfm(-1)) self._devtools_lbl.setCursor(Qt.PointingHandCursor) self._devtools_lbl.mousePressEvent = lambda e: self._copy_devtools_url() dt_row.addWidget(self._devtools_lbl) self._devtools_copy_hint = QLabel("") self._devtools_copy_hint.setProperty("class", "muted") - self._devtools_copy_hint.setFont(QFont(_FN, 8)) + self._devtools_copy_hint.setFont(_qfn(-1)) dt_row.addWidget(self._devtools_copy_hint) dt_row.addStretch() lay.addLayout(dt_row) @@ -992,6 +1043,7 @@ class App(QMainWindow): tc_lay = QVBoxLayout(tc) tc_lay.setContentsMargins(0, 0, 0, 0) self._tree = QTreeWidget() + self._tree.setFont(_qfn()) self._tree.setHeaderHidden(True) self._tree.setSelectionMode(QAbstractItemView.SingleSelection) self._tree.setContextMenuPolicy(Qt.CustomContextMenu) @@ -1138,13 +1190,13 @@ class App(QMainWindow): icon_lbl = QLabel("JS") icon_lbl.setProperty("class", "js_badge") - icon_lbl.setFont(QFont(_FM, 8, QFont.Bold)) + icon_lbl.setFont(_qfm(-1, QFont.Bold)) icon_lbl.setFixedWidth(30) icon_lbl.setAlignment(Qt.AlignCenter) row_lay.addWidget(icon_lbl) name_lbl = QLabel(fn) - name_lbl.setFont(QFont(_FN, 10)) + name_lbl.setFont(_qfn(1)) row_lay.addWidget(name_lbl, 1) is_global = fn in self._global_hook_scripts @@ -1260,6 +1312,160 @@ class App(QMainWindow): self._hook_auto_inject_globals() self._log_add("info", "[Hook] 自动注入全局脚本") + # ── 页面目标 ── + + def _build_targets(self): + page = QWidget() + lay = QVBoxLayout(page) + lay.setContentsMargins(24, 12, 24, 16) + lay.setSpacing(10) + + tip_row = QHBoxLayout() + tip = QLabel("列出当前调试会话可见的微信内置浏览器 / 小程序 / webview 服务") + tip.setProperty("class", "muted") + tip_row.addWidget(tip) + tip_row.addStretch() + self._btn_tgt_refresh = _make_btn("刷新目标", self._do_targets_refresh) + self._btn_tgt_refresh.setEnabled(False) + tip_row.addWidget(self._btn_tgt_refresh) + self._btn_tgt_attach = _make_btn("附加到选中目标", self._do_targets_attach) + self._btn_tgt_attach.setEnabled(False) + tip_row.addWidget(self._btn_tgt_attach) + self._btn_tgt_copy = _make_btn("复制 TargetId", self._do_targets_copy) + self._btn_tgt_copy.setEnabled(False) + tip_row.addWidget(self._btn_tgt_copy) + lay.addLayout(tip_row) + + tc = _make_card() + tc_lay = QVBoxLayout(tc) + tc_lay.setContentsMargins(0, 0, 0, 0) + self._targets_tree = QTreeWidget() + self._targets_tree.setFont(_qfn()) + self._targets_tree.header().setFont(_qfn()) + self._targets_tree.setRootIsDecorated(False) + self._targets_tree.setIndentation(0) + self._targets_tree.setHeaderLabels(["类型", "标题", "URL", "TargetId"]) + self._targets_tree.setSelectionMode(QAbstractItemView.SingleSelection) + self._targets_tree.itemSelectionChanged.connect(self._targets_on_select) + self._targets_tree.itemDoubleClicked.connect(lambda *_: self._do_targets_attach()) + hdr = self._targets_tree.header() + hdr.setSectionResizeMode(0, QHeaderView.ResizeToContents) + hdr.setSectionResizeMode(1, QHeaderView.Interactive) + hdr.setSectionResizeMode(2, QHeaderView.Stretch) + hdr.setSectionResizeMode(3, QHeaderView.Interactive) + hdr.setMinimumSectionSize(50) + self._targets_tree.setColumnWidth(1, 200) + self._targets_tree.setColumnWidth(3, 290) + tc_lay.addWidget(self._targets_tree) + lay.addWidget(tc, 1) + + self._targets_status_lbl = QLabel("状态: 未连接小程序") + self._targets_status_lbl.setProperty("class", "muted") + lay.addWidget(self._targets_status_lbl) + + self._stack.addWidget(page) + self._page_map["targets"] = self._stack.count() - 1 + + def _targets_btns(self, on): + if not hasattr(self, "_btn_tgt_refresh"): + return + self._btn_tgt_refresh.setEnabled(on) + has_sel = bool(on and self._targets_tree.selectedItems()) + self._btn_tgt_attach.setEnabled(has_sel) + self._btn_tgt_copy.setEnabled(has_sel) + + def _targets_on_select(self): + self._targets_btns(bool(self._engine and self._miniapp_connected)) + + def _selected_target(self): + items = self._targets_tree.selectedItems() + if not items: + self._log_add("error", "[页面目标] 请先选择一个目标") + return None + return items[0].data(0, Qt.UserRole) or {} + + def _do_targets_refresh(self): + if not self._engine or not self._loop or not self._loop.is_running(): + self._log_add("error", "[页面目标] 请先启动调试并连接小程序") + return + self._btn_tgt_refresh.setEnabled(False) + self._targets_status_lbl.setText("状态: 正在刷新...") + asyncio.run_coroutine_threadsafe(self._atargets_refresh(), self._loop) + + async def _atargets_refresh(self): + try: + resp = await self._engine.send_cdp_command("Target.getTargets", timeout=8.0) + infos = resp.get("result", {}).get("targetInfos", []) + self._tgt_q.put(("targets", infos)) + except Exception as e: + self._tgt_q.put(("error", f"刷新失败: {e}")) + + def _do_targets_attach(self): + target = self._selected_target() + if not target or not self._engine or not self._loop or not self._loop.is_running(): + return + target_id = target.get("targetId", "") + if not target_id: + self._log_add("error", "[页面目标] 选中项没有 TargetId") + return + self._btn_tgt_attach.setEnabled(False) + self._targets_status_lbl.setText("状态: 正在附加...") + asyncio.run_coroutine_threadsafe(self._atargets_attach(target_id), self._loop) + + async def _atargets_attach(self, target_id): + try: + resp = await self._engine.send_cdp_command( + "Target.attachToTarget", {"targetId": target_id}, timeout=8.0) + self._tgt_q.put(("attached", target_id, resp)) + except Exception as e: + self._tgt_q.put(("error", f"附加失败: {e}")) + + def _do_targets_copy(self): + target = self._selected_target() + if not target: + return + target_id = target.get("targetId", "") + if target_id: + QApplication.clipboard().setText(target_id) + self._targets_status_lbl.setText("状态: TargetId 已复制") + self._log_add("info", f"[页面目标] 已复制 TargetId: {target_id}") + + def _handle_tgt(self, item): + kind = item[0] + c = _TH[self._tn] + if kind == "targets": + targets = item[1] + self._targets_tree.clear() + for target in targets: + target_id = target.get("targetId", "") + title = target.get("title", "") or "--" + url = target.get("url", "") or "--" + typ = target.get("type", "") or "--" + row = QTreeWidgetItem([typ, title, url, target_id]) + row.setData(0, Qt.UserRole, target) + if target.get("attached"): + for col in range(4): + row.setForeground(col, QColor(c["success"])) + self._targets_tree.addTopLevelItem(row) + self._targets_status_lbl.setText(f"状态: 发现 {len(targets)} 个目标") + self._targets_status_lbl.setStyleSheet(f"color: {c['success']};") + self._log_add("info", f"[页面目标] 发现 {len(targets)} 个可选目标") + self._targets_btns(bool(self._miniapp_connected)) + elif kind == "attached": + _, target_id, resp = item + session_id = resp.get("result", {}).get("sessionId", "") + suffix = f", sessionId={session_id}" if session_id else "" + self._targets_status_lbl.setText(f"状态: 已附加 {target_id}{suffix}") + self._targets_status_lbl.setStyleSheet(f"color: {c['success']};") + self._log_add("info", f"[页面目标] 已附加到目标: {target_id}{suffix}") + self._targets_btns(bool(self._miniapp_connected)) + elif kind == "error": + msg = item[1] + self._targets_status_lbl.setText(f"状态: {msg}") + self._targets_status_lbl.setStyleSheet(f"color: {c['error']};") + self._log_add("error", f"[页面目标] {msg}") + self._targets_btns(bool(self._miniapp_connected)) + # ── 云扫描 ── def _build_cloud(self): @@ -1299,6 +1505,8 @@ class App(QMainWindow): tc_lay.addLayout(title_row) self._cloud_tree = QTreeWidget() + self._cloud_tree.setFont(_qfn()) + self._cloud_tree.header().setFont(_qfn()) self._cloud_tree.setRootIsDecorated(False) self._cloud_tree.setIndentation(0) self._cloud_tree.setHeaderLabels(["AppID", "类型", "名称", "参数", "状态", "时间"]) @@ -1338,7 +1546,7 @@ class App(QMainWindow): self._cloud_result = QTextEdit() self._cloud_result.setReadOnly(True) self._cloud_result.setFixedHeight(120) - self._cloud_result.setFont(QFont(_FM, 9)) + self._cloud_result.setFont(_qfm()) lay.addWidget(self._cloud_result) bot = QHBoxLayout() @@ -1462,7 +1670,7 @@ class App(QMainWindow): # 日志区 self._ext_logbox = QTextEdit() self._ext_logbox.setReadOnly(True) - self._ext_logbox.setFont(QFont(_FM, 9)) + self._ext_logbox.setFont(_qfm()) self._ext_logbox.setMaximumHeight(120) main_lay.addWidget(self._ext_logbox) @@ -1531,6 +1739,8 @@ class App(QMainWindow): bc_lay.addWidget(_make_label("内置正则规则 (只读)", bold=True)) self._ext_builtin_tree = QTreeWidget() + self._ext_builtin_tree.setFont(_qfn()) + self._ext_builtin_tree.header().setFont(_qfn()) self._ext_builtin_tree.setHeaderLabels(["分类", "正则/说明"]) bh = self._ext_builtin_tree.header() bh.setStretchLastSection(True) @@ -1763,7 +1973,7 @@ class App(QMainWindow): lbl_id = QLabel(appid) lbl_id.setFixedWidth(180) - lbl_id.setFont(QFont(_FM, 9)) + lbl_id.setFont(_qfm()) lbl_id.setStyleSheet(f"color: {c['text1']};") row_lay.addWidget(lbl_id) @@ -1781,7 +1991,7 @@ class App(QMainWindow): app_name = self._ext_get_app_name(appid, pkgs, output_base) if is_decompiled else f"{len(pkgs)} pkg (未反编译)" lbl_name = QLabel(app_name) lbl_name.setMinimumWidth(100) - lbl_name.setFont(QFont(_FN, 9)) + lbl_name.setFont(_qfn()) lbl_name.setStyleSheet(f"color: {c['text2']};") row_lay.addWidget(lbl_name, 1) @@ -1824,7 +2034,8 @@ class App(QMainWindow): self._ext_app_widgets[appid] = { "row": row, "btn_dec": btn_dec, "btn_scan": btn_scan, - "btn_view": btn_view, "btn_del": btn_del, "lbl_name": lbl_name, + "btn_view": btn_view, "btn_del": btn_del, + "lbl_name": lbl_name, "lbl_id": lbl_id, } # 插入在最前面(最新的在上面) @@ -2100,7 +2311,7 @@ class App(QMainWindow): title_row.addWidget(btn_fold) title_lbl = QLabel(f"{label} ({len(items)})") - title_lbl.setFont(QFont(_FN, 11, QFont.Weight.Bold)) + title_lbl.setFont(_qfn(2, QFont.Weight.Bold)) title_lbl.setStyleSheet( f"color: {c['text1']}; border-left: 4px solid {accent}; padding-left: 8px;" f"background: transparent;") @@ -2126,7 +2337,7 @@ class App(QMainWindow): content_lay.setSpacing(0) if items: content_lbl = QLabel() - content_lbl.setFont(QFont(_FM, 9)) + content_lbl.setFont(_qfm()) content_lbl.setWordWrap(True) content_lbl.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) content_lbl.setStyleSheet( @@ -2319,11 +2530,11 @@ class App(QMainWindow): rl.setSpacing(6) lbl_n = QLabel(name) lbl_n.setFixedWidth(120) - lbl_n.setFont(QFont(_FM, 9)) + lbl_n.setFont(_qfm()) lbl_n.setStyleSheet(f"color: {c['text1']};") rl.addWidget(lbl_n) lbl_p = QLabel(pat if len(pat) < 60 else pat[:60] + "...") - lbl_p.setFont(QFont(_FM, 9)) + lbl_p.setFont(_qfm()) lbl_p.setStyleSheet(f"color: {c['text2']};") lbl_p.setToolTip(pat) lbl_p.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) @@ -2458,7 +2669,7 @@ class App(QMainWindow): input_lbl = QLabel("输入测试文本:") lay.addWidget(input_lbl) input_box = QTextEdit() - input_box.setFont(QFont(_FM, 9)) + input_box.setFont(_qfm()) input_box.setPlaceholderText("在此粘贴或输入要测试的文本...") lay.addWidget(input_box, 1) @@ -2466,7 +2677,7 @@ class App(QMainWindow): lay.addWidget(result_lbl) result_box = QTextEdit() result_box.setReadOnly(True) - result_box.setFont(QFont(_FM, 9)) + result_box.setFont(_qfm()) lay.addWidget(result_box, 1) def do_test(): @@ -2594,7 +2805,7 @@ class App(QMainWindow): warn_lay.setContentsMargins(16, 12, 16, 12) warn_lay.setSpacing(6) warn_title = QLabel("⚠ 风险提示") - warn_title.setFont(QFont(_FN, 11, QFont.Bold)) + warn_title.setFont(_qfn(2, QFont.Bold)) warn_title.setStyleSheet("color: #e6a23c;") warn_lay.addWidget(warn_title) warn_text = QLabel( @@ -2637,11 +2848,11 @@ class App(QMainWindow): btn_row = QHBoxLayout() self._btn_vc_enable = _make_btn("▶ 开启调试", self._do_vc_enable) - self._btn_vc_enable.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_vc_enable.setFont(_qfn(1, QFont.Bold)) self._btn_vc_enable.setEnabled(False) btn_row.addWidget(self._btn_vc_enable) self._btn_vc_disable = _make_btn("■ 关闭调试", self._do_vc_disable) - self._btn_vc_disable.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_vc_disable.setFont(_qfn(1, QFont.Bold)) self._btn_vc_disable.setEnabled(False) btn_row.addWidget(self._btn_vc_disable) btn_row.addStretch() @@ -2789,7 +3000,7 @@ class App(QMainWindow): lc_lay.setContentsMargins(0, 0, 0, 0) self._logbox = QTextEdit() self._logbox.setReadOnly(True) - self._logbox.setFont(QFont(_FM, 9)) + self._logbox.setFont(_qfm()) lc_lay.addWidget(self._logbox) lay.addWidget(lc, 1) @@ -2831,10 +3042,37 @@ class App(QMainWindow): self._update_theme_label() self._update_toggle_colors() self._refresh_sb_app_card() + self._ext_restyle_rows() self._ext_refresh_custom_patterns() self._hl_sb() self._auto_save() + def _ext_restyle_rows(self): + """切换主题时更新应用列表行的内联样式。""" + c = _TH[self._tn] + row_ss = ( + f"QFrame {{ background: {c['input']}; border-radius: 8px; }}" + f"QFrame QLabel {{ background: transparent; }}" + ) + for appid, widgets in self._ext_app_widgets.items(): + row = widgets.get("row") + if row: + row.setStyleSheet(row_ss) + lbl_id = widgets.get("lbl_id") + if lbl_id: + lbl_id.setStyleSheet(f"color: {c['text1']};") + lbl_name = widgets.get("lbl_name") + if lbl_name: + lbl_name.setStyleSheet(f"color: {c['text2']};") + # 反编译按钮颜色也跟主题走 + btn_dec = widgets.get("btn_dec") + if btn_dec: + state = self._ext_app_states.get(appid, {}) + if state.get("decompiled"): + btn_dec.setStyleSheet( + f"QPushButton {{ background: {c['success']}; color: #111; " + f"border-radius: 6px; padding: 4px 8px; font-size: 9px; }}") + def _update_theme_label(self): txt = "☀ 浅色模式" if self._tn == "dark" else "☽ 深色模式" self._sb_theme.setText(txt) @@ -2864,6 +3102,10 @@ class App(QMainWindow): "auto_decompile": self._tog_auto_dec.isChecked(), "auto_scan": self._tog_auto_scan.isChecked(), "global_hook_scripts": list(self._global_hook_scripts), + "font_ui": _FN, + "font_ui_size": _FN_SIZE, + "font_mono": _FM, + "font_mono_size": _FM_SIZE, } _save_cfg(data) @@ -2963,6 +3205,7 @@ class App(QMainWindow): self._btn_stop.setEnabled(False) self._btn_fetch.setEnabled(False) self._nav_btns(False) + self._targets_btns(False) if self._loop and self._loop.is_running(): self._loop.call_soon_threadsafe(self._loop.stop) @@ -2985,10 +3228,13 @@ class App(QMainWindow): self._btn_stop.setEnabled(False) self._btn_fetch.setEnabled(False) self._nav_btns(False) + self._targets_btns(False) self._btn_autostop.setEnabled(False) self._redirect_guard_on = False self._guard_switch.setChecked(False) self._guard_label.setText("防跳转: 关闭") + self._targets_tree.clear() + self._targets_status_lbl.setText("状态: 未连接小程序") self._devtools_lbl.setText("") self._devtools_copy_hint.setText("") # 引擎停止,清除侧栏和运行状态卡片的小程序信息 @@ -3064,6 +3310,8 @@ class App(QMainWindow): if not self._miniapp_connected: return self._nav_btns(True) + self._targets_btns(True) + self._targets_status_lbl.setText("状态: 可刷新目标") self._btn_vc_enable.setEnabled(True) self._btn_vc_disable.setEnabled(True) self._vc_status_lbl.setText("状态: 就绪") @@ -3622,6 +3870,12 @@ class App(QMainWindow): except queue.Empty: break self._handle_cld(item) + for _ in range(50): + try: + item = self._tgt_q.get_nowait() + except queue.Empty: + break + self._handle_tgt(item) for _ in range(50): try: item = self._ext_q.get_nowait() @@ -3641,6 +3895,8 @@ class App(QMainWindow): if not is_connected and self._miniapp_connected: if not self._all_routes: self._nav_btns(False) + self._targets_btns(False) + self._targets_status_lbl.setText("状态: 未连接小程序") self._btn_vc_enable.setEnabled(False) self._btn_vc_disable.setEnabled(False) self._vc_status_lbl.setText("状态: 未连接小程序") @@ -3936,7 +4192,7 @@ if __name__ == "__main__": pass app = QApplication(sys.argv) - app.setFont(QFont(_FN, 9)) + app.setFont(QFont(_FN, _FN_SIZE)) _ico = os.path.join(_BASE_DIR, "icon.png") if os.path.exists(_ico): app.setWindowIcon(QIcon(_ico)) diff --git a/gui_config.json b/gui_config.json index 394131c..8a08bf3 100644 --- a/gui_config.json +++ b/gui_config.json @@ -7,5 +7,9 @@ "extract_custom_patterns": {}, "auto_decompile": false, "auto_scan": false, - "global_hook_scripts": [] + "global_hook_scripts": [], + "font_ui": "PingFang SC", + "font_ui_size": 12, + "font_mono": "Menlo", + "font_mono_size": 15 } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5e805c1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "first" +version = "0.1.0" +description = "WeChat mini program security debugging tool based on Frida and CDP proxy." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "frida>=17.0.0", + "websockets>=12.0", + "protobuf>=4.0.0", + "pyside6>=6.5.0", + "pycryptodome", +] + +[tool.uv] +package = false diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc index 10cf21257111e65747bb7f1a7aa43a55515d0f8c..e025f2093e5a11167f6642f2c1a08eea05d9fc00 100644 GIT binary patch delta 19 ZcmbQpIFXS%pO=@50SF@Je3{7I1^_5T1kL~e delta 19 ZcmbQpIFXS%pO=@50SN3Ho=oI!0{|x61c(3t diff --git a/src/__pycache__/cli.cpython-310.pyc b/src/__pycache__/cli.cpython-310.pyc index c52b43e205e8a9b80faa792fa1c981a10b9db364..9ca9a2edb5dc99e23fe99735ab07df1193ce3c24 100644 GIT binary patch delta 20 acmZn?X%gYi=jG*M0D{OlUp8`o=L7&Q?*(uG delta 20 acmZn?X%gYi=jG*M00R4lCmXrHa{>S@K?N!R diff --git a/src/__pycache__/cloud_audit.cpython-310.pyc b/src/__pycache__/cloud_audit.cpython-310.pyc index cb76612c5662f875e85dc9c2da94ec6a33467829..e219f9acad8e4e507354d30ce844bfecdc06abb5 100644 GIT binary patch delta 285 zcmZp+ZMNmk=jG*M0D{OlUp8{z7I02UEJ?h@UR;`-oLXFbi!C`nFD*0u7ISJ|*)5j5 z#N5V{V=ysUUc_1vEa(a3YqA!>*+muzrJJioCNMJIn#?06#i+ko xUF;q+qrqf0si|yW)!LgEN^N6gtlu0V6Trw_1kzRwA{r)dmW^U;oy;d!3IK6WR1^RJ delta 269 zcmZp+ZMNmk=jG*M00R4lCmXqM3ruDb6j8jzoSIj5i@mrsIXShs_!dh_VoBmHw&eW0 zw9Irn7NFwEnt~eMQ9LR6<#{>zi79TGIjMFWK=E5_MX9;@WvRDV%QI4oQg1OQrlb@} z0VSe16Vua+QqvPlQg5-9761(dN~Yu|-(pS9FU>2N+$VUC(O`3~&>}_wZJ_cZYY<@$ zCML^^SSx}BJ%M~p)*?8&$O55sbG66>M#iYgJYrIe+MCtI?lCj!OlFgs$_7?#uz8`> fHb%y}%@HyIjNFAFZB-zme)4A7D8`n_d~&4#+2c#4 diff --git a/src/__pycache__/codex.cpython-310.pyc b/src/__pycache__/codex.cpython-310.pyc index 5ae5b11a70c32bede7067c2b8d1ad251b3b976d6..1c28f9b345378d97229f79c47d62bdceaff658d0 100644 GIT binary patch delta 20 acmZ2yw9be-pO=@50SF@JeA&pYEC~QO7X?57 delta 20 acmZ2yw9be-pO=@50SN3Ho^0e+mIMGcX$1NJ diff --git a/src/__pycache__/constants.cpython-310.pyc b/src/__pycache__/constants.cpython-310.pyc index e7f7cbac89ad6264d86d393195c4b3ca65a55c1d..3c79ebd77e242aeafd7e0b7d516f4ff53785d566 100644 GIT binary patch delta 20 acmdlfu~ULOpO=@50SF@JeA&pY#{&R18U-K# delta 20 acmdlfu~ULOpO=@50SN3Ho^0gS;{gCPYy{c> diff --git a/src/__pycache__/engine.cpython-310.pyc b/src/__pycache__/engine.cpython-310.pyc index d9c62f077e5019800bcf9c86e7eb31eae46cb988..fb57231131ebd229b877ecd6f3978818e2dc7fea 100644 GIT binary patch delta 22 ccmZ41#kj1CkvpH4mx}=iBIkVB$Sv&(07n`I1^@s6 delta 22 ccmZ41#kj1CkvpH4mx}=iF4?}=$Sv&(07vr%6aWAK diff --git a/src/__pycache__/extractor.cpython-310.pyc b/src/__pycache__/extractor.cpython-310.pyc index 91883b56187995809d60b80da0c4a9f78ed41200..0250170d11c084a27d00bd6e36964c7c38f049b5 100644 GIT binary patch delta 188 zcmeymp6UB~Cfb8m^$r8X4OXCIwr;~n|qjC7`bDVVm0)%?KPv-V>aJp*2t)5 z(JLvgsAAU3O)26AY5=lIimO=lGD>oDs#x@r)6#*Qtm6DUAg83F1jwmO1j?}LCFkd* z6>)-eumI&NfMf+&M=9K|emV+d?^$Lpe3sQ?pGE<9-Ku!ZnXXa&Av4P}^ Li#NCBb8uy1&hS-Fw7j)`&8<{l;&Moy(z4Lxmp&1m(_x0y9E>RI*5 zD{^zHSoBINN{T=_sz4&e#YNm8HnUzyF;FfwuMEh{O##X!=jWvXMKbd;fn-*31&{>l zFX9BLX34F$Em} delta 20 acmbQrKb4<5pO=@50SN3Ho^0gqVg~>&g9O(A diff --git a/src/__pycache__/navigator.cpython-310.pyc b/src/__pycache__/navigator.cpython-310.pyc index 56db18762f977b3ca11127be73e7d9e79f8d985d..698e48135cbb2b604a50cb5e64b94700572f5160 100644 GIT binary patch delta 20 acmbPZJja+jpO=@50SF@JeA&o7Q4#<)YXyt| delta 20 acmbPZJja+jpO=@50SN3Ho^0fvCGy#+=9 diff --git a/src/__pycache__/userscript.cpython-310.pyc b/src/__pycache__/userscript.cpython-310.pyc index e7895e2590844c7988e695b6dee822f9973e285b..93df31833e1c4c648f67ef3e44f08a2bcd21b0e9 100644 GIT binary patch delta 20 acmZpYX_Dd2=jG*M0D{OlUp8`o=LG;T69t3- delta 20 acmZpYX_Dd2=jG*M00R4lCmXrH^8x@aWd%L} diff --git a/src/__pycache__/wxapkg.cpython-310.pyc b/src/__pycache__/wxapkg.cpython-310.pyc index 0a97dbc8a67030f8e367189c0e65b7a48011778f..dcb988e439e7ff3dd28211b072604852140dde3f 100644 GIT binary patch delta 20 acmaE$|3IHRpO=@50SF@JeA&o-RU807c?M1Z delta 20 acmaE$|3IHRpO=@50SG=iz1YZoRU809Nd|xb diff --git a/src/extractor.py b/src/extractor.py index 48c646c..a7fe3f0 100644 --- a/src/extractor.py +++ b/src/extractor.py @@ -1,4 +1,4 @@ -""" +r""" extractor.py — 敏感信息提取引擎 直接使用 heartk_fast.py 的完整正则规则,保证提取结果一致 支持自定义正则规则,自定义类别会出现在导出报告中 diff --git a/src/third_party/__pycache__/__init__.cpython-310.pyc b/src/third_party/__pycache__/__init__.cpython-310.pyc index d347d2063d93a9a6aef59caec87dfd2deb491c85..d94d43dcdc160d74d9a362ced14838349af983ec 100644 GIT binary patch delta 19 ZcmbQsIG2$-pO=@50SF@Je3{5S2>>bp1o8j? delta 19 ZcmbQsIG2$-pO=@50SN3Ho=oJP1OOnul +title First - Launcher +cd /d "%~dp0" -:: Skip dependency check if already installed before -if exist ".deps_installed" goto :launch +set "CHOICE_FILE=.launch_choice" -:: Check if requirements.txt exists -if not exist "requirements.txt" ( +:: ══════════════════════════════════════════ +:: Read saved choice and launch directly +:: ══════════════════════════════════════════ +if exist "%CHOICE_FILE%" ( + set /p SAVED=<"%CHOICE_FILE%" + if "!SAVED!"=="uv" goto :RUN_UV + if "!SAVED!"=="system" goto :RUN_SYSTEM +) + +:: ══════════════════════════════════════════ +:: First run: ask user to choose environment +:: ══════════════════════════════════════════ +echo. +echo ============================================ +echo First ^| Launch Configuration +echo ============================================ +echo. +echo Please choose how to run First: +echo. +echo [1] uv virtual environment (Recommended) +echo Isolated dependencies, won't affect system Python +echo. +echo [2] System Python +echo Use system-installed Python directly +echo. +set /p CHOICE=" Enter choice (1/2): " + +if "%CHOICE%"=="1" goto :SETUP_UV +if "%CHOICE%"=="2" goto :SETUP_SYSTEM +echo Invalid choice. Exiting. +pause +exit /b 1 + +:: ══════════════════════════════════════════ +:: Branch: uv virtual environment +:: ══════════════════════════════════════════ +:SETUP_UV +where uv >nul 2>nul +if errorlevel 1 ( echo. - echo [WARNING] requirements.txt not found! + echo [WARNING] uv not found on this system. echo. - set /p "choice= Skip dependency install and launch directly? [Y/N]: " + set /p INSTALLUV=" Install uv now? (pip install uv) [Y/N]: " + if /i "!INSTALLUV!"=="Y" ( + echo Installing uv... + pip install uv + if errorlevel 1 ( + echo [ERROR] uv installation failed. Please run manually: pip install uv + pause + exit /b 1 + ) + ) else ( + echo Cancelled. + pause + exit /b 0 + ) +) + +:RUN_UV +echo. +echo Syncing uv virtual environment... +echo. +uv sync -i https://pypi.tuna.tsinghua.edu.cn/simple +if errorlevel 1 ( echo. - if /i "!choice!"=="Y" goto :launch - echo Cancelled. - timeout /t 2 >nul + echo [ERROR] uv sync failed. Check network or pyproject.toml. + pause + exit /b 1 +) +echo uv>"%CHOICE_FILE%" +start "" ".venv\Scripts\pythonw.exe" gui.py +exit /b 0 + +:: ══════════════════════════════════════════ +:: Branch: System Python +:: ══════════════════════════════════════════ +:SETUP_SYSTEM +where python >nul 2>nul +if errorlevel 1 ( + echo. + echo [ERROR] python not found. Please install Python 3.9+ first. + pause exit /b 1 ) -:: requirements.txt found, ask to install echo. -echo [OK] requirements.txt found. -echo. -set /p "install= Install dependencies? [Y/N]: " -echo. -if /i "!install!"=="Y" ( - echo Installing via Tsinghua mirror, please wait... +echo Checking dependencies... +python -c "import frida, websockets, google.protobuf, PySide6, Crypto" >nul 2>nul +if errorlevel 1 ( + echo Some dependencies are missing. echo. - pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - if errorlevel 1 ( - echo. - echo [ERROR] Installation failed! Check your network or pip config. + set /p INSTALLDEPS=" Install dependencies now? (pip install -r requirements.txt) [Y/N]: " + if /i "!INSTALLDEPS!"=="Y" ( + echo Installing dependencies... + pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + if errorlevel 1 ( + echo [ERROR] Installation failed. Try manually: pip install -r requirements.txt + pause + exit /b 1 + ) + ) else ( + echo Cancelled. pause - exit /b 1 + exit /b 0 ) - echo. - echo [OK] Done! Will skip this step on next launch. - echo. > .deps_installed - echo. -) else ( - echo. > .deps_installed ) -:: Launch gui.py without command window -:launch +:RUN_SYSTEM +echo system>"%CHOICE_FILE%" start "" pythonw gui.py exit /b 0 diff --git a/启动.sh b/启动.sh index 176828f..a77b865 100755 --- a/启动.sh +++ b/启动.sh @@ -1,51 +1,137 @@ #!/bin/bash cd "$(dirname "$0")" -# Skip dependency check if already installed before -if [ -f ".deps_installed" ]; then - python3 gui.py & - exit 0 -fi +# ── 配置文件路径 ── +CHOICE_FILE=".launch_choice" -# Check if requirements.txt exists -if [ ! -f "requirements.txt" ]; then - echo - echo " [WARNING] requirements.txt not found!" - echo - read -p " Skip dependency install and launch directly? [Y/N]: " choice - echo - if [[ "$choice" =~ ^[Yy]$ ]]; then +# ══════════════════════════════════════════ +# 对话框工具(macOS 用 osascript,Linux 降级为 read) +# ══════════════════════════════════════════ +dialog_choice() { + # $1=标题 $2=提示文字 $3=按钮1 $4=按钮2 + # 返回 0 = 按钮1,1 = 按钮2 + if [[ "$OSTYPE" == "darwin"* ]]; then + local btn + btn=$(osascript -e "button returned of (display dialog \"$2\" with title \"$1\" buttons {\"$4\", \"$3\"} default button \"$3\")" 2>/dev/null) + [[ "$btn" == "$3" ]] && return 0 || return 1 + else + echo "" + echo " $1" + echo " $2" + echo " [1] $3 [2] $4" + read -rp " 请输入选择 (1/2): " sel + [[ "$sel" == "1" ]] && return 0 || return 1 + fi +} + +dialog_info() { + # $1=标题 $2=内容 + if [[ "$OSTYPE" == "darwin"* ]]; then + osascript -e "display dialog \"$2\" with title \"$1\" buttons {\"确定\"} default button \"确定\"" >/dev/null 2>&1 + else + echo "" + echo " [$1] $2" + echo "" + fi +} + +# ══════════════════════════════════════════ +# 读取上次选择 +# ══════════════════════════════════════════ +if [[ -f "$CHOICE_FILE" ]]; then + SAVED=$(cat "$CHOICE_FILE") + # 直接按上次结果启动 + if [[ "$SAVED" == "uv" ]]; then + echo " [启动] 使用 uv 虚拟环境..." + uv sync -i https://pypi.tuna.tsinghua.edu.cn/simple + if [[ $? -ne 0 ]]; then + dialog_info "启动失败" "uv sync 失败,请检查网络或 uv 配置。" + exit 1 + fi + .venv/bin/python gui.py & + exit 0 + elif [[ "$SAVED" == "system" ]]; then + echo " [启动] 使用系统 Python..." python3 gui.py & exit 0 fi - echo " Cancelled." - exit 1 fi -# requirements.txt found, ask to install -echo -echo " [OK] requirements.txt found." -echo -read -p " Install dependencies? [Y/N]: " install -echo +# ══════════════════════════════════════════ +# 首次运行:让用户选择环境 +# ══════════════════════════════════════════ +dialog_choice "First — 启动配置" \ + "请选择运行方式:\n\n ✦ uv 虚拟环境(推荐)\n 自动隔离依赖,不影响系统 Python\n\n ✦ 系统 Python\n 使用系统已安装的 Python 直接运行" \ + "uv 虚拟环境" "系统 Python" +USE_UV=$? -if [[ "$install" =~ ^[Yy]$ ]]; then - echo " Installing dependencies, please wait..." - echo - pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - if [ $? -ne 0 ]; then - echo - echo " [ERROR] Installation failed! Check your network or pip config." +# ══════════════════════════════════════════ +# 分支:uv 虚拟环境 +# ══════════════════════════════════════════ +if [[ $USE_UV -eq 0 ]]; then + + # 检查 uv 是否已安装 + if ! command -v uv >/dev/null 2>&1; then + dialog_choice "未找到 uv" \ + "系统中未检测到 uv 工具。\n是否现在安装 uv?\n\n(将执行:pip install uv)" \ + "安装 uv" "取消" + if [[ $? -eq 0 ]]; then + echo " 正在安装 uv..." + pip install uv + if [[ $? -ne 0 ]]; then + dialog_info "安装失败" "uv 安装失败,请手动执行:pip install uv" + exit 1 + fi + else + echo " 已取消。" + exit 0 + fi + fi + + echo " 正在同步 uv 虚拟环境..." + uv sync -i https://pypi.tuna.tsinghua.edu.cn/simple + if [[ $? -ne 0 ]]; then + dialog_info "同步失败" "uv sync 失败,请检查网络连接或 pyproject.toml 配置。" exit 1 fi - echo - echo " [OK] Done! Will skip this step on next launch." - touch .deps_installed - echo -else - touch .deps_installed -fi -# Launch -python3 gui.py & -exit 0 + echo "uv" > "$CHOICE_FILE" + .venv/bin/python gui.py & + exit 0 + +# ══════════════════════════════════════════ +# 分支:系统 Python +# ══════════════════════════════════════════ +else + + # 检查 python3 + if ! command -v python3 >/dev/null 2>&1; then + dialog_info "未找到 Python" "系统中未检测到 python3,请先安装 Python 3.9+。" + exit 1 + fi + + # 检查是否缺少依赖 + MISSING=0 + python3 -c "import frida, websockets, google.protobuf, PySide6, Crypto" 2>/dev/null || MISSING=1 + + if [[ $MISSING -eq 1 ]]; then + dialog_choice "缺少依赖库" \ + "检测到部分依赖库未安装。\n是否现在安装?\n\n(将执行:pip install -r requirements.txt)" \ + "立即安装" "取消" + if [[ $? -eq 0 ]]; then + echo " 正在安装依赖..." + pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + if [[ $? -ne 0 ]]; then + dialog_info "安装失败" "依赖安装失败,请检查网络或手动执行:\npip install -r requirements.txt" + exit 1 + fi + else + echo " 已取消。" + exit 0 + fi + fi + + echo "system" > "$CHOICE_FILE" + python3 gui.py & + exit 0 +fi