Skip to content

MASTG-DEMO-0119: Bypassing Frida D-Bus Port Detection to Extract Sensitive Data

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

Sample

This sample uses the same code as Detecting Frida Hooks Before Sensitive Cryptographic Operations, which encrypts and decrypts a sensitive API key using CommonCrypto's CCCrypt. The code includes a runtime hook detection mechanism that probes 127.0.0.1:27042 with a D-Bus AUTH message and terminates via exit(0) if a D-Bus endpoint responds. This demo demonstrates bypassing the detection by hooking connect() to block connections to Frida's default port, causing detectHooking() to return false so the termination path is never reached.

See Reverse Engineering Tools Detection for more context on bypassing runtime detection mechanisms.

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.

../MASTG-DEMO-0118/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 the bypass script.
  4. Tap the Start button.
  5. Stop the script by pressing Ctrl+C.
  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
const connectPtr = Module.findGlobalExportByName("connect");

if (connectPtr === null) {
  console.log("[-] connect export not found.");
} else {
  Interceptor.attach(connectPtr, {
    onEnter(args) {
      const sockAddr = args[1];
      if (sockAddr.isNull()) {
        return;
      }

      const family = sockAddr.add(1).readU8();
      if (family !== 2) {
        return;
      }

      const rawPort = sockAddr.add(2).readU16();
      const port = ((rawPort >> 8) & 0xFF) | ((rawPort & 0xFF) << 8);

      if (port === 27042) {
        this.shouldBlock = true;
        console.log("[*] Blocking connect() to localhost:" + port);
      }
    },

    onLeave(retval) {
      if (this.shouldBlock) {
        retval.replace(-1);
      }
    }
  });

  console.log("[+] connect() hooked: filtering Frida D-Bus ports");
}

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");
}
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp.MASTestApp-iOS -l ./bypass.js -o output.txt

Observation

The output shows that the connect() call to a Frida port was blocked, followed by 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.

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
[+] connect() hooked: filtering Frida D-Bus ports
[+] CCCrypt hooked: extracting sensitive cryptographic data
[*] Blocking connect() to localhost:27042
[*] Blocking connect() to localhost:27042

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

Backtrace:
0x1007cfe2c MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1007d0a9c 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>(_:)
0x1007cfc38 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1007d0a20 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>(_:)
[*] Blocking connect() to localhost:27042

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

Backtrace:
0x1007cfe2c MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1007d0a9c 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>(_:)
0x1007cfc38 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in static MastgTest.crypt(operation:data:key:iv:)
0x1007d0a20 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 connect() hook successfully prevented the D-Bus port detection from reaching Frida's endpoint, causing detectHooking() to return false. With detection bypassed, the app proceeded with its cryptographic operations, which were intercepted by the CCCrypt hooks to extract the sensitive API key sk-OWASP-MAS-SuperSecretKey-1234567890 in plaintext.