demo
ios
MASTG-TEST-0354
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 ]( " \0 AUTH \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
Install the app on a device ( Installing Apps ).
Make sure you have Frida (iOS) installed on your machine and frida-server running on the device.
Run run.sh to spawn the app with Frida.
Tap the Start button.
Observe that the app terminates before the hooks can capture any data.
run.sh script.js
#!/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 [ + ] 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.