verifyos_cli/parsers/
macho_scanner.rs1use 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];