frooky 0.1.0__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.
- frooky/__init__.py +4 -0
- frooky/__main__.py +5 -0
- frooky/android/android_decoder.js +375 -0
- frooky/android/base_script.js +602 -0
- frooky/android/native_decoder.js +161 -0
- frooky/cli.py +74 -0
- frooky/frida_runner.py +397 -0
- frooky/ios/base_script.js +668 -0
- frooky/resources.py +7 -0
- frooky-0.1.0.dist-info/METADATA +993 -0
- frooky-0.1.0.dist-info/RECORD +15 -0
- frooky-0.1.0.dist-info/WHEEL +5 -0
- frooky-0.1.0.dist-info/entry_points.txt +2 -0
- frooky-0.1.0.dist-info/licenses/LICENSE +674 -0
- frooky-0.1.0.dist-info/top_level.txt +1 -0
frooky/__init__.py
ADDED
frooky/__main__.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes a hex dump of a byte array. The dump is limited by the length parameter.
|
|
3
|
+
* @param {Uint8Array} bytes - Byte array to be decoded to hexadecimal.
|
|
4
|
+
* @param {number} length - Number of bytes which will be decoded.
|
|
5
|
+
* @returns {string} The hexadecimal decoded bytes (e.g., "0x22aa3482ef...")
|
|
6
|
+
*/
|
|
7
|
+
function byteArrayHexDump(bytes, length) {
|
|
8
|
+
let appendix = "...";
|
|
9
|
+
if (bytes.length < length) {
|
|
10
|
+
length = bytes.length;
|
|
11
|
+
appendix = "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let hexString = "0x";
|
|
15
|
+
for (let i = 0; i < length; i++) {
|
|
16
|
+
hexString =
|
|
17
|
+
hexString + ("0" + (bytes[i] & 0xff).toString(16)).slice(-2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return hexString + appendix;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a byte value to its uri-encoded representation
|
|
25
|
+
* @param {number} byte - The byte value to encode (0-255)
|
|
26
|
+
* @returns {string} The uri-encoded string (e.g., "%20", "%0A")
|
|
27
|
+
*/
|
|
28
|
+
function getUriCode(byte) {
|
|
29
|
+
const text = byte.toString(16);
|
|
30
|
+
if (byte < 16) {
|
|
31
|
+
return "%0" + text;
|
|
32
|
+
}
|
|
33
|
+
return "%" + text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Tries to decode a byte array to either a string or a hex dump depending on the content of the array.
|
|
38
|
+
* @param {Uint8Array} bytes - Byte array to be decoded to hexadecimal.
|
|
39
|
+
* @param {number} length - Number of bytes which will be decoded.
|
|
40
|
+
* @returns {string} The decoded bytes (e.g., "This is some decoded string." or "0x22aa3482ef...")
|
|
41
|
+
*/
|
|
42
|
+
function byteToString(bytes, length) {
|
|
43
|
+
if (bytes.length < length) {
|
|
44
|
+
length = bytes.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// try to decode strings
|
|
49
|
+
let result = "";
|
|
50
|
+
for (let i = 0; i < length; ++i) {
|
|
51
|
+
result += getUriCode(bytes[i]);
|
|
52
|
+
}
|
|
53
|
+
return decodeURIComponent(result).replace(/\0.*$/g, "");
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// make a hex dump in case, the byte array contains raw binary data
|
|
56
|
+
return byteArrayHexDump(bytes, length);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates a simple hash from a string.
|
|
62
|
+
* @param {string} str - String to hash.
|
|
63
|
+
* @returns {number} Hash value as a 32-bit integer.
|
|
64
|
+
*/
|
|
65
|
+
function simpleHash(str) {
|
|
66
|
+
let h = 0;
|
|
67
|
+
for (let i = 0; i < str.length; i++) {
|
|
68
|
+
h = (h << 5) - h + str.charCodeAt(i);
|
|
69
|
+
h = h | 0;
|
|
70
|
+
}
|
|
71
|
+
return h;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Decodes an RSA key (public or private) and extracts key parameters.
|
|
76
|
+
* @param {Object} value - Reference to the Java key object.
|
|
77
|
+
* @returns {Object} Object containing key parameters (modulusHex, modulusBitLength, publicExponentDec, privateExponentDec, keyHash).
|
|
78
|
+
*/
|
|
79
|
+
function decodeRSAKey(value) {
|
|
80
|
+
let out = {};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Load RSA interfaces
|
|
84
|
+
let RSAKey = Java.use('java.security.interfaces.RSAKey');
|
|
85
|
+
let RSAPub = Java.use('java.security.interfaces.RSAPublicKey');
|
|
86
|
+
let RSAPriv = Java.use('java.security.interfaces.RSAPrivateKey');
|
|
87
|
+
let RSAPrivateCrt = null;
|
|
88
|
+
try {
|
|
89
|
+
RSAPrivateCrt = Java.use('java.security.interfaces.RSAPrivateCrtKey');
|
|
90
|
+
} catch (_) {
|
|
91
|
+
RSAPrivateCrt = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Any RSA key, public or private, for modulus
|
|
95
|
+
try {
|
|
96
|
+
let anyRsa = Java.cast(value, RSAKey);
|
|
97
|
+
let modBI = anyRsa.getModulus();
|
|
98
|
+
out.modulusHex = modBI.toString(16);
|
|
99
|
+
out.modulusBitLength = modBI.bitLength();
|
|
100
|
+
} catch (_) {
|
|
101
|
+
// not an RSAKey or keystore backend hides it, ignore
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Public key exponent
|
|
105
|
+
try {
|
|
106
|
+
let vpub = Java.cast(value, RSAPub);
|
|
107
|
+
let expBI = vpub.getPublicExponent();
|
|
108
|
+
if (expBI) {
|
|
109
|
+
out.publicExponentDec = expBI.toString(10);
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {
|
|
112
|
+
// not an RSAPublicKey
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Private key exponents, may be unavailable for keystore backed keys
|
|
116
|
+
if (RSAPrivateCrt !== null) {
|
|
117
|
+
try {
|
|
118
|
+
let vprivCrt = Java.cast(value, RSAPrivateCrt);
|
|
119
|
+
let dBI = vprivCrt.getPrivateExponent();
|
|
120
|
+
let eBI = vprivCrt.getPublicExponent();
|
|
121
|
+
if (dBI) {
|
|
122
|
+
out.privateExponentDec = dBI.toString(10);
|
|
123
|
+
}
|
|
124
|
+
if (eBI) {
|
|
125
|
+
out.publicExponentDec = eBI.toString(10);
|
|
126
|
+
}
|
|
127
|
+
} catch (_) {
|
|
128
|
+
// not an RSAPrivateCrtKey
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
try {
|
|
132
|
+
let vpriv = Java.cast(value, RSAPriv);
|
|
133
|
+
let dBI2 = vpriv.getPrivateExponent();
|
|
134
|
+
if (dBI2) {
|
|
135
|
+
out.privateExponentDec = dBI2.toString(10);
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {
|
|
138
|
+
// not an RSAPrivateKey
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (_) {
|
|
142
|
+
// key interface logic failed, out remains minimal
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (out.modulusHex != null) {
|
|
146
|
+
out.keyHash = simpleHash(out.modulusHex);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Decodes a Java object according to its type.
|
|
154
|
+
* @param {string} type - Java type of the value (e.g., "java.util.Set", "java.lang.String" or "int")
|
|
155
|
+
* @param {Object} value - Reference to the object.
|
|
156
|
+
* @returns {string} The type-appropriate decoded string (e.g., "[1,50,21]", "Hello World" or "-12")
|
|
157
|
+
*/
|
|
158
|
+
function decodeValue(type, value) {
|
|
159
|
+
let readableValue = "";
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
if (value == null) {
|
|
163
|
+
readableValue = "void";
|
|
164
|
+
} else {
|
|
165
|
+
switch (type) {
|
|
166
|
+
case "java.util.Set":
|
|
167
|
+
readableValue = value.toArray().toString();
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case "java.util.Map":
|
|
171
|
+
let entrySet = value.entrySet();
|
|
172
|
+
readableValue = entrySet.toArray().toString();
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case "[B":
|
|
176
|
+
// for performance reasons only decode the first 256 bytes of the full byte array
|
|
177
|
+
readableValue = byteToString(value, 256);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "[C":
|
|
181
|
+
readableValue = "";
|
|
182
|
+
for (let i in value) {
|
|
183
|
+
readableValue = readableValue + value[i];
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case "java.io.File":
|
|
188
|
+
readableValue = value.getAbsolutePath();
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case "java.util.Date":
|
|
192
|
+
let DateFormat = Java.use('java.text.DateFormat');
|
|
193
|
+
let formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM.value, DateFormat.SHORT.value);
|
|
194
|
+
readableValue = formatter.format(value);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case "androidx.sqlite.db.SupportSQLiteQuery":
|
|
198
|
+
readableValue = value.getSql();
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case "android.content.ClipData$Item":
|
|
202
|
+
readableValue = value.getText().toString();
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case "androidx.datastore.preferences.core.Preferences$Key":
|
|
206
|
+
case "java.lang.Object":
|
|
207
|
+
case "android.net.Uri":
|
|
208
|
+
case "java.lang.CharSequence":
|
|
209
|
+
readableValue = value.toString();
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
1. No `RSAKey.class.isAssignableFrom` or `Java.isInstanceOf` for the RSA interfaces, instead each RSA interface is tried with `Java.cast` inside a `try` block. If the object does not implement that interface, the cast throws and is ignored. If it does, the cast succeeds and you can call the RSA methods.
|
|
214
|
+
2. Modulus is obtained through `RSAKey.getModulus()`, which should cover both `OpenSSLRSAPublicKey` and `AndroidKeyStoreRSAPrivateKey` as long as the backend exposes the modulus.
|
|
215
|
+
3. Exponents come from `RSAPublicKey.getPublicExponent()` and `RSAPrivateKey` or `RSAPrivateCrtKey` for the private exponent, but note that keystore backed private keys may refuse to expose the private exponent, so missing private exponent is expected in that case, while the public key parameters should still be visible.
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
case "java.security.PrivateKey":
|
|
219
|
+
case "java.security.PublicKey":
|
|
220
|
+
case "java.security.Key":
|
|
221
|
+
try {
|
|
222
|
+
readableValue = decodeRSAKey(value);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
readableValue = value;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case "[Ljava.lang.Object;":
|
|
229
|
+
let out = "";
|
|
230
|
+
for (let i in value) {
|
|
231
|
+
out = out + value[i] + ", ";
|
|
232
|
+
}
|
|
233
|
+
readableValue = out;
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case "java.util.Enumeration":
|
|
237
|
+
let elements = [];
|
|
238
|
+
while (value.hasMoreElements()) {
|
|
239
|
+
elements.push(value.nextElement().toString());
|
|
240
|
+
}
|
|
241
|
+
readableValue = JSON.stringify(elements);
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case "android.database.Cursor":
|
|
245
|
+
readableValue = decodeCursor(value);
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
readableValue = value;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.error("Value decoding exception: " + e);
|
|
255
|
+
readableValue = value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return readableValue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Decodes a `android.database.Cursor` object.
|
|
263
|
+
* @param {object} value - Reference to the object.
|
|
264
|
+
* @returns {string} The decoded rows and columns.
|
|
265
|
+
*/
|
|
266
|
+
function decodeCursor(value){
|
|
267
|
+
let out = "";
|
|
268
|
+
let cursor = value;
|
|
269
|
+
let originalCursorPosition = cursor.getPosition();
|
|
270
|
+
|
|
271
|
+
// rows
|
|
272
|
+
for (let i = 0; i < cursor.getColumnCount(); i++) {
|
|
273
|
+
let columnName = cursor.getColumnName(i);
|
|
274
|
+
out = out + columnName + " | ";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
out = out + "\n----------------------\n";
|
|
278
|
+
|
|
279
|
+
// columns
|
|
280
|
+
if (cursor.moveToFirst()) {
|
|
281
|
+
do {
|
|
282
|
+
for (let i = 0; i < cursor.getColumnCount(); i++) {
|
|
283
|
+
try {
|
|
284
|
+
let columnValue = cursor.getString(i);
|
|
285
|
+
out = out + columnValue + " | ";
|
|
286
|
+
} catch (e) {
|
|
287
|
+
out = out + " | ";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
out = out + "\n";
|
|
291
|
+
} while (cursor.moveToNext());
|
|
292
|
+
|
|
293
|
+
cursor.moveToPosition(originalCursorPosition);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Decodes Java values according to their types.
|
|
300
|
+
* @param {[string]} types - Java types of the value (e.g., ["java.util.Set", "java.lang.String", "int"])
|
|
301
|
+
* @param {[string]]} value - Reference to the objects.
|
|
302
|
+
* @returns {[string]} The type-appropriate decoded strings (e.g., ["java.util.Set":"[1,50,21]", "java.lang.String":"Hello World", "int":"-12"])
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
// Module-level cached references for performance
|
|
306
|
+
let _toStringMethod = null;
|
|
307
|
+
let _toStringMethodInitialized = false;
|
|
308
|
+
let _SystemCls = null;
|
|
309
|
+
let _SystemClsInitialized = false;
|
|
310
|
+
|
|
311
|
+
function getToStringMethod() {
|
|
312
|
+
if (!_toStringMethodInitialized) {
|
|
313
|
+
try {
|
|
314
|
+
let ObjCls = Java.use('java.lang.Object');
|
|
315
|
+
_toStringMethod = ObjCls.class.getDeclaredMethod('toString', []);
|
|
316
|
+
_toStringMethod.setAccessible(true);
|
|
317
|
+
} catch (_) {
|
|
318
|
+
_toStringMethod = null;
|
|
319
|
+
}
|
|
320
|
+
_toStringMethodInitialized = true;
|
|
321
|
+
}
|
|
322
|
+
return _toStringMethod;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getSystemCls() {
|
|
326
|
+
if (!_SystemClsInitialized) {
|
|
327
|
+
try {
|
|
328
|
+
_SystemCls = Java.use('java.lang.System');
|
|
329
|
+
} catch (_) {
|
|
330
|
+
_SystemCls = null;
|
|
331
|
+
}
|
|
332
|
+
_SystemClsInitialized = true;
|
|
333
|
+
}
|
|
334
|
+
return _SystemCls;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function decodeArguments(types, args) {
|
|
338
|
+
let parameters = [];
|
|
339
|
+
let toStringMethod = getToStringMethod();
|
|
340
|
+
let SystemCls = getSystemCls();
|
|
341
|
+
|
|
342
|
+
for (let i in types) {
|
|
343
|
+
let declaredType = types[i];
|
|
344
|
+
let argVal = args[i];
|
|
345
|
+
let entry = { declaredType: declaredType, value: decodeValue(declaredType, argVal) };
|
|
346
|
+
|
|
347
|
+
// Attach runtime info if this is a Java object
|
|
348
|
+
if (argVal && typeof argVal === 'object') {
|
|
349
|
+
let runtimeType = null;
|
|
350
|
+
try { runtimeType = argVal.$className || (argVal.getClass ? argVal.getClass().getName() : null); } catch (_) {}
|
|
351
|
+
if (runtimeType) {
|
|
352
|
+
entry.runtimeType = runtimeType;
|
|
353
|
+
if (SystemCls) {
|
|
354
|
+
try {
|
|
355
|
+
entry.instanceId = '' + SystemCls.identityHashCode(argVal);
|
|
356
|
+
} catch (_) {}
|
|
357
|
+
}
|
|
358
|
+
// Robust toString retrieval: prefer reflected method, fallback to direct call
|
|
359
|
+
try {
|
|
360
|
+
if (toStringMethod) {
|
|
361
|
+
entry.instanceToString = '' + toStringMethod.invoke(argVal, []);
|
|
362
|
+
} else {
|
|
363
|
+
entry.instanceToString = '' + argVal.toString();
|
|
364
|
+
}
|
|
365
|
+
} catch (e1) {
|
|
366
|
+
try { entry.instanceToString = '' + argVal.toString(); } catch (e2) { entry.instanceToString = '<toString-unavailable>'; }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
parameters.push(entry);
|
|
372
|
+
}
|
|
373
|
+
return parameters;
|
|
374
|
+
}
|
|
375
|
+
|