frida-fusion 0.1.13__tar.gz → 0.1.15__tar.gz

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.

Potentially problematic release.


This version of frida-fusion might be problematic. Click here for more details.

Files changed (37) hide show
  1. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/PKG-INFO +2 -2
  2. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/README.md +1 -1
  3. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/__meta__.py +2 -2
  4. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/args.py +31 -0
  5. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/config.py +3 -0
  6. frida_fusion-0.1.15/frida_fusion/exceptions.py +19 -0
  7. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/fusion.py +56 -7
  8. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/helpers.js +54 -4
  9. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/module.py +23 -5
  10. frida_fusion-0.1.15/frida_fusion/modules/reflection/reflection-stalker.js +293 -0
  11. frida_fusion-0.1.15/frida_fusion/modules/reflection/reflection-stalker.py +206 -0
  12. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/PKG-INFO +2 -2
  13. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/SOURCES.txt +3 -0
  14. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/LICENSE +0 -0
  15. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/__init__.py +0 -0
  16. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/__main__.py +0 -0
  17. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/__init__.py +0 -0
  18. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/color.py +0 -0
  19. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/database.py +0 -0
  20. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/logger.py +0 -0
  21. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/libs/scriptlocation.py +0 -0
  22. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/__init__.py +0 -0
  23. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/android_setings/__init__.py +0 -0
  24. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/android_setings/settings.js +0 -0
  25. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/android_setings/settings.py +0 -0
  26. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/crypto/__init__.py +0 -0
  27. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/crypto/crypto.js +0 -0
  28. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/crypto/crypto.py +0 -0
  29. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
  30. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +0 -0
  31. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/dependency_links.txt +0 -0
  32. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/entry_points.txt +0 -0
  33. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/requires.txt +0 -0
  34. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/frida_fusion.egg-info/top_level.txt +0 -0
  35. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/pyproject.toml +0 -0
  36. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/setup.cfg +0 -0
  37. {frida_fusion-0.1.13 → frida_fusion-0.1.15}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frida-fusion
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Hook your mobile tests with Frida
5
5
  Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
6
6
  Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
@@ -105,7 +105,7 @@ void fusion_printMethods(String name);
105
105
  void fusion_waitForClass(String name, CallbackFunction onReady)
106
106
 
107
107
  # Conversions
108
- byte[] fusion_stringToBase64(String message);
108
+ String fusion_stringToBase64(String message);
109
109
  String fusion_bytesToBase64(byte[] byteArray);
110
110
  String fusion_encodeHex(byte[] byteArray);
111
111
  ```
@@ -70,7 +70,7 @@ void fusion_printMethods(String name);
70
70
  void fusion_waitForClass(String name, CallbackFunction onReady)
71
71
 
72
72
  # Conversions
73
- byte[] fusion_stringToBase64(String message);
73
+ String fusion_stringToBase64(String message);
74
74
  String fusion_bytesToBase64(byte[] byteArray);
75
75
  String fusion_encodeHex(byte[] byteArray);
76
76
  ```
@@ -1,8 +1,8 @@
1
- __version__ = '0.1.13'
1
+ __version__ = '0.1.15'
2
2
  __title__ = "Frida Fusion"
3
3
  __description__ = "📱 frida-fusion - runtime mobile exploration"
4
4
  __url__ = "https://github.com/helviojunior/frida-fusion"
5
- __build__ = 0xf8869e4
5
+ __build__ = 0xef43fce
6
6
  __author__ = "Helvio Junior (M4v3r1ck)"
7
7
  __author_email__ = "helvio_junior@hotmail.com"
8
8
  __license__ = "GPL-3.0"
@@ -6,6 +6,8 @@ import sys
6
6
 
7
7
  from .libs.color import Color
8
8
  from .__meta__ import __description__
9
+ from .module import ModuleManager
10
+
9
11
 
10
12
  class Arguments(object):
11
13
  ''' Holds arguments used by the Frida Fusion '''
@@ -43,8 +45,37 @@ class Arguments(object):
43
45
  modules_group = parser.add_argument_group('Modules')
44
46
  self._add_modules_args(modules_group)
45
47
 
48
+ # Add module args
49
+ for mod in self._get_requested_module_list():
50
+ flags = parser.add_argument_group(f'{mod.name} Flags')
51
+ mod.create_instance().add_params(flags)
52
+
46
53
  return parser.parse_args()
47
54
 
55
+ def _get_requested_module_list(self):
56
+ mods = ModuleManager.list_modules()
57
+ parser = argparse.ArgumentParser()
58
+ parser.add_argument('-m',
59
+ '--module',
60
+ action='append',
61
+ dest='enabled_modules')
62
+
63
+ t_args, _ = parser.parse_known_args([
64
+ word
65
+ for word in sys.argv
66
+ if word != "-h" and word != "--list-modules"
67
+ ])
68
+ if t_args is None or t_args.enabled_modules is None:
69
+ return []
70
+ return [
71
+ m
72
+ for md in t_args.enabled_modules
73
+ for mn in md.split(",")
74
+ if mn.strip() != ""
75
+ for _, m in mods.items()
76
+ if m.safe_name() == mn.lower()
77
+ ]
78
+
48
79
  def _add_app_args(self, app):
49
80
  app.add_argument('-f',
50
81
  '--package',
@@ -4,6 +4,7 @@ import os
4
4
  import errno
5
5
  import sys
6
6
  import signal
7
+ from argparse import Namespace
7
8
  from pathlib import Path
8
9
 
9
10
  from .module import Module, ModuleManager, InternalModule, ExternalModule
@@ -17,6 +18,7 @@ class Configuration(object):
17
18
  version = '0.0.0'
18
19
 
19
20
  initialized = False # Flag indicating config has been initialized
21
+ args: Namespace = {}
20
22
  debug_level = 0
21
23
  cmd_line = ''
22
24
  base_path = str(Path(__file__).resolve().parent)
@@ -98,6 +100,7 @@ class Configuration(object):
98
100
  sys.exit(0)
99
101
 
100
102
  args = Arguments().args
103
+ Configuration.args = args
101
104
 
102
105
  Color.pl('{+} {W}Startup parameters')
103
106
 
@@ -0,0 +1,19 @@
1
+
2
+ class SilentKillError(Exception):
3
+ def __init__(self, *args, **kwargs):
4
+ # args -> vão para a Exception (ex.: a mensagem)
5
+ # kwargs -> metadados opcionais seus
6
+ self.meta = kwargs
7
+ self._msg = kwargs.get("message") or ""
8
+ if args is not None and len(args) > 0 and self._msg == "":
9
+ self._msg = args[0]
10
+ super().__init__(*args)
11
+
12
+ def __str__(self):
13
+ if self._msg != "":
14
+ return self._msg
15
+ else:
16
+ return str(super(SilentKillError, self).__str__())
17
+
18
+ def __repr__(self):
19
+ return str(self)
@@ -2,6 +2,7 @@
2
2
  # -*- coding: UTF-8 -*-
3
3
  from __future__ import annotations
4
4
 
5
+ from .exceptions import SilentKillError
5
6
  from .libs.color import Color
6
7
  from .libs.scriptlocation import ScriptLocation
7
8
 
@@ -64,10 +65,12 @@ class Fusion(object):
64
65
  try:
65
66
  self.session.detach()
66
67
  except:
67
- try:
68
- self.device.kill(self.pid)
69
- except:
70
- pass
68
+ pass
69
+
70
+ try:
71
+ self.device.kill(self.pid)
72
+ except:
73
+ pass
71
74
 
72
75
  def get_device(self):
73
76
  try:
@@ -150,6 +153,13 @@ class Fusion(object):
150
153
 
151
154
  for m in self._modules:
152
155
  files_js += m.js_files()
156
+ dyn = m.dynamic_script()
157
+ if dyn is not None and dyn.strip(" \r\n") != "":
158
+ dyn += "\n\n"
159
+ line_cnt = len(dyn.split("\n")) - 1
160
+ self.script_trace[f"dyn_{m.safe_name()}.js"] = (offset, offset + line_cnt)
161
+ offset += line_cnt
162
+ src += dyn
153
163
 
154
164
  if os.path.isfile(Configuration.frida_scripts):
155
165
  files_js += [Configuration.frida_scripts]
@@ -220,9 +230,16 @@ class Fusion(object):
220
230
  sys.exit(1)
221
231
 
222
232
  def attach(self, pid: int):
223
- self.running = True
233
+ Fusion.running = True
224
234
  self.pid = pid
225
235
 
236
+ if self.session is not None:
237
+ try:
238
+ self.session.off("detached", self.on_detached)
239
+ except:
240
+ pass
241
+ self.session = None
242
+
226
243
  self.session = self.device.attach(self.pid)
227
244
  self.session.on("detached", self.on_detached)
228
245
 
@@ -231,7 +248,14 @@ class Fusion(object):
231
248
  self.device.resume(self.pid)
232
249
 
233
250
  def std_spawn(self):
234
- self.running = True
251
+ Fusion.running = True
252
+
253
+ if self.session is not None:
254
+ try:
255
+ self.session.off("detached", self.on_detached)
256
+ except:
257
+ pass
258
+ self.session = None
235
259
 
236
260
  self.pid = self.device.spawn([Configuration.package])
237
261
  self.session = self.device.attach(self.pid)
@@ -242,7 +266,14 @@ class Fusion(object):
242
266
  self.device.resume(self.pid)
243
267
 
244
268
  def wait_spawn(self):
245
- self.running = True
269
+ Fusion.running = True
270
+
271
+ if self.session is not None:
272
+ try:
273
+ self.session.off("detached", self.on_detached)
274
+ except:
275
+ pass
276
+ self.session = None
246
277
 
247
278
  self.pid = self.device.spawn([Configuration.package])
248
279
  self.device.resume(self.pid)
@@ -365,6 +396,18 @@ class Fusion(object):
365
396
  else:
366
397
  self.print_message(mLevel, message, script_location=script_location)
367
398
 
399
+ except SilentKillError as sk:
400
+ skm = str(sk)
401
+
402
+ self.print_message("D", "Silent kill requested",
403
+ script_location=Logger.get_caller_info(stack_index=1))
404
+ Fusion.running = False
405
+ time.sleep(0.2)
406
+ if skm != "":
407
+ Logger.pl(f'\n{skm}')
408
+ Logger.pl('\n{+} {O}Exiting...{O}{W}')
409
+ self.done.set()
410
+
368
411
  except Exception as err:
369
412
  script_location = ScriptLocation(file_name=Fusion._script_name)
370
413
  self.print_message("E", message, script_location=script_location)
@@ -436,6 +479,7 @@ class Fusion(object):
436
479
  elif crash is not None:
437
480
  Logger.pl("[CRASH] details: " + str(crash))
438
481
 
482
+ Logger.pl("")
439
483
  self.done.set()
440
484
 
441
485
  def _raise_key_value_event(self,
@@ -451,6 +495,8 @@ class Fusion(object):
451
495
  module=module,
452
496
  received_data=received_data
453
497
  )
498
+ except SilentKillError as ske:
499
+ raise ske
454
500
  except Exception as e:
455
501
  if Configuration.debug_level >= 2:
456
502
  self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
@@ -468,6 +514,8 @@ class Fusion(object):
468
514
  stack_trace=stack_trace,
469
515
  received_data=received_data
470
516
  )
517
+ except SilentKillError as ske:
518
+ raise ske
471
519
  except Exception as e:
472
520
  if Configuration.debug_level >= 2:
473
521
  self.print_message("E", f"Error resizing event to module {m.name}: {str(e)}")
@@ -626,6 +674,7 @@ class Fusion(object):
626
674
  if len(self._modules) > 0:
627
675
  Logger.pl("{+} Starting selected modules")
628
676
  for m in self._modules:
677
+ m.load_from_arguments(Configuration.args)
629
678
  m.start_module(
630
679
  package=Configuration.package,
631
680
  db_path=Configuration.db_path
@@ -2,12 +2,24 @@
2
2
  Author: Helvio Junior - M4v3r1ck
3
3
  */
4
4
 
5
+ function fusion_rawSend(payload1){
6
+ send(payload1);
7
+ }
8
+
5
9
  function fusion_Send(payload1, payload2){
6
10
  const info = fusion_getCallerInfo();
7
- send({
8
- payload: payload1,
9
- location: info
10
- }, payload2);
11
+
12
+ const message = {
13
+ payload: payload1,
14
+ location: info
15
+ };
16
+
17
+ // if payload1 is objet and has "type"
18
+ if (payload1 && typeof payload1 === 'object' && 'type' in payload1) {
19
+ message.type = payload1.type;
20
+ }
21
+
22
+ send(message, payload2);
11
23
  }
12
24
 
13
25
  function fusion_waitForClass(name, onReady) {
@@ -80,6 +92,12 @@ function fusion_bytesToBase64(byteArray){
80
92
  }
81
93
  }
82
94
 
95
+ function fusion_normalizePtr(addr) {
96
+ let p = ptr(addr);
97
+ if (Process.arch === 'arm64') p = p.and('0x00FFFFFFFFFFFFFF'); // limpa TBI
98
+ return p;
99
+ }
100
+
83
101
  function fusion_getCallerInfo() {
84
102
  try{
85
103
  const stack = new Error().stack.split("\n");
@@ -244,6 +262,38 @@ function fusion_getClassName(obj)
244
262
 
245
263
  }
246
264
 
265
+ function fusion_getReadableRange(p) {
266
+ try { p = ptr(p); } catch (_) { return null; }
267
+ const range = Process.findRangeByAddress(p); // não lança exceção
268
+ if (!range) return null;
269
+ // range.protection exemplo: 'r-x', 'rw-'
270
+ return range.protection.indexOf('r') !== -1 ? range : null;
271
+ }
272
+
273
+ function fusion_isAddressReadable(p) {
274
+ const r = fusion_getReadableRange(p);
275
+ if (!r) return false;
276
+ // tenta ler 1 byte para confirmar acessibilidade
277
+ try { Memory.readU8(ptr(p)); return true; }
278
+ catch (_) { return false; }
279
+ }
280
+
281
+ function fusion_describeAddress(p) {
282
+ try { p = ptr(p); } catch (_) { return { ok:false, reason:'not a pointer' }; }
283
+ if (Process.arch === 'arm64') p = p.and('0x00FFFFFFFFFFFFFF'); // remove top byte
284
+ if (!fusion_isAddressReadable(p)) return { ok:false, reason:'invalid pointer' };
285
+ const range = Process.findRangeByAddress(p);
286
+ if (!range) return { ok:false, reason:'unmapped' };
287
+ return {
288
+ ok: true,
289
+ base: range.base,
290
+ size: range.size,
291
+ protection: range.protection,
292
+ file: range.file ? range.file.path : null
293
+ };
294
+ }
295
+
296
+
247
297
  Java.perform(function () {
248
298
  const Thread = Java.use('java.lang.Thread');
249
299
  const UEH = Java.registerClass({
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  import sys
3
3
  import re
4
+ from argparse import _ArgumentGroup, Namespace
5
+
4
6
  import frida
5
7
  import pkgutil
6
8
  import importlib
@@ -35,12 +37,21 @@ class ModuleBase(object):
35
37
  def start_module(self, **kwargs) -> bool:
36
38
  raise Exception('Method "start_module" is not yet implemented.')
37
39
 
40
+ def load_from_arguments(self, args: Namespace) -> bool:
41
+ return True
42
+
38
43
  def js_files(self) -> list:
39
44
  return []
40
45
 
46
+ def dynamic_script(self) -> str:
47
+ return ""
48
+
41
49
  def suppress_messages(self):
42
50
  pass
43
51
 
52
+ def add_params(self, flags: _ArgumentGroup):
53
+ pass
54
+
44
55
  def key_value_event(self,
45
56
  script_location: ScriptLocation = None,
46
57
  stack_trace: str = None,
@@ -125,6 +136,9 @@ class ExternalModule(Module):
125
136
 
126
137
 
127
138
  class ModuleManager:
139
+ initialized = False # Flag indicating modules has been initialized
140
+ modules: dict[str, Module] = {}
141
+
128
142
  @classmethod
129
143
  def _safe_import_from_path(cls, path: Path, loaded_files: set):
130
144
  """
@@ -188,9 +202,12 @@ class ModuleManager:
188
202
 
189
203
  @classmethod
190
204
  def list_modules(cls) -> dict:
205
+ if ModuleManager.initialized:
206
+ return ModuleManager.modules
207
+
191
208
  try:
192
209
  base_module = Module.get_base_module()
193
- modules: dict[str, Module] = {}
210
+ ModuleManager.modules = {}
194
211
 
195
212
  # --- 1) Varredura padrão do seu pacote interno: <este_arquivo>/modules ---
196
213
  base_path = Path(__file__).resolve().parent / "modules"
@@ -242,13 +259,13 @@ class ModuleManager:
242
259
  for i_class in ModuleBase.__subclasses__():
243
260
  t = i_class()
244
261
  key = t.safe_name()
245
- if key in modules:
262
+ if key in ModuleManager.modules:
246
263
  raise ModuleLoaderError(
247
264
  f"Duplicated Module name: {i_class.__module__}.{i_class.__qualname__}"
248
265
  )
249
266
 
250
267
  if str(i_class.__module__) in internal_mods:
251
- modules[key] = InternalModule(
268
+ ModuleManager.modules[key] = InternalModule(
252
269
  name=t.name,
253
270
  description=t.description,
254
271
  module=str(i_class.__module__),
@@ -256,7 +273,7 @@ class ModuleManager:
256
273
  class_name=i_class,
257
274
  )
258
275
  else:
259
- modules[key] = ExternalModule(
276
+ ModuleManager.modules[key] = ExternalModule(
260
277
  name=t.name,
261
278
  description=t.description,
262
279
  module=str(i_class.__module__),
@@ -264,7 +281,8 @@ class ModuleManager:
264
281
  class_name=i_class,
265
282
  )
266
283
 
267
- return modules
284
+ ModuleManager.initialized = True
285
+ return ModuleManager.modules
268
286
 
269
287
  except Exception as e:
270
288
  # Envolve a exceção original para manter contexto
@@ -0,0 +1,293 @@
1
+ // Script Frida: Method.invoke -> build object + enviar via fusion_sendKeyValueData
2
+ Java.perform(function () {
3
+
4
+ var jlrmethod = Java.use("java.lang.reflect.Method");
5
+ var origInvoke = jlrmethod.invoke; // referência original
6
+
7
+ function safeGetClassName(obj) {
8
+ try {
9
+ if (obj === null || obj === undefined) return null;
10
+ var cls = obj.getClass();
11
+ return cls.getName();
12
+ } catch (e) {
13
+ try { if (obj && obj.$className) return obj.$className; } catch(e2) {}
14
+ try { return Object.prototype.toString.call(obj); } catch(_) { return typeof obj; }
15
+ }
16
+ }
17
+
18
+ function buildParamInfo(param, depth) {
19
+ depth = depth || 0;
20
+ var maxDepth = 3;
21
+ var info = { type: null, class_name: null, value: null };
22
+
23
+ if (param === null || param === undefined) {
24
+ info.type = 'null';
25
+ return info;
26
+ }
27
+
28
+ var className = safeGetClassName(param);
29
+ info.class_name = className;
30
+ info.type = className;
31
+
32
+ try {
33
+ if (className && className.charAt(0) === '[') {
34
+ info.type = className;
35
+ if (className === '[B') {
36
+ try {
37
+ var buffer = Java.array('byte', param);
38
+ try { info.value = toBase64(buffer); } catch (e) {
39
+ var sb = []; for (var i=0;i<Math.min(buffer.length,256);i++) sb.push((buffer[i]&0xff).toString(16));
40
+ info.value = sb.join('');
41
+ }
42
+ } catch (errByte) { info.value = '[unreadable byte array]'; }
43
+ return info;
44
+ }
45
+ if (className.indexOf('Ljava.lang.String') !== -1) {
46
+ try {
47
+ var arrLen = param.length;
48
+ var vals = [];
49
+ for (var i=0;i<Math.min(arrLen,50);i++) { vals.push(String(param[i])); }
50
+ info.value = { length: arrLen, preview: vals };
51
+ } catch(e) { info.value = '[unreadable string array]'; }
52
+ return info;
53
+ }
54
+ if (depth >= maxDepth) { info.value = '[array - depth limit]'; return info; }
55
+ try {
56
+ var n = param.length;
57
+ var items = [];
58
+ for (var j=0; j<Math.min(n,50); j++) items.push(buildParamInfo(param[j], depth+1));
59
+ info.value = { length: n, preview: items };
60
+ } catch(e) { info.value = '[unreadable array]'; }
61
+ return info;
62
+ }
63
+ } catch(eArrDetect) {}
64
+
65
+ try {
66
+ if (className === 'java.lang.String' || (typeof param === 'string')) { info.value = String(param); return info; }
67
+ if (className && (
68
+ className.indexOf('java.lang.Number') !== -1 ||
69
+ className.indexOf('java.lang.Integer') !== -1 ||
70
+ className.indexOf('java.lang.Long') !== -1 ||
71
+ className.indexOf('java.lang.Boolean') !== -1 ||
72
+ className.indexOf('java.lang.Double') !== -1 ||
73
+ className.indexOf('java.lang.Float') !== -1 ||
74
+ className.indexOf('java.lang.Short') !== -1 ||
75
+ className.indexOf('java.lang.Character') !== -1)) {
76
+ try { info.value = param.toString(); } catch(e){ info.value = String(param); }
77
+ return info;
78
+ }
79
+ try { info.value = param.toString(); } catch (e) { info.value = '[toString failed]'; }
80
+ return info;
81
+ } catch(errGeneric) {
82
+ info.value = '[error building param]';
83
+ return info;
84
+ }
85
+ }
86
+
87
+ function buildInvokeObject(methodRef, targetObject, parameters) {
88
+ var obj = {
89
+ timestamp: (new Date()).toISOString(),
90
+ method: {
91
+ toString: String(methodRef),
92
+ name: null,
93
+ declaring_class: null,
94
+ b64: null
95
+ },
96
+ target: {
97
+ class_name: null,
98
+ toString: null
99
+ },
100
+ parameters: [],
101
+ return: {
102
+ class_name: null,
103
+ value: null
104
+ }
105
+ };
106
+
107
+ try {
108
+ if (methodRef && methodRef.getName) obj.method.name = methodRef.getName(); else obj.method.name = String(methodRef);
109
+ try {
110
+ var decl = methodRef.getDeclaringClass();
111
+ obj.method.declaring_class = decl ? decl.getName() : null;
112
+ } catch(e) { obj.method.declaring_class = null; }
113
+ } catch(_) {}
114
+
115
+ try {
116
+ var tName = "" + methodRef;
117
+ try { obj.method.b64 = fusion_stringToBase64(tName); } catch(e){ obj.method.b64 = null; }
118
+ } catch(_) { obj.method.b64 = null; }
119
+
120
+ try {
121
+ if (targetObject === null || targetObject === undefined) {
122
+ obj.target.class_name = null; obj.target.toString = null;
123
+ } else {
124
+ obj.target.class_name = safeGetClassName(targetObject);
125
+ try { obj.target.toString = targetObject.toString(); } catch(e){ obj.target.toString = '[toString failed]'; }
126
+ }
127
+ } catch(e) {}
128
+
129
+ try {
130
+ var type = Object.prototype.toString.call(parameters);
131
+ if (parameters === null || parameters === undefined) {
132
+ obj.parameters = [];
133
+ } else if (type === '[object Array]') {
134
+ var arrLen = parameters.length;
135
+ for (var i=0; i<Math.min(arrLen,200); i++) {
136
+ obj.parameters.push({ index: i, info: buildParamInfo(parameters[i], 0) });
137
+ }
138
+ if (arrLen > 200) obj.parameters_truncated = true;
139
+ } else {
140
+ try {
141
+ var maybeLen = parameters.length;
142
+ if (typeof maybeLen === 'number') {
143
+ for (var k=0; k<Math.min(maybeLen,100); k++) obj.parameters.push({ index: k, info: buildParamInfo(parameters[k],0) });
144
+ } else {
145
+ obj.parameters.push({ index: 0, info: buildParamInfo(parameters,0) });
146
+ }
147
+ } catch(e2) {
148
+ obj.parameters.push({ index: 0, info: buildParamInfo(parameters,0) });
149
+ }
150
+ }
151
+ } catch(errParams) {
152
+ obj.parameters = [{ index:0, info: { type: '[error enumerating parameters]' } }];
153
+ }
154
+
155
+ return obj;
156
+ }
157
+
158
+ // coleta backtrace da Thread Java atual e retorna [pretty, raw]
159
+ function collectBacktrace() {
160
+ var pretty = "[unavailable]";
161
+ var raw = [];
162
+ try {
163
+ var Thread = Java.use('java.lang.Thread');
164
+ var trace = Thread.currentThread().getStackTrace(); // array of StackTraceElement
165
+ var prettyLines = [];
166
+ for (var i=0; i<trace.length; i++) {
167
+ try {
168
+ var ste = trace[i];
169
+ var line = ste.toString(); // ex: com.foo.Bar.method(File.java:123)
170
+ prettyLines.push(line);
171
+ raw.push(String(ste));
172
+ } catch(eSte) { /* ignore */ }
173
+ }
174
+ pretty = prettyLines.join('\n');
175
+ } catch(e) {
176
+ try {
177
+ // fallback: usar Java.perform stack (menos informativo)
178
+ pretty = (new Error()).stack;
179
+ raw = [pretty];
180
+ } catch(e2) {}
181
+ }
182
+ return { pretty: pretty, raw: raw };
183
+ }
184
+
185
+ // sobrescreve invoke para montar objeto e enviar via fusion_sendKeyValueData
186
+ jlrmethod.invoke.implementation = function(object, parameters) {
187
+ // monta objeto
188
+ var infoObj = buildInvokeObject(this, object, parameters);
189
+
190
+ // coletar backtrace
191
+ var bt = collectBacktrace();
192
+
193
+ // chamar o método original e capturar retorno
194
+ var retvalue;
195
+ try {
196
+ retvalue = origInvoke.call(this, object, parameters);
197
+ } catch (invokeErr) {
198
+ // enviar também quando lançar (pode ser útil)
199
+ try {
200
+ fusion_sendKeyValueData("java.lang.reflect.Method!invoke!throw", [
201
+ { key: "method", value: String(infoObj.method.toString) },
202
+ { key: "declaring_class", value: String(infoObj.method.declaring_class) },
203
+ { key: "method_b64", value: String(infoObj.method.b64) },
204
+ { key: "target_class", value: String(infoObj.target.class_name) },
205
+ { key: "params_count", value: String(infoObj.parameters.length) },
206
+ { key: "params_preview", value: JSON.stringify(infoObj.parameters) },
207
+ { key: "backtrace", value: bt.pretty },
208
+ { key: "backtrace_raw", value: JSON.stringify(bt.raw) },
209
+ { key: "error", value: String(invokeErr) }
210
+ ]);
211
+ } catch(eSendErr) { fusion_sendMessage("W", "fusion_sendKeyValueData error: " + eSendErr); }
212
+
213
+ throw invokeErr; // rethrow para manter comportamento
214
+ }
215
+
216
+ // preencher retorno infoObj.return
217
+ try {
218
+ if (retvalue === null || retvalue === undefined) {
219
+ infoObj.return.class_name = null; infoObj.return.value = null;
220
+ } else {
221
+ infoObj.return.class_name = safeGetClassName(retvalue);
222
+ if (infoObj.return.class_name && infoObj.return.class_name.charAt(0) === '[') {
223
+ if (infoObj.return.class_name === '[B') {
224
+ try { infoObj.return.value = toBase64(Java.array('byte', retvalue)); } catch(e) { infoObj.return.value = '[unreadable byte array]'; }
225
+ } else {
226
+ try {
227
+ var lenR = retvalue.length, itemsR = [];
228
+ for (var ri=0; ri<Math.min(lenR,50); ri++) { itemsR.push(String(retvalue[ri])); }
229
+ infoObj.return.value = { length: lenR, preview: itemsR };
230
+ } catch(eArrR) { infoObj.return.value = '[unreadable array]'; }
231
+ }
232
+ } else {
233
+ try { infoObj.return.value = retvalue.toString(); } catch(e) { infoObj.return.value = '[toString failed]'; }
234
+ }
235
+ }
236
+ } catch(eRetFill) { infoObj.return.value = '[error reading return]'; }
237
+
238
+ // preparar payload para fusion_sendKeyValueData (array de {key, value})
239
+ var payload = [];
240
+ try {
241
+ payload.push({ key: "method", value: String(infoObj.method.toString) });
242
+ payload.push({ key: "method_name", value: String(infoObj.method.name) });
243
+ payload.push({ key: "declaring_class", value: String(infoObj.method.declaring_class) });
244
+ payload.push({ key: "method_b64", value: String(infoObj.method.b64) });
245
+ payload.push({ key: "target_class", value: String(infoObj.target.class_name) });
246
+ payload.push({ key: "target_tostring", value: String(infoObj.target.toString) });
247
+ payload.push({ key: "params_count", value: String(infoObj.parameters.length) });
248
+
249
+ // params_preview como JSON string (pode truncar se muito grande)
250
+ try {
251
+ var paramsJson = JSON.stringify(infoObj.parameters);
252
+ if (paramsJson.length > 20000) paramsJson = paramsJson.slice(0,20000) + "...(truncated)";
253
+ payload.push({ key: "params_preview", value: paramsJson });
254
+ } catch(ePJ) {
255
+ payload.push({ key: "params_preview", value: "[error serializing params]" });
256
+ }
257
+
258
+ // retorno
259
+ try {
260
+ var retSummary = (infoObj.return.class_name === null && infoObj.return.value === null) ? "null" :
261
+ ("(" + String(infoObj.return.class_name) + ") " + String(infoObj.return.value));
262
+ if (retSummary.length > 4000) retSummary = retSummary.slice(0,4000) + "...(truncated)";
263
+ payload.push({ key: "return_summary", value: retSummary });
264
+ payload.push({ key: "return_class", value: String(infoObj.return.class_name) });
265
+ } catch(eRetPush) {
266
+ payload.push({ key: "return_summary", value: "[error]" });
267
+ }
268
+
269
+ // backtrace
270
+ payload.push({ key: "backtrace", value: bt.pretty });
271
+ payload.push({ key: "backtrace_raw", value: JSON.stringify(bt.raw) });
272
+
273
+ // timestamp
274
+ payload.push({ key: "timestamp", value: infoObj.timestamp });
275
+
276
+ } catch(ePayload) {
277
+ fusion_sendMessage("W", "Erro ao preparar payload: " + ePayload);
278
+ }
279
+
280
+ // enviar pelo canal customizado
281
+ try {
282
+ fusion_sendKeyValueData("java.lang.reflect.Method!invoke!call", payload);
283
+ } catch(eSend) {
284
+ // se fusion_sendKeyValueData não existir, fallback para console + fusion_sendMessage (se existir)
285
+ try { fusion_sendMessage("E", "fusion_sendKeyValueData missing or failed"); } catch(_) {}
286
+ }
287
+
288
+ return retvalue;
289
+ };
290
+
291
+ try { fusion_sendMessage("I", "Reflection module with fusion_sendKeyValueData loaded!"); } catch(e) { fusion_sendMessage("W", "Reflection module loaded!"); }
292
+
293
+ });
@@ -0,0 +1,206 @@
1
+ import errno
2
+ import sys
3
+ import json
4
+ import os.path
5
+ from pathlib import Path
6
+ from argparse import _ArgumentGroup, Namespace
7
+ from frida_fusion.libs.logger import Logger
8
+ from frida_fusion.libs.database import Database
9
+ from frida_fusion.module import ModuleBase
10
+ from frida_fusion.libs.scriptlocation import ScriptLocation
11
+ from frida_fusion.exceptions import SilentKillError
12
+
13
+
14
+ class Reflection(ModuleBase):
15
+
16
+ _EXCLUSION_LIST = [
17
+ "android.view.View.onDraw",
18
+ "android.graphics.Picture",
19
+ "com.facebook.fbreact.specs.NativeAnimatedModuleSpec",
20
+ "com.facebook.react.uimanager.BaseViewManager.setTransform",
21
+ "com.facebook.react.uimanager.ReanimatedUIManager.updateView",
22
+ "com.facebook.fbreact.specs.NativeStatusBarManagerAndroidSpec"
23
+ ]
24
+
25
+ class StalkerDB(Database):
26
+ dbName = ""
27
+
28
+ def __init__(self, db_name: str):
29
+ super().__init__(
30
+ auto_create=True,
31
+ db_name=db_name
32
+ )
33
+ self.create_db()
34
+
35
+ def create_db(self):
36
+ super().create_db()
37
+ conn = self.connect_to_db(check=False)
38
+
39
+ # definindo um cursor
40
+ cursor = conn.cursor()
41
+
42
+ # criando a tabela (schema)
43
+ cursor.execute("""
44
+ CREATE TABLE IF NOT EXISTS [reflection_stalker] (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ method TEXT NOT NULL,
47
+ method_b64 TEXT NULL,
48
+ method_name TEXT NULL,
49
+ target_class TEXT NULL,
50
+ target_tostring TEXT NULL,
51
+ params_count TEXT NULL,
52
+ params TEXT NULL,
53
+ error TEXT NULL,
54
+ return_summary TEXT NULL,
55
+ return_class TEXT NULL,
56
+ stack_trace TEXT NULL,
57
+ created_date datetime not null DEFAULT (datetime('now','localtime'))
58
+ );
59
+ """)
60
+
61
+ conn.commit()
62
+
63
+ # Must get the constraints
64
+ self.get_constraints(conn)
65
+
66
+ def __init__(self):
67
+ super().__init__('Reflection Stalker', 'Monitor reflection calls/invoke by Helvio Junior (M4v3r1ck)')
68
+ self.mod_path = str(Path(__file__).resolve().parent)
69
+ self._stalker_db = None
70
+ self._suppress_messages = False
71
+ self._ignore_list = []
72
+ self.js_file = os.path.join(self.mod_path, "reflection-stalker.js")
73
+
74
+ def start_module(self, **kwargs) -> bool:
75
+ if 'db_path' not in kwargs:
76
+ raise Exception("parameter db_path not found")
77
+
78
+ self._stalker_db = Reflection.StalkerDB(db_name=kwargs['db_path'])
79
+
80
+ def js_files(self) -> list:
81
+ return [
82
+ self.js_file
83
+ ]
84
+
85
+ def suppress_messages(self):
86
+ self._suppress_messages = True
87
+
88
+ def dynamic_script(self) -> str:
89
+ return f""
90
+
91
+
92
+ def add_params(self, flags: _ArgumentGroup):
93
+ flags.add_argument('--ignore-stalker-text',
94
+ dest='reflection_stalker_ignore',
95
+ metavar='text',
96
+ action='append',
97
+ help='Text to ignore at screen output. You can specify multiple values repeating the flag.')
98
+
99
+ def load_from_arguments(self, args: Namespace) -> bool:
100
+ self._ignore_list = []
101
+
102
+ if args.reflection_stalker_ignore is None or len(args.reflection_stalker_ignore) == 0:
103
+ return True
104
+
105
+ self._ignore_list = list(set([
106
+ txt.lower()
107
+ for t1 in args.reflection_stalker_ignore
108
+ if (txt := t1.strip()) != ''
109
+ ]))
110
+
111
+ return True
112
+
113
+ def key_value_event(self,
114
+ script_location: ScriptLocation = None,
115
+ stack_trace: str = None,
116
+ module: str = None,
117
+ received_data: dict = None
118
+ ) -> bool:
119
+
120
+
121
+ if module in ["java.lang.reflect.Method!invoke!throw",
122
+ "java.lang.reflect.Method!invoke!call"
123
+ ]:
124
+
125
+ # Exclusion list
126
+ method = received_data.get('method', '')
127
+ for em in Reflection._EXCLUSION_LIST:
128
+ if em in method:
129
+ return
130
+
131
+ params_preview = received_data.get('params_preview', '[]')
132
+ try:
133
+ params = json.dumps(json.loads(params_preview), default=Logger.json_serial)
134
+ except Exception:
135
+ params = params_preview
136
+
137
+ return_summary = received_data.get('return_summary', None)
138
+ try:
139
+ return_summary = json.dumps(json.loads(return_summary), default=Logger.json_serial)
140
+ except Exception:
141
+ pass
142
+
143
+ backtrace = received_data.get('backtrace_raw', None)
144
+ try:
145
+ if backtrace is None:
146
+ raise Exception()
147
+
148
+ if isinstance(backtrace, str):
149
+ backtrace = json.loads(backtrace)
150
+
151
+ bt = "Stack trace:\n at "
152
+ bt += '\n at '.join([
153
+ ln
154
+ for ln in backtrace
155
+ if 'dalvik.system.VMStack.getThreadStackTrace' not in ln
156
+ and 'java.lang.Thread.getStackTrace' not in ln
157
+ ])
158
+
159
+ backtrace = bt
160
+ except Exception:
161
+ backtrace = received_data.get('backtrace', stack_trace)
162
+
163
+ self._stalker_db.insert_one(
164
+ table_name='reflection_stalker',
165
+ stack_trace=backtrace,
166
+ error=received_data.get('error', ''),
167
+ method=received_data.get('method', ''),
168
+ method_b64=received_data.get('method_b64', ''),
169
+ method_name=received_data.get('method_name', ''),
170
+ target_class=received_data.get('target_class', ''),
171
+ target_tostring=received_data.get('target_tostring', ''),
172
+ params_count=received_data.get('params_count', ''),
173
+ params=params,
174
+ return_summary=return_summary,
175
+ return_class=received_data.get('return_class', '')
176
+ )
177
+
178
+ if not self._suppress_messages:
179
+ txt = json.dumps(received_data, indent=4)
180
+ for em in self._ignore_list:
181
+ if em in txt.lower():
182
+ return
183
+
184
+ Logger.print_message(
185
+ level="I",
186
+ message=f"Reflection: {received_data.get('method', '')}",
187
+ script_location=script_location
188
+ )
189
+
190
+
191
+ Logger.print_message(
192
+ level="D",
193
+ message=f"Reflection: {module}\n{txt}",
194
+ script_location=script_location
195
+ )
196
+
197
+ return True
198
+
199
+ def data_event(self,
200
+ script_location: ScriptLocation = None,
201
+ stack_trace: str = None,
202
+ received_data: str = None
203
+ ) -> bool:
204
+ return True
205
+
206
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frida-fusion
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Hook your mobile tests with Frida
5
5
  Author-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
6
6
  Maintainer-email: "Helvio Junior (M4v3r1ck)" <helvio_junior@hotmail.com>
@@ -105,7 +105,7 @@ void fusion_printMethods(String name);
105
105
  void fusion_waitForClass(String name, CallbackFunction onReady)
106
106
 
107
107
  # Conversions
108
- byte[] fusion_stringToBase64(String message);
108
+ String fusion_stringToBase64(String message);
109
109
  String fusion_bytesToBase64(byte[] byteArray);
110
110
  String fusion_encodeHex(byte[] byteArray);
111
111
  ```
@@ -7,6 +7,7 @@ frida_fusion/__main__.py
7
7
  frida_fusion/__meta__.py
8
8
  frida_fusion/args.py
9
9
  frida_fusion/config.py
10
+ frida_fusion/exceptions.py
10
11
  frida_fusion/fusion.py
11
12
  frida_fusion/module.py
12
13
  frida_fusion.egg-info/PKG-INFO
@@ -28,5 +29,7 @@ frida_fusion/modules/android_setings/settings.py
28
29
  frida_fusion/modules/crypto/__init__.py
29
30
  frida_fusion/modules/crypto/crypto.js
30
31
  frida_fusion/modules/crypto/crypto.py
32
+ frida_fusion/modules/reflection/reflection-stalker.js
33
+ frida_fusion/modules/reflection/reflection-stalker.py
31
34
  frida_fusion/modules/tls_unpinning/__init__.py
32
35
  frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py
File without changes
File without changes
File without changes