Skip to content

MASTG-DEMO-0117: Extracting Sensitive Data from CCCrypt via Frida Hooking

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

Sample

This sample encrypts and decrypts a sensitive API key using CommonCrypto's CCCrypt. The app does not implement any runtime hook detection mechanisms. On the contrary, Detecting Frida Hooks Before Sensitive Cryptographic Operations demonstrates a runtime hook detection mechanism.

Environment

This demo was built using Xcode 26.2.9 and tested on an iPhone running iOS 16.7.10 (jailbroken with Dopamine 2.4.9).

Note

This is a series of correlated tests.

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// SUMMARY: This sample demonstrates a sensitive cryptographic operation that can be intercepted at runtime because the app does not implement runtime hook detection.

import SwiftUI
import Foundation
import CommonCrypto
import Security

class MastgTest {
    static func mastgTest(completion: @escaping (String) -> Void) {
        completion(runCryptoDemo())
    }

    private static func runCryptoDemo() -> String {
        let sensitiveApiKey = "sk-OWASP-MAS-SuperSecretKey-1234567890"
        let key = Data("0123456789abcdef0123456789abcdef".utf8)
        let plaintext = Data(sensitiveApiKey.utf8)

        guard let iv = randomBytes(count: kCCBlockSizeAES128) else {
            return "Error: Could not generate IV."
        }

        // FAIL: [MASTG-TEST-0354] The app calls CCCrypt with sensitive data without detecting runtime hooks.
        guard let encryptedData = crypt(
            operation: CCOperation(kCCEncrypt),
            data: plaintext,
            key: key,
            iv: iv
        ) else {
            return "Error: Encryption failed."
        }

        // FAIL: [MASTG-TEST-0354] The app decrypts sensitive data without detecting runtime hooks.
        guard let decryptedData = crypt(
            operation: CCOperation(kCCDecrypt),
            data: encryptedData,
            key: key,
            iv: iv
        ), let decryptedString = String(data: decryptedData, encoding: .utf8) else {
            return "Error: Decryption failed."
        }

        return """
        Encryption and decryption successful.
        IV: \(iv.base64EncodedString())
        Encrypted: \(encryptedData.base64EncodedString())
        Decrypted: \(decryptedString)
        """
    }

    private static func randomBytes(count: Int) -> Data? {
        var data = Data(count: count)
        let status = data.withUnsafeMutableBytes { buffer -> OSStatus in
            guard let baseAddress = buffer.baseAddress else {
                return errSecParam
            }
            return SecRandomCopyBytes(kSecRandomDefault, count, baseAddress)
        }
        return status == errSecSuccess ? data : nil
    }

    private static func crypt(operation: CCOperation, data: Data, key: Data, iv: Data) -> Data? {
        var output = Data(count: data.count + kCCBlockSizeAES128)
        let outputCapacity = output.count
        var outputLength: size_t = 0

        let status = output.withUnsafeMutableBytes { outputBytes in
            data.withUnsafeBytes { dataBytes in
                key.withUnsafeBytes { keyBytes in
                    iv.withUnsafeBytes { ivBytes in
                        CCCrypt(
                            operation,
                            CCAlgorithm(kCCAlgorithmAES),
                            CCOptions(kCCOptionPKCS7Padding),
                            keyBytes.baseAddress,
                            key.count,
                            ivBytes.baseAddress,
                            dataBytes.baseAddress,
                            data.count,
                            outputBytes.baseAddress,
                            outputCapacity,
                            &outputLength
                        )
                    }
                }
            }
        }

        guard status == kCCSuccess else {
            return nil
        }

        output.removeSubrange(outputLength..<output.count)
        return output
    }
}

Steps

  1. Install the app on a device ( Installing Apps).
  2. Make sure you have Frida (iOS) installed on your machine and frida-server running on the device.
  3. Run run.sh to spawn the app with Frida.
  4. Tap 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
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
function operationName(operation) {
  if (operation === 0) {
    return "kCCEncrypt";
  }
  if (operation === 1) {
    return "kCCDecrypt";
  }
  return "unknown(" + operation + ")";
}

function algorithmName(algorithm) {
  if (algorithm === 0) {
    return "kCCAlgorithmAES";
  }
  return "unknown(" + algorithm + ")";
}

function isPrintable(text) {
  return /^[\x09\x0a\x0d\x20-\x7e]+$/.test(text);
}

function readUtf8(pointer, length) {
  try {
    const value = pointer.readUtf8String(length);
    if (value !== null && isPrintable(value)) {
      return value;
    }
  } catch (_) {
  }
  return null;
}

function bytesToHex(pointer, length) {
  try {
    const bytes = new Uint8Array(pointer.readByteArray(length));
    return Array.prototype.map.call(bytes, function (byte) {
      return ("0" + byte.toString(16)).slice(-2);
    }).join("");
  } catch (error) {
    return "<unreadable: " + error.message + ">";
  }
}

function formatBuffer(pointer, length) {
  if (pointer.isNull() || length <= 0) {
    return "<empty>";
  }

  const text = readUtf8(pointer, length);
  if (text !== null) {
    return text;
  }

  const hex = bytesToHex(pointer, length);
  if (hex.length > 128) {
    return "0x" + hex.slice(0, 128) + "...";
  }
  return "0x" + hex;
}

function readSizeT(pointer) {
  if (pointer.isNull()) {
    return 0;
  }

  try {
    return pointer.readU64().toNumber();
  } catch (_) {
    return pointer.readU32();
  }
}

const cccrypt = Module.findGlobalExportByName("CCCrypt");

if (cccrypt === null) {
  console.log("[-] CCCrypt export not found.");
} else {
  Interceptor.attach(cccrypt, {
    onEnter(args) {
      this.operation = args[0].toInt32();
      this.algorithm = args[1].toInt32();
      this.dataIn = args[6];
      this.dataInLength = args[7].toInt32();
      this.dataOut = args[8];
      this.dataOutMoved = args[10];

      this.backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress)
        .slice(0, 8);

      console.log("\n[*] CCCrypt called");
      console.log("    Operation: " + operationName(this.operation));
      console.log("    Algorithm: " + algorithmName(this.algorithm));
      console.log("    Input: " + formatBuffer(this.dataIn, this.dataInLength));
    },

    onLeave(retval) {
      const status = retval.toInt32();
      const outputLength = readSizeT(this.dataOutMoved);

      console.log("    Return status: " + status);
      console.log("    Output: " + formatBuffer(this.dataOut, outputLength));

      console.log("\nBacktrace:");
      for (let i = 0; i < this.backtrace.length; i++) {
        console.log(this.backtrace[i]);
      }
    }
  });

  console.log("[+] CCCrypt hooked: extracting sensitive cryptographic data");
}

Observation

The output contains two CCCrypt calls found at runtime. The encryption call reveals the sensitive API key as plaintext input, and the decryption call reveals the same API key as plaintext output. Backtraces are also provided to help identify the locations in the code.

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
[+] CCCrypt hooked: extracting sensitive cryptographic data

[*] CCCrypt called
    Operation: kCCEncrypt
    Algorithm: kCCAlgorithmAES
    Input: sk-OWASP-MAS-SuperSecretKey-1234567890
    Return status: 0
    Output: 0xcc2b9758f2c54f40c950c980d380804530a86f51b2e9ada2317aef5d0741b94a32c421e365ce47229349f313aed40d0b

Backtrace:
0x104689934 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x104689b84 MASTestApp.debug.dylib!partial apply for closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1c7a19f50 Foundation!Data.InlineSlice.withUnsafeBytes<A>(_:)
0x1c7a13ed4 Foundation!Data.withUnsafeBytes<A>(_:)
0x104689740 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x104689b08 MASTestApp.debug.dylib!partial apply for closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1c7a19f50 Foundation!Data.InlineSlice.withUnsafeBytes<A>(_:)
0x1c7a13ed4 Foundation!Data.withUnsafeBytes<A>(_:)

[*] CCCrypt called
    Operation: kCCDecrypt
    Algorithm: kCCAlgorithmAES
    Input: 0xcc2b9758f2c54f40c950c980d380804530a86f51b2e9ada2317aef5d0741b94a32c421e365ce47229349f313aed40d0b
    Return status: 0
    Output: sk-OWASP-MAS-SuperSecretKey-1234567890

Backtrace:
0x104689934 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x104689b84 MASTestApp.debug.dylib!partial apply for closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1c7a19f50 Foundation!Data.InlineSlice.withUnsafeBytes<A>(_:)
0x1c7a13ed4 Foundation!Data.withUnsafeBytes<A>(_:)
0x104689740 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x104689b08 MASTestApp.debug.dylib!partial apply for closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1c7a19f50 Foundation!Data.InlineSlice.withUnsafeBytes<A>(_:)
0x1c7a13ed4 Foundation!Data.withUnsafeBytes<A>(_:)

Evaluation

The test case fails because the hook executes successfully and the sensitive API key sk-OWASP-MAS-SuperSecretKey-1234567890 is extracted in plaintext from the CCCrypt calls. The app lacks runtime integrity verification, allowing instrumentation tools to intercept cryptographic operations without any defensive response.