8KSec — FriddInTheMiddle Lab
My hands-on iOS solution
1. What is the security feature used by the application?
The application uses active runtime tamper detection. The app continuously checks for malicious tools while it runs, not only at launch. These runtime checks look for instrumentation frameworks (for example Frida), injected dynamic libraries, and other runtime anomalies.
2. My strategy
The app reveals two runtime checks: port detection for common malicious tools and dylib detection. Options include reverse-engineering and patching the Mach-O (static), using third-party jailbreak tweaks, changing Frida's default ports and binary names, or developing a custom runtime-interception script. For this lab (where static changes are not allowed), I used runtime interception via Frida.
3. Port detection bypassing
The app probes typical Frida ports (27042 and 27043) by calling connect(). Conceptually, to make the app believe Frida is not present, make connect() behave as if no listener exists (i.e. fail with ECONNREFUSED).
// frida-port-bypass.js
// Purpose: Prevent the app from detecting Frida by checking its default ports (27042, 27043)
(function () {
function ntohs(n) { return ((n & 0xff) << 8) | ((n >> 8) & 0xff); }
function setErrno(code) {
const errnoPtrFn = Module.findExportByName(null, "___error");
if (!errnoPtrFn) return;
const getErrnoPtr = new NativeFunction(errnoPtrFn, "pointer", []);
Memory.writeS32(getErrnoPtr(), code); // e.g. ECONNREFUSED = 61
}
const connectPtr = Module.findExportByName(null, "connect");
if (!connectPtr) return;
Interceptor.attach(connectPtr, {
onEnter(args) {
try {
const sockaddr = args[1];
const portBE = Memory.readU16(sockaddr.add(2));
const port = ntohs(portBE);
this.shouldBlock = (port === 27042 || port === 27043);
} catch (_) { this.shouldBlock = false; }
},
onLeave(retval) {
if (this.shouldBlock) { setErrno(61); retval.replace(-1); }
}
});
console.log("[+] Port scan bypass armed (connect).");
})();
4. Dylib detection bypassing
The app inspects loaded images and flags anything containing frida/gadget in the dylib name. Hook _dyld_get_image_name and return a benign name for suspicious images.
// dylib-scan-bypass.js
(function () {
const dyldGetImageName = Module.findExportByName(null, "_dyld_get_image_name");
if (!dyldGetImageName) return;
const safeName = Memory.allocUtf8String("/usr/lib/libobjc.A.dylib");
Interceptor.replace(dyldGetImageName, new NativeCallback(function (imageIndex) {
const orig = new NativeFunction(dyldGetImageName, "pointer", ["uint32"]);
const p = orig(imageIndex);
const name = p.isNull() ? "" : Memory.readUtf8String(p) || "";
const low = name.toLowerCase();
if (low.indexOf("frida") >= 0 || low.indexOf("gadget") >= 0) { return safeName; }
return p;
}, "pointer", ["uint32"]));
console.log("[+] Dylib scan bypass armed (_dyld_get_image_name).");
})();
5. Hooking the method that captures the flag
To capture the flag reliably, keep the Frida scripts active while triggering the UI action that calls dummyFunction(flag:). Memory scanning can locate the ASCII marker CTF{ and print surrounding text.
// memory_dump_ctf.js
// Scan readable memory for "CTF{" strings
console.log("[*] Starting memory scan for CTF flag.");
var ranges = Process.enumerateRangesSync({ protection: 'r--', coalesce: true });
ranges.forEach(function(range) {
try {
Memory.scan(range.base, range.size, "43 54 46 7B", {
onMatch: function(address, size) {
console.log("[*] Possible flag at:", address);
try { console.log("[*] Flag:", Memory.readUtf8String(address)); } catch(e) {}
},
onError: function(reason) {},
onComplete: function() {}
});
} catch(e) {}
});
console.log("[+] Memory scan complete.");
6. Conclusion & Flag
My approach was to run the port + dylib bypass scripts so the app does not detect the malicious tools, trigger the UI action that calls dummyFunction(flag:), then run a memory scan for the ASCII pattern CTF{ and print the surrounding UTF-8 string to capture the flag.
Using this approach I was able to capture the flag during the lab while keeping the app running.