Skip to content

MASTG-DEMO-0046: Runtime Use of kSecAccessControlBiometryCurrentSet with Frida

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

Sample

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

../MASTG-DEMO-0045/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,
      .biometryAny,
      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 ../MASTG-DEMO-0044/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(..., 0x2) called with kSecAccessControlBiometryAny

Backtrace:
0x102350198 MASTestApp!specialized static MastgTest.createAccessControl()
0x1023504b0 MASTestApp!specialized static MastgTest.storeTokenInKeychain(secretToken:)
0x102351b98 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 with kSecAccessControlBiometryAny (see LAPublicDefines.h), which accepts any additional biometrics added after the Keychain entry was created.

When it is required that the associated keychain item become inaccessible when changes are made to the biometric database (e.g., when a new fingerprint or face is added), the app must use thekSecAccessControlBiometryCurrentSet flag instead.