Skip to content

MASTG-DEMO-0044: Runtime Use of kSecAccessControlUserPresence with Frida

Download MASTG-DEMO-0044 IPA Open MASTG-DEMO-0044 Folder Build MASTG-DEMO-0044 IPA

Sample

This demo uses the same sample as Uses of kSecAccessControlUserPresence with r2.

../MASTG-DEMO-0043/MastgTest.swift
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import SwiftUI
import LocalAuthentication
import Security

struct MastgTest {

  static func mastgTest(completion: @escaping (String) -> Void) {
    let account = "com.mastg.sectoken"
    let tokenData = "8767086b9f6f976g-a8df76".data(using: .utf8)!

    // 1. Store the token in the Keychain with ACL flags
    // 1a. Create an accesscontrol object requiring user presence
    guard let accessControl = SecAccessControlCreateWithFlags(
      nil,
      kSecAttrAccessibleWhenUnlocked,
      .userPresence,
      nil
    ) else {
      completion("❌ Failed to create access control")
      return
    }

    // 1b. Build your additem query
    // Optional: you may provide a customized context to alter the default config, e.g. to set a "reuse duration".
    // See https://developer.apple.com/documentation/localauthentication/accessing-keychain-items-with-face-id-or-touch-id#Optionally-Provide-a-Customized-Context
    // Keychain services automatically makes use of the LocalAuthentication framework, even if you don't provide one.
    //
    //  let context = LAContext()
    //  context.touchIDAuthenticationAllowableReuseDuration = 10
    let storeQuery: [String: Any] = [
      kSecClass as String:          kSecClassGenericPassword,
      kSecAttrAccount as String:    account,
      kSecValueData as String:      tokenData,
      kSecAttrAccessControl as String: accessControl,
      // kSecUseAuthenticationContext as String: context,
    ]

    // Before adding, we delete any existing item
    SecItemDelete(storeQuery as CFDictionary)
    let storeStatus = SecItemAdd(storeQuery as CFDictionary, nil)
    guard storeStatus == errSecSuccess else {
      completion("❌ Failed to store token in Keychain (status \(storeStatus))")
      return
    }

    // 2. Now let's retrieve the token
    // Optional: you may provide a context with a localized reason.
    // See https://developer.apple.com/documentation/localauthentication/accessing-keychain-items-with-face-id-or-touch-id#Provide-a-Prompt-When-Reading-the-Item
    // Keychain services will use LAContext to prompt the user even if you don't provide one.
    //
    // let context = LAContext()
    // context.localizedReason = "Access your token from the keychain"
    let fetchQuery: [String: Any] = [
      kSecClass as String:         kSecClassGenericPassword,
      kSecAttrAccount as String:   account,
      kSecReturnData as String:    true,
      kSecMatchLimit as String:    kSecMatchLimitOne,
      //kSecUseAuthenticationContext as String: context,
    ]

    var result: CFTypeRef?
    let fetchStatus = SecItemCopyMatching(fetchQuery as CFDictionary, &result)

    if fetchStatus == errSecSuccess,
       let data = result as? Data,
       let token = String(data: data, encoding: .utf8) {
      completion("✅ Retrieved token: \(token)")
    } else {
      completion("❌ Authentication failed or token inaccessible (status \(fetchStatus))")
    }
  }
}

Steps

  1. Install the app on a device ( Installing Apps)
  2. Make sure you have Frida for iOS installed on your machine and the frida-server running on the device
  3. Run run.sh to spawn your app with Frida
  4. Click the Start button
  5. Stop the script by pressing Ctrl+C
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp.MASTestApp-iOS -l ./script.js -o output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const AccessControlFlags = {
    kSecAccessControlUserPresence: 1 << 0,
    kSecAccessControlBiometryAny: 1 << 1,
    kSecAccessControlBiometryCurrentSet: 1 << 3,
    kSecAccessControlDevicePasscode: 1 << 4,
    kSecAccessControlWatch: 1 << 5,
    kSecAccessControlOr: 1 << 14,
    kSecAccessControlAnd: 1 << 15,
    kSecAccessControlPrivateKeyUsage: 1 << 30,
    kSecAccessControlApplicationPassword: 1 << 31,
  };


Interceptor.attach(Module.getExportByName(null, 'SecAccessControlCreateWithFlags'), {
    /* 
        func SecAccessControlCreateWithFlags(
        _ allocator: CFAllocator?,
        _ protection: CFTypeRef,
        _ flags: SecAccessControlCreateFlags,
        _ error: UnsafeMutablePointer<Unmanaged<CFError>?>?
        )  -> SecAccessControl?
    */
  onEnter(args) {
    const flags = args[2]
    const flags_description = parseAccessControlFlags(flags)
    console.log(`\SecAccessControlCreateWithFlags(..., 0x${flags.toString(16)}) called with ${flags_description}\n`)

      // Use an arrow function so that `this` remains the same as in onEnter
      const printBacktrace = (maxLines = 8) => {
          console.log("\nBacktrace:");
          let backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE)
              .map(DebugSymbol.fromAddress);

          for (let i = 0; i < Math.min(maxLines, backtrace.length); i++) {
              console.log(backtrace[i]);
          }
      }
      printBacktrace();
  }
});


function parseAccessControlFlags(value) {
    const result = [];
    for (const [name, bit] of Object.entries(AccessControlFlags)) {
      if ((value & bit) === bit) {
        result.push(name);
      }
    }
    return result;
  }

Observation

output.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SecAccessControlCreateWithFlags(..., 0x1) called with kSecAccessControlUserPresence

Backtrace:
0x102cc0198 MASTestApp!specialized static MastgTest.createAccessControl()
0x102cc04b0 MASTestApp!specialized static MastgTest.storeTokenInKeychain(secretToken:)
0x102cc1b98 MASTestApp!closure #1 in closure #1 in closure #1 in ContentView.body.getter
0x19908bc54 SwiftUI!partial apply for closure #1 in closure #2 in ContextMenuBridge.contextMenuInteraction(_:willPerformPreviewActionForMenuWith:animator:)
0x198f986d0 SwiftUI!partial apply for specialized thunk for @callee_guaranteed () -> (@out A, @error @owned Error)
0x1996657c4 SwiftUI!specialized static MainActor.assumeIsolated<A>(_:file:line:)
0x1996321a8 SwiftUI!ButtonAction.callAsFunction()
0x198b14c2c SwiftUI!partial apply for implicit closure #2 in implicit closure #1 in PlatformItemListButtonStyle.makeBody(configuration:)

The output reveals the use of SecAccessControlCreateWithFlags in the app and lists all used flags.

Evaluation

The test fails because the output shows the runtime use of SecAccessControlCreateWithFlags(..., kSecAccessControlUserPresence) which allows for a fallback to passcode authentication.

Since this data requires protection with biometrics, It's recommended to use the kSecAccessControlBiometryCurrentSet or kSecAccessControlBiometryAny flags instead, being kSecAccessControlBiometryCurrentSet the most secure.