Back to Blog
iOS Security
Dec 14, 2025
4 min read

8KSec — TraceTheChat

My hands-on iOS solution

8KSec — TraceTheChat

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
Info.plist

B. Ivestigating the application mach-o

  • After looking in the binaries for any hardcoded strings for any hints, I found nothing interested.
Mach-O

C. Investigating Frameworks

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);
UI

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 labeled to
  • SS_SS — parameter types: Swift.String, Swift.String (message, recipient)
  • t — end of parameter tuple
  • y — returns () (Void)
  • F — end of function signature
Trace The Application

3. Hooking the method & Finding the sent message

Hooking The Method
  • 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 Swift String -> NSString bridge (tries patterns for _bridgeToObjectiveC), wraps the bridge in a NativeFunction with signature pointer(pointer, pointer), then Interceptor.attaches the dispatch address. On entry it reads ABI register pairs (x0,x1) → message and (x2,x3) → recipient, calls the bridge to get NSString and ObjC.Object(...).toString() (handles SSO vs heap-backed strings), and logs the decoded message/to plus raw register values, short hexdumps and a 5-frame backtrace (via Thread.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('-----------------------------------------------------');
    }
  });
})();