mirror of
https://github.com/frida/frida-python
synced 2026-06-08 14:16:17 +00:00
9bf5f1ef89
No matter how fast we make the exports enumeration we will still have to transfer about 12 MB of data for a typical iOS app. Thus it is a better idea to move the processing to the target app, as it'll take a lot less time even if there might be limited processing power.
1369 lines
42 KiB
Python
1369 lines
42 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import binascii
|
|
import os
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
from frida.core import Module, ModuleFunction, ObjCMethod
|
|
|
|
|
|
class TracerProfileBuilder(object):
|
|
_RE_REL_ADDRESS = re.compile("(?P<module>[^\s!]+)!(?P<offset>(0x)?[0-9a-fA-F]+)")
|
|
|
|
def __init__(self):
|
|
self._spec = []
|
|
|
|
def include_modules(self, *module_name_globs):
|
|
for m in module_name_globs:
|
|
self._spec.append(('include', 'module', m))
|
|
return self
|
|
|
|
def exclude_modules(self, *module_name_globs):
|
|
for m in module_name_globs:
|
|
self._spec.append(('exclude', 'module', m))
|
|
return self
|
|
|
|
def include(self, *function_name_globs):
|
|
for f in function_name_globs:
|
|
self._spec.append(('include', 'function', f))
|
|
return self
|
|
|
|
def exclude(self, *function_name_globs):
|
|
for f in function_name_globs:
|
|
self._spec.append(('exclude', 'function', f))
|
|
return self
|
|
|
|
def include_relative_address(self, *address_rel_offsets):
|
|
for f in address_rel_offsets:
|
|
m = TracerProfileBuilder._RE_REL_ADDRESS.search(f)
|
|
if m is None:
|
|
continue
|
|
self._spec.append(('include', 'relative_function', {
|
|
'module': m.group('module'),
|
|
'offset': int(m.group('offset'), base=16)
|
|
}))
|
|
|
|
def include_objc_method(self, *function_name_globs):
|
|
for f in function_name_globs:
|
|
match = re.search(r"([+-])\[(\S+)\s+(\S+)\]", f)
|
|
if not match:
|
|
raise Exception("Format: -[Class foo:bar:] or +[Class baz]")
|
|
mtype, cls, name = match.groups()
|
|
self._spec.append(('include', 'objc_method', {
|
|
'type': mtype,
|
|
'cls': cls,
|
|
'name': name
|
|
}))
|
|
|
|
def build(self):
|
|
return TracerProfile(self._spec)
|
|
|
|
class TracerProfile(object):
|
|
def __init__(self, spec):
|
|
self._spec = spec
|
|
|
|
def resolve(self, session):
|
|
script = session.create_script(name="profile-resolver", source=self._create_resolver_script())
|
|
result = [None, None]
|
|
completed = threading.Event()
|
|
def on_message(message, data):
|
|
assert message['type'] == 'send'
|
|
stanza = message['payload']
|
|
if stanza['name'] == '+result':
|
|
result[0] = stanza['payload']
|
|
else:
|
|
result[1] = Exception(stanza['payload'])
|
|
completed.set()
|
|
script.on('message', on_message)
|
|
script.load()
|
|
script.post_message(self._spec)
|
|
completed.wait()
|
|
if result[1] is not None:
|
|
raise result[1]
|
|
|
|
data = result[0]
|
|
|
|
modules = {}
|
|
for module_id, m in data['modules'].iteritems():
|
|
module = Module(m['name'], int(m['base'], 16), m['size'], m['path'], session)
|
|
modules[int(module_id)] = module
|
|
|
|
working_set = []
|
|
for target in data['targets']:
|
|
module_id = target.get('module')
|
|
if module_id is not None:
|
|
module = modules[module_id]
|
|
relative_address = int(target["address"], 16) - module.base_address
|
|
exported = not target.get('private', False)
|
|
mf = ModuleFunction(module, target["name"], relative_address, exported)
|
|
working_set.append(mf)
|
|
else:
|
|
objc = target['objc']
|
|
method = objc['method']
|
|
of = ObjCMethod(method['type'], objc['className'], method['name'], int(target['address'], 16))
|
|
working_set.append(of)
|
|
return working_set
|
|
|
|
def _create_resolver_script(self):
|
|
return """\
|
|
"use strict";
|
|
|
|
recv(function (spec) {
|
|
try {
|
|
send({
|
|
name: '+result',
|
|
payload: resolve(spec)
|
|
});
|
|
} catch (e) {
|
|
send({
|
|
name: '+error',
|
|
payload: e.stack
|
|
});
|
|
}
|
|
});
|
|
|
|
function resolve(spec) {
|
|
const workingSet = spec.reduce(function (workingSet, item) {
|
|
const operation = item[0];
|
|
const scope = item[1];
|
|
const param = item[2];
|
|
switch (scope) {
|
|
case 'module':
|
|
if (operation === 'include')
|
|
workingSet = includeModule(param, workingSet);
|
|
else if (operation === 'exclude')
|
|
workingSet = excludeModule(param, workingSet);
|
|
break;
|
|
case 'function':
|
|
if (operation === 'include')
|
|
workingSet = includeFunction(param, workingSet);
|
|
else if (operation === 'exclude')
|
|
workingSet = excludeFunction(param, workingSet);
|
|
break;
|
|
case 'relative_function':
|
|
if (operation === 'include')
|
|
workingSet = includeRelativeFunction(param, workingSet);
|
|
break;
|
|
case 'objc_method':
|
|
if (operation === 'include')
|
|
workingSet = includeObjCMethod(param, workingSet);
|
|
break;
|
|
}
|
|
return workingSet;
|
|
}, {});
|
|
|
|
const modules = {};
|
|
const targets = [];
|
|
for (let address in workingSet) {
|
|
if (workingSet.hasOwnProperty(address)) {
|
|
const target = workingSet[address];
|
|
const moduleId = target.module;
|
|
if (moduleId !== undefined && !modules.hasOwnProperty(moduleId)) {
|
|
const m = allModules()[moduleId];
|
|
delete m._cachedFunctionExports;
|
|
modules[moduleId] = m;
|
|
}
|
|
targets.push(target);
|
|
}
|
|
}
|
|
return {
|
|
modules: modules,
|
|
targets: targets
|
|
};
|
|
}
|
|
|
|
function includeModule(pattern, workingSet) {
|
|
const mm = new Minimatch(pattern);
|
|
const modules = allModules();
|
|
for (let moduleIndex = 0; moduleIndex !== modules.length; moduleIndex++) {
|
|
const module = modules[moduleIndex];
|
|
if (mm.match(module.name)) {
|
|
const functions = allFunctionExports(module);
|
|
for (let functionIndex = 0; functionIndex !== functions.length; functionIndex++) {
|
|
const func = functions[functionIndex];
|
|
workingSet[func.address.toString()] = func;
|
|
}
|
|
}
|
|
}
|
|
return workingSet;
|
|
}
|
|
|
|
function excludeModule(pattern, workingSet) {
|
|
const mm = new Minimatch(pattern);
|
|
const modules = allModules();
|
|
for (let address in workingSet) {
|
|
if (workingSet.hasOwnProperty(address)) {
|
|
const target = workingSet[address];
|
|
const moduleId = target.module;
|
|
if (moduleId !== undefined) {
|
|
const module = modules[moduleId];
|
|
if (mm.match(module.name))
|
|
delete workingSet[address];
|
|
}
|
|
}
|
|
}
|
|
return workingSet;
|
|
}
|
|
|
|
function includeFunction(pattern, workingSet) {
|
|
const mm = new Minimatch(pattern);
|
|
const modules = allModules();
|
|
for (let moduleIndex = 0; moduleIndex !== modules.length; moduleIndex++) {
|
|
const functions = allFunctionExports(modules[moduleIndex]);
|
|
for (let functionIndex = 0; functionIndex !== functions.length; functionIndex++) {
|
|
const func = functions[functionIndex];
|
|
if (mm.match(func.name))
|
|
workingSet[func.address.toString()] = func;
|
|
}
|
|
}
|
|
return workingSet;
|
|
}
|
|
|
|
function excludeFunction(pattern, workingSet) {
|
|
const mm = new Minimatch(pattern);
|
|
for (let address in workingSet) {
|
|
if (workingSet.hasOwnProperty(address)) {
|
|
const target = workingSet[address];
|
|
if (mm.match(target.name))
|
|
delete workingSet[address];
|
|
}
|
|
}
|
|
return workingSet;
|
|
}
|
|
|
|
function includeRelativeFunction(func, workingSet) {
|
|
const relativeToModule = func.module;
|
|
const modules = allModules();
|
|
for (let moduleIndex = 0; moduleIndex !== modules.length; moduleIndex++) {
|
|
const module = modules[moduleIndex];
|
|
if (module.path === relativeToModule || module.name === relativeToModule) {
|
|
const relativeAddress = ptr(func.offset);
|
|
const absoluteAddress = module.base.add(relativeAddress);
|
|
workingSet[absoluteAddress] = {
|
|
name: "sub_" + relativeAddress.toString(16),
|
|
address: absoluteAddress,
|
|
module: moduleIndex,
|
|
private: true
|
|
};
|
|
}
|
|
}
|
|
return workingSet;
|
|
}
|
|
|
|
let cachedObjCState = null;
|
|
function includeObjCMethod(method, workingSet) {
|
|
if (!ObjC.available)
|
|
throw new Error("Objective C runtime is not available");
|
|
|
|
if (cachedObjCState === null)
|
|
cachedObjCState = getObjCState();
|
|
const api = cachedObjCState.api;
|
|
const classInfo = cachedObjCState.classInfo;
|
|
|
|
const type = method.type;
|
|
const cls = method.cls;
|
|
const sel = api.sel_registerName(Memory.allocUtf8String(method.name));
|
|
|
|
function addImps(clsPtr, superImpPtr) {
|
|
const info = classInfo[clsPtr];
|
|
const name = info.name;
|
|
|
|
let clsHandle = ptr(clsPtr);
|
|
if (type === '+') {
|
|
clsHandle = api.object_getClass(clsHandle);
|
|
}
|
|
|
|
const imp = api.class_getMethodImplementation(clsHandle, sel);
|
|
const impPtr = imp.toString();
|
|
if (impPtr !== superImpPtr) {
|
|
workingSet[impPtr] = {
|
|
objc: {
|
|
className: name,
|
|
method: {
|
|
type: type,
|
|
name: method.name
|
|
}
|
|
},
|
|
address: impPtr
|
|
};
|
|
}
|
|
|
|
const subclasses = info.subclasses;
|
|
for (let i = 0; i !== subclasses.length; i++)
|
|
addImps(subclasses[i], impPtr);
|
|
}
|
|
|
|
for (let i = 0; i !== ObjC.classes.length; i++) {
|
|
const name = ObjC.classes[i];
|
|
if (name === cls) {
|
|
addImps(ObjC.use(name).classHandle.toString(), "");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return workingSet;
|
|
}
|
|
|
|
function getObjCState() {
|
|
const api = {};
|
|
const apiSpec = {
|
|
sel_registerName: 1,
|
|
class_getSuperclass: 1,
|
|
class_getMethodImplementation: 2,
|
|
object_getClass: 1
|
|
};
|
|
Object.keys(apiSpec).forEach(function (name) {
|
|
const funPtr = Module.findExportByName("libobjc.A.dylib", name);
|
|
const argCount = apiSpec[name];
|
|
const argTypes = [];
|
|
for (let i = 0; i !== argCount; i++)
|
|
argTypes.push('pointer');
|
|
api[name] = new NativeFunction(funPtr, 'pointer', argTypes);
|
|
});
|
|
|
|
const classInfo = {};
|
|
const allClasses = ObjC.classes;
|
|
let name, clsPtr;
|
|
for (let i = 0; i !== allClasses.length; i++) {
|
|
name = allClasses[i];
|
|
clsPtr = ObjC.use(name).classHandle.toString();
|
|
classInfo[clsPtr] = {
|
|
name: name,
|
|
subclasses: []
|
|
};
|
|
}
|
|
for (let i = 0; i !== allClasses.length; i++) {
|
|
name = allClasses[i];
|
|
const clsHandle = ObjC.use(name).classHandle;
|
|
clsPtr = clsHandle.toString();
|
|
const superCls = api.class_getSuperclass(clsHandle);
|
|
if (!superCls.isNull()) {
|
|
const superClsPtr = superCls.toString();
|
|
classInfo[superClsPtr].subclasses.push(clsPtr);
|
|
}
|
|
}
|
|
|
|
return {
|
|
api: api,
|
|
classInfo: classInfo
|
|
};
|
|
}
|
|
|
|
let cachedModules = null;
|
|
function allModules() {
|
|
if (cachedModules === null)
|
|
cachedModules = Process.enumerateModulesSync();
|
|
return cachedModules;
|
|
}
|
|
|
|
function allFunctionExports(module) {
|
|
if (!module.hasOwnProperty('_cachedFunctionExports')) {
|
|
const moduleId = allModules().indexOf(module);
|
|
module._cachedFunctionExports = Module.enumerateExportsSync(module.path)
|
|
.filter(isFunctionExport)
|
|
.map(function (e) {
|
|
e.module = moduleId;
|
|
return e;
|
|
});
|
|
}
|
|
|
|
return module._cachedFunctionExports;
|
|
}
|
|
|
|
function isFunctionExport(e) {
|
|
return e.type === 'function';
|
|
}
|
|
|
|
|
|
// MINIMATCH BEGIN
|
|
//
|
|
// Copyright 2009, 2010, 2011 Isaac Z. Schlueter.
|
|
// All rights reserved.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person
|
|
// obtaining a copy of this software and associated documentation
|
|
// files (the "Software"), to deal in the Software without
|
|
// restriction, including without limitation the rights to use,
|
|
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following
|
|
// conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
// OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
const GLOBSTAR = Minimatch.GLOBSTAR = {};
|
|
const qmark = '[^/]';
|
|
const star = qmark + '*?';
|
|
const twoStarDot = '(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?';
|
|
const twoStarNoDot = '(?:(?!(?:\\\/|^)\\.).)*?';
|
|
const reSpecials = charSet('().*{}+?[]^$\\!');
|
|
|
|
function charSet(s) {
|
|
return s.split("").reduce(function (set, c) {
|
|
set[c] = true;
|
|
return set;
|
|
}, {});
|
|
}
|
|
|
|
const slashSplit = /\/+/;
|
|
|
|
function ext(a, b) {
|
|
a = a || {};
|
|
b = b || {};
|
|
const t = {};
|
|
Object.keys(b).forEach(function (k) {
|
|
t[k] = b[k];
|
|
});
|
|
Object.keys(a).forEach(function (k) {
|
|
t[k] = a[k];
|
|
});
|
|
return t;
|
|
}
|
|
|
|
function Minimatch(pattern, options) {
|
|
if (!options)
|
|
options = {};
|
|
pattern = pattern.trim();
|
|
|
|
this.options = options;
|
|
this.set = [];
|
|
this.pattern = pattern;
|
|
this.regexp = null;
|
|
this.negate = false;
|
|
this.comment = false;
|
|
this.empty = false;
|
|
|
|
this.make();
|
|
}
|
|
|
|
Minimatch.prototype.make = function () {
|
|
if (this._made)
|
|
return;
|
|
|
|
const pattern = this.pattern;
|
|
const options = this.options;
|
|
|
|
if (!options.nocomment && pattern.charAt(0) === '#') {
|
|
this.comment = true;
|
|
return;
|
|
}
|
|
if (!pattern) {
|
|
this.empty = true;
|
|
return;
|
|
}
|
|
|
|
this.parseNegate();
|
|
|
|
let set = this.globSet = [this.pattern];
|
|
|
|
set = this.globParts = set.map(function (s) {
|
|
return s.split(slashSplit);
|
|
});
|
|
|
|
set = set.map(function (s, si, set) {
|
|
return s.map(this.parse, this);
|
|
}, this);
|
|
|
|
set = set.filter(function (s) {
|
|
return s.indexOf(false) === -1;
|
|
});
|
|
|
|
this.set = set;
|
|
};
|
|
|
|
Minimatch.prototype.parseNegate = function () {
|
|
const pattern = this.pattern;
|
|
let negate = false;
|
|
const options = this.options;
|
|
let negateOffset = 0;
|
|
|
|
if (options.nonegate)
|
|
return;
|
|
|
|
for (let i = 0, l = pattern.length; i < l && pattern.charAt(i) === '!'; i++) {
|
|
negate = !negate;
|
|
negateOffset++;
|
|
}
|
|
|
|
if (negateOffset)
|
|
this.pattern = pattern.substr(negateOffset);
|
|
this.negate = negate;
|
|
};
|
|
|
|
const SUBPARSE = {};
|
|
Minimatch.prototype.parse = function (pattern, isSub) {
|
|
const options = this.options;
|
|
|
|
if (!options.noglobstar && pattern === '**')
|
|
return GLOBSTAR;
|
|
if (pattern === '')
|
|
return '';
|
|
|
|
let re = '';
|
|
let hasMagic = !!options.nocase;
|
|
let escaping = false;
|
|
const patternListStack = [];
|
|
let stateChar;
|
|
let inClass = false;
|
|
let reClassStart = -1;
|
|
let classStart = -1;
|
|
let plType, cs;
|
|
const patternStart = pattern.charAt(0) === '.' ? '' : options.dot ? '(?!(?:^|\\\/)\\.{1,2}(?:$|\\\/))' : '(?!\\.)';
|
|
|
|
function clearStateChar () {
|
|
if (stateChar) {
|
|
switch (stateChar) {
|
|
case '*':
|
|
re += star;
|
|
hasMagic = true;
|
|
break;
|
|
case '?':
|
|
re += qmark;
|
|
hasMagic = true;
|
|
break;
|
|
default:
|
|
re += '\\' + stateChar;
|
|
break;
|
|
}
|
|
stateChar = false;
|
|
}
|
|
}
|
|
|
|
for (let i = 0, len = pattern.length, c; (i < len) && (c = pattern.charAt(i)); i++) {
|
|
if (escaping && reSpecials[c]) {
|
|
re += '\\' + c;
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
switch (c) {
|
|
case '/':
|
|
return false;
|
|
|
|
case '\\':
|
|
clearStateChar();
|
|
escaping = true;
|
|
continue;
|
|
|
|
case '?':
|
|
case '*':
|
|
case '+':
|
|
case '@':
|
|
case '!':
|
|
if (inClass) {
|
|
if (c === '!' && i === classStart + 1)
|
|
c = '^';
|
|
re += c;
|
|
continue;
|
|
}
|
|
|
|
clearStateChar();
|
|
stateChar = c;
|
|
|
|
if (options.noext)
|
|
clearStateChar();
|
|
|
|
continue;
|
|
|
|
case '(': {
|
|
if (inClass) {
|
|
re += '(';
|
|
continue;
|
|
}
|
|
|
|
if (!stateChar) {
|
|
re += '\\(';
|
|
continue;
|
|
}
|
|
|
|
plType = stateChar;
|
|
patternListStack.push({ type: plType, start: i - 1, reStart: re.length });
|
|
re += stateChar === '!' ? '(?:(?!' : '(?:';
|
|
stateChar = false;
|
|
|
|
continue;
|
|
}
|
|
case ')': {
|
|
if (inClass || !patternListStack.length) {
|
|
re += '\\)';
|
|
continue;
|
|
}
|
|
|
|
clearStateChar();
|
|
hasMagic = true;
|
|
re += ')';
|
|
plType = patternListStack.pop().type;
|
|
switch (plType) {
|
|
case '!':
|
|
re += '[^/]*?)';
|
|
break;
|
|
case '?':
|
|
case '+':
|
|
case '*':
|
|
re += plType;
|
|
break;
|
|
case '@':
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
case '|':
|
|
if (inClass || !patternListStack.length || escaping) {
|
|
re += '\\|';
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
clearStateChar();
|
|
re += '|';
|
|
|
|
continue;
|
|
|
|
case '[':
|
|
clearStateChar();
|
|
|
|
if (inClass) {
|
|
re += '\\' + c;
|
|
continue;
|
|
}
|
|
|
|
inClass = true;
|
|
classStart = i;
|
|
reClassStart = re.length;
|
|
re += c;
|
|
|
|
continue;
|
|
|
|
case ']':
|
|
if (i === classStart + 1 || !inClass) {
|
|
re += '\\' + c;
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
if (inClass) {
|
|
cs = pattern.substring(classStart + 1, i);
|
|
try {
|
|
new RegExp('[' + cs + ']');
|
|
} catch (er) {
|
|
const sp = this.parse(cs, SUBPARSE);
|
|
re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]';
|
|
hasMagic = hasMagic || sp[1];
|
|
inClass = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
hasMagic = true;
|
|
inClass = false;
|
|
re += c;
|
|
|
|
continue;
|
|
|
|
default:
|
|
clearStateChar();
|
|
|
|
if (escaping)
|
|
escaping = false;
|
|
else if (reSpecials[c] && !(c === '^' && inClass))
|
|
re += '\\';
|
|
|
|
re += c;
|
|
}
|
|
}
|
|
|
|
if (inClass) {
|
|
cs = pattern.substr(classStart + 1);
|
|
const sp = this.parse(cs, SUBPARSE);
|
|
re = re.substr(0, reClassStart) + '\\[' + sp[0];
|
|
hasMagic = hasMagic || sp[1];
|
|
}
|
|
|
|
for (let pl = patternListStack.pop(); pl; pl = patternListStack.pop()) {
|
|
let tail = re.slice(pl.reStart + 3);
|
|
tail = tail.replace(/((?:\\{2})*)(\\?)\|/g, function (_, $1, $2) {
|
|
if (!$2)
|
|
$2 = '\\';
|
|
|
|
return $1 + $1 + $2 + '|';
|
|
});
|
|
|
|
const t = pl.type === '*' ? star : pl.type === '?' ? qmark : '\\' + pl.type;
|
|
hasMagic = true;
|
|
re = re.slice(0, pl.reStart) + t + '\\(' + tail;
|
|
}
|
|
|
|
clearStateChar();
|
|
if (escaping)
|
|
re += '\\\\';
|
|
|
|
let addPatternStart = false;
|
|
switch (re.charAt(0)) {
|
|
case '.':
|
|
case '[':
|
|
case '(':
|
|
addPatternStart = true;
|
|
}
|
|
|
|
if (re !== '' && hasMagic)
|
|
re = '(?=.)' + re;
|
|
|
|
if (addPatternStart)
|
|
re = patternStart + re;
|
|
|
|
if (isSub === SUBPARSE)
|
|
return [re, hasMagic];
|
|
|
|
if (!hasMagic)
|
|
return globUnescape(pattern);
|
|
|
|
const flags = options.nocase ? 'i' : '';
|
|
const regExp = new RegExp('^' + re + '$', flags);
|
|
|
|
regExp._glob = pattern;
|
|
regExp._src = re;
|
|
|
|
return regExp;
|
|
};
|
|
|
|
Minimatch.prototype.makeRe = function () {
|
|
if (this.regexp || this.regexp === false)
|
|
return this.regexp;
|
|
|
|
const set = this.set;
|
|
|
|
if (!set.length) {
|
|
this.regexp = false;
|
|
return this.regexp;
|
|
}
|
|
const options = this.options;
|
|
|
|
const twoStar = options.noglobstar ? star : options.dot ? twoStarDot : twoStarNoDot;
|
|
const flags = options.nocase ? 'i' : '';
|
|
|
|
let re = set.map(function (pattern) {
|
|
return pattern.map(function (p) {
|
|
return (p === GLOBSTAR) ? twoStar : (typeof p === 'string') ? regExpEscape(p) : p._src;
|
|
}).join('\\\/');
|
|
}).join('|');
|
|
|
|
re = '^(?:' + re + ')$';
|
|
|
|
if (this.negate)
|
|
re = '^(?!' + re + ').*$';
|
|
|
|
try {
|
|
this.regexp = new RegExp(re, flags);
|
|
} catch (ex) {
|
|
this.regexp = false;
|
|
}
|
|
return this.regexp;
|
|
};
|
|
|
|
Minimatch.prototype.match = function (f, partial) {
|
|
if (this.comment)
|
|
return false;
|
|
if (this.empty)
|
|
return f === '';
|
|
|
|
if (f === '/' && partial)
|
|
return true;
|
|
|
|
const options = this.options;
|
|
|
|
f = f.split(slashSplit);
|
|
|
|
const set = this.set;
|
|
|
|
let filename;
|
|
let i;
|
|
for (i = f.length - 1; i >= 0; i--) {
|
|
filename = f[i];
|
|
if (filename)
|
|
break;
|
|
}
|
|
|
|
for (i = 0; i < set.length; i++) {
|
|
const pattern = set[i];
|
|
const file = (options.matchBase && pattern.length === 1) ? [filename] : f;
|
|
const hit = this.matchOne(file, pattern, partial);
|
|
if (hit) {
|
|
if (options.flipNegate)
|
|
return true;
|
|
return !this.negate;
|
|
}
|
|
}
|
|
|
|
if (options.flipNegate)
|
|
return false;
|
|
return this.negate;
|
|
};
|
|
|
|
Minimatch.prototype.matchOne = function (file, pattern, partial) {
|
|
const options = this.options;
|
|
|
|
let fi, pi, fl, pl;
|
|
for (fi = 0, pi = 0, fl = file.length, pl = pattern.length; (fi < fl) && (pi < pl); fi++, pi++) {
|
|
const p = pattern[pi];
|
|
const f = file[fi];
|
|
|
|
if (p === false)
|
|
return false;
|
|
|
|
if (p === GLOBSTAR) {
|
|
let fr = fi;
|
|
const pr = pi + 1;
|
|
|
|
if (pr === pl) {
|
|
for (; fi < fl; fi++) {
|
|
if (file[fi] === '.' || file[fi] === '..' || (!options.dot && file[fi].charAt(0) === '.'))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
while (fr < fl) {
|
|
const swallowee = file[fr];
|
|
|
|
if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
|
|
return true;
|
|
} else {
|
|
if (swallowee === '.' || swallowee === '..' || (!options.dot && swallowee.charAt(0) === '.'))
|
|
break;
|
|
fr++;
|
|
}
|
|
}
|
|
|
|
if (partial && fr === fl)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
let hit;
|
|
if (typeof p === 'string') {
|
|
if (options.nocase)
|
|
hit = f.toLowerCase() === p.toLowerCase();
|
|
else
|
|
hit = f === p;
|
|
} else {
|
|
hit = f.match(p);
|
|
}
|
|
|
|
if (!hit)
|
|
return false;
|
|
}
|
|
|
|
if (fi === fl && pi === pl) {
|
|
return true;
|
|
} else if (fi === fl) {
|
|
return partial;
|
|
} else if (pi === pl) {
|
|
const emptyFileEnd = (fi === fl - 1) && (file[fi] === '');
|
|
return emptyFileEnd;
|
|
}
|
|
|
|
throw new Error('wtf?');
|
|
};
|
|
|
|
function globUnescape(s) {
|
|
return s.replace(/\\(.)/g, '$1');
|
|
}
|
|
|
|
function regExpEscape(s) {
|
|
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
}
|
|
// MINIMATCH END
|
|
"""
|
|
|
|
class Tracer(object):
|
|
def __init__(self, reactor, repository, profile):
|
|
self._reactor = reactor
|
|
self._repository = repository
|
|
self._profile = profile
|
|
self._script = None
|
|
|
|
def start_trace(self, session, ui):
|
|
def on_create(*args):
|
|
ui.on_trace_handler_create(*args)
|
|
self._repository.on_create(on_create)
|
|
|
|
def on_load(*args):
|
|
ui.on_trace_handler_load(*args)
|
|
self._repository.on_load(on_load)
|
|
|
|
def on_update(function, handler, source):
|
|
self._script.post_message({
|
|
'to': "/targets",
|
|
'name': '+update',
|
|
'payload': {
|
|
'items': [{
|
|
'absolute_address': hex(function.absolute_address),
|
|
'handler': handler
|
|
}]
|
|
}
|
|
})
|
|
self._repository.on_update(on_update)
|
|
|
|
def on_message(message, data):
|
|
self._reactor.schedule(lambda: self._process_message(message, data, ui))
|
|
|
|
ui.on_trace_progress('resolve')
|
|
working_set = self._profile.resolve(session)
|
|
ui.on_trace_progress('instrument')
|
|
self._script = session.create_script(name="tracer", source=self._create_trace_script())
|
|
self._script.on('message', on_message)
|
|
self._script.load()
|
|
for chunk in [working_set[i:i+1000] for i in range(0, len(working_set), 1000)]:
|
|
targets = [{
|
|
'absolute_address': hex(function.absolute_address),
|
|
'handler': self._repository.ensure_handler(function)
|
|
} for function in chunk]
|
|
self._script.post_message({
|
|
'to': "/targets",
|
|
'name': '+add',
|
|
'payload': {
|
|
'items': targets
|
|
}
|
|
})
|
|
|
|
self._script.post_message({
|
|
'to': "/targets",
|
|
'name': '+start',
|
|
'payload': {}
|
|
})
|
|
|
|
return working_set
|
|
|
|
def stop(self):
|
|
if self._script is not None:
|
|
try:
|
|
self._script.unload()
|
|
except:
|
|
pass
|
|
self._script = None
|
|
|
|
def _create_trace_script(self):
|
|
return """\
|
|
var started = new Date();
|
|
var pending = [];
|
|
var timer = null;
|
|
var handlers = {};
|
|
var state = {};
|
|
function onStanza(stanza) {
|
|
if (stanza.to === "/targets") {
|
|
if (stanza.name === '+add') {
|
|
add(stanza.payload.items);
|
|
} else if (stanza.name === '+update') {
|
|
update(stanza.payload.items);
|
|
} else if (stanza.name === '+start') {
|
|
start();
|
|
}
|
|
}
|
|
|
|
recv(onStanza);
|
|
}
|
|
function add(targets) {
|
|
targets.forEach(function (target) {
|
|
var targetAddress = target.absolute_address;
|
|
eval("var handler = " + target.handler);
|
|
target = null;
|
|
|
|
var h = [handler];
|
|
handlers[targetAddress] = h;
|
|
function log(message) {
|
|
send({
|
|
from: "/events",
|
|
name: '+add',
|
|
payload: {
|
|
items: [[new Date().getTime() - started.getTime(), targetAddress, message]]
|
|
}
|
|
});
|
|
}
|
|
|
|
pending.push(function attachToTarget() {
|
|
Interceptor.attach(ptr(targetAddress), {
|
|
onEnter: function onEnter(args) {
|
|
h[0].onEnter.call(this, log, args, state);
|
|
},
|
|
onLeave: function onLeave(retval) {
|
|
h[0].onLeave.call(this, log, retval, state);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
scheduleNext();
|
|
}
|
|
function update(targets) {
|
|
targets.forEach(function (target) {
|
|
eval("var handler = " + target.handler);
|
|
handlers[target.absolute_address][0] = handler;
|
|
});
|
|
}
|
|
function start() {
|
|
pending.push(function acknowledgeStart() {
|
|
send({
|
|
from: "/targets",
|
|
name: '+started',
|
|
payload: {}
|
|
});
|
|
});
|
|
scheduleNext();
|
|
}
|
|
function scheduleNext() {
|
|
if (timer === null) {
|
|
timer = setTimeout(processNext, 0);
|
|
}
|
|
}
|
|
function processNext() {
|
|
timer = null;
|
|
|
|
if (pending.length > 0) {
|
|
var work = pending.shift();
|
|
work();
|
|
scheduleNext();
|
|
}
|
|
}
|
|
recv(onStanza);
|
|
"""
|
|
|
|
def _process_message(self, message, data, ui):
|
|
handled = False
|
|
if message['type'] == 'send':
|
|
stanza = message['payload']
|
|
if stanza['from'] == "/events" and stanza['name'] == '+add':
|
|
events = [(timestamp, int(target_address.rstrip("L"), 16), message) for timestamp, target_address, message in stanza['payload']['items']]
|
|
|
|
ui.on_trace_events(events)
|
|
|
|
target_addresses = set([target_address for timestamp, target_address, message in events])
|
|
for target_address in target_addresses:
|
|
self._repository.sync_handler(target_address)
|
|
|
|
handled = True
|
|
elif stanza['from'] == "/targets" and stanza['name'] == '+started':
|
|
ui.on_trace_progress('ready')
|
|
handled = True
|
|
if not handled:
|
|
print(message)
|
|
|
|
class Repository(object):
|
|
def __init__(self):
|
|
self._on_create_callback = None
|
|
self._on_load_callback = None
|
|
self._on_update_callback = None
|
|
|
|
def ensure_handler(self, function):
|
|
raise NotImplementedError("not implemented")
|
|
|
|
def sync_handler(self, function_address):
|
|
pass
|
|
|
|
def on_create(self, callback):
|
|
self._on_create_callback = callback
|
|
|
|
def on_load(self, callback):
|
|
self._on_load_callback = callback
|
|
|
|
def on_update(self, callback):
|
|
self._on_update_callback = callback
|
|
|
|
def _notify_create(self, function, handler, source):
|
|
if self._on_create_callback is not None:
|
|
self._on_create_callback(function, handler, source)
|
|
|
|
def _notify_load(self, function, handler, source):
|
|
if self._on_load_callback is not None:
|
|
self._on_load_callback(function, handler, source)
|
|
|
|
def _notify_update(self, function, handler, source):
|
|
if self._on_update_callback is not None:
|
|
self._on_update_callback(function, handler, source)
|
|
|
|
def _create_stub_handler(self, function):
|
|
if isinstance(function, ObjCMethod):
|
|
display_name = function.display_name()
|
|
_nonlocal_i = {'val': 2}
|
|
def objc_arg(m):
|
|
r = ':" + args[%d] + " ' % _nonlocal_i['val']
|
|
_nonlocal_i['val'] += 1
|
|
return r
|
|
|
|
log_str = '"' + re.sub(r':', objc_arg, display_name) + '"'
|
|
else:
|
|
display_name = function.name
|
|
|
|
args = ""
|
|
argc = 0
|
|
varargs = False
|
|
try:
|
|
with open(os.devnull, 'w') as devnull:
|
|
man_argv = ["man"]
|
|
if platform.system() != "Darwin":
|
|
man_argv.extend(["-E", "UTF-8"])
|
|
man_argv.extend(["-P", "col -b", "2", function.name])
|
|
output = subprocess.check_output(man_argv, stderr=devnull)
|
|
match = re.search(r"^SYNOPSIS(?:.|\n)*?((?:^.+$\n)* {5}" + function.name + r"\(.*\n(^.+$\n)*)(?:.|\n)*^DESCRIPTION", output.decode('UTF-8', errors='replace'), re.MULTILINE)
|
|
if match:
|
|
decl = match.group(1)
|
|
for argm in re.finditer(r"([^* ]*)\s*(,|\))", decl):
|
|
arg = argm.group(1)
|
|
if arg == 'void':
|
|
continue
|
|
if arg == '...':
|
|
args += '+ ", ..."'
|
|
varargs = True
|
|
continue
|
|
|
|
args += '%(pre)s%(arg)s=" + args[%(argc)s]' % {"arg": arg, "argc": argc, "pre": '"' if argc == 0 else '+ ", '}
|
|
argc += 1
|
|
except (subprocess.CalledProcessError, OSError): # WindowsError or FileNotFoundError
|
|
pass
|
|
except AttributeError: # Python 2.6 support; which probably won't stay around for that much longer
|
|
pass
|
|
if args == "":
|
|
args = '""'
|
|
|
|
log_str = '"%(name)s(" + %(args)s + ")"' % { "name": function.name, "args": args }
|
|
|
|
return """\
|
|
/*
|
|
* Auto-generated by Frida. Please modify to match the signature of %(display_name)s.
|
|
* This stub is currently auto-generated from manpages when available.
|
|
*
|
|
* For full API reference, see: http://www.frida.re/docs/javascript-api/
|
|
*/
|
|
|
|
{
|
|
/**
|
|
* Called synchronously when about to call %(display_name)s.
|
|
*
|
|
* @this {object} - Object allowing you to store state for use in onLeave.
|
|
* @param {function} log - Call this function with a string to be presented to the user.
|
|
* @param {array} args - Function arguments represented as an array of NativePointer objects.
|
|
* For example use Memory.readUtf8String(args[0]) if the first argument is a pointer to a C string encoded as UTF-8.
|
|
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
|
|
* @param {object} state - Object allowing you to keep state across function calls.
|
|
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
|
|
* However, do not use this to store function arguments across onEnter/onLeave, but instead
|
|
* use "this" which is an object for keeping state local to an invocation.
|
|
*/
|
|
onEnter: function onEnter(log, args, state) {
|
|
log(%(log_str)s);
|
|
},
|
|
|
|
/**
|
|
* Called synchronously when about to return from %(display_name)s.
|
|
*
|
|
* See onEnter for details.
|
|
*
|
|
* @this {object} - Object allowing you to access state stored in onEnter.
|
|
* @param {function} log - Call this function with a string to be presented to the user.
|
|
* @param {NativePointer} retval - Return value represented as a NativePointer object.
|
|
* @param {object} state - Object allowing you to keep state across function calls.
|
|
*/
|
|
onLeave: function onLeave(log, retval, state) {
|
|
}
|
|
}
|
|
""" % {"display_name": display_name, "log_str": log_str}
|
|
|
|
class MemoryRepository(Repository):
|
|
def __init__(self):
|
|
super(MemoryRepository, self).__init__()
|
|
self._handlers = {}
|
|
|
|
def ensure_handler(self, function):
|
|
handler = self._handlers.get(function)
|
|
if handler is None:
|
|
handler = self._create_stub_handler(function)
|
|
self._handlers[function] = handler
|
|
self._notify_create(function, handler, "memory")
|
|
else:
|
|
self._notify_load(function, handler, "memory")
|
|
return handler
|
|
|
|
class FileRepository(Repository):
|
|
def __init__(self):
|
|
super(FileRepository, self).__init__()
|
|
self._handlers = {}
|
|
self._repo_dir = os.path.join(os.getcwd(), "__handlers__")
|
|
|
|
def ensure_handler(self, function):
|
|
entry = self._handlers.get(function.absolute_address)
|
|
if entry is not None:
|
|
(function, handler, handler_file, handler_mtime, last_sync) = entry
|
|
return handler
|
|
|
|
handler = None
|
|
handler_files_to_try = []
|
|
|
|
if isinstance(function, ModuleFunction):
|
|
module_dir = os.path.join(self._repo_dir, to_filename(function.module.name))
|
|
module_handler_file = os.path.join(module_dir, to_handler_filename(function.name))
|
|
handler_files_to_try.append(module_handler_file)
|
|
|
|
any_module_handler_file = os.path.join(self._repo_dir, to_handler_filename(function.name))
|
|
handler_files_to_try.append(any_module_handler_file)
|
|
|
|
for handler_file in handler_files_to_try:
|
|
if os.path.isfile(handler_file):
|
|
with open(handler_file, 'r') as f:
|
|
handler = f.read()
|
|
self._notify_load(function, handler, handler_file)
|
|
break
|
|
|
|
if handler is None:
|
|
handler = self._create_stub_handler(function)
|
|
handler_file = handler_files_to_try[0]
|
|
handler_dir = os.path.dirname(handler_file)
|
|
if not os.path.isdir(handler_dir):
|
|
os.makedirs(handler_dir)
|
|
with open(handler_file, 'w') as f:
|
|
f.write(handler)
|
|
self._notify_create(function, handler, handler_file)
|
|
|
|
handler_mtime = os.stat(handler_file).st_mtime
|
|
self._handlers[function.absolute_address] = (function, handler, handler_file, handler_mtime, time.time())
|
|
|
|
return handler
|
|
|
|
def sync_handler(self, function_address):
|
|
(function, handler, handler_file, handler_mtime, last_sync) = self._handlers[function_address]
|
|
delta = time.time() - last_sync
|
|
if delta >= 1.0:
|
|
changed = False
|
|
|
|
try:
|
|
new_mtime = os.stat(handler_file).st_mtime
|
|
if new_mtime != handler_mtime:
|
|
with open(handler_file, 'r') as f:
|
|
new_handler = f.read()
|
|
changed = new_handler != handler
|
|
handler = new_handler
|
|
handler_mtime = new_mtime
|
|
except:
|
|
pass
|
|
|
|
self._handlers[function_address] = (function, handler, handler_file, handler_mtime, time.time())
|
|
|
|
if changed:
|
|
self._notify_update(function, handler, handler_file)
|
|
|
|
class UI(object):
|
|
def on_trace_progress(self, operation):
|
|
pass
|
|
|
|
def on_trace_events(self, events):
|
|
pass
|
|
|
|
def on_trace_handler_create(self, function, handler, source):
|
|
pass
|
|
|
|
def on_trace_handler_load(self, function, handler, source):
|
|
pass
|
|
|
|
|
|
def main():
|
|
from frida.application import ConsoleApplication
|
|
|
|
class TracerApplication(ConsoleApplication, UI):
|
|
def _add_options(self, parser):
|
|
pb = TracerProfileBuilder()
|
|
def process_builder_arg(option, opt_str, value, parser, method, **kwargs):
|
|
method(value)
|
|
parser.add_option("-I", "--include-module", help="include MODULE", metavar="MODULE",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.include_modules,))
|
|
parser.add_option("-X", "--exclude-module", help="exclude MODULE", metavar="MODULE",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.exclude_modules,))
|
|
parser.add_option("-i", "--include", help="include FUNCTION", metavar="FUNCTION",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.include,))
|
|
parser.add_option("-x", "--exclude", help="exclude FUNCTION", metavar="FUNCTION",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.exclude,))
|
|
parser.add_option("-a", "--add", help="add MODULE!OFFSET", metavar="MODULE!OFFSET",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.include_relative_address,))
|
|
parser.add_option("-m", "--include-objc-method", help="include OBJC_METHOD", metavar="OBJC_METHOD",
|
|
type='string', action='callback', callback=process_builder_arg, callback_args=(pb.include_objc_method,))
|
|
self._profile_builder = pb
|
|
|
|
def _usage(self):
|
|
return "usage: %prog [options] target"
|
|
|
|
def _initialize(self, parser, options, args):
|
|
self._tracer = None
|
|
self._targets = None
|
|
self._profile = self._profile_builder.build()
|
|
|
|
def _needs_target(self):
|
|
return True
|
|
|
|
def _start(self):
|
|
self._tracer = Tracer(self._reactor, FileRepository(), self._profile)
|
|
self._targets = self._tracer.start_trace(self._session, self)
|
|
|
|
def _stop(self):
|
|
print("Stopping...")
|
|
self._tracer.stop()
|
|
self._tracer = None
|
|
|
|
def on_trace_progress(self, operation):
|
|
if operation == 'resolve':
|
|
self._update_status("Resolving functions...")
|
|
elif operation == 'instrument':
|
|
self._update_status("Instrumenting functions...")
|
|
elif operation == 'ready':
|
|
if len(self._targets) == 1:
|
|
plural = ""
|
|
else:
|
|
plural = "s"
|
|
self._update_status("Started tracing %d function%s. Press ENTER to stop." % (len(self._targets), plural))
|
|
self._resume()
|
|
|
|
def on_trace_events(self, events):
|
|
self._status_updated = False
|
|
for timestamp, target_address, message in events:
|
|
print("%6d ms\t%s" % (timestamp, message))
|
|
|
|
def on_trace_handler_create(self, function, handler, source):
|
|
print("%s: Auto-generated handler at \"%s\"" % (function, source))
|
|
|
|
def on_trace_handler_load(self, function, handler, source):
|
|
print("%s: Loaded handler at \"%s\"" % (function, source))
|
|
|
|
app = TracerApplication()
|
|
app.run()
|
|
|
|
def to_filename(name):
|
|
result = ""
|
|
for c in name:
|
|
if c.isalnum() or c == ".":
|
|
result += c
|
|
else:
|
|
result += "_"
|
|
return result
|
|
|
|
def to_handler_filename(name):
|
|
full_filename = to_filename(name)
|
|
if len(full_filename) <= 41:
|
|
return full_filename + ".js"
|
|
crc = binascii.crc32(full_filename.encode())
|
|
return full_filename[0:32] + "_%08x.js" % crc
|
|
|
|
if __name__ == '__main__':
|
|
main()
|