Repository navigation
fix(AutoCheckinBigGod): 任务退出时清理所属 Frida 进程及子进程 #1951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,17 @@ | |
| class ScriptTask(BaseTask): | ||
|
|
||
| def run(self): | ||
| self._adb_connected = False | ||
| self._restore_game_after_checkin = False | ||
| try: | ||
| self._run_checkin() | ||
| finally: | ||
| self._cleanup(restore_game=self._restore_game_after_checkin) | ||
| session = getattr(self, 'session', None) | ||
| if session is not None: | ||
| session.close() | ||
|
|
||
| def _run_checkin(self): | ||
| self.gl_uid = "" | ||
| self.gl_token = "" | ||
| self.gl_deviceid = "" | ||
|
|
@@ -71,6 +82,7 @@ def run(self): | |
| self.set_next_run('AutoCheckinBigGod', success=False, finish=True) | ||
| raise TaskEnd('AutoCheckinBigGod') | ||
| logger.info('ADB已连接') | ||
| self._adb_connected = True | ||
|
|
||
| if not self._ensure_frida_server_running(): | ||
| logger.error('Frida Server启动失败,请检查模拟器环境') | ||
|
|
@@ -80,6 +92,7 @@ def run(self): | |
|
|
||
| # [2/5] 启动大神APP | ||
| logger.info('[2/5] 启动大神APP...') | ||
| self._restore_game_after_checkin = True | ||
| pid = self._launch_app() | ||
| if not pid: | ||
| logger.error('无法启动大神APP,请确保模拟器中已安装大神APP') | ||
|
|
@@ -141,7 +154,6 @@ def run(self): | |
|
|
||
| if not rewards: | ||
| logger.info('没有可领取的礼包') | ||
| self._cleanup(restore_game=True) | ||
| self.set_next_run('AutoCheckinBigGod', success=True, finish=True) | ||
| raise TaskEnd('AutoCheckinBigGod') | ||
|
|
||
|
|
@@ -154,21 +166,56 @@ def run(self): | |
| time.sleep(0.5) | ||
|
|
||
| logger.info(f'完成! 成功领取 {success_count}/{len(rewards)} 个礼包') | ||
| self._cleanup(restore_game=True) | ||
| self.set_next_run('AutoCheckinBigGod', success=True, finish=True) | ||
| raise TaskEnd('AutoCheckinBigGod') | ||
|
|
||
| # ======================== 清理 ======================== | ||
|
|
||
| def _cleanup(self, restore_game=False): | ||
| logger.info('清理:关闭大神APP和Frida Server...') | ||
| @staticmethod | ||
| def _stop_frida_process(proc): | ||
| if proc is None: | ||
| return | ||
| try: | ||
| if self._frida_session is not None: | ||
| self._frida_session.kill() | ||
| except Exception: | ||
| pass | ||
| if proc.poll() is None: | ||
| if sys.platform == 'win32' and isinstance(proc.pid, int): | ||
| # The Windows CLI launcher may have a Python child process. | ||
| # Target only this task's Popen PID and its descendants. | ||
| subprocess.run( | ||
| ['taskkill', '/PID', str(proc.pid), '/T', '/F'], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High · 清理过程会运行攻击者控制的可执行文件 当任务在 Windows 上运行且 Frida 进程仍在运行时,如果攻击者能够将 请从可信的 Windows 系统目录解析 给 AI 代理的提示在 `tasks/AutoCheckinBigGod/script_task.py` 第 184 行:
**清理过程会运行攻击者控制的可执行文件**
当任务在 Windows 上运行且 Frida 进程仍在运行时,如果攻击者能够将 `taskkill.exe` 放入一个搜索顺序早于系统工具所在目录的目录,`_stop_frida_process` 会按可执行文件名解析 `taskkill`,因此 Windows 可能会运行攻击者放置在较早搜索位置的 `taskkill.exe`,而不是系统工具;攻击者的代码将以该任务的权限运行。
请从可信的 Windows 系统目录解析 `taskkill.exe`,或者使用不会按名称搜索可执行文件的进程树终止机制。Original comment in English🟠 High · Cleanup runs attacker-controlled executable When the task runs on Windows with a live Frida process and an attacker can place Resolve Prompt for AI agentsIn `tasks/AutoCheckinBigGod/script_task.py` at line 184:
**Cleanup runs attacker-controlled executable**
When the task runs on Windows with a live Frida process and an attacker can place `taskkill.exe` in a directory searched before the system utility, `_stop_frida_process` resolves `taskkill` by executable name, so Windows can run an attacker-planted `taskkill.exe` from an earlier search location instead of the system utility; the attacker’s code runs with the task’s privileges.
Resolve `taskkill.exe` from the trusted Windows system directory, or use a process-tree termination mechanism that does not search for an executable by name. |
||
| capture_output=True, timeout=5, creationflags=_NO_WINDOW) | ||
| if proc.poll() is None: | ||
| proc.terminate() | ||
| try: | ||
| proc.wait(timeout=3) | ||
| except subprocess.TimeoutExpired: | ||
| proc.kill() | ||
| proc.wait(timeout=3) | ||
| except Exception as e: | ||
| logger.warning(f'关闭Frida进程失败: {e}') | ||
| try: | ||
| proc.kill() | ||
| proc.wait(timeout=3) | ||
| except Exception: | ||
| pass | ||
| finally: | ||
| for pipe in (proc.stdin, proc.stdout, proc.stderr): | ||
| if pipe is not None: | ||
| try: | ||
| pipe.close() | ||
| except Exception: | ||
| pass | ||
|
|
||
| def _close_frida_repl(self): | ||
| self._stop_frida_process(getattr(self, '_frida_session', None)) | ||
| self._frida_session = None | ||
| self._frida_attached_pid = None | ||
|
|
||
| def _cleanup(self, restore_game=False): | ||
| logger.info('清理:关闭大神APP和Frida Server...') | ||
| self._close_frida_repl() | ||
| self.frida_pid = None | ||
| if not getattr(self, '_adb_connected', False): | ||
| return | ||
| try: | ||
| self._adb_shell(['am', 'force-stop', GL_PACKAGE]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · 已打开的游戏会被关闭 当 ADB 已连接,但 Frida 服务器设置在此任务启动应用之前失败,并且 BigGod 原本就在运行时, 只有在此任务启动了应用时才强制停止它,并保留或恢复原本已在运行的应用状态。 另请参阅 给 AI 代理的提示在 `tasks/AutoCheckinBigGod/script_task.py` 第 220 行:
**已打开的游戏会被关闭**
当 ADB 已连接,但 Frida 服务器设置在此任务启动应用之前失败,并且 BigGod 原本就在运行时,`_cleanup` 会在 `_restore_game_after_checkin` 为 false 的情况下对 `GL_PACKAGE` 执行强制停止,导致此任务未启动的应用也被关闭。
只有在此任务启动了应用时才强制停止它,并保留或恢复原本已在运行的应用状态。
另请参阅 `tasks/AutoCheckinBigGod/script_task.py:55`。Original comment in English🟡 Medium · An already-open game is closed When ADB connects, Frida server setup fails before this task launches the app, and BigGod was already running, Only force-stop the app if this task launched it, and preserve or restore the state of an app that was already running. Also at Prompt for AI agentsIn `tasks/AutoCheckinBigGod/script_task.py` at line 220:
**An already-open game is closed**
When ADB connects, Frida server setup fails before this task launches the app, and BigGod was already running, `_cleanup` force-stops `GL_PACKAGE` while `_restore_game_after_checkin` is false, leaving the app this task did not launch closed.
Only force-stop the app if this task launched it, and preserve or restore the state of an app that was already running.
Also at `tasks/AutoCheckinBigGod/script_task.py:55`. |
||
| except Exception: | ||
|
|
@@ -444,14 +491,9 @@ def _ensure_frida_repl(self, pid): | |
| return self._frida_session | ||
| # PID 变了,需要重新 attach | ||
| logger.info(f'目标PID变更 ({self._frida_attached_pid} -> {pid}),重新attach...') | ||
| try: | ||
| self._frida_session.kill() | ||
| except Exception: | ||
| pass | ||
| else: | ||
| logger.warning('Frida REPL 进程已退出,重新启动...') | ||
| self._frida_session = None | ||
| self._frida_attached_pid = None | ||
| self._close_frida_repl() | ||
|
|
||
| frida_exe = self._get_frida_exe() | ||
| if hasattr(self, '_adb_serial') and self._adb_serial: | ||
|
|
@@ -476,7 +518,7 @@ def _ensure_frida_repl(self, pid): | |
| if proc.poll() is not None: | ||
| out = proc.stdout.read().decode('utf-8', errors='ignore') | ||
| logger.warning(f'Frida REPL 进程提前退出: {out[:300]}') | ||
| self._frida_session = None | ||
| self._close_frida_repl() | ||
| return None | ||
|
|
||
| # 直接向 stdin 写 ping,不走 _run_frida_script 避免副作用 | ||
|
|
@@ -488,15 +530,11 @@ def _ensure_frida_repl(self, pid): | |
|
|
||
| # 所有重试都失败 | ||
| logger.warning('Frida REPL 启动超时') | ||
| try: | ||
| proc.kill() | ||
| except Exception: | ||
| pass | ||
| self._frida_session = None | ||
| self._close_frida_repl() | ||
| return None | ||
| except Exception as e: | ||
| logger.warning(f'启动Frida REPL失败: {e}') | ||
| self._frida_session = None | ||
| self._close_frida_repl() | ||
| return None | ||
|
|
||
| def _ping_frida_repl(self, proc, timeout=8): | ||
|
|
@@ -560,7 +598,7 @@ def _run_frida_script(self, pid, script_content, timeout=10): | |
| proc.stdin.flush() | ||
| except (BrokenPipeError, OSError) as e: | ||
| logger.warning(f'Frida REPL stdin写入失败: {e}') | ||
| self._frida_session = None | ||
| self._close_frida_repl() | ||
| return self._run_frida_script_oneshot(pid, script_content, timeout) | ||
|
|
||
| # 从 stdout 读取直到看到 __DONE__ 标记或超时 | ||
|
|
@@ -593,6 +631,8 @@ def _read_until_done(): | |
| if reader.is_alive(): | ||
| timed_out.set() | ||
| logger.warning(f'Frida REPL 读取超时 ({timeout}s)') | ||
| self._close_frida_repl() | ||
| reader.join(timeout=1) | ||
|
|
||
| return '\n'.join(output_lines) if output_lines else '' | ||
|
|
||
|
|
@@ -601,6 +641,7 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') as f: | ||
| f.write(script_content) | ||
| script_path = f.name | ||
| process = None | ||
| try: | ||
| frida_exe = self._get_frida_exe() | ||
|
|
||
|
|
@@ -619,11 +660,12 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): | |
| time.sleep(2) | ||
|
|
||
| process.stdin.close() | ||
| process.stdin = None | ||
| try: | ||
| stdout_bytes, stderr_bytes = process.communicate(timeout=timeout) | ||
| except subprocess.TimeoutExpired: | ||
| process.kill() | ||
| stdout_bytes, stderr_bytes = process.communicate() | ||
| logger.warning(f'Frida单次执行超时 ({timeout}s)') | ||
| return None # finally closes the launcher and its Python child together. | ||
|
|
||
| stdout = stdout_bytes.decode('utf-8', errors='ignore') if stdout_bytes else '' | ||
| stderr = stderr_bytes.decode('utf-8', errors='ignore') if stderr_bytes else '' | ||
|
|
@@ -632,6 +674,7 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): | |
| logger.warning(f'Frida脚本执行失败: {e}') | ||
| return None | ||
| finally: | ||
| self._stop_frida_process(process) | ||
| try: | ||
| os.unlink(script_path) | ||
| except: | ||
|
|
@@ -1144,4 +1187,4 @@ def _claim_reward(self, bag_id, title): | |
| config = Config('oas1') | ||
| device = Device(config) | ||
| t = ScriptTask(config, device) | ||
| t.run() | ||
| t.run() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import subprocess | ||
| import sys | ||
| from types import SimpleNamespace | ||
| from unittest.mock import Mock | ||
|
|
||
| import pytest | ||
|
|
||
| from module.exception import TaskEnd | ||
| from tasks.AutoCheckinBigGod.script_task import ScriptTask | ||
|
|
||
|
|
||
| def task_with_process(): | ||
| task = object.__new__(ScriptTask) | ||
| task._frida_session = subprocess.Popen( | ||
| [sys.executable, '-c', 'import time; time.sleep(60)'], | ||
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | ||
| creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) | ||
| task._frida_attached_pid = 123 | ||
| task.frida_pid = 123 | ||
| return task | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('exit_kind', ['return', 'task_end', 'error', 'interrupt']) | ||
| def test_run_reaps_owned_process_on_every_exit(exit_kind): | ||
| task = task_with_process() | ||
| proc = task._frida_session | ||
| commands = [] | ||
| task._adb_shell = lambda command: commands.append(command) | ||
| task.device = SimpleNamespace(app_start=Mock()) | ||
| task.session = SimpleNamespace(close=Mock()) | ||
|
|
||
| def checkin(): | ||
| assert proc.poll() is None # The process remains usable during the task. | ||
| task._adb_connected = True | ||
| task._restore_game_after_checkin = True | ||
| if exit_kind == 'task_end': | ||
| raise TaskEnd('AutoCheckinBigGod') | ||
| if exit_kind == 'error': | ||
| raise RuntimeError('failed') | ||
| if exit_kind == 'interrupt': | ||
| raise KeyboardInterrupt() | ||
|
|
||
| task._run_checkin = checkin | ||
| try: | ||
| if exit_kind == 'return': | ||
| task.run() | ||
| else: | ||
| expected = {'task_end': TaskEnd, 'error': RuntimeError, 'interrupt': KeyboardInterrupt}[exit_kind] | ||
| with pytest.raises(expected): | ||
| task.run() | ||
| assert proc.poll() is not None | ||
| assert all(pipe.closed for pipe in (proc.stdin, proc.stdout, proc.stderr)) | ||
| assert task._frida_session is None and task._frida_attached_pid is None | ||
| assert task.frida_pid is None | ||
| assert ['su -c "killall frida-server"'] in commands | ||
| task.device.app_start.assert_called_once() | ||
| task.session.close.assert_called_once() | ||
| finally: | ||
| if proc.poll() is None: | ||
| proc.kill() | ||
| proc.wait(timeout=3) | ||
|
|
||
|
|
||
| def test_failed_adb_check_does_not_touch_unspecified_device(): | ||
| task = object.__new__(ScriptTask) | ||
| task._run_checkin = Mock(side_effect=TaskEnd('AutoCheckinBigGod')) | ||
| task._adb_shell = Mock() | ||
|
Comment on lines
+66
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · 失败的 ADB 检查被绕过 当实际的 请测试真实的连接失败路径,并断言该路径不会发出 ADB 命令。 给 AI 代理的提示在 `tests/test_biggod_frida_cleanup.py` 第 66-67 行:
**失败的 ADB 检查被绕过**
当实际的 `_run_checkin` 在因连接检查失败而抛出异常之前访问 ADB 时,`test_failed_adb_check_does_not_touch_unspecified_device` 将 `_run_checkin` 替换为一个立即抛出异常的 mock。因此,即使实际的 ADB 失败路径在抛出异常前调用了 `_adb_shell` 并访问了未指定的设备,该测试仍会通过。
请测试真实的连接失败路径,并断言该路径不会发出 ADB 命令。Original comment in English🟡 Medium · Failed ADB check is bypassed When the actual Exercise the real failed-connection path and assert that it does not issue ADB commands. Prompt for AI agentsIn `tests/test_biggod_frida_cleanup.py` at lines 66-67:
**Failed ADB check is bypassed**
When the actual `_run_checkin` touches ADB before raising on a failed connection check, `test_failed_adb_check_does_not_touch_unspecified_device` replaces `_run_checkin` with a mock that raises immediately, so the test passes even if the real failed-ADB path calls `_adb_shell` before raising and touches an unspecified device.
Exercise the real failed-connection path and assert that it does not issue ADB commands. |
||
| with pytest.raises(TaskEnd): | ||
| task.run() | ||
| task._adb_shell.assert_not_called() | ||
|
|
||
|
|
||
| def test_stdin_failure_reaps_repl_before_fallback(): | ||
| task = object.__new__(ScriptTask) | ||
| proc = Mock() | ||
| proc.poll.return_value = None | ||
| proc.stdin.write.side_effect = BrokenPipeError() | ||
| task._frida_session = proc | ||
| task._frida_attached_pid = 123 | ||
| def fallback(*args): | ||
| proc.wait.assert_called_once() | ||
| assert task._frida_session is None | ||
| return 'fallback' | ||
| task._run_frida_script_oneshot = fallback | ||
| assert task._run_frida_script(123, 'test') == 'fallback' | ||
| proc.terminate.assert_called_once() | ||
|
|
||
|
|
||
| @pytest.mark.parametrize('error', [OSError('failed'), subprocess.TimeoutExpired('frida', 10)]) | ||
| def test_oneshot_exception_reaps_process(monkeypatch, error): | ||
| task = object.__new__(ScriptTask) | ||
| task._get_frida_exe = lambda: 'frida' | ||
| proc = Mock() | ||
| proc.poll.return_value = None | ||
| proc.communicate.side_effect = error | ||
| monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.subprocess.Popen', lambda *a, **kw: proc) | ||
| monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.time.sleep', lambda seconds: None) | ||
| assert task._run_frida_script_oneshot(123, 'test') is None | ||
| proc.terminate.assert_called_once() | ||
| proc.wait.assert_called_once() | ||
|
|
||
|
|
||
| def test_unresponsive_process_is_killed_and_reaped(): | ||
| proc = Mock() | ||
| proc.poll.return_value = None | ||
| proc.wait.side_effect = [subprocess.TimeoutExpired('frida', 3), 0] | ||
| ScriptTask._stop_frida_process(proc) | ||
| proc.terminate.assert_called_once() | ||
| proc.kill.assert_called_once() | ||
| assert proc.wait.call_count == 2 | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.platform != 'win32', reason='Windows launcher process tree') | ||
| def test_windows_cleanup_targets_only_owned_process_tree(monkeypatch): | ||
| proc = Mock(pid=12345) | ||
| proc.poll.return_value = None | ||
| taskkill = Mock() | ||
| monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.subprocess.run', taskkill) | ||
| ScriptTask._stop_frida_process(proc) | ||
| assert taskkill.call_args.args[0] == ['taskkill', '/PID', '12345', '/T', '/F'] | ||
| proc.wait.assert_called_once() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · 其他 Frida 客户端会失去服务器
当此任务退出时,如果另一个客户端正在同一 ADB 设备上使用已运行的 Frida 服务器,
_cleanup会执行killall frida-server,却不检查该服务器是否由此任务启动,从而断开另一个客户端并导致其 Frida 操作失败。只有在此任务启动了 Frida 服务器时,才停止该服务器。
给 AI 代理的提示
Original comment in English
🟡 Medium · Other Frida clients lose their server
When another client uses a Frida server already running on the same ADB device when this task exits,
_cleanuprunskillall frida-serverwithout checking whether this task started the server, disconnecting the other client and causing its Frida operations to fail.Only stop the Frida server when this task started it.
Prompt for AI agents