AndroidFridaManager 1.9.4__py3-none-any.whl → 1.9.6__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 +121 -20
- AndroidFridaManager/job_manager.py +69 -24
- {androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/METADATA +2 -2
- androidfridamanager-1.9.6.dist-info/RECORD +11 -0
- {androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/WHEEL +1 -1
- androidfridamanager-1.9.4.dist-info/RECORD +0 -11
- {androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/entry_points.txt +0 -0
- {androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/licenses/LICENSE +0 -0
- {androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/top_level.txt +0 -0
AndroidFridaManager/about.py
CHANGED
AndroidFridaManager/job.py
CHANGED
|
@@ -32,7 +32,7 @@ class Job:
|
|
|
32
32
|
display_name: Human-readable name for UI display.
|
|
33
33
|
hooks_registry: List of methods/functions this job hooks (for conflict detection).
|
|
34
34
|
priority: Job priority (lower = higher priority, default 50).
|
|
35
|
-
state: Current state ("initialized", "running", "stopping").
|
|
35
|
+
state: Current state ("initialized", "running", "stopping", "error", "stopped").
|
|
36
36
|
started_at: Timestamp when job was started (None if not started).
|
|
37
37
|
"""
|
|
38
38
|
|
|
@@ -75,6 +75,50 @@ class Job:
|
|
|
75
75
|
self.priority = priority
|
|
76
76
|
self.started_at: Optional[datetime.datetime] = None
|
|
77
77
|
|
|
78
|
+
# State propagation for callers to wait on job readiness
|
|
79
|
+
self._ready_event = threading.Event()
|
|
80
|
+
self._error_message: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
def _set_state(self, new_state: str, error_msg: Optional[str] = None) -> None:
|
|
83
|
+
"""Set state and signal ready event if terminal state reached.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
new_state: New state value ("initialized", "running", "error", "stopping", "stopped").
|
|
87
|
+
error_msg: Optional error message when transitioning to "error" state.
|
|
88
|
+
"""
|
|
89
|
+
self.state = new_state
|
|
90
|
+
if error_msg:
|
|
91
|
+
self._error_message = error_msg
|
|
92
|
+
# Signal ready event when job reaches a terminal state (running, error, stopped)
|
|
93
|
+
if new_state in ("running", "error", "stopped"):
|
|
94
|
+
self._ready_event.set()
|
|
95
|
+
|
|
96
|
+
def wait_until_ready(self, timeout: float = 10.0) -> bool:
|
|
97
|
+
"""Wait for job to reach running or error state.
|
|
98
|
+
|
|
99
|
+
Blocks until the job thread signals that hooks have been loaded
|
|
100
|
+
successfully (state="running") or an error occurred (state="error").
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
timeout: Maximum seconds to wait (default: 10.0).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if job is running successfully, False if error or timeout.
|
|
107
|
+
"""
|
|
108
|
+
if self._ready_event.wait(timeout=timeout):
|
|
109
|
+
return self.state == "running"
|
|
110
|
+
# Timeout occurred
|
|
111
|
+
self._error_message = f"Timeout waiting for job to start after {timeout}s"
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def get_error(self) -> Optional[str]:
|
|
115
|
+
"""Get error message if job failed.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Error message string if job is in error state, None otherwise.
|
|
119
|
+
"""
|
|
120
|
+
return self._error_message
|
|
121
|
+
|
|
78
122
|
|
|
79
123
|
def create_job_script(self):
|
|
80
124
|
self.instrument(self.process_session)
|
|
@@ -93,18 +137,39 @@ class Job:
|
|
|
93
137
|
|
|
94
138
|
|
|
95
139
|
def invoke_handle_hooking(self):
|
|
96
|
-
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.stop_event.
|
|
140
|
+
try:
|
|
141
|
+
if self.is_script_created == False:
|
|
142
|
+
self.instrument(self.process_session)
|
|
143
|
+
self.is_script_created = True
|
|
144
|
+
self.script.on("message", self.wrap_custom_hooking_handler_with_job_id(self.custom_hooking_handler))
|
|
145
|
+
self.script.load()
|
|
146
|
+
self._set_state("running")
|
|
147
|
+
self.logger.info("[+] hooks successfully loaded")
|
|
148
|
+
|
|
149
|
+
#if self.is_running_as_thread:
|
|
150
|
+
# Keep the thread alive to handle messages until stop_event is set
|
|
151
|
+
while not self.stop_event.is_set():
|
|
152
|
+
self.stop_event.wait(1) # Sleep for 1 second and check again
|
|
153
|
+
except frida.TransportError as e:
|
|
154
|
+
error_msg = f"TransportError during script load: {e} - target app may have crashed or restarted"
|
|
155
|
+
self._set_state("error", error_msg)
|
|
156
|
+
self.logger.error(f"[-] {error_msg}")
|
|
157
|
+
except frida.InvalidOperationError as e:
|
|
158
|
+
error_msg = f"InvalidOperationError during script load: {e}"
|
|
159
|
+
self._set_state("error", error_msg)
|
|
160
|
+
self.logger.error(f"[-] {error_msg}")
|
|
161
|
+
except frida.ProcessNotFoundError as e:
|
|
162
|
+
error_msg = f"ProcessNotFoundError: Target process no longer exists: {e}"
|
|
163
|
+
self._set_state("error", error_msg)
|
|
164
|
+
self.logger.error(f"[-] {error_msg}")
|
|
165
|
+
except frida.ProtocolError as e:
|
|
166
|
+
error_msg = f"ProtocolError: Connection issue with target: {e}"
|
|
167
|
+
self._set_state("error", error_msg)
|
|
168
|
+
self.logger.error(f"[-] {error_msg}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
error_msg = f"Unexpected error in hook thread: {type(e).__name__}: {e}"
|
|
171
|
+
self._set_state("error", error_msg)
|
|
172
|
+
self.logger.error(f"[-] {error_msg}")
|
|
108
173
|
|
|
109
174
|
|
|
110
175
|
def wrap_custom_hooking_handler_with_job_id(self, handler):
|
|
@@ -134,15 +199,51 @@ class Job:
|
|
|
134
199
|
raise FridaBasedException("Connection is closed. Probably the target app crashed")
|
|
135
200
|
|
|
136
201
|
|
|
137
|
-
def close_job(self):
|
|
138
|
-
|
|
202
|
+
def close_job(self, timeout: float = 5.0) -> bool:
|
|
203
|
+
"""Stop the job and cleanup resources.
|
|
204
|
+
|
|
205
|
+
Uses a staged shutdown approach:
|
|
206
|
+
1. Set stop_event FIRST to signal thread to exit wait loop
|
|
207
|
+
2. Wait briefly for thread to notice stop_event
|
|
208
|
+
3. Try to unload script (may hang if connection broken)
|
|
209
|
+
4. Set final stopped state
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
timeout: Maximum seconds to wait for thread to stop.
|
|
213
|
+
Default 5.0 seconds. Use 0 for no wait.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
True if job stopped cleanly, False if timed out.
|
|
217
|
+
"""
|
|
218
|
+
self._set_state("stopping")
|
|
219
|
+
|
|
220
|
+
# Step 1: Signal thread to exit wait loop FIRST
|
|
139
221
|
self.stop_event.set()
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
|
|
223
|
+
# Step 2: Wait for thread to notice stop_event (short timeout)
|
|
224
|
+
thread_timeout = min(1.0, timeout) if timeout > 0 else 1.0
|
|
225
|
+
timed_out = False
|
|
226
|
+
if self.thread and self.thread.is_alive():
|
|
227
|
+
self.thread.join(timeout=thread_timeout)
|
|
228
|
+
if self.thread.is_alive():
|
|
229
|
+
self.logger.warning(
|
|
230
|
+
f"Job {self.job_id} thread did not stop within {thread_timeout}s"
|
|
231
|
+
)
|
|
232
|
+
timed_out = True
|
|
233
|
+
|
|
234
|
+
# Step 3: Try to unload script (may hang if connection broken)
|
|
142
235
|
if self.script:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
236
|
+
try:
|
|
237
|
+
self.script.unload()
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.warning(f"Script unload failed (connection may be broken): {e}")
|
|
240
|
+
|
|
241
|
+
# Step 4: Set final state
|
|
242
|
+
self._set_state("stopped")
|
|
243
|
+
|
|
244
|
+
status = "timed out" if timed_out else "stopped"
|
|
245
|
+
self.logger.info(f"Job {self.job_id} {status}")
|
|
246
|
+
return not timed_out
|
|
146
247
|
|
|
147
248
|
|
|
148
249
|
def get_id(self):
|
|
@@ -424,29 +424,48 @@ class JobManager(object):
|
|
|
424
424
|
return None
|
|
425
425
|
|
|
426
426
|
|
|
427
|
-
def stop_jobs(self):
|
|
427
|
+
def stop_jobs(self, timeout_per_job: float = 3.0) -> dict:
|
|
428
|
+
"""Stop all running jobs.
|
|
429
|
+
|
|
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 = {}
|
|
428
437
|
jobs_to_stop = [job_id for job_id, job in self.jobs.items() if job.state == "running"]
|
|
438
|
+
|
|
429
439
|
for job_id in jobs_to_stop:
|
|
430
440
|
try:
|
|
431
|
-
self.logger.info('[job manager] Job: {
|
|
432
|
-
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)
|
|
433
443
|
except frida.InvalidOperationError:
|
|
434
|
-
self.logger.error('[job manager] Job: {
|
|
435
|
-
|
|
444
|
+
self.logger.error(f'[job manager] Job: {job_id} - Error stopping')
|
|
445
|
+
results[job_id] = False
|
|
446
|
+
|
|
447
|
+
return results
|
|
436
448
|
|
|
437
449
|
|
|
438
|
-
def stop_job_with_id(self, job_id: str) ->
|
|
450
|
+
def stop_job_with_id(self, job_id: str, timeout: float = 5.0) -> bool:
|
|
439
451
|
"""Stop a specific job by ID.
|
|
440
452
|
|
|
441
453
|
Args:
|
|
442
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.
|
|
443
459
|
"""
|
|
444
|
-
if job_id in self.jobs:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
450
469
|
|
|
451
470
|
|
|
452
471
|
def get_last_created_job(self):
|
|
@@ -461,10 +480,38 @@ class JobManager(object):
|
|
|
461
480
|
raise ValueError(f"Job with ID {job_id} not found.")
|
|
462
481
|
|
|
463
482
|
|
|
464
|
-
def detach_from_app(self):
|
|
465
|
-
|
|
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():
|
|
466
498
|
self.process_session.detach()
|
|
467
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
|
+
|
|
468
515
|
|
|
469
516
|
def stop_app(self, app_package):
|
|
470
517
|
cmd = self._build_adb_command(["shell", "am", "force-stop", app_package])
|
|
@@ -670,23 +717,21 @@ class JobManager(object):
|
|
|
670
717
|
"""Get the current session mode ('spawn' or 'attach')."""
|
|
671
718
|
return self._mode
|
|
672
719
|
|
|
673
|
-
def reset_session(self) -> None:
|
|
720
|
+
def reset_session(self, timeout_per_job: float = 2.0, detach_timeout: float = 2.0) -> None:
|
|
674
721
|
"""Reset the session state for a new connection.
|
|
675
722
|
|
|
676
|
-
|
|
723
|
+
Args:
|
|
724
|
+
timeout_per_job: Max seconds to wait per job when stopping.
|
|
725
|
+
detach_timeout: Max seconds to wait for session detach.
|
|
677
726
|
"""
|
|
678
|
-
# Stop all running jobs
|
|
679
|
-
self.stop_jobs()
|
|
727
|
+
# Stop all running jobs with timeout
|
|
728
|
+
self.stop_jobs(timeout_per_job=timeout_per_job)
|
|
680
729
|
|
|
681
730
|
# Clear hook registry
|
|
682
731
|
self._hook_registry.clear()
|
|
683
732
|
|
|
684
|
-
# Detach from app
|
|
685
|
-
|
|
686
|
-
try:
|
|
687
|
-
self.process_session.detach()
|
|
688
|
-
except Exception:
|
|
689
|
-
pass
|
|
733
|
+
# Detach from app with timeout
|
|
734
|
+
self.detach_from_app(timeout=detach_timeout)
|
|
690
735
|
|
|
691
736
|
# Reset state
|
|
692
737
|
self.process_session = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AndroidFridaManager
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.6
|
|
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=tytVSs7JHtb8wVUCXH89lg4vd5mn04MrHfzmOgihOUQ,98
|
|
4
|
+
AndroidFridaManager/job.py,sha256=jDDaqiG_ri-vAwwk045X80mzftd8INxh7UIv9YOELDU,10525
|
|
5
|
+
AndroidFridaManager/job_manager.py,sha256=Ce2lHLezRVKuj7r_RodH6Q8l43zL_cL5ZGhE1zGB-E8,27336
|
|
6
|
+
androidfridamanager-1.9.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
androidfridamanager-1.9.6.dist-info/METADATA,sha256=Dmw-7QAphEDeTviAvvTbHllZzksDSnO7lSfdgAF-3Xw,5141
|
|
8
|
+
androidfridamanager-1.9.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
androidfridamanager-1.9.6.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
+
androidfridamanager-1.9.6.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
+
androidfridamanager-1.9.6.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=st5zrhGhtBpwZErvuVGrS-VQL9AmtAxFwL9AYgf-rqI,98
|
|
4
|
-
AndroidFridaManager/job.py,sha256=sMkTk1rHdfNmh9jFNQuiL4oqsUEBCJcbP6p2DZQPbL0,6172
|
|
5
|
-
AndroidFridaManager/job_manager.py,sha256=oPkzFK8XpwFhDHKuJmAoqylUDaV6VIKL_AikJBcg3iQ,25772
|
|
6
|
-
androidfridamanager-1.9.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
androidfridamanager-1.9.4.dist-info/METADATA,sha256=bghFYQPx8KRnUXKXZNyfHkPmBuTx3yBjT5_jXSZD9HM,5141
|
|
8
|
-
androidfridamanager-1.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
androidfridamanager-1.9.4.dist-info/entry_points.txt,sha256=GmNngu2fDNCxUcquFRegBa7GWknPKG1jsM4lvWeyKnY,64
|
|
10
|
-
androidfridamanager-1.9.4.dist-info/top_level.txt,sha256=oH2lVMSRlghmt-_tVrOEUqvY462P9hd5Ktgp5-1qF3o,20
|
|
11
|
-
androidfridamanager-1.9.4.dist-info/RECORD,,
|
{androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{androidfridamanager-1.9.4.dist-info → androidfridamanager-1.9.6.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|