Skip to content

MASTG-DEMO-0118: Detecting Frida Hooks Before Sensitive Cryptographic Operations

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

Sample

This sample encrypts and decrypts a sensitive API key using CommonCrypto's CCCrypt. Unlike the unprotected variant in Extracting Sensitive Data from CCCrypt via Frida Hooking, this version checks whether a Frida control endpoint is reachable on the device before performing sensitive cryptographic operations. The detection logic attempts to connect to 127.0.0.1 on Frida's default local port (27042), which is commonly exposed by frida-server on jailbroken/rooted iOS devices, and sends a D-Bus AUTH probe. If the endpoint responds like a D-Bus service, the app treats it as a Frida runtime artifact and terminates before any cryptographic operations are performed.

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
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// SUMMARY: This sample demonstrates runtime hook detection before sensitive cryptographic operations by checking for local Frida ports responding to D-Bus AUTH.

import SwiftUI
import Foundation
import CommonCrypto
import Security
import Darwin

class MastgTest {
    static func mastgTest(completion: @escaping (String) -> Void) {
        if detectHooking() {
            exit(0)
        }

        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."
        }

        if detectHooking() {
            exit(0)
        }

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

        if detectHooking() {
            exit(0)
        }

        // PASS: [MASTG-TEST-0354] The app checks for runtime hooks before decrypting sensitive data.
        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 detectHooking() -> Bool {
        let ports: [in_port_t] = [27042]
        return ports.contains(where: respondsToDBusAuth)
    }

    private static func respondsToDBusAuth(_ port: in_port_t) -> Bool {
        let descriptor = socket(AF_INET, SOCK_STREAM, 0)
        guard descriptor >= 0 else {
            return false
        }

        defer {
            close(descriptor)
        }

        var timeout = timeval(tv_sec: 1, tv_usec: 0)
        setsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout<timeval>.size))
        setsockopt(descriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout, socklen_t(MemoryLayout<timeval>.size))

        var address = sockaddr_in()
        address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
        address.sin_family = sa_family_t(AF_INET)
        address.sin_port = port.bigEndian
        address.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))

        let connected = withUnsafePointer(to: &address) { pointer in
            pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { socketAddress in
                connect(descriptor, socketAddress, socklen_t(MemoryLayout<sockaddr_in>.size)) == 0
            }
        }

        guard connected else {
            return false
        }

        let authProbe = [UInt8]("\0AUTH\r\n".utf8)
        let sent = authProbe.withUnsafeBytes { buffer in
            send(descriptor, buffer.baseAddress, buffer.count, 0)
        }

        guard sent == authProbe.count else {
            return false
        }

        var response = [UInt8](repeating: 0, count: 256)
        let received = recv(descriptor, &response, response.count, 0)
        guard received > 0 else {
            return false
        }

        let responseText = String(decoding: response.prefix(received), as: UTF8.self).uppercased()
        return responseText.contains("REJECTED") || responseText.contains("OK")
    }

    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. Observe that the app terminates before the hooks can capture any data.
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 no CCCrypt calls found at runtime. The app terminated after the script loaded but before any hooks could capture the sensitive API key.

output.txt
1
[+] CCCrypt hooked: extracting sensitive cryptographic data

Evaluation

The test passes because the hooking attempt fails due to the app's defensive response. In this setup, frida-server exposes a local Frida endpoint while the app is spawned with Frida. The app probes 127.0.0.1:27042 by sending a D-Bus AUTH message; when the endpoint returns a D-Bus authentication response, the app terminates before CCCrypt hooks execute, so no sensitive data is extracted.

Note

Even if the test passes, it might still be possible to bypass the app's defensive response. For example, an attacker could run Frida on non-default ports or hook connect, send, or recv so the app cannot observe the D-Bus authentication response. Bypassing Frida D-Bus Port Detection to Extract Sensitive Data demonstrates such a bypass. Reverse Engineering Tools Detection describes common reverse engineering tool detection techniques and their limitations.