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
19#[derive(Debug, Default)]
20pub struct CapabilityScan {
21    pub detected: HashSet<&'static str>,
22    pub evidence: HashSet<&'static str>,
23}
24
25pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
26    let executable =
27        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
28    scan_usage_from_executable(&executable)
29}
30
31#[derive(Debug, Default)]
32pub struct PrivateApiScan {
33    pub hits: Vec<&'static str>,
34}
35
36#[derive(Debug, Default)]
37pub struct SdkScan {
38    pub hits: Vec<&'static str>,
39}
40
41pub fn scan_private_api_from_app_bundle(
42    app_bundle_path: &Path,
43) -> Result<PrivateApiScan, UsageScanError> {
44    let executable =
45        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
46    scan_private_api_from_executable(&executable)
47}
48
49pub fn scan_sdks_from_app_bundle(app_bundle_path: &Path) -> Result<SdkScan, UsageScanError> {
50    let executable =
51        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
52    scan_sdks_from_executable(&executable)
53}
54
55pub fn scan_capabilities_from_app_bundle(
56    app_bundle_path: &Path,
57) -> Result<CapabilityScan, UsageScanError> {
58    let executable =
59        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
60    scan_capabilities_from_executable(&executable)
61}
62
63fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
64    let app_name = app_bundle_path
65        .file_name()
66        .and_then(|n| n.to_str())
67        .unwrap_or("")
68        .trim_end_matches(".app");
69
70    if app_name.is_empty() {
71        return None;
72    }
73
74    let executable_path = app_bundle_path.join(app_name);
75    if executable_path.exists() {
76        Some(executable_path)
77    } else {
78        None
79    }
80}
81
82fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
83    let bytes = std::fs::read(path)?;
84    let mut scan = UsageScan::default();
85
86    for (signature, requirement) in SIGNATURES {
87        if contains_subslice(&bytes, signature.as_bytes()) {
88            scan.evidence.insert(*signature);
89            match requirement {
90                Requirement::Key(key) => {
91                    scan.required_keys.insert(*key);
92                }
93                Requirement::AnyLocation => {
94                    scan.requires_location_key = true;
95                }
96            }
97        }
98    }
99
100    Ok(scan)
101}
102
103fn scan_private_api_from_executable(path: &Path) -> Result<PrivateApiScan, UsageScanError> {
104    let bytes = std::fs::read(path)?;
105    let mut scan = PrivateApiScan::default();
106
107    for signature in PRIVATE_API_SIGNATURES {
108        if contains_subslice(&bytes, signature.as_bytes()) {
109            scan.hits.push(*signature);
110        }
111    }
112
113    scan.hits.sort_unstable();
114    scan.hits.dedup();
115
116    Ok(scan)
117}
118
119fn scan_sdks_from_executable(path: &Path) -> Result<SdkScan, UsageScanError> {
120    let bytes = std::fs::read(path)?;
121    let mut scan = SdkScan::default();
122
123    for signature in SDK_SIGNATURES {
124        if contains_subslice(&bytes, signature.as_bytes()) {
125            scan.hits.push(*signature);
126        }
127    }
128
129    scan.hits.sort_unstable();
130    scan.hits.dedup();
131
132    Ok(scan)
133}
134
135fn scan_capabilities_from_executable(path: &Path) -> Result<CapabilityScan, UsageScanError> {
136    let bytes = std::fs::read(path)?;
137    let mut scan = CapabilityScan::default();
138
139    for (signature, capability) in CAPABILITY_SIGNATURES {
140        if contains_subslice(&bytes, signature.as_bytes()) {
141            scan.evidence.insert(*signature);
142            scan.detected.insert(*capability);
143        }
144    }
145
146    Ok(scan)
147}
148
149fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
150    haystack
151        .windows(needle.len())
152        .any(|window| window == needle)
153}
154
155#[derive(Clone, Copy)]
156enum Requirement {
157    Key(&'static str),
158    AnyLocation,
159}
160
161const SIGNATURES: &[(&str, Requirement)] = &[
162    (
163        "AVCaptureDevice",
164        Requirement::Key("NSCameraUsageDescription"),
165    ),
166    (
167        "AVAudioSession",
168        Requirement::Key("NSMicrophoneUsageDescription"),
169    ),
170    (
171        "AVAudioRecorder",
172        Requirement::Key("NSMicrophoneUsageDescription"),
173    ),
174    (
175        "PHPhotoLibrary",
176        Requirement::Key("NSPhotoLibraryUsageDescription"),
177    ),
178    (
179        "PHPhotoLibraryAddOnly",
180        Requirement::Key("NSPhotoLibraryAddUsageDescription"),
181    ),
182    ("CLLocationManager", Requirement::AnyLocation),
183    (
184        "CBCentralManager",
185        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
186    ),
187    (
188        "CBPeripheralManager",
189        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
190    ),
191    (
192        "CBPeripheral",
193        Requirement::Key("NSBluetoothPeripheralUsageDescription"),
194    ),
195    ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
196    (
197        "EKEventStore",
198        Requirement::Key("NSCalendarsUsageDescription"),
199    ),
200    (
201        "EKReminder",
202        Requirement::Key("NSRemindersUsageDescription"),
203    ),
204    (
205        "CNContactStore",
206        Requirement::Key("NSContactsUsageDescription"),
207    ),
208    (
209        "SFSpeechRecognizer",
210        Requirement::Key("NSSpeechRecognitionUsageDescription"),
211    ),
212    (
213        "CMMotionManager",
214        Requirement::Key("NSMotionUsageDescription"),
215    ),
216    ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
217    (
218        "MPMediaLibrary",
219        Requirement::Key("NSAppleMusicUsageDescription"),
220    ),
221    (
222        "HKHealthStore",
223        Requirement::Key("NSHealthShareUsageDescription"),
224    ),
225];
226
227const PRIVATE_API_SIGNATURES: &[&str] = &[
228    "LSApplicationWorkspace",
229    "LSApplicationProxy",
230    "LSAppWorkspace",
231    "SBApplication",
232    "SpringBoard",
233    "MobileGestalt",
234    "UICallApplication",
235    "UIGetScreenImage",
236    "_MGCopyAnswer",
237];
238
239const SDK_SIGNATURES: &[&str] = &[
240    "FirebaseApp",
241    "FIRApp",
242    "GADMobileAds",
243    "FBSDKCoreKit",
244    "FBSDKLoginKit",
245    "Amplitude",
246    "Mixpanel",
247    "Segment",
248    "SentrySDK",
249    "AppsFlyerLib",
250    "Adjust",
251];
252
253const CAPABILITY_SIGNATURES: &[(&str, &str)] = &[
254    ("AVCaptureDevice", "camera"),
255    ("CLLocationManager", "location"),
256];