frida-fusion 0.1.3__tar.gz → 0.1.5__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 (30) hide show
  1. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/PKG-INFO +58 -4
  2. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/README.md +57 -3
  3. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/__meta__.py +1 -1
  4. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/config.py +21 -9
  5. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/fusion.py +11 -8
  6. frida-fusion-0.1.5/frida_fusion/libs/helpers.js +305 -0
  7. frida-fusion-0.1.5/frida_fusion/module.py +272 -0
  8. frida-fusion-0.1.5/frida_fusion/modules/crypto/__init__.py +0 -0
  9. {frida-fusion-0.1.3/frida_fusion/modules → frida-fusion-0.1.5/frida_fusion/modules/crypto}/crypto.py +9 -6
  10. frida-fusion-0.1.5/frida_fusion/modules/tls_unpinning/__init__.py +0 -0
  11. frida-fusion-0.1.5/frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py +66 -0
  12. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/PKG-INFO +58 -4
  13. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/SOURCES.txt +6 -2
  14. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/pyproject.toml +2 -0
  15. frida-fusion-0.1.3/frida_fusion/module.py +0 -116
  16. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/__init__.py +0 -0
  17. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/__main__.py +0 -0
  18. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/args.py +0 -0
  19. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/libs/__init__.py +0 -0
  20. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/libs/color.py +0 -0
  21. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/libs/database.py +0 -0
  22. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/libs/logger.py +0 -0
  23. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion/modules/__init__.py +0 -0
  24. {frida-fusion-0.1.3/frida_fusion/modules → frida-fusion-0.1.5/frida_fusion/modules/crypto}/crypto.js +0 -0
  25. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/dependency_links.txt +0 -0
  26. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/entry_points.txt +0 -0
  27. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/requires.txt +0 -0
  28. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/frida_fusion.egg-info/top_level.txt +0 -0
  29. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/setup.cfg +0 -0
  30. {frida-fusion-0.1.3 → frida-fusion-0.1.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: frida-fusion
3
- Version: 0.1.3
3
+ Version: 0.1.5
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>
@@ -64,11 +64,65 @@ Modules:
64
64
 
65
65
  ## Install
66
66
 
67
- > :information_source: We recommend using `pipx` rather than `pip` for system-wide installation.
67
+ ```
68
+ pip3 install frida-fusion
69
+ ```
70
+
71
+ ## Module engine
72
+
73
+ You can check available modules with `frida-fusion --list-modules` command.
74
+
75
+ ```bash
76
+ frida-fusion --list-modules
77
+
78
+ [ FRIDA ]—o—( FUSION )—o—[ MOBILE TESTS ] // v0.1.4
79
+ > hook your mobile tests with Frida
80
+
68
81
 
82
+ Available modules
83
+ Module Name : Description
84
+ Crypto : Hook cryptography/hashing functions
69
85
  ```
70
- python3 -m pipx install frida-fusion
86
+
87
+ ### External modules
88
+
89
+ You can develop or download community modules and load into frida-fusion.
90
+
91
+ To pass to the Frida Fusion the external module path you can use the environment variable `FUSION_MODULES` with the full path of modules
92
+
93
+ At linux:
94
+
95
+ ```bash
96
+ export FUSION_MODULES=/tmp/modules
97
+
98
+ # List all modules
99
+ frida-fusion --list-modules
100
+
101
+ # Using available module
102
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
71
103
  ```
72
104
 
73
- > :information_source: Check whether you also need to run the `python3 -m pipx ensurepath` command.
105
+ At windows:
74
106
 
107
+ ```bash
108
+ $env:FUSION_MODULES = "C:\extra_mods"
109
+
110
+ # List all modules
111
+ frida-fusion --list-modules
112
+
113
+ # Using available module
114
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
115
+ ```
116
+
117
+ ### Community modules
118
+
119
+ You can also use one of community developed modules
120
+
121
+ ```bash
122
+ cd /tmp/
123
+ git clone https://github.com/helviojunior/frida-fusion-community-modules
124
+ export FUSION_MODULES=/tmp/frida-fusion-community-modules
125
+
126
+ # List all modules
127
+ frida-fusion --list-modules
128
+ ```
@@ -34,11 +34,65 @@ Modules:
34
34
 
35
35
  ## Install
36
36
 
37
- > :information_source: We recommend using `pipx` rather than `pip` for system-wide installation.
37
+ ```
38
+ pip3 install frida-fusion
39
+ ```
40
+
41
+ ## Module engine
42
+
43
+ You can check available modules with `frida-fusion --list-modules` command.
44
+
45
+ ```bash
46
+ frida-fusion --list-modules
47
+
48
+ [ FRIDA ]—o—( FUSION )—o—[ MOBILE TESTS ] // v0.1.4
49
+ > hook your mobile tests with Frida
50
+
38
51
 
52
+ Available modules
53
+ Module Name : Description
54
+ Crypto : Hook cryptography/hashing functions
39
55
  ```
40
- python3 -m pipx install frida-fusion
56
+
57
+ ### External modules
58
+
59
+ You can develop or download community modules and load into frida-fusion.
60
+
61
+ To pass to the Frida Fusion the external module path you can use the environment variable `FUSION_MODULES` with the full path of modules
62
+
63
+ At linux:
64
+
65
+ ```bash
66
+ export FUSION_MODULES=/tmp/modules
67
+
68
+ # List all modules
69
+ frida-fusion --list-modules
70
+
71
+ # Using available module
72
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
41
73
  ```
42
74
 
43
- > :information_source: Check whether you also need to run the `python3 -m pipx ensurepath` command.
75
+ At windows:
44
76
 
77
+ ```bash
78
+ $env:FUSION_MODULES = "C:\extra_mods"
79
+
80
+ # List all modules
81
+ frida-fusion --list-modules
82
+
83
+ # Using available module
84
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
85
+ ```
86
+
87
+ ### Community modules
88
+
89
+ You can also use one of community developed modules
90
+
91
+ ```bash
92
+ cd /tmp/
93
+ git clone https://github.com/helviojunior/frida-fusion-community-modules
94
+ export FUSION_MODULES=/tmp/frida-fusion-community-modules
95
+
96
+ # List all modules
97
+ frida-fusion --list-modules
98
+ ```
@@ -1,4 +1,4 @@
1
- __version__ = '0.1.3'
1
+ __version__ = '0.1.5'
2
2
  __title__ = "Frida Fusion"
3
3
  __description__ = "📱 frida-fusion - runtime mobile exploration"
4
4
  __url__ = "https://github.com/helviojunior/frida-fusion"
@@ -6,7 +6,7 @@ import sys
6
6
  import signal
7
7
  from pathlib import Path
8
8
 
9
- from .module import Module
9
+ from .module import Module, ModuleManager, InternalModule, ExternalModule
10
10
  from .libs.color import Color
11
11
  from .libs.logger import Logger
12
12
  from .__meta__ import __version__
@@ -71,15 +71,27 @@ class Configuration(object):
71
71
  #show_help = any(['-h' == word for word in sys.argv])
72
72
 
73
73
  if list_modules:
74
- mods = Module.list_modules()
74
+ mods = ModuleManager.list_modules()
75
+
76
+ if len(mods) == 0:
77
+ Color.pl('{!} {R}error: no modules found{R}{W}\r\n')
78
+ sys.exit(1)
79
+
75
80
  max_name = max(iter([
76
81
  len(m.name) + 3
77
82
  for _, m in mods.items()
78
83
  ] + [15]))
79
- Color.pl(f"Available modules")
80
- Color.pl(f" {'Module Name'.ljust(max_name)} : Description")
81
- for _, m in mods.items():
82
- Color.pl(f" {m.name.ljust(max_name)} : {m.description}")
84
+ Color.pl(f"Available internal modules")
85
+ for m in [m for _, m in mods.items() if isinstance(m, InternalModule)]:
86
+ Color.pl(f" {m.safe_name().ljust(max_name)} : {m.description}")
87
+
88
+ Color.pl(f"\nAvailable external modules")
89
+ ext_mods = [m for _, m in mods.items() if isinstance(m, ExternalModule)]
90
+ if len(ext_mods) == 0:
91
+ Color.pl((" No external modules available. You can set {G}FUSION_MODULES{W} environment variable "
92
+ "to set an external modules Path"))
93
+ for m in ext_mods:
94
+ Color.pl(f" {m.safe_name().ljust(max_name)} : {m.description}")
83
95
 
84
96
  print("")
85
97
  sys.exit(0)
@@ -167,19 +179,19 @@ class Configuration(object):
167
179
  Logger.pl(' {C}min debug level:{O} %s{W}' % str(args.debug_level).upper())
168
180
 
169
181
  if args.enabled_modules is not None and isinstance(args.enabled_modules, list):
170
- mods = Module.list_modules()
182
+ mods = ModuleManager.list_modules()
171
183
  for mod in args.enabled_modules:
172
184
  fm = next(iter([
173
185
  m
174
186
  for _, m in mods.items()
175
- if m.name.lower() == mod.lower()
187
+ if m.safe_name() == mod.lower()
176
188
  ]), None)
177
189
  if fm is None:
178
190
  Color.pl(
179
191
  '{!} {R}error: module {O}%s{R} not found{W}\r\n' % mod)
180
192
  sys.exit(1)
181
193
 
182
- name = fm.name.lower()
194
+ name = fm.safe_name()
183
195
  if name not in Configuration.enabled_modules.keys():
184
196
  Configuration.enabled_modules[name] = fm
185
197
 
@@ -216,7 +216,7 @@ class Fusion(object):
216
216
  self.session = self.device.attach(self.pid)
217
217
  self.session.on("detached", self.on_detached)
218
218
 
219
- Logger.pl("{+} Iniciando scripts frida...")
219
+ Logger.pl("{+} Starting frida scripts")
220
220
  self.load_all_scripts()
221
221
  self.device.resume(self.pid)
222
222
 
@@ -227,7 +227,7 @@ class Fusion(object):
227
227
  self.session = self.device.attach(self.pid)
228
228
  self.session.on("detached", self.on_detached)
229
229
 
230
- Logger.pl("{+} Iniciando scripts frida...")
230
+ Logger.pl("{+} Starting frida scripts")
231
231
  self.load_all_scripts()
232
232
  self.device.resume(self.pid)
233
233
 
@@ -242,7 +242,7 @@ class Fusion(object):
242
242
  self.session = self.device.attach(self.pid)
243
243
  self.session.on("detached", self.on_detached)
244
244
 
245
- Logger.pl("{+} Iniciando scripts frida...")
245
+ Logger.pl("{+} Starting frida scripts")
246
246
  self.load_all_scripts()
247
247
 
248
248
  def make_handler(self, script_name):
@@ -631,16 +631,19 @@ class Fusion(object):
631
631
  Configuration.initialize()
632
632
 
633
633
  try:
634
+ print(f" 🛠️ Starting Frida Fusion instrumentation")
635
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
636
+ Logger.pl('{+} {C}Start time {O}%s{W}' % timestamp)
637
+
634
638
  self._modules = [
635
639
  m.create_instance()
636
640
  for _, m in Configuration.enabled_modules.items()
637
641
  ]
638
- for m in self._modules:
639
- m.start_db(db_path=Configuration.db_path)
640
642
 
641
- print(f" 🛠️ Iniciando instrumentação Frida")
642
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
643
- Logger.pl('{+} {C}Start time {O}%s{W}' % timestamp)
643
+ if len(self._modules) > 0:
644
+ Logger.pl("{+} Starting selected modules")
645
+ for m in self._modules:
646
+ m.start_module(db_path=Configuration.db_path)
644
647
 
645
648
  self.get_device()
646
649
  if self.device is not None:
@@ -0,0 +1,305 @@
1
+ /* Android Scripts
2
+ Author: Hélvio - M4v3r1ck
3
+ */
4
+
5
+
6
+ function waitForClass(name, onReady) {
7
+ var intv = setInterval(function () {
8
+ try {
9
+ var C = Java.use(name);
10
+ clearInterval(intv);
11
+ onReady(C);
12
+ } catch (e) { /* ainda não carregou */ }
13
+ }, 100);
14
+ }
15
+
16
+ function printStackTrace(){
17
+ var trace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
18
+ trace = trace.replace("java.lang.Exception\n", "Stack trace:\n");
19
+ sendMessage("*", trace);
20
+ }
21
+
22
+ function toBytes(message){
23
+ try{
24
+ const StringClass = Java.use('java.lang.String');
25
+ var bTxt = StringClass.$new(message).getBytes('utf-8');
26
+
27
+ return bTxt;
28
+ } catch (err) {
29
+ sendMessage("*", err)
30
+ }
31
+ }
32
+
33
+ function toBase64(message){
34
+ try{
35
+ const StringClass = Java.use('java.lang.String');
36
+ const Base64Class = Java.use('android.util.Base64');
37
+ var bTxt = StringClass.$new(message).getBytes('utf-8');
38
+ var b64Msg = Base64Class.encodeToString(bTxt, 0x00000002); //Base64Class.NO_WRAP = 0x00000002
39
+
40
+ return b64Msg;
41
+ } catch (err) {
42
+ sendMessage("*", err)
43
+ }
44
+ }
45
+
46
+ function bytesToBase64(message){
47
+
48
+ if (message === null || message === undefined) return "IA==";
49
+ try {
50
+ // 1) Confirma tipo byte[], se não tenta converter em string
51
+ message = Java.array('byte', message);
52
+
53
+ // 2) Tem 'length' numérico
54
+ const len = message.length;
55
+ if (typeof len !== "number") return "IA==";
56
+
57
+ // 3) (opcional) Exigir conteúdo
58
+ if (len === 0) return "IA==";
59
+
60
+ } catch (e) {
61
+ return "IA==";
62
+ }
63
+
64
+ try{
65
+
66
+ const Base64Class = Java.use('android.util.Base64');
67
+ var b64Msg = Base64Class.encodeToString(message, 0x00000002); //Base64Class.NO_WRAP = 0x00000002
68
+
69
+ return b64Msg;
70
+ } catch (err) {
71
+ sendMessage("*", err)
72
+ return "IA==";
73
+ }
74
+ }
75
+
76
+ function getCallerInfo() {
77
+ try{
78
+ const stack = new Error().stack.split("\n");
79
+
80
+ //Skip Error and getCallerInfo from stack trace
81
+ for (let i = 2; i < stack.length; i++) {
82
+ const line = stack[i].trim();
83
+
84
+ // Extrai: functionName (file:line:col)
85
+ // ou apenas (file:line:col) se não tiver nome
86
+ const m = line.match(/at\s+(?:(\S+)\s+)?[\( ]?(\S+):(\d+)\)?$/);
87
+ if (m) {
88
+ const func = m[1] || "";
89
+ const file = m[2];
90
+ const ln = parseInt(m[3], 10);
91
+
92
+ // Ignora funções cujo nome comece com "send" (qualquer case)
93
+ if (/^send/i.test(func)) continue;
94
+ if (/^isend/i.test(func)) continue;
95
+
96
+ return { file_name: file, function_name: func, line: ln };
97
+ }
98
+ }
99
+ } catch (err) {
100
+ console.log(`Error: ${err}`)
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function iSend(payload1, payload2){
106
+ try{
107
+ const info = getCallerInfo();
108
+ send({
109
+ payload: payload1,
110
+ location: info
111
+ }, payload2);
112
+ } catch (err) {
113
+ //sendMessage("*", err)
114
+ console.log(`Error: ${err}`)
115
+ }
116
+ }
117
+
118
+ function sendData(mType, jData, bData){
119
+ //iSend('{"type" : "'+ mType +'", "jdata" : "'+ jData +'"}', bData);
120
+ iSend({
121
+ type: mType,
122
+ jdata: jData
123
+ }, bData)
124
+ }
125
+
126
+ function sendKeyValueData(module, items) {
127
+ var st = getB64StackTrace();
128
+
129
+ var data = [];
130
+
131
+ // Force as String
132
+ for (let i = 0; i < items.length; i++) {
133
+ data = data.concat([{key: `${items[i].key}`, value:`${items[i].value}`}]);
134
+ }
135
+
136
+ iSend({
137
+ type: "key_value_data",
138
+ module: module,
139
+ data: data,
140
+ stack_trace: st
141
+ }, null);
142
+
143
+
144
+ /*
145
+ var jData = `{"type" : "key_value_data", "module": "${module}", "data": [`;
146
+ for (let i = 0; i < items.length; i++) {
147
+ if (i > 0) {
148
+ jData += `, `
149
+ }
150
+ jData += `{"key": "${items[i].key}", "value": "${items[i].value}"}`
151
+ }
152
+ jData += `], "stack_trace": "${st}"}`;
153
+ iSend(jData, "");
154
+ */
155
+ }
156
+
157
+ function sendMessage(level, message){
158
+ try{
159
+ const StringClass = Java.use('java.lang.String');
160
+ const Base64Class = Java.use('android.util.Base64');
161
+ var bTxt = StringClass.$new(message).getBytes('utf-8');
162
+ var b64Msg = Base64Class.encodeToString(bTxt, 0x00000002); //Base64Class.NO_WRAP = 0x00000002
163
+
164
+ //send('{"type" : "message", "level" : "'+ level +'", "message" : "'+ b64Msg +'"}');
165
+ iSend({
166
+ type: "message",
167
+ level: level,
168
+ message: b64Msg
169
+ }, null)
170
+ } catch (err) {
171
+ sendMessage("*", err)
172
+ //sendMessage('-', 'secret_key_spec.$init.overload error: ' + err + '\n' + err.stack);
173
+ }
174
+ }
175
+
176
+ function sendError(error) {
177
+ try{
178
+ sendMessage("-", error + '\n' + error.stack);
179
+ } catch (err) {
180
+ sendMessage("*", err)
181
+ }
182
+ }
183
+
184
+ function encodeHex(byteArray) {
185
+
186
+ const HexClass = Java.use('org.apache.commons.codec.binary.Hex');
187
+ const StringClass = Java.use('java.lang.String');
188
+ const hexChars = HexClass.encodeHex(byteArray);
189
+ //sendMessage("*", StringClass.$new(hexChars).toString());
190
+ //Buffer.from(bufStr, 'utf8');
191
+ //sendMessage("*", new Uint8Array(byteArray));
192
+ return StringClass.$new(hexChars).toString();
193
+
194
+ }
195
+
196
+ function getB64StackTrace(){
197
+
198
+ try{
199
+ const StringClass = Java.use('java.lang.String');
200
+ const Base64Class = Java.use('android.util.Base64');
201
+ var trace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
202
+ trace = trace.replace("java.lang.Exception\n", "Stack trace:\n");
203
+ var bTrace = StringClass.$new(trace).getBytes('utf-8');
204
+ var b64Msg = Base64Class.encodeToString(bTrace, 0x00000002); //Base64Class.NO_WRAP = 0x00000002
205
+
206
+ return b64Msg
207
+
208
+ } catch (err) {
209
+ sendMessage("*", err);
210
+ return '';
211
+ }
212
+ }
213
+
214
+ function enumMethods(targetClass)
215
+ {
216
+ var hook = Java.use(targetClass);
217
+ var ownMethods = hook.class.getDeclaredMethods();
218
+ hook.$dispose;
219
+
220
+ return ownMethods;
221
+ }
222
+
223
+ function printMethods(hook)
224
+ {
225
+ var ownMethods = hook.class.getDeclaredMethods();
226
+ ownMethods.forEach(function(s) {
227
+ //sendMessage(s);
228
+ sendMessage('*', s);
229
+ });
230
+
231
+ }
232
+
233
+ function intToHex(intVal)
234
+ {
235
+ return intVal.toString(16);
236
+ }
237
+
238
+
239
+ Java.perform(function () {
240
+ const Thread = Java.use('java.lang.Thread');
241
+ const UEH = Java.registerClass({
242
+ name: 'br.com.sec4us.UehProxy',
243
+ implements: [Java.use('java.lang.Thread$UncaughtExceptionHandler')],
244
+ methods: {
245
+ uncaughtException: [{
246
+ returnType: 'void',
247
+ argumentTypes: ['java.lang.Thread', 'java.lang.Throwable'],
248
+ implementation: function (t, e) {
249
+ try {
250
+ const Throwable = Java.use('java.lang.Throwable');
251
+ const sw = Java.use('java.io.StringWriter').$new();
252
+ const pw = Java.use('java.io.PrintWriter').$new(sw);
253
+ Throwable.$new(e).printStackTrace(pw);
254
+ send({ type: 'java-uncaught', thread: t.getName(), stack: sw.toString() });
255
+ } catch (err) { send({ type: 'java-uncaught-error', err: err+'' }); }
256
+ // Opcional: impedir que o app morra? Não é garantido; normalmente o processo cai.
257
+ }
258
+ }]
259
+ }
260
+ });
261
+
262
+ // Define globalmente
263
+ Thread.setDefaultUncaughtExceptionHandler(UEH.$new());
264
+ });
265
+
266
+ function formatBacktrace(frames) {
267
+ return frames.map((addr, i) => {
268
+ const sym = DebugSymbol.fromAddress(addr);
269
+ const mod = Process.findModuleByAddress(addr);
270
+ const off = (mod && addr.sub(mod.base)) ? "0x" + addr.sub(mod.base).toString(16) : String(addr);
271
+ const name = (sym && sym.name) ? sym.name : "<unknown>";
272
+ const modname = mod ? mod.name : "<unknown>";
273
+ return `${i.toString().padStart(2)} ${name} (${modname}+${off})`;
274
+ });
275
+ }
276
+
277
+ Process.setExceptionHandler(function (details) {
278
+ let frames;
279
+ try {
280
+ frames = Thread.backtrace(details.context, Backtracer.ACCURATE);
281
+ } catch (e) {
282
+ frames = Thread.backtrace(details.context, Backtracer.FUZZY);
283
+ }
284
+
285
+ const pretty = formatBacktrace(frames);
286
+
287
+ send({
288
+ type: "native-exception",
289
+ details: {
290
+ message: details.message,
291
+ type: details.type,
292
+ address: String(details.address),
293
+ memory: details.memory,
294
+ context: details.context,
295
+ nativeContext: String(details.nativeContext),
296
+ backtrace: pretty, // <— pilha simbólica
297
+ backtrace_raw: frames.map(String) // <— opcional: endereços puros
298
+ }
299
+ });
300
+
301
+ // true = tenta engolir a exceção; se quiser ver o processo cair, retorne false
302
+ return false;
303
+ });
304
+
305
+ sendMessage("W", "Helper functions have been successfully initialized.")
@@ -0,0 +1,272 @@
1
+ import os
2
+ import sys
3
+ import re
4
+ import frida
5
+ import pkgutil
6
+ import importlib
7
+ import requests
8
+ import importlib.util
9
+ from pathlib import Path
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ from .__meta__ import __version__
14
+ from .libs.logger import Logger
15
+
16
+ if TYPE_CHECKING:
17
+ from .fusion import Fusion # só no type checker
18
+
19
+
20
+ class ModuleLoaderError(Exception):
21
+ pass
22
+
23
+
24
+ class ModuleBase(object):
25
+
26
+ name = ''
27
+ description = ''
28
+ mod_path = ''
29
+
30
+ def __init__(self, name, description):
31
+ self.name = name
32
+ self.description = description
33
+ self.mod_path = str(Path(__file__).resolve().parent)
34
+ pass
35
+
36
+ def safe_name(self):
37
+ return ModuleBase.get_safe_name(self.name)
38
+
39
+ def start_module(self, **kwargs) -> bool:
40
+ raise Exception('Method "start_module" is not yet implemented.')
41
+
42
+ def js_files(self) -> list:
43
+ return []
44
+
45
+ def key_value_event(self,
46
+ script_location: "Fusion.ScriptLocation" = None,
47
+ stack_trace: str = None,
48
+ module: str = None,
49
+ received_data: dict = None
50
+ ) -> bool:
51
+ raise Exception('Method "key_value_event" is not yet implemented.')
52
+
53
+ def data_event(self,
54
+ script_location: "Fusion.ScriptLocation" = None,
55
+ stack_trace: str = None,
56
+ received_data: str = None
57
+ ) -> bool:
58
+ raise Exception('Method "data_event" is not yet implemented.')
59
+
60
+ @staticmethod
61
+ def get_safe_name(name):
62
+ name = name.replace(" ", "_").lower()
63
+ return re.sub(r'[^a-zA-Z0-9_.-]+', '', name)
64
+
65
+ @classmethod
66
+ def _get_codeshare(cls, uri: str) -> dict:
67
+
68
+ if uri is None or len(uri) <= 10:
69
+ raise Exception("Invalid codeshare uri. Uri must be only user/project_name.")
70
+
71
+ uri = uri.strip(" /@.")
72
+
73
+ headers = {
74
+ "Accept": "application/vnd.github+json",
75
+ "User-Agent": f"Frida-fusion v{__version__}, Frida v{frida.__version__}"
76
+ }
77
+
78
+ try:
79
+ resp = requests.get(f"https://codeshare.frida.re/api/project/{uri}", headers=headers, timeout=30)
80
+ resp.raise_for_status()
81
+ data = resp.json()
82
+ if data is None:
83
+ raise Exception("data is empty")
84
+
85
+ if data.get('source', None) is None or data.get('source', '').strip(" \r\n") == "":
86
+ raise Exception("source code is empty")
87
+
88
+ return data
89
+
90
+ except Exception as e:
91
+ raise ModuleLoaderError("Error getting codeshare data") from e
92
+
93
+
94
+ class Module(object):
95
+ modules = {}
96
+
97
+ def __init__(self, name, description, module, qualname, class_name):
98
+ self.name = name
99
+ self.description = description
100
+ self.module = module
101
+ self.qualname = qualname
102
+ self._class = class_name
103
+ pass
104
+
105
+ def safe_name(self):
106
+ return ModuleBase.get_safe_name(self.name)
107
+
108
+ def create_instance(self):
109
+ return self._class()
110
+
111
+ @classmethod
112
+ def get_base_module(cls) -> str:
113
+ file = Path(__file__).stem
114
+
115
+ parent_module = f'.{cls.__module__}.'.replace(f'.{file}.', '').strip(' .')
116
+
117
+ return '.'.join((parent_module, 'modules'))
118
+
119
+
120
+ class InternalModule(Module):
121
+ pass
122
+
123
+
124
+ class ExternalModule(Module):
125
+ pass
126
+
127
+
128
+ class ModuleManager:
129
+ @classmethod
130
+ def _safe_import_from_path(cls, path: Path, loaded_files: set):
131
+ """
132
+ Importa um .py arbitrário usando um nome único e registra o arquivo
133
+ para não ser importado duas vezes.
134
+ """
135
+ real = path.resolve()
136
+
137
+ try:
138
+ if real in loaded_files:
139
+ return
140
+
141
+ # nome único, estável, baseado no caminho
142
+ pseudo_name = (
143
+ "fusion_ext_"
144
+ + "_".join(real.parts).replace(":", "_").replace("\\", "_").replace("/", "_")
145
+ .replace(".", "_")
146
+ )
147
+ spec = importlib.util.spec_from_file_location(pseudo_name, real)
148
+ if spec and spec.loader:
149
+ mod = importlib.util.module_from_spec(spec)
150
+ sys.modules[pseudo_name] = mod
151
+ spec.loader.exec_module(mod)
152
+ loaded_files.add(real)
153
+ except Exception as ie:
154
+ Logger.pl('\n{!} {R}Error loading external module: {G}%s{R}\n {O} %s{W}' % (str(ie), str(real)))
155
+ pass
156
+
157
+ @classmethod
158
+ def _import_via_pkgutil(cls, roots: list[Path], loaded_files: set):
159
+ """
160
+ Varre roots com pkgutil.walk_packages. Isso encontra
161
+ - módulos .py no nível do root
162
+ - pacotes (pastas com __init__.py) e seus submódulos
163
+ NÃO entra em subpastas sem __init__.py (por isso depois complementamos).
164
+ """
165
+ str_roots = [str(p) for p in roots]
166
+ for loader, modname, is_pkg in pkgutil.walk_packages(str_roots):
167
+ try:
168
+ mod = importlib.import_module(modname)
169
+ mfile = getattr(mod, "__file__", None)
170
+ if mfile:
171
+ loaded_files.add(Path(mfile).resolve())
172
+ except Exception as ie:
173
+ Logger.pl(
174
+ '\n{!} {R}Error loading internal module: {G}%s{R}\n {O} %s{W}' % (
175
+ str(ie), str(loader.path)))
176
+ pass
177
+
178
+ @classmethod
179
+ def _load_any_py_recursively(cls, root: Path, loaded_files: set):
180
+ """
181
+ Carrega *todo* arquivo .py sob root (rglob), incluindo subpastas sem __init__.py,
182
+ sem duplicar o que já foi importado.
183
+ """
184
+ for py in root.rglob("*.py"):
185
+ # exclui caches e similares
186
+ if any(part in {"__pycache__"} for part in py.parts):
187
+ continue
188
+ cls._safe_import_from_path(py, loaded_files)
189
+
190
+ @classmethod
191
+ def list_modules(cls) -> dict:
192
+ try:
193
+ base_module = Module.get_base_module()
194
+ modules: dict[str, Module] = {}
195
+
196
+ # --- 1) Varredura padrão do seu pacote interno: <este_arquivo>/modules ---
197
+ base_path = Path(__file__).resolve().parent / "modules"
198
+ internal_mod_roots = [p for p in base_path.iterdir() if p.is_dir()]
199
+
200
+ internal_mods = []
201
+
202
+ # Vamos usar pkgutil para o pacote interno (mantém o comportamento)
203
+ loaded_files: set[Path] = set()
204
+ mods = [str(p) for p in internal_mod_roots]
205
+ for loader, modname, is_pkg in pkgutil.walk_packages(mods):
206
+ if not is_pkg:
207
+ # Reconstrói o caminho relativo para montar o import dentro do pacote base
208
+ mod_path = Path(getattr(loader, "path", ""))
209
+ try:
210
+ rel = mod_path.resolve().relative_to(base_path.resolve())
211
+ dotted = "." + ".".join(rel.parts) if rel.parts else ""
212
+ except Exception:
213
+ dotted = ""
214
+ importlib.import_module(f"{base_module}{dotted}.{modname}")
215
+ internal_mods.append(f"{base_module}{dotted}.{modname}")
216
+
217
+ # --- 2) Varredura de caminhos externos via FUSION_MODULES ---
218
+ env_value = os.environ.get("FUSION_MODULES", "").strip()
219
+ if env_value:
220
+ extra_roots = [Path(p).expanduser() for p in env_value.split(os.pathsep) if p.strip()]
221
+ existing_roots = [p for p in extra_roots if p.exists() and p.is_dir()]
222
+
223
+ # Para que pkgutil encontre módulos top-level nesses roots
224
+ # (sem precisar de nomes de pacote), colocamos cada root no sys.path
225
+ # durante a varredura. Usamos um conjunto para restaurar depois se preferir.
226
+ original_sys_path = list(sys.path)
227
+ try:
228
+ for root in existing_roots:
229
+ if str(root) not in sys.path:
230
+ sys.path.insert(0, str(root))
231
+
232
+ # 2a) Encontrar módulos top-level e pacotes (com __init__.py)
233
+ cls._import_via_pkgutil(existing_roots, loaded_files)
234
+
235
+ # 2b) Complementar: carregar QUALQUER .py (inclusive subpastas sem __init__.py)
236
+ for root in existing_roots:
237
+ cls._load_any_py_recursively(root, loaded_files)
238
+ finally:
239
+ # opcional: restaurar sys.path (seguro para evitar vazamentos)
240
+ sys.path[:] = original_sys_path
241
+
242
+ # --- 3) Instanciar subclasses de ModuleBase e montar o registry ---
243
+ for i_class in ModuleBase.__subclasses__():
244
+ t = i_class()
245
+ key = t.safe_name()
246
+ if key in modules:
247
+ raise ModuleLoaderError(
248
+ f"Duplicated Module name: {i_class.__module__}.{i_class.__qualname__}"
249
+ )
250
+
251
+ if str(i_class.__module__) in internal_mods:
252
+ modules[key] = InternalModule(
253
+ name=t.name,
254
+ description=t.description,
255
+ module=str(i_class.__module__),
256
+ qualname=str(i_class.__qualname__),
257
+ class_name=i_class,
258
+ )
259
+ else:
260
+ modules[key] = ExternalModule(
261
+ name=t.name,
262
+ description=t.description,
263
+ module=str(i_class.__module__),
264
+ qualname=str(i_class.__qualname__),
265
+ class_name=i_class,
266
+ )
267
+
268
+ return modules
269
+
270
+ except Exception as e:
271
+ # Envolve a exceção original para manter contexto
272
+ raise ModuleLoaderError("Error listing modules") from e
@@ -3,13 +3,13 @@ from pathlib import Path
3
3
  import base64
4
4
  import string
5
5
 
6
- from ..libs.color import Color
7
- from ..libs.database import Database
8
- from ..module import ModuleBase
6
+ from frida_fusion.libs.logger import Logger
7
+ from frida_fusion.libs.database import Database
8
+ from frida_fusion.module import ModuleBase
9
9
 
10
10
  from typing import TYPE_CHECKING
11
11
  if TYPE_CHECKING:
12
- from ..fusion import Fusion # só no type checker
12
+ from frida_fusion.fusion import Fusion # só no type checker
13
13
 
14
14
 
15
15
  class Crypto(ModuleBase):
@@ -274,8 +274,11 @@ class Crypto(ModuleBase):
274
274
  self._crypto_db = None
275
275
  self.mod_path = str(Path(__file__).resolve().parent)
276
276
 
277
- def start_db(self, db_path: str) -> bool:
278
- self._crypto_db = Crypto.CryptoDB(db_name=db_path)
277
+ def start_module(self, **kwargs) -> bool:
278
+ if 'db_path' not in kwargs:
279
+ raise Exception("parameter db_path not found")
280
+
281
+ self._crypto_db = Crypto.CryptoDB(db_name=kwargs['db_path'])
279
282
  return True
280
283
 
281
284
  def js_files(self) -> list:
@@ -0,0 +1,66 @@
1
+ import errno
2
+ import os.path
3
+ import tempfile
4
+ from pathlib import Path
5
+ from frida_fusion.libs.logger import Logger
6
+ from frida_fusion.module import ModuleBase
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from frida_fusion.fusion import Fusion # só no type checker
12
+
13
+
14
+ class TlsUnpinning(ModuleBase):
15
+
16
+ def __init__(self):
17
+ super().__init__('Multiple unpinning', 'Use frida_multiple_unpinning by Maurizio Siddu (@akabe1)')
18
+ self.mod_path = str(Path(__file__).resolve().parent)
19
+ self.js_file = os.path.join(self.mod_path, "frida_multiple_unpinning.js")
20
+
21
+ def start_module(self, **kwargs) -> bool:
22
+ if not os.path.isfile(self.js_file):
23
+ Logger.pl("{+} Downloading CodeShare script from @akabe1/frida-multiple-unpinning")
24
+ data = self._get_codeshare("@akabe1/frida-multiple-unpinning/")
25
+ if data.get('source', None) is None or data.get('source', '').strip(" \r\n") == "":
26
+ raise Exception("source code is empty")
27
+
28
+ try:
29
+ with open(self.js_file, "w", encoding='utf-8') as f:
30
+ f.write(data.get('source', ''))
31
+ except IOError as x:
32
+ if x.errno == errno.EACCES:
33
+ Logger.pl('{!} {R}error: could not open output file to write {O}permission denied{R}{W}\r\n')
34
+ elif x.errno == errno.EISDIR:
35
+ Logger.pl('{!} {R}error: could not open output file to write {O}it is an directory{R}{W}\r\n')
36
+ else:
37
+ Logger.pl('{!} {R}error: could not open output file to write{W}\r\n')
38
+
39
+ # Try to save locally
40
+ self.js_file = str(Path("frida_multiple_unpinning.js").resolve().absolute())
41
+ with open(self.js_file, "w", encoding='utf-8') as f:
42
+ f.write(data.get('source', ''))
43
+
44
+ return True
45
+
46
+ def js_files(self) -> list:
47
+ return [
48
+ self.js_file
49
+ ]
50
+
51
+ def key_value_event(self,
52
+ script_location: "Fusion.ScriptLocation" = None,
53
+ stack_trace: str = None,
54
+ module: str = None,
55
+ received_data: dict = None
56
+ ) -> bool:
57
+ return True
58
+
59
+ def data_event(self,
60
+ script_location: "Fusion.ScriptLocation" = None,
61
+ stack_trace: str = None,
62
+ received_data: str = None
63
+ ) -> bool:
64
+ return True
65
+
66
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: frida-fusion
3
- Version: 0.1.3
3
+ Version: 0.1.5
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>
@@ -64,11 +64,65 @@ Modules:
64
64
 
65
65
  ## Install
66
66
 
67
- > :information_source: We recommend using `pipx` rather than `pip` for system-wide installation.
67
+ ```
68
+ pip3 install frida-fusion
69
+ ```
70
+
71
+ ## Module engine
72
+
73
+ You can check available modules with `frida-fusion --list-modules` command.
74
+
75
+ ```bash
76
+ frida-fusion --list-modules
77
+
78
+ [ FRIDA ]—o—( FUSION )—o—[ MOBILE TESTS ] // v0.1.4
79
+ > hook your mobile tests with Frida
80
+
68
81
 
82
+ Available modules
83
+ Module Name : Description
84
+ Crypto : Hook cryptography/hashing functions
69
85
  ```
70
- python3 -m pipx install frida-fusion
86
+
87
+ ### External modules
88
+
89
+ You can develop or download community modules and load into frida-fusion.
90
+
91
+ To pass to the Frida Fusion the external module path you can use the environment variable `FUSION_MODULES` with the full path of modules
92
+
93
+ At linux:
94
+
95
+ ```bash
96
+ export FUSION_MODULES=/tmp/modules
97
+
98
+ # List all modules
99
+ frida-fusion --list-modules
100
+
101
+ # Using available module
102
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
71
103
  ```
72
104
 
73
- > :information_source: Check whether you also need to run the `python3 -m pipx ensurepath` command.
105
+ At windows:
74
106
 
107
+ ```bash
108
+ $env:FUSION_MODULES = "C:\extra_mods"
109
+
110
+ # List all modules
111
+ frida-fusion --list-modules
112
+
113
+ # Using available module
114
+ frida-fusion -f [app_id] -U --script-path . -m [module_name]
115
+ ```
116
+
117
+ ### Community modules
118
+
119
+ You can also use one of community developed modules
120
+
121
+ ```bash
122
+ cd /tmp/
123
+ git clone https://github.com/helviojunior/frida-fusion-community-modules
124
+ export FUSION_MODULES=/tmp/frida-fusion-community-modules
125
+
126
+ # List all modules
127
+ frida-fusion --list-modules
128
+ ```
@@ -17,7 +17,11 @@ frida_fusion.egg-info/top_level.txt
17
17
  frida_fusion/libs/__init__.py
18
18
  frida_fusion/libs/color.py
19
19
  frida_fusion/libs/database.py
20
+ frida_fusion/libs/helpers.js
20
21
  frida_fusion/libs/logger.py
21
22
  frida_fusion/modules/__init__.py
22
- frida_fusion/modules/crypto.js
23
- frida_fusion/modules/crypto.py
23
+ frida_fusion/modules/crypto/__init__.py
24
+ frida_fusion/modules/crypto/crypto.js
25
+ frida_fusion/modules/crypto/crypto.py
26
+ frida_fusion/modules/tls_unpinning/__init__.py
27
+ frida_fusion/modules/tls_unpinning/frida_multiple_unpinning.py
@@ -60,6 +60,8 @@ exclude = ["frida_fusion*tests", "frida_fusion*images", "frida_fusion*docs"]
60
60
  'resources/**/*',
61
61
  'modules/*',
62
62
  'modules/**/*',
63
+ 'libs/*',
64
+ 'libs/**/*',
63
65
  ]
64
66
 
65
67
  [project.urls]
@@ -1,116 +0,0 @@
1
- import os
2
- import pkgutil
3
- import importlib
4
- from pathlib import Path
5
-
6
- from .libs.database import Database
7
- from typing import TYPE_CHECKING
8
- if TYPE_CHECKING:
9
- from .fusion import Fusion # só no type checker
10
-
11
-
12
- class ModuleBase(object):
13
-
14
- name = ''
15
- description = ''
16
- mod_path = ''
17
-
18
- def __init__(self, name, description):
19
- self.name = name
20
- self.description = description
21
- self.mod_path = str(Path(__file__).resolve().parent)
22
- pass
23
-
24
- def start_db(self, db_path: str) -> bool:
25
- raise Exception('Method "start_db" is not yet implemented.')
26
-
27
- def js_files(self) -> list:
28
- return []
29
-
30
- def key_value_event(self,
31
- script_location: "Fusion.ScriptLocation" = None,
32
- stack_trace: str = None,
33
- module: str = None,
34
- received_data: dict = None
35
- ) -> bool:
36
- raise Exception('Method "key_value_event" is not yet implemented.')
37
-
38
- def data_event(self,
39
- script_location: "Fusion.ScriptLocation" = None,
40
- stack_trace: str = None,
41
- received_data: str = None
42
- ) -> bool:
43
- raise Exception('Method "data_event" is not yet implemented.')
44
-
45
-
46
- class Module(object):
47
- modules = {}
48
-
49
- def __init__(self, name, description, module, qualname, class_name):
50
- self.name = name
51
- self.description = description
52
- self.module = module
53
- self.qualname = qualname
54
- self._class = class_name
55
- pass
56
-
57
- def create_instance(self):
58
- return self._class()
59
-
60
- @classmethod
61
- def get_instance(cls, name: str):
62
- if len(Module.modules) == 0:
63
- Module.modules = Module.list_modules()
64
-
65
- selected_modules = [
66
- mod for mod in Module.modules
67
- if mod == name
68
- ]
69
-
70
- mod = None
71
- if len(selected_modules) == 1:
72
- mod = Module.modules[selected_modules[0]].create_instance()
73
-
74
- return mod
75
-
76
- @classmethod
77
- def get_base_module(cls) -> str:
78
- file = Path(__file__).stem
79
-
80
- parent_module = f'.{cls.__module__}.'.replace(f'.{file}.', '').strip(' .')
81
-
82
- return '.'.join((parent_module, 'modules'))
83
-
84
- @classmethod
85
- def list_modules(cls) -> dict:
86
- try:
87
-
88
- base_module = Module.get_base_module()
89
-
90
- modules = {}
91
-
92
- base_path = os.path.join(
93
- Path(__file__).resolve().parent, 'modules'
94
- )
95
-
96
- for loader, modname, ispkg in pkgutil.walk_packages([base_path]):
97
- if not ispkg:
98
- importlib.import_module(f'{base_module}.{modname}')
99
-
100
- for iclass in ModuleBase.__subclasses__():
101
- t = iclass()
102
- if t.name in modules:
103
- raise Exception(f'Duplicated Module name: {iclass.__module__}.{iclass.__qualname__}')
104
-
105
- modules[t.name.lower()] = Module(
106
- name=t.name,
107
- description=t.description,
108
- module=str(iclass.__module__),
109
- qualname=str(iclass.__qualname__),
110
- class_name=iclass
111
- )
112
-
113
- return modules
114
-
115
- except Exception as e:
116
- raise Exception('Error listing command modules', e)
File without changes
File without changes