AndroidFridaManager 1.9.3__py3-none-any.whl → 1.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AndroidFridaManager/about.py +1 -1
- AndroidFridaManager/job.py +102 -9
- AndroidFridaManager/job_manager.py +361 -29
- {androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/METADATA +2 -2
- androidfridamanager-1.9.5.dist-info/RECORD +11 -0
- {androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/WHEEL +1 -1
- androidfridamanager-1.9.3.dist-info/RECORD +0 -11
- {androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/entry_points.txt +0 -0
- {androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/licenses/LICENSE +0 -0
- {androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/top_level.txt +0 -0
AndroidFridaManager/about.py
CHANGED
AndroidFridaManager/job.py
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Frida Job management for AndroidFridaManager.
|
|
3
4
|
|
|
5
|
+
This module provides the Job class for managing individual Frida instrumentation
|
|
6
|
+
jobs, including script loading, message handling, and lifecycle management.
|
|
7
|
+
"""
|
|
4
8
|
|
|
5
9
|
import threading
|
|
6
10
|
import frida
|
|
7
11
|
import uuid
|
|
8
12
|
import logging
|
|
13
|
+
import datetime
|
|
14
|
+
from typing import Optional, List, Callable, Any
|
|
15
|
+
|
|
9
16
|
|
|
10
17
|
# Define a custom exception for handling frida based exceptions
|
|
11
18
|
class FridaBasedException(Exception):
|
|
12
19
|
pass
|
|
13
20
|
|
|
21
|
+
|
|
14
22
|
class Job:
|
|
15
|
-
|
|
23
|
+
"""Represents a single Frida instrumentation job.
|
|
24
|
+
|
|
25
|
+
A job encapsulates a Frida script, its message handler, and lifecycle
|
|
26
|
+
management. Jobs run in separate threads and can be started/stopped
|
|
27
|
+
independently.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
job_id: Unique identifier for this job.
|
|
31
|
+
job_type: Category of job (e.g., "fritap", "dexray", "trigdroid", "custom").
|
|
32
|
+
display_name: Human-readable name for UI display.
|
|
33
|
+
hooks_registry: List of methods/functions this job hooks (for conflict detection).
|
|
34
|
+
priority: Job priority (lower = higher priority, default 50).
|
|
35
|
+
state: Current state ("initialized", "running", "stopping").
|
|
36
|
+
started_at: Timestamp when job was started (None if not started).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
frida_script_name: str,
|
|
42
|
+
custom_hooking_handler: Callable,
|
|
43
|
+
process: Any,
|
|
44
|
+
job_type: str = "custom",
|
|
45
|
+
display_name: Optional[str] = None,
|
|
46
|
+
hooks_registry: Optional[List[str]] = None,
|
|
47
|
+
priority: int = 50,
|
|
48
|
+
):
|
|
49
|
+
"""Initialize a new Job.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
frida_script_name: Path to the Frida script file.
|
|
53
|
+
custom_hooking_handler: Callback function for handling Frida messages.
|
|
54
|
+
process: Frida session/process to attach the script to.
|
|
55
|
+
job_type: Category of job for coordination (default: "custom").
|
|
56
|
+
display_name: Human-readable name (default: script filename).
|
|
57
|
+
hooks_registry: List of hooked methods for conflict detection.
|
|
58
|
+
priority: Job priority, lower = higher (default: 50).
|
|
59
|
+
"""
|
|
16
60
|
self.frida_script_name = frida_script_name
|
|
17
61
|
self.job_id = str(uuid.uuid4())
|
|
18
62
|
self.state = "initialized"
|
|
@@ -24,6 +68,13 @@ class Job:
|
|
|
24
68
|
self.is_script_created = False
|
|
25
69
|
self.logger = logging.getLogger(__name__)
|
|
26
70
|
|
|
71
|
+
# New metadata fields for job coordination
|
|
72
|
+
self.job_type = job_type
|
|
73
|
+
self.display_name = display_name or frida_script_name
|
|
74
|
+
self.hooks_registry = hooks_registry or []
|
|
75
|
+
self.priority = priority
|
|
76
|
+
self.started_at: Optional[datetime.datetime] = None
|
|
77
|
+
|
|
27
78
|
|
|
28
79
|
def create_job_script(self):
|
|
29
80
|
self.instrument(self.process_session)
|
|
@@ -31,7 +82,8 @@ class Job:
|
|
|
31
82
|
|
|
32
83
|
|
|
33
84
|
def run_job(self):
|
|
34
|
-
|
|
85
|
+
"""Start the job execution in a separate thread."""
|
|
86
|
+
self.started_at = datetime.datetime.now()
|
|
35
87
|
self.run_job_as_thread()
|
|
36
88
|
|
|
37
89
|
|
|
@@ -82,15 +134,38 @@ class Job:
|
|
|
82
134
|
raise FridaBasedException("Connection is closed. Probably the target app crashed")
|
|
83
135
|
|
|
84
136
|
|
|
85
|
-
def close_job(self):
|
|
137
|
+
def close_job(self, timeout: float = 5.0) -> bool:
|
|
138
|
+
"""Stop the job and cleanup resources.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
timeout: Maximum seconds to wait for thread to stop.
|
|
142
|
+
Default 5.0 seconds. Use 0 for no wait.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if job stopped cleanly, False if timed out.
|
|
146
|
+
"""
|
|
86
147
|
self.state = "stopping"
|
|
87
148
|
self.stop_event.set()
|
|
88
|
-
|
|
89
|
-
|
|
149
|
+
|
|
150
|
+
timed_out = False
|
|
151
|
+
if self.thread and self.thread.is_alive():
|
|
152
|
+
self.thread.join(timeout=timeout if timeout > 0 else None)
|
|
153
|
+
if self.thread.is_alive():
|
|
154
|
+
self.logger.warning(
|
|
155
|
+
f"Job {self.job_id} thread did not stop within {timeout}s"
|
|
156
|
+
)
|
|
157
|
+
timed_out = True
|
|
158
|
+
|
|
159
|
+
# Try to unload script even if thread timed out
|
|
90
160
|
if self.script:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
161
|
+
try:
|
|
162
|
+
self.script.unload()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
self.logger.warning(f"Error unloading script: {e}")
|
|
165
|
+
|
|
166
|
+
status = "timed out" if timed_out else "stopped"
|
|
167
|
+
self.logger.info(f"Job {self.job_id} {status}")
|
|
168
|
+
return not timed_out
|
|
94
169
|
|
|
95
170
|
|
|
96
171
|
def get_id(self):
|
|
@@ -98,4 +173,22 @@ class Job:
|
|
|
98
173
|
|
|
99
174
|
|
|
100
175
|
def get_script_of_job(self):
|
|
101
|
-
return self.script
|
|
176
|
+
return self.script
|
|
177
|
+
|
|
178
|
+
def get_info(self) -> dict:
|
|
179
|
+
"""Get job information as a dictionary for UI display.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dictionary containing job metadata and state information.
|
|
183
|
+
"""
|
|
184
|
+
return {
|
|
185
|
+
"job_id": self.job_id,
|
|
186
|
+
"job_type": self.job_type,
|
|
187
|
+
"display_name": self.display_name,
|
|
188
|
+
"state": self.state,
|
|
189
|
+
"priority": self.priority,
|
|
190
|
+
"hooks_count": len(self.hooks_registry),
|
|
191
|
+
"hooks_registry": self.hooks_registry.copy(),
|
|
192
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
193
|
+
"script_name": self.frida_script_name,
|
|
194
|
+
}
|
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Frida JobManager for coordinating multiple instrumentation jobs.
|
|
4
|
+
|
|
5
|
+
This module provides the JobManager class for managing Frida sessions and
|
|
6
|
+
coordinating multiple instrumentation jobs on Android devices.
|
|
7
|
+
"""
|
|
3
8
|
|
|
4
9
|
import atexit
|
|
5
10
|
import subprocess
|
|
6
11
|
import frida
|
|
7
|
-
from typing import Optional, Dict, Union, List
|
|
12
|
+
from typing import Optional, Dict, Union, List, Callable
|
|
8
13
|
from .job import Job, FridaBasedException
|
|
9
14
|
import time
|
|
10
15
|
import re
|
|
11
16
|
import logging
|
|
12
17
|
|
|
18
|
+
|
|
13
19
|
class JobManager(object):
|
|
14
|
-
"""
|
|
20
|
+
"""Job manager with multi-device support and hook coordination.
|
|
15
21
|
|
|
22
|
+
Manages Frida sessions and coordinates multiple instrumentation jobs,
|
|
23
|
+
providing hook conflict detection and session status information.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
jobs: Dictionary of active jobs by job_id.
|
|
27
|
+
process_session: Current Frida session.
|
|
28
|
+
device: Current Frida device.
|
|
29
|
+
package_name: Name of the attached/spawned package.
|
|
30
|
+
pid: Process ID (-1 if attach mode).
|
|
31
|
+
"""
|
|
16
32
|
|
|
17
33
|
def __init__(self, host="", enable_spawn_gating=False, device_serial: Optional[str] = None) -> None:
|
|
18
|
-
"""
|
|
19
|
-
Init a new job manager with optional device targeting.
|
|
34
|
+
"""Init a new job manager with optional device targeting.
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
:
|
|
23
|
-
:
|
|
24
|
-
|
|
36
|
+
Args:
|
|
37
|
+
host: Remote host for Frida connection (ip:port format).
|
|
38
|
+
enable_spawn_gating: Enable spawn gating for child process tracking.
|
|
39
|
+
device_serial: Specific device serial to target (e.g., 'emulator-5554').
|
|
40
|
+
If None, uses default device selection.
|
|
25
41
|
"""
|
|
26
|
-
|
|
27
|
-
self.jobs = {}
|
|
42
|
+
self.jobs: Dict[str, Job] = {}
|
|
28
43
|
self.is_first_job = True
|
|
29
44
|
self.process_session = None
|
|
30
45
|
self.host = host
|
|
@@ -42,6 +57,11 @@ class JobManager(object):
|
|
|
42
57
|
self._device_serial = device_serial
|
|
43
58
|
self._multiple_devices = self._check_multiple_devices()
|
|
44
59
|
|
|
60
|
+
# Hook coordination (NEW)
|
|
61
|
+
self._hook_registry: Dict[str, str] = {} # hook_target -> job_id
|
|
62
|
+
self._mode: Optional[str] = None # "spawn" or "attach"
|
|
63
|
+
self._paused: bool = False # Track if spawned process is paused
|
|
64
|
+
|
|
45
65
|
atexit.register(self.cleanup)
|
|
46
66
|
|
|
47
67
|
def _ensure_logging_setup(self):
|
|
@@ -134,13 +154,80 @@ class JobManager(object):
|
|
|
134
154
|
'''
|
|
135
155
|
|
|
136
156
|
def spawn(self, target_process):
|
|
157
|
+
"""Spawn a new process and attach to it.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
target_process: Package name to spawn.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Process ID of the spawned process.
|
|
164
|
+
"""
|
|
165
|
+
self._mode = "spawn"
|
|
137
166
|
self.package_name = target_process
|
|
138
167
|
self.logger.info("[*] spawning app: "+ target_process)
|
|
139
168
|
pid = self.device.spawn(target_process)
|
|
140
169
|
self.process_session = self.device.attach(pid)
|
|
170
|
+
self.pid = pid
|
|
171
|
+
self._paused = True # Spawned processes start paused
|
|
141
172
|
self.logger.info(f"Spawned {target_process} with PID {pid}")
|
|
142
173
|
return pid
|
|
143
174
|
|
|
175
|
+
def spawn_paused(self, target_process: str) -> int:
|
|
176
|
+
"""Spawn a process but keep it paused for multi-tool loading.
|
|
177
|
+
|
|
178
|
+
Unlike the regular spawn flow where resume happens on first job,
|
|
179
|
+
this method keeps the process paused until explicitly resumed
|
|
180
|
+
via resume_app(). This allows loading multiple Jobs before
|
|
181
|
+
the app starts executing.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
target_process: Package name to spawn.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Process ID of the spawned (paused) process.
|
|
188
|
+
"""
|
|
189
|
+
self._mode = "spawn"
|
|
190
|
+
self.package_name = target_process
|
|
191
|
+
self.logger.info(f"[*] spawning app (paused): {target_process}")
|
|
192
|
+
pid = self.device.spawn(target_process)
|
|
193
|
+
self.process_session = self.device.attach(pid)
|
|
194
|
+
self.pid = pid
|
|
195
|
+
self._paused = True
|
|
196
|
+
# Mark that auto-resume should NOT happen
|
|
197
|
+
self.is_first_job = False # Prevent auto-resume in start_job()
|
|
198
|
+
self.logger.info(f"Spawned {target_process} with PID {pid} (PAUSED - awaiting manual resume)")
|
|
199
|
+
return pid
|
|
200
|
+
|
|
201
|
+
def resume_app(self) -> bool:
|
|
202
|
+
"""Resume a paused spawned process.
|
|
203
|
+
|
|
204
|
+
Call this after loading all Jobs to start the app with
|
|
205
|
+
all hooks already installed.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if resumed successfully, False if not paused or failed.
|
|
209
|
+
"""
|
|
210
|
+
if not self._paused or self.pid == -1:
|
|
211
|
+
self.logger.warning("No paused process to resume")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
self.device.resume(self.pid)
|
|
216
|
+
self._paused = False
|
|
217
|
+
time.sleep(1) # Required for Java.perform stability
|
|
218
|
+
self.logger.info(f"Resumed process {self.pid}")
|
|
219
|
+
return True
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.logger.error(f"Failed to resume process: {e}")
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def is_paused(self) -> bool:
|
|
225
|
+
"""Check if the spawned process is currently paused.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if process is spawned and waiting for resume.
|
|
229
|
+
"""
|
|
230
|
+
return self._paused
|
|
144
231
|
|
|
145
232
|
def start_android_app(self, package_name: str, main_activity: Optional[str] = None, extras: Optional[Dict[str, Union[str, bool]]] = None):
|
|
146
233
|
"""
|
|
@@ -190,6 +277,13 @@ class JobManager(object):
|
|
|
190
277
|
|
|
191
278
|
|
|
192
279
|
def attach_app(self, target_process, foreground=False):
|
|
280
|
+
"""Attach to a running process.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
target_process: Package name or PID to attach to.
|
|
284
|
+
foreground: If True, attach to the frontmost application.
|
|
285
|
+
"""
|
|
286
|
+
self._mode = "attach"
|
|
193
287
|
self.package_name = target_process
|
|
194
288
|
|
|
195
289
|
if foreground:
|
|
@@ -261,11 +355,44 @@ class JobManager(object):
|
|
|
261
355
|
raise FridaBasedException(f"Frida-Error: {fe}")
|
|
262
356
|
|
|
263
357
|
|
|
264
|
-
def start_job(
|
|
358
|
+
def start_job(
|
|
359
|
+
self,
|
|
360
|
+
frida_script_name: str,
|
|
361
|
+
custom_hooking_handler_name: Callable,
|
|
362
|
+
job_type: str = "custom",
|
|
363
|
+
display_name: Optional[str] = None,
|
|
364
|
+
hooks_registry: Optional[List[str]] = None,
|
|
365
|
+
priority: int = 50,
|
|
366
|
+
) -> Optional[Job]:
|
|
367
|
+
"""Start a new instrumentation job.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
frida_script_name: Path to the Frida script file.
|
|
371
|
+
custom_hooking_handler_name: Callback for handling Frida messages.
|
|
372
|
+
job_type: Category of job (e.g., "fritap", "dexray", "trigdroid").
|
|
373
|
+
display_name: Human-readable name for UI display.
|
|
374
|
+
hooks_registry: List of hooked methods for conflict detection.
|
|
375
|
+
priority: Job priority (lower = higher priority).
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The created Job instance, or None if no session exists.
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
FridaBasedException: If Frida encounters an error.
|
|
382
|
+
"""
|
|
383
|
+
job = None # Initialize before try block for safe exception handling
|
|
265
384
|
try:
|
|
266
385
|
if self.process_session:
|
|
267
|
-
job = Job(
|
|
268
|
-
|
|
386
|
+
job = Job(
|
|
387
|
+
frida_script_name,
|
|
388
|
+
custom_hooking_handler_name,
|
|
389
|
+
self.process_session,
|
|
390
|
+
job_type=job_type,
|
|
391
|
+
display_name=display_name,
|
|
392
|
+
hooks_registry=hooks_registry,
|
|
393
|
+
priority=priority,
|
|
394
|
+
)
|
|
395
|
+
self.logger.info(f"[*] created job: {job.job_id} ({job.display_name})")
|
|
269
396
|
self.jobs[job.job_id] = job
|
|
270
397
|
self.last_created_job = job
|
|
271
398
|
job.run_job()
|
|
@@ -274,13 +401,14 @@ class JobManager(object):
|
|
|
274
401
|
self.first_instrumenation_script = custom_hooking_handler_name
|
|
275
402
|
if self.pid != -1:
|
|
276
403
|
self.device.resume(self.pid)
|
|
277
|
-
|
|
404
|
+
self._paused = False # Mark as resumed
|
|
405
|
+
time.sleep(1) # without it Java.perform silently fails
|
|
278
406
|
|
|
279
407
|
return job
|
|
280
408
|
|
|
281
409
|
else:
|
|
282
410
|
self.logger.error("[-] no frida session. Aborting...")
|
|
283
|
-
|
|
411
|
+
return None
|
|
284
412
|
|
|
285
413
|
except frida.TransportError as fe:
|
|
286
414
|
raise FridaBasedException(f"Problems while attaching to frida-server: {fe}")
|
|
@@ -291,26 +419,53 @@ class JobManager(object):
|
|
|
291
419
|
except frida.ProcessNotFoundError as pe:
|
|
292
420
|
raise FridaBasedException(f"ProcessNotFoundError: {pe}")
|
|
293
421
|
except KeyboardInterrupt:
|
|
294
|
-
|
|
295
|
-
|
|
422
|
+
if job:
|
|
423
|
+
self.stop_app_with_last_job(job, self.package_name)
|
|
424
|
+
return None
|
|
425
|
+
|
|
296
426
|
|
|
427
|
+
def stop_jobs(self, timeout_per_job: float = 3.0) -> dict:
|
|
428
|
+
"""Stop all running jobs.
|
|
297
429
|
|
|
298
|
-
|
|
430
|
+
Args:
|
|
431
|
+
timeout_per_job: Maximum seconds to wait per job.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dictionary mapping job_id to success status.
|
|
435
|
+
"""
|
|
436
|
+
results = {}
|
|
299
437
|
jobs_to_stop = [job_id for job_id, job in self.jobs.items() if job.state == "running"]
|
|
438
|
+
|
|
300
439
|
for job_id in jobs_to_stop:
|
|
301
440
|
try:
|
|
302
|
-
self.logger.info('[job manager] Job: {
|
|
303
|
-
self.stop_job_with_id(job_id)
|
|
441
|
+
self.logger.info(f'[job manager] Job: {job_id} - Stopping')
|
|
442
|
+
results[job_id] = self.stop_job_with_id(job_id, timeout=timeout_per_job)
|
|
304
443
|
except frida.InvalidOperationError:
|
|
305
|
-
self.logger.error('[job manager] Job: {
|
|
306
|
-
|
|
444
|
+
self.logger.error(f'[job manager] Job: {job_id} - Error stopping')
|
|
445
|
+
results[job_id] = False
|
|
307
446
|
|
|
447
|
+
return results
|
|
308
448
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
449
|
+
|
|
450
|
+
def stop_job_with_id(self, job_id: str, timeout: float = 5.0) -> bool:
|
|
451
|
+
"""Stop a specific job by ID.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
job_id: UUID of the job to stop.
|
|
455
|
+
timeout: Maximum seconds to wait for job to stop.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
True if job stopped cleanly, False if timed out or not found.
|
|
459
|
+
"""
|
|
460
|
+
if job_id not in self.jobs:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
job = self.jobs[job_id]
|
|
464
|
+
# Unregister hooks before closing
|
|
465
|
+
self.unregister_hooks(job_id)
|
|
466
|
+
success = job.close_job(timeout=timeout)
|
|
467
|
+
del self.jobs[job_id]
|
|
468
|
+
return success
|
|
314
469
|
|
|
315
470
|
|
|
316
471
|
def get_last_created_job(self):
|
|
@@ -325,10 +480,38 @@ class JobManager(object):
|
|
|
325
480
|
raise ValueError(f"Job with ID {job_id} not found.")
|
|
326
481
|
|
|
327
482
|
|
|
328
|
-
def detach_from_app(self):
|
|
329
|
-
|
|
483
|
+
def detach_from_app(self, timeout: float = 3.0) -> bool:
|
|
484
|
+
"""Detach from the current app session.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
timeout: Maximum seconds to wait for detach.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
True if detached successfully, False if timed out or failed.
|
|
491
|
+
"""
|
|
492
|
+
if not self.process_session:
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
import concurrent.futures
|
|
496
|
+
|
|
497
|
+
def _detach():
|
|
330
498
|
self.process_session.detach()
|
|
331
499
|
|
|
500
|
+
try:
|
|
501
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
502
|
+
future = executor.submit(_detach)
|
|
503
|
+
future.result(timeout=timeout)
|
|
504
|
+
self.process_session = None
|
|
505
|
+
return True
|
|
506
|
+
except concurrent.futures.TimeoutError:
|
|
507
|
+
self.logger.warning(f"Detach timed out after {timeout}s")
|
|
508
|
+
self.process_session = None # Clear anyway to avoid reuse
|
|
509
|
+
return False
|
|
510
|
+
except Exception as e:
|
|
511
|
+
self.logger.warning(f"Detach failed: {e}")
|
|
512
|
+
self.process_session = None
|
|
513
|
+
return False
|
|
514
|
+
|
|
332
515
|
|
|
333
516
|
def stop_app(self, app_package):
|
|
334
517
|
cmd = self._build_adb_command(["shell", "am", "force-stop", app_package])
|
|
@@ -411,3 +594,152 @@ class JobManager(object):
|
|
|
411
594
|
raise FridaBasedException("Unable to find device")
|
|
412
595
|
except frida.ServerNotRunningError:
|
|
413
596
|
raise FridaBasedException("Frida server not running. Start frida-server and try it again.")
|
|
597
|
+
|
|
598
|
+
# ==================== Hook Coordination Methods ====================
|
|
599
|
+
|
|
600
|
+
def register_hooks(self, job_id: str, hooks: List[str]) -> List[str]:
|
|
601
|
+
"""Register hooks for a job and detect conflicts.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
job_id: UUID of the job registering hooks.
|
|
605
|
+
hooks: List of hook targets (method names, function signatures).
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
List of conflicting hooks that are already registered by other jobs.
|
|
609
|
+
"""
|
|
610
|
+
conflicts = []
|
|
611
|
+
for hook in hooks:
|
|
612
|
+
if hook in self._hook_registry:
|
|
613
|
+
existing_job_id = self._hook_registry[hook]
|
|
614
|
+
if existing_job_id != job_id:
|
|
615
|
+
conflicts.append(hook)
|
|
616
|
+
self.logger.warning(
|
|
617
|
+
f"Hook conflict: '{hook}' already registered by job {existing_job_id[:8]}"
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
self._hook_registry[hook] = job_id
|
|
621
|
+
|
|
622
|
+
if conflicts:
|
|
623
|
+
self.logger.warning(
|
|
624
|
+
f"Job {job_id[:8]} has {len(conflicts)} hook conflict(s)"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
return conflicts
|
|
628
|
+
|
|
629
|
+
def unregister_hooks(self, job_id: str) -> None:
|
|
630
|
+
"""Remove all hooks registered by a job.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
job_id: UUID of the job whose hooks should be removed.
|
|
634
|
+
"""
|
|
635
|
+
hooks_to_remove = [
|
|
636
|
+
hook for hook, jid in self._hook_registry.items() if jid == job_id
|
|
637
|
+
]
|
|
638
|
+
for hook in hooks_to_remove:
|
|
639
|
+
del self._hook_registry[hook]
|
|
640
|
+
|
|
641
|
+
if hooks_to_remove:
|
|
642
|
+
self.logger.debug(
|
|
643
|
+
f"Unregistered {len(hooks_to_remove)} hooks for job {job_id[:8]}"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
def check_hook_conflicts(self, hooks: List[str]) -> Dict[str, str]:
|
|
647
|
+
"""Check for potential conflicts before registering hooks.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
hooks: List of hook targets to check.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Dictionary mapping conflicting hooks to their owning job IDs.
|
|
654
|
+
"""
|
|
655
|
+
return {
|
|
656
|
+
hook: self._hook_registry[hook]
|
|
657
|
+
for hook in hooks
|
|
658
|
+
if hook in self._hook_registry
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
def get_hook_registry(self) -> Dict[str, str]:
|
|
662
|
+
"""Get a copy of the current hook registry.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Dictionary mapping hook targets to job IDs.
|
|
666
|
+
"""
|
|
667
|
+
return self._hook_registry.copy()
|
|
668
|
+
|
|
669
|
+
# ==================== Session Info Methods ====================
|
|
670
|
+
|
|
671
|
+
def has_active_session(self) -> bool:
|
|
672
|
+
"""Check if there's an active Frida session.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
True if a session is active, False otherwise.
|
|
676
|
+
"""
|
|
677
|
+
return self.process_session is not None
|
|
678
|
+
|
|
679
|
+
def get_session_info(self) -> Dict[str, any]:
|
|
680
|
+
"""Get current session information for UI display.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Dictionary containing session state information.
|
|
684
|
+
"""
|
|
685
|
+
return {
|
|
686
|
+
"package": self.package_name,
|
|
687
|
+
"pid": self.pid,
|
|
688
|
+
"mode": self._mode or ("spawn" if self.pid != -1 else "attach"),
|
|
689
|
+
"device_serial": self._device_serial,
|
|
690
|
+
"has_session": self.process_session is not None,
|
|
691
|
+
"job_count": len(self.jobs),
|
|
692
|
+
"running_job_count": len(self.running_jobs()),
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
def get_running_jobs_info(self) -> List[Dict[str, any]]:
|
|
696
|
+
"""Get information about running jobs for UI display.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
List of dictionaries containing job information.
|
|
700
|
+
"""
|
|
701
|
+
return [
|
|
702
|
+
job.get_info()
|
|
703
|
+
for job in self.jobs.values()
|
|
704
|
+
if job.state == "running"
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
def get_all_jobs_info(self) -> List[Dict[str, any]]:
|
|
708
|
+
"""Get information about all jobs (running and stopped).
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
List of dictionaries containing job information.
|
|
712
|
+
"""
|
|
713
|
+
return [job.get_info() for job in self.jobs.values()]
|
|
714
|
+
|
|
715
|
+
@property
|
|
716
|
+
def mode(self) -> Optional[str]:
|
|
717
|
+
"""Get the current session mode ('spawn' or 'attach')."""
|
|
718
|
+
return self._mode
|
|
719
|
+
|
|
720
|
+
def reset_session(self, timeout_per_job: float = 2.0, detach_timeout: float = 2.0) -> None:
|
|
721
|
+
"""Reset the session state for a new connection.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
timeout_per_job: Max seconds to wait per job when stopping.
|
|
725
|
+
detach_timeout: Max seconds to wait for session detach.
|
|
726
|
+
"""
|
|
727
|
+
# Stop all running jobs with timeout
|
|
728
|
+
self.stop_jobs(timeout_per_job=timeout_per_job)
|
|
729
|
+
|
|
730
|
+
# Clear hook registry
|
|
731
|
+
self._hook_registry.clear()
|
|
732
|
+
|
|
733
|
+
# Detach from app with timeout
|
|
734
|
+
self.detach_from_app(timeout=detach_timeout)
|
|
735
|
+
|
|
736
|
+
# Reset state
|
|
737
|
+
self.process_session = None
|
|
738
|
+
self.pid = -1
|
|
739
|
+
self.package_name = ""
|
|
740
|
+
self._mode = None
|
|
741
|
+
self.is_first_job = True
|
|
742
|
+
self.last_created_job = None
|
|
743
|
+
self.init_last_job = False
|
|
744
|
+
|
|
745
|
+
self.logger.info("[*] Session reset complete")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AndroidFridaManager
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.5
|
|
4
4
|
Summary: A python API in order to install and run the frida-server on an Android device.
|
|
5
5
|
Home-page: https://github.com/fkie-cad/AndroidFridaManager
|
|
6
6
|
Author: Daniel Baier
|
|
@@ -33,7 +33,7 @@ Dynamic: requires-dist
|
|
|
33
33
|
Dynamic: requires-python
|
|
34
34
|
Dynamic: summary
|
|
35
35
|
|
|
36
|
-
 [](https://badge.fury.io/py/AndroidFridaManager) [](https://github.com/fkie-cad/AndroidFridaManager/actions/workflows/publish-to-pypi.yml)
|
|
37
37
|
|
|
38
38
|
# AndroidFridaManager
|
|
39
39
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
AndroidFridaManager/FridaManager.py,sha256=pTcis4oLPBN5U7CdBqdUufwuG6QQDWTPBleTC7rtTbI,32145
|
|
2
|
+
AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
|
|
3
|
+
AndroidFridaManager/about.py,sha256=wGo_88YOuzXo2vF1EyCEmIO4uxM7wOJLC-FrGLp__Mw,98
|
|
4
|
+
AndroidFridaManager/job.py,sha256=Yzir0XkkpFj5cFIcMRGJlLnIcIuDVZ9jBLFMqwQKTFA,7041
|
|
5
|
+
AndroidFridaManager/job_manager.py,sha256=Ce2lHLezRVKuj7r_RodH6Q8l43zL_cL5ZGhE1zGB-E8,27336
|
|
6
|
+
androidfridamanager-1.9.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
androidfridamanager-1.9.5.dist-info/METADATA,sha256=pjOCtO1KPNhyybRz8O5a56LXfeTqnKRcm0P0Kii5kpA,5141
|
|
8
|
+
androidfridamanager-1.9.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
androidfridamanager-1.9.5.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
+
androidfridamanager-1.9.5.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
+
androidfridamanager-1.9.5.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
AndroidFridaManager/FridaManager.py,sha256=pTcis4oLPBN5U7CdBqdUufwuG6QQDWTPBleTC7rtTbI,32145
|
|
2
|
-
AndroidFridaManager/__init__.py,sha256=T6AKtrGSLQ9M5bJoWDQcsRTJbSEbksdgrx3AAAdozRI,171
|
|
3
|
-
AndroidFridaManager/about.py,sha256=T8Pv1VrgHp0Mn_kJwh5M0yzCUK5pILMP4LdeITJzGtM,98
|
|
4
|
-
AndroidFridaManager/job.py,sha256=1NNcfCjkyUtwUkMXSgT4uswA8UStHo3jxbeJwJoWhc8,3352
|
|
5
|
-
AndroidFridaManager/job_manager.py,sha256=S3biHhYrk-DUUfrHA-g8vbOqwgl4FnWELrUjMxsFyG8,15983
|
|
6
|
-
androidfridamanager-1.9.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
androidfridamanager-1.9.3.dist-info/METADATA,sha256=sScuKDxdfBmE7Ag4i-Nd9pu9Ps1Cif29Is4gDOunTvU,5141
|
|
8
|
-
androidfridamanager-1.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
androidfridamanager-1.9.3.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
-
androidfridamanager-1.9.3.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
-
androidfridamanager-1.9.3.dist-info/RECORD,,
|
{androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{androidfridamanager-1.9.3.dist-info → androidfridamanager-1.9.5.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|