Back to Blog
iOS SecurityDec 14, 20254 min read
8KSec — TraceTheChat
My hands-on iOS solution
TraceTheChat
TraceTheChat is a tiny messaging app that sends typed messages to a hidden/“mysterious” recipient. The UI looks normal, but the message dispatching is implemented inside an obfuscated Swift class/module (TraceTheChat) so the message routing internals are hidden from casual inspection.
1. Investigating the application
A. Info.plist
- After investigating the info plist there is nothing interesting
B. Ivestigating the application mach-o
- After looking in the binaries for any hardcoded strings for any hints, I found nothing interested.
C. Investigating Frameworks
D. Investigatign the application UI
- After analysing the application UI, I found that the most interesting component is the send button, and most likely it will lead to exploit the app.
<_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0xe42c27e90; frame = (319.667 733.667; 39.3333 23);
2. Trace the application
- Trace the application using frida looking for involved methods
frida-trace -U -f com.8ksec.TraceTheChat -i "*TraceTheChat*"
- I found this intresting method
13849 ms | $s12TraceTheChat13MessageRouterC8dispatch_2toySS_SStF()
which is equivalent to
TraceTheChat.MessageRouter.dispatch(_: to:) -> Void
- $s — Swift symbol prefix
- 12TraceTheChat — module name:
TraceTheChat(12 characters) - 13MessageRouter — type:
MessageRouter(13 characters) - C — class
- 8dispatch — method name:
dispatch(8 characters) - _2to — parameter labels: first unlabeled (
_), second labeledto - SS_SS — parameter types:
Swift.String,Swift.String(message, recipient) - t — end of parameter tuple
- y — returns
()(Void) - F — end of function signature
3. Hooking the method & Finding the sent message
- After finding the interesting method
$s12TraceTheChat13MessageRouterC8dispatch_2toySS_SStF(), now we are going to hook this method and solve the challenge. - Frida interceptor: Resolves the mangled Swift symbol
$s12TraceTheChat13MessageRouterC8dispatch_2toySS_SStF(i.e.TraceTheChat.MessageRouter.dispatch(_: to:)) by scanning loaded modules, locates the SwiftString -> NSStringbridge (tries patterns for_bridgeToObjectiveC), wraps the bridge in aNativeFunctionwith signaturepointer(pointer, pointer), thenInterceptor.attaches thedispatchaddress. On entry it reads ABI register pairs(x0,x1)→ message and(x2,x3)→ recipient, calls the bridge to getNSStringandObjC.Object(...).toString()(handles SSO vs heap-backed strings), and logs the decodedmessage/toplus raw register values, short hexdumps and a 5-frame backtrace (viaThread.backtrace/DebugSymbol.fromAddress).
// intercept_dispatch.js
// Hooks: TraceTheChat.MessageRouter.dispatch(_: to:) -> Void
// Style: standalone Interceptor + symbol lookup + Swift String bridging
(function () {
'use strict';
if (!ObjC.available) throw new Error('Objective-C runtime not available');
// ---------- utils ----------
function findSymbol(re) {
for (const m of Process.enumerateModules()) {
try {
const s = m.enumerateSymbols().find(sym =>
re.test(sym.name) && sym.address && !sym.address.isNull()
);
if (s) return s.address;
} catch (_) {}
}
return null;
}
function hexdumpShort(ptrVal) {
try {
if (ptrVal && !ptrVal.isNull())
return hexdump(ptrVal, { length: 32, ansi: false })
.split('\n').slice(0, 2).join(' ');
} catch (_) {}
return '<n/a>';
}
function backtrace(ctx, n) {
try {
return Thread.backtrace(ctx, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.slice(0, n).map(s => ' ' + s.toString()).join('\n');
} catch (_) {
return ' <no bt>';
}
}
// ---------- locate target symbols ----------
const dispatchAddr = findSymbol(/\$s12TraceTheChat13MessageRouterC8dispatch_2toySS_SStF$/);
if (!dispatchAddr) throw new Error('MessageRouter.dispatch not found');
// Swift String -> NSString bridge (Foundation extension)
const bridgeAddr =
findSymbol(/\$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF$/) ||
findSymbol(/_\$sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF$/) ||
findSymbol(/\$sSS10FoundationE.*bridgeToObjectiveC.*So8NSStringCyF$/);
if (!bridgeAddr) throw new Error('Swift String -> NSString bridge not found');
const bridgeToNSString = new NativeFunction(bridgeAddr, 'pointer', ['pointer', 'pointer']);
// Helper to convert Swift String (passed in register pairs) to JS string
function swiftStringToJS(x0, x1) {
try {
const ns = bridgeToNSString(x0, x1);
if (ns.isNull()) return '<nil>';
return new ObjC.Object(ns).toString();
} catch (e) {
return `<bridge-error: ${e}>`;
}
}
console.log('[*] dispatch @ ' + dispatchAddr);
console.log('[*] bridge @ ' + bridgeAddr);
// ---------- hook ----------
Interceptor.attach(dispatchAddr, {
onEnter(args) {
// Swift method signature (two Strings): x0/x1 = message, x2/x3 = to
const msg = swiftStringToJS(args[0], args[1]);
const to = swiftStringToJS(args[2], args[3]);
console.log('--- dispatch(_:to:) --------------------------------');
console.log('message = "' + msg + '"');
console.log('to = "' + to + '"');
// Diagnostics (optional): raw regs & brief hexdumps
console.log('x0=' + args[0] + ' x1=' + args[1]);
console.log(' HEX: ' + hexdumpShort(args[0]));
console.log('x2=' + args[2] + ' x3=' + args[3]);
console.log(' HEX: ' + hexdumpShort(args[2]));
console.log('bt:\n' + backtrace(this.context, 5));
console.log('-----------------------------------------------------');
}
});
})();