Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 68 additions & 25 deletions tasks/AutoCheckinBigGod/script_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown

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 代理的提示
在 `tasks/AutoCheckinBigGod/script_task.py` 第 55 行:

**其他 Frida 客户端会失去服务器**

当此任务退出时,如果另一个客户端正在同一 ADB 设备上使用已运行的 Frida 服务器,`_cleanup` 会执行 `killall frida-server`,却不检查该服务器是否由此任务启动,从而断开另一个客户端并导致其 Frida 操作失败。

只有在此任务启动了 Frida 服务器时,才停止该服务器。
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, _cleanup runs killall frida-server without 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
In `tasks/AutoCheckinBigGod/script_task.py` at line 55:

**Other Frida clients lose their server**

When another client uses a Frida server already running on the same ADB device when this task exits, `_cleanup` runs `killall frida-server` without 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.

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 = ""
Expand All @@ -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启动失败,请检查模拟器环境')
Expand All @@ -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')
Expand Down Expand Up @@ -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')

Expand All @@ -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'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · 清理过程会运行攻击者控制的可执行文件

当任务在 Windows 上运行且 Frida 进程仍在运行时,如果攻击者能够将 taskkill.exe 放入一个搜索顺序早于系统工具所在目录的目录,_stop_frida_process 会按可执行文件名解析 taskkill,因此 Windows 可能会运行攻击者放置在较早搜索位置的 taskkill.exe,而不是系统工具;攻击者的代码将以该任务的权限运行。

请从可信的 Windows 系统目录解析 taskkill.exe,或者使用不会按名称搜索可执行文件的进程树终止机制。

给 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 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.

Prompt for AI agents
In `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])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · 已打开的游戏会被关闭

当 ADB 已连接,但 Frida 服务器设置在此任务启动应用之前失败,并且 BigGod 原本就在运行时,_cleanup 会在 _restore_game_after_checkin 为 false 的情况下对 GL_PACKAGE 执行强制停止,导致此任务未启动的应用也被关闭。

只有在此任务启动了应用时才强制停止它,并保留或恢复原本已在运行的应用状态。

另请参阅 tasks/AutoCheckinBigGod/script_task.py:55。

给 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, _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.

Prompt for AI agents
In `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:
Expand Down Expand Up @@ -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:
Expand All @@ -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 避免副作用
Expand All @@ -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):
Expand Down Expand Up @@ -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__ 标记或超时
Expand Down Expand Up @@ -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 ''

Expand All @@ -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()

Expand All @@ -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 ''
Expand All @@ -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:
Expand Down Expand Up @@ -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()
121 changes: 121 additions & 0 deletions tests/test_biggod_frida_cleanup.py
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · 失败的 ADB 检查被绕过

当实际的 _run_checkin 在因连接检查失败而抛出异常之前访问 ADB 时,test_failed_adb_check_does_not_touch_unspecified_device 将 _run_checkin 替换为一个立即抛出异常的 mock。因此,即使实际的 ADB 失败路径在抛出异常前调用了 _adb_shell 并访问了未指定的设备,该测试仍会通过。

请测试真实的连接失败路径,并断言该路径不会发出 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 _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.

Prompt for AI agents
In `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()
Loading