1use {
16 crate::{
17 cryptography::DigestType, embedded_signature::EmbeddedSignature, error::AppleCodesignError,
18 },
19 goblin::mach::{
20 constants::{SEG_LINKEDIT, SEG_TEXT},
21 header::MH_EXECUTE,
22 load_command::{
23 CommandVariant, LinkeditDataCommand, LC_BUILD_VERSION, SIZEOF_LINKEDIT_DATA_COMMAND,
24 },
25 parse_magic_and_ctx,
26 segment::Segment,
27 Mach, MachO, SingleArch,
28 },
29 rayon::prelude::*,
30 scroll::Pread,
31};
32
33pub struct MachOBinary<'a> {
35 pub index: Option<usize>,
39
40 pub macho: MachO<'a>,
42
43 pub data: &'a [u8],
45}
46
47impl<'a> MachOBinary<'a> {
48 pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
50 let macho = MachO::parse(data, 0)?;
51
52 Ok(Self {
53 index: None,
54 macho,
55 data,
56 })
57 }
58}
59
60impl<'a> MachOBinary<'a> {
61 pub fn linkedit_index_and_segment(&self) -> Option<(usize, &Segment<'a>)> {
63 self.macho
64 .segments
65 .iter()
66 .enumerate()
67 .find(|(_, segment)| matches!(segment.name(), Ok(SEG_LINKEDIT)))
68 }
69
70 pub fn linkedit_segment(&self) -> Option<&Segment<'a>> {
72 self.linkedit_index_and_segment().map(|(_, x)| x)
73 }
74
75 pub fn linkedit_segment_assert_last(&self) -> Result<&Segment<'a>, AppleCodesignError> {
77 let last_segment = self
78 .segments_by_file_offset()
79 .last()
80 .copied()
81 .ok_or(AppleCodesignError::MissingLinkedit)?;
82
83 if !matches!(last_segment.name(), Ok(SEG_LINKEDIT)) {
84 Err(AppleCodesignError::LinkeditNotLast)
85 } else {
86 Ok(last_segment)
87 }
88 }
89
90 pub fn find_signature_data(
98 &self,
99 ) -> Result<Option<MachOSignatureData<'a>>, AppleCodesignError> {
100 if let Some(linkedit_data_command) = self.code_signature_load_command() {
101 let (linkedit_segment_index, linkedit) = self
103 .linkedit_index_and_segment()
104 .ok_or(AppleCodesignError::MissingLinkedit)?;
105
106 let linkedit_segment_start_offset = linkedit.fileoff as usize;
107 let linkedit_segment_end_offset = linkedit_segment_start_offset + linkedit.data.len();
108 let signature_file_start_offset = linkedit_data_command.dataoff as usize;
109 let signature_file_end_offset =
110 signature_file_start_offset + linkedit_data_command.datasize as usize;
111 let signature_segment_start_offset =
112 linkedit_data_command.dataoff as usize - linkedit.fileoff as usize;
113 let signature_segment_end_offset =
114 signature_segment_start_offset + linkedit_data_command.datasize as usize;
115
116 let signature_data =
117 &linkedit.data[signature_segment_start_offset..signature_segment_end_offset];
118
119 Ok(Some(MachOSignatureData {
120 linkedit_segment_index,
121 linkedit_segment_start_offset,
122 linkedit_segment_end_offset,
123 signature_file_start_offset,
124 signature_file_end_offset,
125 signature_segment_start_offset,
126 signature_segment_end_offset,
127 linkedit_segment_data: linkedit.data,
128 signature_data,
129 }))
130 } else {
131 Ok(None)
132 }
133 }
134
135 pub fn code_signature(&self) -> Result<Option<EmbeddedSignature>, AppleCodesignError> {
140 if let Some(signature) = self.find_signature_data()? {
141 Ok(Some(EmbeddedSignature::from_bytes(
142 signature.signature_data,
143 )?))
144 } else {
145 Ok(None)
146 }
147 }
148
149 pub fn executable_segment_boundary(&self) -> Result<(u64, u64), AppleCodesignError> {
151 let segment = self
152 .macho
153 .segments
154 .iter()
155 .find(|segment| matches!(segment.name(), Ok(SEG_TEXT)))
156 .ok_or_else(|| AppleCodesignError::InvalidBinary("no __TEXT segment".into()))?;
157
158 Ok((segment.fileoff, segment.fileoff + segment.data.len() as u64))
159 }
160
161 pub fn is_executable(&self) -> bool {
163 self.macho.header.filetype == MH_EXECUTE
164 }
165
166 pub fn code_signature_linkedit_start_offset(&self) -> Option<u32> {
168 let segment = self.linkedit_segment();
169
170 if let (Some(segment), Some(command)) = (segment, self.code_signature_load_command()) {
171 Some((command.dataoff as u64 - segment.fileoff) as u32)
172 } else {
173 None
174 }
175 }
176
177 pub fn code_signature_linkedit_end_offset(&self) -> Option<u32> {
179 let start_offset = self.code_signature_linkedit_start_offset()?;
180
181 self.code_signature_load_command()
182 .map(|command| start_offset + command.datasize)
183 }
184
185 pub fn segments_by_file_offset(&self) -> Vec<&Segment<'a>> {
190 let mut segments = self.macho.segments.iter().collect::<Vec<_>>();
191
192 segments.sort_by(|a, b| a.fileoff.cmp(&b.fileoff));
193
194 segments
195 }
196
197 pub fn code_limit_binary_offset(&self) -> Result<u64, AppleCodesignError> {
202 let last_segment = self.linkedit_segment_assert_last()?;
203
204 if let Some(offset) = self.code_signature_linkedit_start_offset() {
205 Ok(last_segment.fileoff + offset as u64)
206 } else {
207 Ok(last_segment.fileoff + last_segment.data.len() as u64)
208 }
209 }
210
211 pub fn linkedit_data_before_signature(&self) -> Option<&[u8]> {
215 let segment = self.linkedit_segment();
216
217 if let Some(segment) = segment {
218 if let Some(offset) = self.code_signature_linkedit_start_offset() {
219 Some(&segment.data[0..offset as usize])
220 } else {
221 Some(segment.data)
222 }
223 } else {
224 None
225 }
226 }
227
228 pub fn digested_code_data(&self) -> Result<&[u8], AppleCodesignError> {
232 let code_limit = self.code_limit_binary_offset()?;
233
234 Ok(&self.data[0..code_limit as _])
235 }
236
237 pub fn code_digests_size(
239 &self,
240 digest: DigestType,
241 page_size: usize,
242 ) -> Result<usize, AppleCodesignError> {
243 let empty = digest.digest_data(b"")?;
244
245 Ok(self.digested_code_data()?.chunks(page_size).count() * empty.len())
246 }
247
248 pub fn code_digests(
250 &self,
251 digest: DigestType,
252 page_size: usize,
253 ) -> Result<Vec<Vec<u8>>, AppleCodesignError> {
254 let data = self.digested_code_data()?;
255
256 if data.len() > 64 * 1024 * 1024 {
259 data.par_chunks(page_size)
260 .map(|c| digest.digest_data(c))
261 .collect::<Result<Vec<_>, AppleCodesignError>>()
262 } else {
263 self.digested_code_data()?
264 .chunks(page_size)
265 .map(|chunk| digest.digest_data(chunk))
266 .collect::<Result<Vec<_>, AppleCodesignError>>()
267 }
268 }
269
270 pub fn code_signature_load_command(&self) -> Option<LinkeditDataCommand> {
272 self.macho.load_commands.iter().find_map(|lc| {
273 if let CommandVariant::CodeSignature(command) = lc.command {
274 Some(command)
275 } else {
276 None
277 }
278 })
279 }
280
281 pub fn embedded_info_plist(&self) -> Result<Option<Vec<u8>>, AppleCodesignError> {
283 for segment in &self.macho.segments {
286 if matches!(segment.name(), Ok(SEG_TEXT)) {
287 for (section, data) in segment.sections()? {
288 if matches!(section.name(), Ok("__info_plist")) {
289 return Ok(Some(data.to_vec()));
290 }
291 }
292 }
293 }
294
295 Ok(None)
296 }
297
298 pub fn check_signing_capability(&self) -> Result<(), AppleCodesignError> {
311 let last_segment = self.linkedit_segment_assert_last()?;
312
313 if let Some(offset) = self.code_signature_linkedit_end_offset() {
323 if offset as usize == last_segment.data.len() {
324 Ok(())
325 } else {
326 Err(AppleCodesignError::DataAfterSignature)
327 }
328 } else {
329 let last_load_command = self
330 .macho
331 .load_commands
332 .iter()
333 .last()
334 .ok_or_else(|| AppleCodesignError::InvalidBinary("no load commands".into()))?;
335
336 let first_section = self
337 .macho
338 .segments
339 .iter()
340 .map(|segment| segment.sections())
341 .collect::<Result<Vec<_>, _>>()?
342 .into_iter()
343 .flatten()
344 .next()
345 .ok_or_else(|| AppleCodesignError::InvalidBinary("no sections".into()))?;
346
347 let load_commands_end_offset =
348 last_load_command.offset + last_load_command.command.cmdsize();
349
350 if first_section.0.offset as usize - load_commands_end_offset
351 >= SIZEOF_LINKEDIT_DATA_COMMAND
352 {
353 Ok(())
354 } else {
355 Err(AppleCodesignError::LoadCommandNoRoom)
356 }
357 }
358 }
359
360 pub fn find_targeting(&self) -> Result<Option<MachoTarget>, AppleCodesignError> {
362 let ctx = parse_magic_and_ctx(self.data, 0)?
363 .1
364 .expect("context should have been parsed before");
365
366 for lc in &self.macho.load_commands {
367 if lc.command.cmd() == LC_BUILD_VERSION {
368 let build_version = self
369 .data
370 .pread_with::<BuildVersionCommand>(lc.offset, ctx.le)?;
371
372 return Ok(Some(MachoTarget {
373 platform: build_version.platform.into(),
374 minimum_os_version: parse_version_nibbles(build_version.minos),
375 sdk_version: parse_version_nibbles(build_version.sdk),
376 }));
377 }
378 }
379
380 for lc in &self.macho.load_commands {
381 let command = match lc.command {
382 CommandVariant::VersionMinMacosx(c) => Some((c, Platform::MacOs)),
383 CommandVariant::VersionMinIphoneos(c) => Some((c, Platform::IOs)),
384 CommandVariant::VersionMinTvos(c) => Some((c, Platform::TvOs)),
385 CommandVariant::VersionMinWatchos(c) => Some((c, Platform::WatchOs)),
386 _ => None,
387 };
388
389 if let Some((command, platform)) = command {
390 return Ok(Some(MachoTarget {
391 platform,
392 minimum_os_version: parse_version_nibbles(command.version),
393 sdk_version: parse_version_nibbles(command.sdk),
394 }));
395 }
396 }
397
398 Ok(None)
399 }
400}
401
402pub struct MachOSignatureData<'a> {
404 pub linkedit_segment_index: usize,
406
407 pub linkedit_segment_start_offset: usize,
409
410 pub linkedit_segment_end_offset: usize,
412
413 pub signature_file_start_offset: usize,
415
416 pub signature_file_end_offset: usize,
418
419 pub signature_segment_start_offset: usize,
421
422 pub signature_segment_end_offset: usize,
424
425 pub linkedit_segment_data: &'a [u8],
427
428 pub signature_data: &'a [u8],
430}
431
432#[derive(Clone, Debug, Pread)]
434pub struct BuildVersionCommand {
435 pub cmd: u32,
437 pub cmdsize: u32,
441 pub platform: u32,
443 pub minos: u32,
447 pub sdk: u32,
451 pub ntools: u32,
453}
454
455#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457pub enum Platform {
458 MacOs,
459 IOs,
460 TvOs,
461 WatchOs,
462 BridgeOs,
463 MacCatalyst,
464 IosSimulator,
465 TvOsSimulator,
466 WatchOsSimulator,
467 DriverKit,
468 Unknown(u32),
469}
470
471impl std::fmt::Display for Platform {
472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 match self {
474 Self::MacOs => f.write_str("macOS"),
475 Self::IOs => f.write_str("iOS"),
476 Self::TvOs => f.write_str("tvOS"),
477 Self::WatchOs => f.write_str("watchOS"),
478 Self::BridgeOs => f.write_str("bridgeOS"),
479 Self::MacCatalyst => f.write_str("macCatalyst"),
480 Self::IosSimulator => f.write_str("iOSSimulator"),
481 Self::TvOsSimulator => f.write_str("tvOSSimulator"),
482 Self::WatchOsSimulator => f.write_str("watchOSSimulator"),
483 Self::DriverKit => f.write_str("driverKit"),
484 Self::Unknown(v) => f.write_fmt(format_args!("Unknown ({v})")),
485 }
486 }
487}
488
489impl From<u32> for Platform {
490 fn from(v: u32) -> Self {
491 match v {
492 1 => Self::MacOs,
493 2 => Self::IOs,
494 3 => Self::TvOs,
495 4 => Self::WatchOs,
496 5 => Self::BridgeOs,
497 6 => Self::MacCatalyst,
498 7 => Self::IosSimulator,
499 8 => Self::TvOsSimulator,
500 9 => Self::WatchOsSimulator,
501 10 => Self::DriverKit,
502 _ => Self::Unknown(v),
503 }
504 }
505}
506
507impl From<Platform> for u32 {
508 fn from(val: Platform) -> Self {
509 match val {
510 Platform::MacOs => 1,
511 Platform::IOs => 2,
512 Platform::TvOs => 3,
513 Platform::WatchOs => 4,
514 Platform::BridgeOs => 5,
515 Platform::MacCatalyst => 6,
516 Platform::IosSimulator => 7,
517 Platform::TvOsSimulator => 8,
518 Platform::WatchOsSimulator => 9,
519 Platform::DriverKit => 10,
520 Platform::Unknown(v) => v,
521 }
522 }
523}
524
525impl Platform {
526 pub fn sha256_digest_support(&self) -> Result<semver::VersionReq, AppleCodesignError> {
528 let version = match self {
529 Self::MacOs => ">=10.11.4",
531 Self::IOs | Self::TvOs => ">=11.0.0",
533 Self::WatchOs => ">9999",
535 Self::Unknown(0) => ">9999",
537 _ => "*",
539 };
540
541 Ok(semver::VersionReq::parse(version)?)
542 }
543}
544
545pub struct MachoTarget {
547 pub platform: Platform,
549 pub minimum_os_version: semver::Version,
551 pub sdk_version: semver::Version,
553}
554
555impl MachoTarget {
556 pub fn to_build_version_command_vec(&self, endian: object::Endianness) -> Vec<u8> {
558 let command = object::macho::BuildVersionCommand {
559 cmd: object::U32::new(endian, object::macho::LC_BUILD_VERSION),
560 cmdsize: object::U32::new(
561 endian,
562 std::mem::size_of::<object::macho::BuildVersionCommand<object::Endianness>>() as _,
563 ),
564 platform: object::U32::new(endian, self.platform.into()),
565 minos: object::U32::new(
566 endian,
567 semver_to_macho_target_version(&self.minimum_os_version),
568 ),
569 sdk: object::U32::new(endian, semver_to_macho_target_version(&self.sdk_version)),
570 ntools: object::U32::new(endian, 0),
571 };
572
573 object::bytes_of(&command).to_vec()
574 }
575}
576
577pub fn parse_version_nibbles(v: u32) -> semver::Version {
579 let major = v >> 16;
580 let minor = v << 16 >> 24;
581 let patch = v & 0xff;
582
583 semver::Version::new(major as _, minor as _, patch as _)
584}
585
586pub fn semver_to_macho_target_version(version: &semver::Version) -> u32 {
588 let major = version.major as u32;
589 let minor = version.minor as u32;
590 let patch = version.patch as u32;
591
592 (major << 16) | ((minor & 0xff) << 8) | (patch & 0xff)
593}
594
595pub struct MachFile<'a> {
597 #[allow(unused)]
598 data: &'a [u8],
599
600 machos: Vec<MachOBinary<'a>>,
601}
602
603impl<'a> MachFile<'a> {
604 pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
606 let mach = Mach::parse(data)?;
607
608 let machos = match mach {
609 Mach::Binary(macho) => vec![MachOBinary {
610 index: None,
611 macho,
612 data,
613 }],
614 Mach::Fat(multiarch) => {
615 let mut machos = vec![];
616
617 for (index, arch) in multiarch.arches()?.into_iter().enumerate() {
618 let macho = match multiarch.get(index)? {
619 SingleArch::MachO(m) => m,
620 SingleArch::Archive(_) => continue,
621 };
622
623 machos.push(MachOBinary {
624 index: Some(index),
625 macho,
626 data: arch.slice(data),
627 });
628 }
629
630 machos
631 }
632 };
633
634 Ok(Self { data, machos })
635 }
636
637 pub fn is_fat(&self) -> bool {
639 self.machos.len() > 1
640 }
641
642 pub fn iter_macho(&self) -> impl Iterator<Item = &MachOBinary> {
646 self.machos.iter()
647 }
648
649 pub fn nth_macho(&self, index: usize) -> Result<&MachOBinary<'a>, AppleCodesignError> {
650 self.machos
651 .get(index)
652 .ok_or(AppleCodesignError::InvalidMachOIndex(index))
653 }
654}
655
656impl<'a> IntoIterator for MachFile<'a> {
657 type Item = MachOBinary<'a>;
658 type IntoIter = std::vec::IntoIter<Self::Item>;
659
660 fn into_iter(self) -> Self::IntoIter {
661 self.machos.into_iter()
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use {
668 super::*,
669 crate::embedded_signature::Blob,
670 std::{
671 io::Read,
672 path::{Path, PathBuf},
673 },
674 };
675
676 const MACHO_UNIVERSAL_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
677 const MACHO_64BIT_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf];
678
679 fn find_likely_macho_files(path: &Path) -> Vec<PathBuf> {
683 let mut res = Vec::new();
684
685 let dir = std::fs::read_dir(path).unwrap();
686
687 for entry in dir {
688 let entry = entry.unwrap();
689
690 if let Ok(mut fh) = std::fs::File::open(entry.path()) {
691 let mut magic = [0; 4];
692
693 if let Ok(size) = fh.read(&mut magic) {
694 if size == 4 && (magic == MACHO_UNIVERSAL_MAGIC || magic == MACHO_64BIT_MAGIC) {
695 res.push(entry.path());
696 }
697 }
698 }
699 }
700
701 res
702 }
703
704 fn find_apple_embedded_signature<'a>(macho: &'a MachOBinary) -> Option<EmbeddedSignature<'a>> {
705 if let Ok(Some(signature)) = macho.code_signature() {
706 Some(signature)
707 } else {
708 None
709 }
710 }
711
712 fn validate_macho(path: &Path, macho: &MachOBinary) {
713 if let Some(signature) = find_apple_embedded_signature(macho) {
715 for blob in &signature.blobs {
717 match blob.clone().into_parsed_blob() {
718 Ok(parsed) => {
719 match parsed.blob.to_blob_bytes() {
721 Ok(serialized) => {
722 if serialized != blob.data {
723 println!("blob serialization roundtrip failure on {}: index {}, magic {:?}",
724 path.display(),
725 blob.index,
726 blob.magic,
727 );
728 }
729 }
730 Err(e) => {
731 println!(
732 "blob serialization failure on {}; index {}, magic {:?}: {:?}",
733 path.display(),
734 blob.index,
735 blob.magic,
736 e
737 );
738 }
739 }
740 }
741 Err(e) => {
742 println!(
743 "blob parse failure on {}; index {}, magic {:?}: {:?}",
744 path.display(),
745 blob.index,
746 blob.magic,
747 e
748 );
749 }
750 }
751 }
752
753 if matches!(signature.signature_data(), Ok(Some(_))) {
755 match signature.signed_data() {
756 Ok(Some(signed_data)) => {
757 for signer in signed_data.signers() {
758 if let Err(e) = signer.verify_signature_with_signed_data(&signed_data) {
759 println!(
760 "signature verification failed for {}: {}",
761 path.display(),
762 e
763 );
764 }
765
766 if let Ok(()) =
767 signer.verify_message_digest_with_signed_data(&signed_data)
768 {
769 println!(
770 "message digest verification unexpectedly correct for {}",
771 path.display()
772 );
773 }
774 }
775 }
776 Ok(None) => {
777 eprintln!(
780 "{} has a signature blob without CMS data; weird",
781 path.display()
782 );
783 }
784 Err(e) => {
785 println!("error performing CMS parse of {}: {:?}", path.display(), e);
786 }
787 }
788 }
789 }
790 }
791
792 fn validate_macho_in_dir(dir: &Path) {
793 for path in find_likely_macho_files(dir).into_iter() {
794 if let Ok(file_data) = std::fs::read(&path) {
795 if let Ok(mach) = MachFile::parse(&file_data) {
796 for macho in mach.into_iter() {
797 validate_macho(&path, &macho);
798 }
799 }
800 }
801 }
802 }
803
804 #[test]
805 fn parse_applications_macho_signatures() {
806 if let Ok(dir) = std::fs::read_dir("/Applications") {
810 for entry in dir {
811 let entry = entry.unwrap();
812
813 let search_dir = entry.path().join("Contents").join("MacOS");
814
815 if search_dir.exists() {
816 validate_macho_in_dir(&search_dir);
817 }
818 }
819 }
820
821 for dir in &["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
822 let dir = PathBuf::from(dir);
823
824 if dir.exists() {
825 validate_macho_in_dir(&dir);
826 }
827 }
828 }
829
830 #[test]
831 fn version_nibbles() {
832 assert_eq!(
833 parse_version_nibbles(12 << 16 | 1 << 8 | 2),
834 semver::Version::new(12, 1, 2)
835 );
836 assert_eq!(
837 parse_version_nibbles(11 << 16 | 10 << 8 | 15),
838 semver::Version::new(11, 10, 15)
839 );
840 assert_eq!(
841 semver_to_macho_target_version(&semver::Version::new(12, 1, 2)),
842 12 << 16 | 1 << 8 | 2
843 );
844 }
845}