Skip to content

MASTG-DEMO-0131: Detecting Virtual Device Detection Checks with Frida

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

Sample

The snippet below shows sample code that performs virtual device indicator checks and reports the queried values and indicator results. See Virtual Devices Detection for more information about common virtual device checks and values.

The checks query device properties and Apple Metal GPU availability as generic virtual device indicators, and also check for a specific Corellium virtualization artifact.

Note

The sample avoids NFC and Bluetooth checks because they can require additional entitlements, usage descriptions, or user-controlled state, which would make the demo less deterministic.

Note

The sample avoids App Attest checks because they require server-side validation, which breaks the self-contained requirement for MASTG demos.

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import Foundation
import Metal
import Darwin

struct MastgTest {
    // SUMMARY: This sample demonstrates iOS virtual device detection by executing virtual device indicator checks and reporting a final verdict in the UI.

    static func mastgTest(completion: @escaping (String) -> Void) {
        completion(VirtualDeviceDetector().run())
    }
}

private enum IndicatorLevel {
    case expected
    case suspicious
    case confirmed
    case inconclusive

    var label: String {
        switch self {
        case .expected:
            return "expected"
        case .suspicious:
            return "suspicious"
        case .confirmed:
            return "confirmed"
        case .inconclusive:
            return "inconclusive"
        }
    }
}

private struct IndicatorResult {
    let order: Int
    let name: String
    let observed: String
    let level: IndicatorLevel
    let reason: String
}

private struct VirtualDeviceDetector {
    func run() -> String {
        let machineIdentifier = currentMachineIdentifier()
        let results = [
            machineIndicator(for: machineIdentifier),
            metalIndicator(),
            corelliumIndicator()
        ]

        return buildReport(from: results)
    }

    private func machineIndicator(for machineIdentifier: String) -> IndicatorResult {
        let level: IndicatorLevel = machineIdentifier == "unknown" ? .inconclusive : .expected
        let reason: String

        if machineIdentifier == "unknown" {
            reason = "The app could not resolve the device model."
        } else {
            reason = "The reported model can be cross-checked against the hardware capabilities observed at runtime."
        }

        // PASS: [MASTG-TEST-0367] The app queries hw.machine through sysctlbyname as a virtual-device indicator.
        return IndicatorResult(
            order: 1,
            name: "hw.machine",
            observed: machineIdentifier,
            level: level,
            reason: reason
        )
    }

    private func metalIndicator() -> IndicatorResult {
        // PASS: [MASTG-TEST-0367] The app checks whether a Metal GPU is available.
        if MTLCreateSystemDefaultDevice() != nil {
            return IndicatorResult(
                order: 2,
                name: "Metal GPU",
                observed: "available",
                level: .expected,
                reason: "A Metal device is available, which is consistent with a physical iOS device."
            )
        }

        return IndicatorResult(
            order: 2,
            name: "Metal GPU",
            observed: "missing",
            level: .suspicious,
            reason: "No Metal device is available, which is unusual for a supported physical iOS device."
        )
    }

    private func corelliumIndicator() -> IndicatorResult {
        // PASS: [MASTG-TEST-0367] The app checks for the Corellium daemon file.
        if corelliumDaemonExists() {
            return IndicatorResult(
                order: 3,
                name: "Corellium daemon",
                observed: "present",
                level: .confirmed,
                reason: "The file /usr/libexec/corelliumd exists, which is a strong Corellium indicator."
            )
        }

        return IndicatorResult(
            order: 3,
            name: "Corellium daemon",
            observed: "not found",
            level: .expected,
            reason: "The file /usr/libexec/corelliumd was not found."
        )
    }

    private func buildReport(from results: [IndicatorResult]) -> String {
        let orderedResults = results.sorted { lhs, rhs in
            lhs.order < rhs.order
        }

        let confirmedResults = orderedResults.filter { $0.level == .confirmed }
        let suspiciousResults = orderedResults.filter { $0.level == .suspicious }

        var lines = [
            "Virtual device detection results:",
            "",
            "Indicators:"
        ]

        for result in orderedResults {
            lines.append("- \(result.name): \(result.observed) [\(result.level.label)] - \(result.reason)")
        }

        lines.append("")
        lines.append("Verdict:")

        if !confirmedResults.isEmpty {
            lines.append("- Likely virtual device.")
            for result in confirmedResults {
                lines.append("  - \(result.name): \(result.reason)")
            }
        } else if !suspiciousResults.isEmpty {
            lines.append("- No strong virtual-device verdict yet. Review the suspicious indicator below.")
            for result in suspiciousResults {
                lines.append("  - \(result.name): \(result.reason)")
            }
        } else {
            lines.append("- Likely physical device. No strong virtual-device indicators were observed.")
        }

        return lines.joined(separator: "\n")
    }

    private func currentMachineIdentifier() -> String {
        var size: size_t = 0

        guard sysctlbyname("hw.machine", nil, &size, nil, 0) == 0, size > 1 else {
            return "unknown"
        }

        var buffer = [CChar](repeating: 0, count: Int(size))
        let result = buffer.withUnsafeMutableBufferPointer { pointer in
            sysctlbyname("hw.machine", pointer.baseAddress, &size, nil, 0)
        }

        guard result == 0 else {
            return "unknown"
        }

        return String(cString: buffer)
    }

    private func corelliumDaemonExists() -> Bool {
        var fileInfo = stat()
        return "/usr/libexec/corelliumd".withCString { path in
            stat(path, &fileInfo) == 0
        }
    }
}

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
const BACKTRACE_LIMIT = 6;
const hookedAddresses = new Set();

function printBacktrace(context) {
    console.log("Backtrace:");
    const frames = Thread.backtrace(context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress)
        .slice(0, BACKTRACE_LIMIT);

    for (const frame of frames) {
        console.log(frame);
    }
}

function logEvent(message, context) {
    console.log(message);
    printBacktrace(context);
    console.log("");
}

function hookGlobalExport(symbolName, callbacks) {
    const address = Module.findGlobalExportByName(symbolName);
    if (address === null) {
        console.log(`[skip] ${symbolName} not found`);
        return;
    }

    const addressKey = address.toString();
    if (hookedAddresses.has(addressKey)) {
        return;
    }

    hookedAddresses.add(addressKey);
    Interceptor.attach(address, callbacks);
}

hookGlobalExport("MTLCreateSystemDefaultDevice", {
    onEnter(args) {
        this.contextCopy = this.context;
    },
    onLeave(retval) {
        const outcome = retval.isNull() ? "missing" : "available";
        logEvent(`MTLCreateSystemDefaultDevice() => ${outcome}`, this.contextCopy);
    }
});

hookGlobalExport("sysctlbyname", {
    onEnter(args) {
        this.contextCopy = this.context;
        this.name = args[0].readCString();
        this.outputPtr = args[1];
        this.shouldLog = this.name === "hw.machine" && !this.outputPtr.isNull();
    },
    onLeave(retval) {
        if (!this.shouldLog || retval.toInt32() !== 0) {
            return;
        }

        const value = this.outputPtr.readCString();
        logEvent(`sysctlbyname("hw.machine") => ${value}`, this.contextCopy);
    }
});

function hookStatSymbol(symbolName) {
    hookGlobalExport(symbolName, {
        onEnter(args) {
            this.contextCopy = this.context;
            this.path = args[0].readCString();
            this.shouldLog = this.path === "/usr/libexec/corelliumd";
        },
        onLeave(retval) {
            if (!this.shouldLog) {
                return;
            }

            const outcome = retval.toInt32() === 0 ? "present" : "missing";
            logEvent(`${symbolName}("/usr/libexec/corelliumd") => ${outcome}`, this.contextCopy);
        }
    });
}

hookStatSymbol("stat");

Observation

The output shows the virtual device detection-related API calls captured by the Frida hooks during app execution.

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
sysctlbyname("hw.machine") => iPhone10,6
Backtrace:
0x1c2ac4368 CoreFoundation!___CFGetProductName_block_invoke
0x1c9794780 libdispatch.dylib!_dispatch_client_callout
0x1c9764ddc libdispatch.dylib!_dispatch_once_callout
0x1c2a0fe1c CoreFoundation!_CFBundleInfoPlistProcessInfoDictionary
0x1c2a39e30 CoreFoundation!_CFBundleCopyInfoDictionaryInDirectoryWithVersion
0x1c2a2e588 CoreFoundation!_CFBundleRefreshInfoDictionaryAlreadyLocked

sysctlbyname("hw.machine") => iPhone10,6
Backtrace:
0x104e29fb0 MASTestApp.debug.dylib!closure #1 in VirtualDeviceDetector.currentMachineIdentifier()
0x104e2a000 MASTestApp.debug.dylib!partial apply for closure #1 in VirtualDeviceDetector.currentMachineIdentifier()
0x104e2a108 MASTestApp.debug.dylib!$sSa30withUnsafeMutableBufferPointeryqd__qd__SryxGzqd_0_YKXEqd_0_YKs5ErrorRd_0_r0_lF
0x104e287cc MASTestApp.debug.dylib!VirtualDeviceDetector.currentMachineIdentifier()
0x104e28090 MASTestApp.debug.dylib!VirtualDeviceDetector.run()
0x104e28040 MASTestApp.debug.dylib!static MastgTest.mastgTest(completion:)

MTLCreateSystemDefaultDevice() => available
Backtrace:
0x104e28b4c MASTestApp.debug.dylib!VirtualDeviceDetector.metalIndicator()
0x104e28104 MASTestApp.debug.dylib!VirtualDeviceDetector.run()
0x104e28040 MASTestApp.debug.dylib!static MastgTest.mastgTest(completion:)
0x104e2d018 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in ContentView.body.getter
0x1c686b35c SwiftUI!0x7d735c (0x18ac6f35c)
0x1c686b844 SwiftUI!0x7d7844 (0x18ac6f844)

stat("/usr/libexec/corelliumd") => missing
Backtrace:
0x104e2a5c4 MASTestApp.debug.dylib!closure #1 in VirtualDeviceDetector.corelliumDaemonExists()
0x104e2a5fc MASTestApp.debug.dylib!partial apply for closure #1 in VirtualDeviceDetector.corelliumDaemonExists()
0x1bc937a1c libswiftCore.dylib!String.withCString<A>(_:)
0x104e29dec MASTestApp.debug.dylib!VirtualDeviceDetector.corelliumDaemonExists()
0x104e28d60 MASTestApp.debug.dylib!VirtualDeviceDetector.corelliumIndicator()
0x104e28124 MASTestApp.debug.dylib!VirtualDeviceDetector.run()

The runtime trace shows that tapping the Start button triggers the following checks:

  • sysctlbyname("hw.machine") is called and returns iPhone10,6. The trace shows one early framework-level call from CoreFoundation and one app-level call from VirtualDeviceDetector.currentMachineIdentifier(), confirming that the sample queries the runtime device model.
  • MTLCreateSystemDefaultDevice() is called from VirtualDeviceDetector.metalIndicator() and returns available, confirming that the sample checks for Metal GPU availability.
  • stat("/usr/libexec/corelliumd") is called from VirtualDeviceDetector.corelliumDaemonExists() and returns missing, confirming that the sample checks for the specific Corellium daemon file.

Evaluation

The test passes because the output confirms that the app implements virtual device detection checks at runtime:

  • sysctlbyname("hw.machine") call for device property checks:

    • The app queries the device model identifier.
    • The reported model can be cross-checked against hardware capabilities observed at runtime.
  • MTLCreateSystemDefaultDevice() call for hardware capability checks:

    • The app checks whether a Metal GPU is available.
  • stat("/usr/libexec/corelliumd") call for virtualization engine file checks: