Skip to content

Commit f47b82b

Browse files
authored
Merge pull request #3556 from OWASP/copilot/support-optional-overloads
Support Optional Overload Definitions for More Precise Frida Hooking
2 parents 05ca730 + b658b29 commit f47b82b

File tree

1 file changed

+240
-14
lines changed

1 file changed

+240
-14
lines changed

utils/frida/android/base_script.js

Lines changed: 240 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ function registerHook(
9595

9696
const event = {
9797
id: generateUUID(),
98+
type: "hook",
9899
category: categoryName,
99100
time: new Date().toISOString(),
100101
class: clazz,
@@ -118,26 +119,197 @@ function registerHook(
118119
}
119120

120121
/**
121-
* Takes an array of objects usually defined in the `hooks.js` file of a DEMO and loads all classes and functions stated in there.
122-
* @param {[object]} hook - Contains a list of objects which contains all methods which will be overloaded. (e.g., [{class: "android.security.keystore.KeyGenParameterSpec$Builder", methods: [ "setBlockModes"]}])
123-
* @param {string} categoryName - OWASP MAS category for easier identification (e.g., "CRYPTO")
124-
* @param {function} callback - Callback function. The function takes the information gathered as JSON string.
122+
* Finds the overload index that matches the given argument types.
123+
* @param {Object} methodHandle - Frida method handle with overloads.
124+
* @param {string[]} argTypes - Array of argument type strings (e.g., ["android.net.Uri", "android.content.ContentValues"]).
125+
* @returns {number} The index of the matching overload, or -1 if not found.
126+
*/
127+
function findOverloadIndex(methodHandle, argTypes) {
128+
for (var i = 0; i < methodHandle.overloads.length; i++) {
129+
var overload = methodHandle.overloads[i];
130+
var parameterTypes = parseParameterTypes(overload.toString());
131+
132+
if (parameterTypes.length !== argTypes.length) {
133+
continue;
134+
}
135+
136+
var match = true;
137+
for (var j = 0; j < argTypes.length; j++) {
138+
if (parameterTypes[j] !== argTypes[j]) {
139+
match = false;
140+
break;
141+
}
142+
}
143+
144+
if (match) {
145+
return i;
146+
}
147+
}
148+
return -1;
149+
}
150+
151+
/**
152+
* Builds a normalized list of hook operations for a single hook definition.
153+
* Each operation contains clazz, method, overloadIndex, and args array (decoded parameter types).
154+
* This centralizes selection logic used for both summary emission and hook registration.
155+
*
156+
* The function supports several hook configuration scenarios:
157+
* - If both `methods` and `overloads` are specified, the configuration is considered invalid and no operations are returned.
158+
* - If a single `method` and an explicit list of `overloads` are provided, only those overloads are considered.
159+
* - If only `methods` is provided, all overloads for each method are included.
160+
* - If only `method` is provided, all overloads for that method are included.
161+
* - If neither is provided, or if the configuration is invalid, no operations are returned.
162+
*
163+
* Error handling:
164+
* - If an explicit overload is not found, it is skipped and not included in the operations.
165+
* - If an exception occurs during processing, it is logged and the function returns the operations collected so far.
166+
*
167+
* @param {object} hook - Hook definition object. Supported formats:
168+
* - { class: string, method: string }
169+
* - { class: string, methods: string[] }
170+
* - { class: string, method: string, overloads: Array<{ args: string[] }> }
171+
* @returns {{operations: Array<{clazz:string, method:string, overloadIndex:number, args:string[]}>, count:number}}
172+
*
173+
* @example
174+
* // Hook all overloads of a single method
175+
* buildHookOperations({ class: "android.net.Uri", method: "parse" });
176+
*
177+
* @example
178+
* // Hook all overloads of multiple methods
179+
* buildHookOperations({ class: "android.net.Uri", methods: ["parse", "toString"] });
180+
*
181+
* @example
182+
* // Hook specific overloads of a method
183+
* buildHookOperations({
184+
* class: "android.net.Uri",
185+
* method: "parse",
186+
* overloads: [
187+
* { args: ["java.lang.String"] },
188+
* { args: ["android.net.Uri"] }
189+
* ]
190+
* });
191+
*
192+
* @example
193+
* // Invalid configuration: both methods and overloads
194+
* buildHookOperations({
195+
* class: "android.net.Uri",
196+
* methods: ["parse"],
197+
* overloads: [{ args: ["java.lang.String"] }]
198+
* });
199+
* // Returns { operations: [], count: 0 }
125200
*/
126-
function registerAllHooks(hook, categoryName, callback) {
127-
for(const m in hook.methods){
201+
function buildHookOperations(hook) {
202+
var operations = [];
203+
var errors = [];
204+
205+
try {
206+
// Invalid configuration: methods + overloads (logged elsewhere)
207+
if (hook.methods && hook.overloads && hook.overloads.length > 0) {
208+
var errInvalid = "Invalid hook configuration for " + hook.class + ": 'overloads' is only supported with a singular 'method', not with 'methods'.";
209+
console.error(errInvalid);
210+
errors.push(errInvalid);
211+
return { operations: operations, count: 0, errors: errors, errorCount: errors.length };
212+
}
213+
214+
// Explicit overload list for single method
215+
if (hook.method && hook.overloads && hook.overloads.length > 0) {
128216
try {
129-
var toHook = Java.use(hook.class)[hook.methods[m]];
217+
var handle = Java.use(hook.class)[hook.method];
218+
for (var o = 0; o < hook.overloads.length; o++) {
219+
var def = hook.overloads[o];
220+
var argsExplicit = Array.isArray(def.args) ? def.args : [];
221+
var idx = findOverloadIndex(handle, argsExplicit);
222+
if (idx !== -1) {
223+
var params = parseParameterTypes(handle.overloads[idx].toString());
224+
operations.push({ clazz: hook.class, method: hook.method, overloadIndex: idx, args: params });
225+
} else {
226+
console.warn(
227+
"[frida-android] Warning: Overload not found for class '" +
228+
hook.class +
229+
"', method '" +
230+
hook.method +
231+
"', args [" +
232+
argsExplicit.join(", ") +
233+
"]. This hook will be skipped."
234+
);
235+
errors.push("Overload not found for " + hook.class + ":" + hook.method + " with args [" + argsExplicit.join(", ") + "]");
236+
}
237+
}
238+
} catch (e) {
239+
var errMsg = "Failed to process method '" + hook.method + "' in class '" + hook.class + "': " + e;
240+
console.warn("Warning: " + errMsg);
241+
errors.push(errMsg);
242+
}
243+
return { operations: operations, count: operations.length, errors: errors, errorCount: errors.length };
244+
}
130245

131-
var overloadCount = toHook.overloads.length;
246+
// Single method without explicit overloads: all overloads
247+
if (hook.method && (!hook.overloads || hook.overloads.length === 0)) {
248+
try {
249+
var handleAll = Java.use(hook.class)[hook.method];
250+
for (var i = 0; i < handleAll.overloads.length; i++) {
251+
var paramsAll = parseParameterTypes(handleAll.overloads[i].toString());
252+
operations.push({ clazz: hook.class, method: hook.method, overloadIndex: i, args: paramsAll });
253+
}
254+
} catch (e) {
255+
var errMsg2 = "Failed to process method '" + hook.method + "' in class '" + hook.class + "': " + e;
256+
console.warn("Warning: " + errMsg2);
257+
errors.push(errMsg2);
258+
}
259+
return { operations: operations, count: operations.length, errors: errors, errorCount: errors.length };
260+
}
132261

133-
for (var i = 0; i < overloadCount; i++) {
134-
registerHook(hook.class, hook.methods[m], i, categoryName, callback, hook.maxFrames);
262+
// Multiple methods: all overloads for each
263+
if (hook.methods) {
264+
for (var m = 0; m < hook.methods.length; m++) {
265+
var mName = hook.methods[m];
266+
try {
267+
var handleEach = Java.use(hook.class)[mName];
268+
for (var j = 0; j < handleEach.overloads.length; j++) {
269+
var paramsEach = parseParameterTypes(handleEach.overloads[j].toString());
270+
operations.push({ clazz: hook.class, method: mName, overloadIndex: j, args: paramsEach });
271+
}
272+
} catch (e) {
273+
var errMsg3 = "Failed to process method '" + mName + "' in class '" + hook.class + "': " + e;
274+
console.warn("Warning: " + errMsg3);
275+
errors.push(errMsg3);
135276
}
136-
} catch (err) {
137-
console.error(err)
138-
console.error(`Problem when overloading ${hook.class}:${hook.methods[m]}`);
139277
}
278+
return { operations: operations, count: operations.length, errors: errors, errorCount: errors.length };
279+
}
280+
} catch (e) {
281+
// Log the error to aid debugging; returning partial results
282+
var errMsg4 = "Error in buildHookOperations for hook: " + (hook && hook.class ? hook.class : "<unknown>") + ": " + e;
283+
console.error(errMsg4);
284+
errors.push(errMsg4);
285+
}
286+
287+
return { operations: operations, count: operations.length, errors: errors, errorCount: errors.length };
288+
}
289+
290+
/**
291+
* Takes an array of objects usually defined in the `hooks.js` file of a DEMO and loads all classes and functions stated in there.
292+
* @param {[object]} hook - Contains a list of objects which contains all methods which will be overloaded.
293+
* Basic format: {class: "android.security.keystore.KeyGenParameterSpec$Builder", methods: ["setBlockModes"]}
294+
* With overloads: {class: "android.content.ContentResolver", method: "insert", overloads: [{args: ["android.net.Uri", "android.content.ContentValues"]}]}
295+
* @param {string} categoryName - OWASP MAS category for easier identification (e.g., "CRYPTO")
296+
* @param {function} callback - Callback function. The function takes the information gathered as JSON string.
297+
* @param {{operations: Array<{clazz:string, method:string, overloadIndex:number, args:string[]}>, count:number}} [cachedOperations] - Optional pre-computed hook operations to avoid redundant processing.
298+
*/
299+
function registerAllHooks(hook, categoryName, callback, cachedOperations) {
300+
if (hook.methods && hook.overloads && hook.overloads.length > 0) {
301+
console.error(`Invalid hook configuration for ${hook.class}: 'overloads' is only supported with a singular 'method', not with 'methods'.`);
302+
return;
303+
}
304+
var built = cachedOperations || buildHookOperations(hook);
305+
built.operations.forEach(function (op) {
306+
try {
307+
registerHook(op.clazz, op.method, op.overloadIndex, categoryName, callback, hook.maxFrames);
308+
} catch (err) {
309+
console.error(err);
310+
console.error(`Problem when overloading ${op.clazz}:${op.method}#${op.overloadIndex}`);
140311
}
312+
});
141313
}
142314

143315
Java.perform(function () {
@@ -146,8 +318,62 @@ Java.perform(function () {
146318
console.log(JSON.stringify(event, null, 2))
147319
}
148320

321+
// Pre-compute hook operations once to avoid redundant processing
322+
var hookOperationsCache = [];
149323
target.hooks.forEach(function (hook, _) {
150-
registerAllHooks(hook, target.category, callback);
324+
hookOperationsCache.push({
325+
hook: hook,
326+
built: buildHookOperations(hook)
327+
});
328+
});
329+
330+
// Emit an initial summary of all overloads that will be hooked
331+
try {
332+
// Aggregate map nested by class then method
333+
var aggregate = {};
334+
var total = 0;
335+
var errors = [];
336+
var totalErrors = 0;
337+
hookOperationsCache.forEach(function (cached) {
338+
total += cached.built.count;
339+
if (cached.built.errors && cached.built.errors.length) {
340+
Array.prototype.push.apply(errors, cached.built.errors);
341+
totalErrors += cached.built.errors.length;
342+
}
343+
cached.built.operations.forEach(function (op) {
344+
if (!aggregate[op.clazz]) {
345+
aggregate[op.clazz] = {};
346+
}
347+
if (!aggregate[op.clazz][op.method]) {
348+
aggregate[op.clazz][op.method] = [];
349+
}
350+
aggregate[op.clazz][op.method].push(op.args);
351+
});
352+
});
353+
354+
var overloadList = [];
355+
for (var clazz in aggregate) {
356+
if (!aggregate.hasOwnProperty(clazz)) continue;
357+
var methodsMap = aggregate[clazz];
358+
for (var methodName in methodsMap) {
359+
if (!methodsMap.hasOwnProperty(methodName)) continue;
360+
var entries = methodsMap[methodName].map(function (argsArr) {
361+
return { args: argsArr };
362+
});
363+
overloadList.push({ class: clazz, method: methodName, overloads: entries });
364+
}
365+
}
366+
367+
var summary = { type: "summary", hooks: overloadList, totalHooks: total, errors: errors, totalErrors: totalErrors };
368+
console.log(JSON.stringify(summary, null, 2));
369+
} catch (e) {
370+
// If summary fails, don't block hooking
371+
console.error("Summary generation failed, but hooking will continue. Error:", e);
372+
}
373+
374+
// Register hooks using cached operations
375+
hookOperationsCache.forEach(function (cached) {
376+
registerAllHooks(cached.hook, target.category, callback, cached.built);
151377
});
152378

153379
});

0 commit comments

Comments
 (0)