Skip to main content

verifyos_cli/parsers/
macho_scanner.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, thiserror::Error)]
5pub enum UsageScanError {
6    #[error("Failed to read executable: {0}")]
7    Io(#[from] std::io::Error),
8    #[error("Unable to resolve app executable")]
9    MissingExecutable,
10}
11
12#[derive(Debug, Default)]
13pub struct UsageScan {
14    pub required_keys: HashSet<&'static str>,
15    pub requires_location_key: bool,
16    pub evidence: HashSet<&'static str>,
17}
18
19pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
20    let executable =
21        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
22    scan_usage_from_executable(&executable)
23}
24
25#[derive(Debug, Default)]
26pub struct PrivateApiScan {
27    pub hits: Vec<&'static str>,
28}
29
30pub fn scan_private_api_from_app_bundle(
31    app_bundle_path: &Path,
32) -> Result<PrivateApiScan, UsageScanError> {
33    let executable =
34        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
35    scan_private_api_from_executable(&executable)
36}
37
38fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
39    let app_name = app_bundle_path
40        .file_name()
41        .and_then(|n| n.to_str())
42        .unwrap_or("")
43        .trim_end_matches(".app");
44
45    if app_name.is_empty() {
46        return None;
47    }
48
49    let executable_path = app_bundle_path.join(app_name);
50    if executable_path.exists() {
51        Some(executable_path)
52    } else {
53        None
54    }
55}
56
57fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
58    let bytes = std::fs::read(path)?;
59    let mut scan = UsageScan::default();
60
61    for (signature, requirement) in SIGNATURES {
62        if contains_subslice(&bytes, signature.as_bytes()) {
63            scan.evidence.insert(*signature);
64            match requirement {
65                Requirement::Key(key) => {
66                    scan.required_keys.insert(*key);
67                }
68                Requirement::AnyLocation => {
69                    scan.requires_location_key = true;
70                }
71            }
72        }
73    }
74
75    Ok(scan)
76}
77
78fn scan_private_api_from_executable(path: &Path) -> Result<PrivateApiScan, UsageScanError> {
79    let bytes = std::fs::read(path)?;
80    let mut scan = PrivateApiScan::default();
81
82    for signature in PRIVATE_API_SIGNATURES {
83        if contains_subslice(&bytes, signature.as_bytes()) {
84            scan.hits.push(*signature);
85        }
86    }
87
88    scan.hits.sort_unstable();
89    scan.hits.dedup();
90
91    Ok(scan)
92}
93
94fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
95    haystack
96        .windows(needle.len())
97        .any(|window| window == needle)
98}
99
100#[derive(Clone, Copy)]
101enum Requirement {
102    Key(&'static str),
103    AnyLocation,
104}
105
106const SIGNATURES: &[(&str, Requirement)] = &[
107    (
108        "AVCaptureDevice",
109        Requirement::Key("NSCameraUsageDescription"),
110    ),
111    (
112        "AVAudioSession",
113        Requirement::Key("NSMicrophoneUsageDescription"),
114    ),
115    (
116        "AVAudioRecorder",
117        Requirement::Key("NSMicrophoneUsageDescription"),
118    ),
119    (
120        "PHPhotoLibrary",
121        Requirement::Key("NSPhotoLibraryUsageDescription"),
122    ),
123    (
124        "PHPhotoLibraryAddOnly",
125        Requirement::Key("NSPhotoLibraryAddUsageDescription"),
126    ),
127    ("CLLocationManager", Requirement::AnyLocation),
128    (
129        "CBCentralManager",
130        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
131    ),
132    (
133        "CBPeripheralManager",
134        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
135    ),
136    (
137        "CBPeripheral",
138        Requirement::Key("NSBluetoothPeripheralUsageDescription"),
139    ),
140    ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
141    (
142        "EKEventStore",
143        Requirement::Key("NSCalendarsUsageDescription"),
144    ),
145    (
146        "EKReminder",
147        Requirement::Key("NSRemindersUsageDescription"),
148    ),
149    (
150        "CNContactStore",
151        Requirement::Key("NSContactsUsageDescription"),
152    ),
153    (
154        "SFSpeechRecognizer",
155        Requirement::Key("NSSpeechRecognitionUsageDescription"),
156    ),
157    (
158        "CMMotionManager",
159        Requirement::Key("NSMotionUsageDescription"),
160    ),
161    ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
162    (
163        "MPMediaLibrary",
164        Requirement::Key("NSAppleMusicUsageDescription"),
165    ),
166    (
167        "HKHealthStore",
168        Requirement::Key("NSHealthShareUsageDescription"),
169    ),
170];
171
172const PRIVATE_API_SIGNATURES: &[&str] = &[
173    "LSApplicationWorkspace",
174    "LSApplicationProxy",
175    "LSAppWorkspace",
176    "SBApplication",
177    "SpringBoard",
178    "MobileGestalt",
179    "UICallApplication",
180    "UIGetScreenImage",
181    "_MGCopyAnswer",
182];