Skip to content

Instantly share code, notes, and snippets.

@aemmitt-ns
Last active December 30, 2023 06:26
Show Gist options
  • Save aemmitt-ns/457f44bccac1eefc32e77e812fe27aff to your computer and use it in GitHub Desktop.
Save aemmitt-ns/457f44bccac1eefc32e77e812fe27aff to your computer and use it in GitHub Desktop.
funtime: detailed objective-c runtime tracing. ex `python funtime.py -n Messages '-[NSRegularExpression *]'`
const typeMap = {
"c": "char",
"i": "int",
"s": "short",
"l": "long",
"q": "long long",
"C": "unsigned char",
"I": "unsigned int",
"S": "unsigned short",
"L": "unsigned long",
"Q": "unsigned long long",
"f": "float",
"d": "double",
"B": "bool",
"v": "void",
"*": "char *",
"@": "id",
"#": "Class",
":": "SEL",
"[": "Array",
"{": "struct",
"(": "union",
"b": "Bitfield",
"^": "*",
"r": "char *",
"?": "void *" // just so it works
};
const descMap = {
"NSXPCConnection": (obj) => {
return "service name: " + obj.serviceName();
},
"Protocol": (obj) => {
return obj.description() + " " + object.name();
},
"NSString": (obj) => {
return '@"' + obj.description() + '"';
}
};
const descCache = {};
function getClassName(obj) {
const object = new ObjC.Object(obj);
if (object.$methods.indexOf("- className") != -1) {
return object.className();
} else {
return "id"
}
}
function getDescription(object) {
const klass = object.class();
const name = "" + object.className();
if (!descCache[name]) {
const klasses = Object.keys(descMap);
for(let i = 0; i < klasses.length; i++) {
let k = klasses[i];
if (klass["+ isSubclassOfClass:"](ObjC.classes[k])) {
return descMap[k](object);
}
}
}
descCache[name] = 1;
if (object.$methods.indexOf("- description") != -1) {
return "/* " + object.description() + " */ " + ptr(object);
} else {
return "" + ptr(object);
}
}
function typeDescription(t, obj) {
if (t != "@") {
let p = "";
let nt = t;
if (t[0] == "^") {
nt = t.substring(1);
p = " *";
}
return typeMap[nt[0]] + p;
} else {
return getClassName(obj) + " *";
}
}
function objectDescription(t, obj) {
if (t == "@") {
const object = new ObjC.Object(obj);
return getDescription(object);
} else if (t == "#") {
const object = new ObjC.Object(obj);
return "/* " + obj + " */ " + object.description();
} else if (t == ":") {
// const object = new ObjC.Object(obj);
const description = "" + obj.readCString();
return "/* " + description + " */ " + obj;
} else if (t == "*" || t == "r*") {
return '"' + obj.readCString() + '"';
} else if ("ilsILS".indexOf(t) != -1) {
return "" + obj.toInt32();
} else {
return "" + obj;
}
}
const hookMethods = (selector) => {
if(ObjC.available) {
const resolver = new ApiResolver('objc');
const matches = resolver.enumerateMatches(selector);
matches.forEach(m => {
// console.log(JSON.stringify(element));
const name = m.name;
const t = name[0];
const klass = name.substring(2, name.length-1).split(" ")[0];
const method = name.substring(2, name.length-1).split(" ")[1];
const mparts = method.split(":");
try {
Interceptor.attach(m.address, {
onEnter(args) {
const obj = new ObjC.Object(args[0]);
const sel = args[1];
const sig = obj["- methodSignatureForSelector:"](sel);
this.invocation = null;
if (sig !== null) {
this.invocation = {
"targetType": t,
"targetClass": klass,
"targetMethod": method,
"args": []
};
const nargs = sig["- numberOfArguments"]();
this.invocation.returnType = sig["- methodReturnType"]();
for(let i = 0; i < nargs; i++) {
// console.log(sig["- getArgumentTypeAtIndex:"](i));
const argtype = sig["- getArgumentTypeAtIndex:"](i);
this.invocation.args.push({
"typeString": argtype,
"typeDescription": typeDescription(argtype, args[i]),
"object": args[i],
"objectDescription": objectDescription(argtype, args[i])
});
}
}
},
onLeave(ret) {
if (this.invocation !== null) {
this.invocation.retTypeDescription = typeDescription(this.invocation.returnType, ret);
this.invocation.returnDescription = objectDescription(this.invocation.returnType, ret);
send(JSON.stringify(this.invocation));
}
}
});
} catch (err) {
// sometimes it cant hook copyWithZone? dunno but its not good to hook it anyway.
if (method != "copyWithZone:") {
console.log(`Could not hook [${klass} ${method}] : ${err}`);
}
}
});
}
}
rpc.exports.hook = hookMethods;
from __future__ import print_function
from rich.console import Console
from rich.syntax import Syntax
console = Console()
import frida
import sys
import argparse
import json
import time
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('-s', '--spawn', action='store_true', help='spawn process')
parser.add_argument('-U', '--usb', action='store_true', help='use usb device')
parser.add_argument('-n', '--name', required=True, type=str, help='name of process')
parser.add_argument('methods', metavar='SEL', type=str, nargs='+',
help='a method selector like "*[NSMutable* initWith*]"')
args = parser.parse_args()
device = frida.get_local_device()
if args.usb:
device = frida.get_usb_device()
name = args.name
if name.isdigit():
name = int(name)
if args.spawn:
session = device.spawn(name)
else:
session = device.attach(name)
print(f"Attached to {name} on {device}")
try:
js = open("funtime.js").read()
script = session.create_script(js)
def format_call(info):
obj = info["args"][0]
parts = info["targetMethod"].split(":")
objstr = f"""({obj["typeDescription"]})( {obj["objectDescription"]} )"""
if info["targetType"] == "+":
objstr = obj["typeDescription"].split(" ")[0]
formatted = "\n\t" + f"""{info["targetType"]}[{objstr} """.replace("\n", "\n\t")
if len(parts) == 1:
formatted += "\n\t\t" + parts[0]
else:
for i, arg in enumerate(info["args"][2:]):
formatted += ("\n\t\t" + f"""{parts[i]}: ({arg["typeDescription"]})""" +
f"""( {arg["objectDescription"]} )""".replace("\n", "\n\t\t"))
formatted += "];\n\t"
if info["retTypeDescription"] != "void":
formatted += (f"""return ({info["retTypeDescription"]})""" +
f"""( {info["returnDescription"]} );""".replace("\n", "\n\t\t"))
return formatted + f" // time: {time.time()}"
def on_message(message, data):
if "payload" in message:
payload = json.loads(message["payload"])
formatted = format_call(payload)
console.print(Syntax(formatted, "objc", background_color="black"))
script.on('message', on_message)
script.load()
for method in args.methods:
try:
script.exports.hook(method)
except Exception as e:
print("error", e)
sys.stdin.read()
except KeyboardInterrupt:
pass
# script.unload()
session.detach()
@aemmitt-ns
Copy link
Author

awesome that you are integrating this! i love ipsw, amazing tool. it appears its an issue with calling - methodSignatureForSelector:, what output do you get when adding a console.log(obj) right before this call on line 125?

@blacktop
Copy link

blacktop commented Dec 13, 2022

awesome that you are integrating this! i love ipsw, amazing tool.
❤️

it appears its an issue with calling - methodSignatureForSelector:, what output do you get when adding a console.log(obj) right before this call on line 125?

   • Received 'send':
	- NSRegularExpression(
		SMSApplication * (/* <SMSApplication: 0x12341234> */ 0x12341234),
		SEL (/* release */ 0x12341234)
	) -> undefined (0x12341234)
   • Received 'log' - <SMSApplication: 0x12341234>
   • Received 'log' - <SMSApplication: 0x12341234>
   • Received 'log' - nil
   ⨯ Received 'error' - TypeError: not a function column=1 line=126
   ⨯ Received 'error' - TypeError: cannot read property 'returnType' of undefined column=1 line=154
   • Received 'log' - nil
   ⨯ Received 'error' - TypeError: not a function column=1 line=126
   ⨯ Received 'error' - TypeError: cannot read property 'returnType' of undefined column=1 line=154
   • Received 'log' - <_UIBoxedAutoreleasePoolMark: 0x12341234>
   • Received 'log' - <_UIBoxedAutoreleasePoolMark: 0x12341234>
   • Received 'log' - <_UIWeakReference: 0x12341234>
   • Received 'log' - <_NSCopyOnWriteCalendarWrapper: 0x12341234>
   • Received 'log' - <__NSCFCalendar: 0x12341234>
   • Received 'log' - <_NSRefcountedPthreadMutex: 0x12341234>
   • Received 'log' - ChatRegistry
   • Received 'log' - IMService[
   • Received 'log' - ]
   ⨯ Received 'error' - ReferenceError: 'object' is not defined column=1 line=35
   • Received 'log' - <__NSMallocBlock__: 0x12341234>
   • Received 'log' - [IMRemoteObject] Port Name: (null)  Pid: 0   Process: (null)
   • Received 'send':
	- NSRegularExpression(
		__NSCFConstantString * (@"]"),
		SEL (/* retain */ 0x12341234)
	) -> __NSCFConstantString * (@"]")

I guess I could just check if obj is nil and bail out? methodSignatureForSelector prob shouldn't be failing though right?

@aemmitt-ns
Copy link
Author

no it shouldn't but it looks like something worse is going on because all the objects are weird. i think the release and retain methods of NSObject have been hooked so its getting hit for objects of every class. do you get weird results for '-[NSRegularExpression initWithPattern*]' ?

@blacktop
Copy link

No errors with that input:

   • Received 'log' - obj: <NSRegularExpression: 0x12341234> (null) 0x0, sel: 0x12341234
   • Received 'send':
	- NSRegularExpression(
		NSRegularExpression * (/* <NSRegularExpression: 0x12341234> (null) 0x0 */ 0x12341234),
		SEL (/* initWithPattern:options:error: */ 0x12341234),
		__NSCFConstantString * (@"[⺀-鿿]"),
		unsigned long long (0x1),
		id * (0x0)
	) -> NSRegularExpression * (/* <NSRegularExpression: 0x12341234> [⺀-鿿] 0x1 */ 0x12341234)
   • Received 'log' - obj: <NSRegularExpression: 0x12341234> (null) 0x0, sel: 0x12341234
   • Received 'send':
	- NSRegularExpression(
		NSRegularExpression * (/* <NSRegularExpression: 0x12341234> (null) 0x0 */ 0x12341234),
		SEL (/* initWithPattern:options:error: */ 0x12341234),
		__NSCFConstantString * (@"[⺀-鿿]"),
		unsigned long long (0x1),
		id * (0x0)
	) -> NSRegularExpression * (/* <NSRegularExpression: 0x12341234> [⺀-鿿] 0x1 */ 0x12341234)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment