Skip to main content

verifyos_cli/parsers/
macho_scanner.rs

1use crate::parsers::plist_reader::InfoPlist;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, thiserror::Error)]
6pub enum UsageScanError {
7    #[error("Failed to read executable: {0}")]
8    Io(#[from] std::io::Error),
9    #[error("Unable to resolve app executable")]
10    MissingExecutable,
11}
12
13#[derive(Debug, Default, Clone)]
14pub struct UsageScan {
15    pub required_keys: HashSet<&'static str>,
16    pub privacy_categories: HashSet<&'static str>,
17    pub requires_location_key: bool,
18    pub evidence: HashSet<&'static str>,
19}
20
21#[derive(Debug, Default, Clone)]
22pub struct CapabilityScan {
23    pub detected: HashSet<&'static str>,
24    pub evidence: HashSet<&'static str>,
25}
26
27pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
28    let executable =
29        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
30    scan_usage_from_executable(&executable)
31}
32
33#[derive(Debug, Default, Clone)]
34pub struct PrivateApiScan {
35    pub hits: Vec<&'static str>,
36}
37
38#[derive(Debug, Default, Clone)]
39pub struct SdkScan {
40    pub hits: Vec<&'static str>,
41}
42
43pub fn scan_private_api_from_app_bundle(
44    app_bundle_path: &Path,
45) -> Result<PrivateApiScan, UsageScanError> {
46    let executable =
47        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
48    scan_private_api_from_executable(&executable)
49}
50
51pub fn scan_sdks_from_app_bundle(app_bundle_path: &Path) -> Result<SdkScan, UsageScanError> {
52    let executable =
53        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
54    scan_sdks_from_executable(&executable)
55}
56
57pub fn scan_capabilities_from_app_bundle(
58    app_bundle_path: &Path,
59) -> Result<CapabilityScan, UsageScanError> {
60    let executable =
61        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
62    scan_capabilities_from_executable(&executable)
63}
64
65pub fn scan_instrumentation_from_app_bundle(
66    app_bundle_path: &Path,
67) -> Result<Vec<&'static str>, UsageScanError> {
68    let executable =
69        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
70    let bytes = std::fs::read(&executable)?;
71    let mut hits = Vec::new();
72    for signature in INSTRUMENTATION_SIGNATURES {
73        if contains_subslice(&bytes, signature.as_bytes()) {
74            hits.push(*signature);
75        }
76    }
77    Ok(hits)
78}
79
80fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
81    let info_plist_path = app_bundle_path.join("Info.plist");
82    if info_plist_path.exists() {
83        if let Ok(info_plist) = InfoPlist::from_file(&info_plist_path) {
84            if let Some(executable) = info_plist.get_string("CFBundleExecutable") {
85                let candidate = app_bundle_path.join(executable);
86                if candidate.exists() {
87                    return Some(candidate);
88                }
89            }
90        }
91    }
92
93    let app_name = app_bundle_path
94        .file_name()
95        .and_then(|n| n.to_str())
96        .unwrap_or("")
97        .trim_end_matches(".app");
98
99    if app_name.is_empty() {
100        return None;
101    }
102
103    let fallback = app_bundle_path.join(app_name);
104    fallback.exists().then_some(fallback)
105}
106
107fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
108    let bytes = std::fs::read(path)?;
109    let mut scan = UsageScan::default();
110
111    for (signature, requirement) in SIGNATURES {
112        if contains_subslice(&bytes, signature.as_bytes()) {
113            scan.evidence.insert(*signature);
114            match requirement {
115                Requirement::Key(key) => {
116                    scan.required_keys.insert(*key);
117                }
118                Requirement::PrivacyCategory(cat) => {
119                    scan.privacy_categories.insert(*cat);
120                }
121                Requirement::AnyLocation => {
122                    scan.requires_location_key = true;
123                }
124            }
125        }
126    }
127
128    Ok(scan)
129}
130
131fn scan_private_api_from_executable(path: &Path) -> Result<PrivateApiScan, UsageScanError> {
132    let bytes = std::fs::read(path)?;
133    let mut scan = PrivateApiScan::default();
134
135    for signature in PRIVATE_API_SIGNATURES {
136        if contains_subslice(&bytes, signature.as_bytes()) {
137            scan.hits.push(*signature);
138        }
139    }
140
141    scan.hits.sort_unstable();
142    scan.hits.dedup();
143
144    Ok(scan)
145}
146
147fn scan_sdks_from_executable(path: &Path) -> Result<SdkScan, UsageScanError> {
148    let bytes = std::fs::read(path)?;
149    let mut scan = SdkScan::default();
150
151    for signature in SDK_SIGNATURES {
152        if contains_subslice(&bytes, signature.as_bytes()) {
153            scan.hits.push(*signature);
154        }
155    }
156
157    scan.hits.sort_unstable();
158    scan.hits.dedup();
159
160    Ok(scan)
161}
162
163fn scan_capabilities_from_executable(path: &Path) -> Result<CapabilityScan, UsageScanError> {
164    let bytes = std::fs::read(path)?;
165    let mut scan = CapabilityScan::default();
166
167    for (signature, capability) in CAPABILITY_SIGNATURES {
168        if contains_subslice(&bytes, signature.as_bytes()) {
169            scan.evidence.insert(*signature);
170            scan.detected.insert(*capability);
171        }
172    }
173
174    Ok(scan)
175}
176
177fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
178    haystack
179        .windows(needle.len())
180        .any(|window| window == needle)
181}
182
183#[derive(Clone, Copy)]
184enum Requirement {
185    Key(&'static str),
186    PrivacyCategory(&'static str),
187    AnyLocation,
188}
189
190const SIGNATURES: &[(&str, Requirement)] = &[
191    (
192        "AVCaptureDevice",
193        Requirement::Key("NSCameraUsageDescription"),
194    ),
195    (
196        "AVAudioSession",
197        Requirement::Key("NSMicrophoneUsageDescription"),
198    ),
199    (
200        "AVAudioRecorder",
201        Requirement::Key("NSMicrophoneUsageDescription"),
202    ),
203    (
204        "PHPhotoLibrary",
205        Requirement::Key("NSPhotoLibraryUsageDescription"),
206    ),
207    (
208        "PHPhotoLibraryAddOnly",
209        Requirement::Key("NSPhotoLibraryAddUsageDescription"),
210    ),
211    ("CLLocationManager", Requirement::AnyLocation),
212    (
213        "CBCentralManager",
214        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
215    ),
216    (
217        "CBPeripheralManager",
218        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
219    ),
220    (
221        "CBPeripheral",
222        Requirement::Key("NSBluetoothPeripheralUsageDescription"),
223    ),
224    ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
225    (
226        "EKEventStore",
227        Requirement::Key("NSCalendarsUsageDescription"),
228    ),
229    (
230        "EKReminder",
231        Requirement::Key("NSRemindersUsageDescription"),
232    ),
233    (
234        "CNContactStore",
235        Requirement::Key("NSContactsUsageDescription"),
236    ),
237    (
238        "SFSpeechRecognizer",
239        Requirement::Key("NSSpeechRecognitionUsageDescription"),
240    ),
241    (
242        "CMMotionManager",
243        Requirement::Key("NSMotionUsageDescription"),
244    ),
245    ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
246    (
247        "MPMediaLibrary",
248        Requirement::Key("NSAppleMusicUsageDescription"),
249    ),
250    (
251        "HKHealthStore",
252        Requirement::Key("NSHealthShareUsageDescription"),
253    ),
254    // Required Reason APIs (2024 Mandate)
255    (
256        "systemBootTime",
257        Requirement::PrivacyCategory("NSPrivacyAccessedAPICategorySystemBootTime"),
258    ),
259    (
260        "diskSpace",
261        Requirement::PrivacyCategory("NSPrivacyAccessedAPICategoryDiskSpace"),
262    ),
263    (
264        "activeInputModes",
265        Requirement::PrivacyCategory("NSPrivacyAccessedAPICategoryActiveInputModes"),
266    ),
267    (
268        "userDefaults",
269        Requirement::PrivacyCategory("NSPrivacyAccessedAPICategoryUserDefaults"),
270    ),
271    (
272        "fileModificationDate",
273        Requirement::PrivacyCategory("NSPrivacyAccessedAPICategoryFileModificationDate"),
274    ),
275];
276
277const PRIVATE_API_SIGNATURES: &[&str] = &[
278    "LSApplicationWorkspace",
279    "LSApplicationProxy",
280    "LSAppWorkspace",
281    "SBApplication",
282    "SpringBoard",
283    "MobileGestalt",
284    "UICallApplication",
285    "UIGetScreenImage",
286    "_MGCopyAnswer",
287];
288
289const SDK_SIGNATURES: &[&str] = &[
290    "FirebaseApp",
291    "FIRApp",
292    "GADMobileAds",
293    "FBSDKCoreKit",
294    "FBSDKLoginKit",
295    "Amplitude",
296    "Mixpanel",
297    "Segment",
298    "SentrySDK",
299    "AppsFlyerLib",
300    "Adjust",
301];
302
303const CAPABILITY_SIGNATURES: &[(&str, &str)] = &[
304    ("AVCaptureDevice", "camera"),
305    ("CLLocationManager", "location"),
306];
307
308const INSTRUMENTATION_SIGNATURES: &[&str] = &[
309    "__llvm_profile_runtime",
310    "__llvm_prf_data",
311    "__llvm_prf_names",
312    "__llvm_prf_vnds",
313];
314
315#[cfg(test)]
316mod tests {
317    use super::{resolve_executable_path, scan_usage_from_app_bundle};
318    use plist::{Dictionary, Value};
319    use tempfile::tempdir;
320
321    #[test]
322    fn resolves_executable_from_cf_bundle_executable_before_bundle_name() {
323        let dir = tempdir().expect("temp dir");
324        let app_path = dir.path().join("CustomName.app");
325        std::fs::create_dir_all(&app_path).expect("create app dir");
326
327        let mut dict = Dictionary::new();
328        dict.insert(
329            "CFBundleExecutable".to_string(),
330            Value::String("RunnerBinary".to_string()),
331        );
332        Value::Dictionary(dict)
333            .to_file_xml(app_path.join("Info.plist"))
334            .expect("write plist");
335        std::fs::write(app_path.join("RunnerBinary"), b"plain binary").expect("write executable");
336
337        let resolved = resolve_executable_path(&app_path).expect("resolved executable");
338        assert_eq!(resolved, app_path.join("RunnerBinary"));
339    }
340
341    #[test]
342    fn usage_scan_reads_custom_executable_name_from_info_plist() {
343        let dir = tempdir().expect("temp dir");
344        let app_path = dir.path().join("CustomName.app");
345        std::fs::create_dir_all(&app_path).expect("create app dir");
346
347        let mut dict = Dictionary::new();
348        dict.insert(
349            "CFBundleExecutable".to_string(),
350            Value::String("RunnerBinary".to_string()),
351        );
352        Value::Dictionary(dict)
353            .to_file_xml(app_path.join("Info.plist"))
354            .expect("write plist");
355        std::fs::write(app_path.join("RunnerBinary"), b"AVCaptureDevice")
356            .expect("write executable");
357
358        let scan = scan_usage_from_app_bundle(&app_path).expect("usage scan");
359        assert!(scan.required_keys.contains("NSCameraUsageDescription"));
360        assert!(scan.evidence.contains("AVCaptureDevice"));
361    }
362}