Last active
December 30, 2023 06:26
-
-
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 *]'`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
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?
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*]' ?
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
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?