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 iter_macho_mut(&mut self) -> impl Iterator<Item = &mut MachOBinary<'a>> + '_ {
650 self.machos.iter_mut()
651 }
652
653 pub fn nth_macho(&self, index: usize) -> Result<&MachOBinary<'a>, AppleCodesignError> {
654 self.machos
655 .get(index)
656 .ok_or(AppleCodesignError::InvalidMachOIndex(index))
657 }
658
659 pub fn nth_macho_mut(
660 &mut self,
661 index: usize,
662 ) -> Result<&mut MachOBinary<'a>, AppleCodesignError> {
663 self.machos
664 .get_mut(index)
665 .ok_or(AppleCodesignError::InvalidMachOIndex(index))
666 }
667}
668
669impl<'a> IntoIterator for MachFile<'a> {
670 type Item = MachOBinary<'a>;
671 type IntoIter = std::vec::IntoIter<Self::Item>;
672
673 fn into_iter(self) -> Self::IntoIter {
674 self.machos.into_iter()
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use {
681 super::*,
682 crate::embedded_signature::Blob,
683 std::{
684 io::Read,
685 path::{Path, PathBuf},
686 },
687 };
688
689 const MACHO_UNIVERSAL_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
690 const MACHO_64BIT_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf];
691
692 fn find_likely_macho_files(path: &Path) -> Vec<PathBuf> {
696 let mut res = Vec::new();
697
698 let dir = std::fs::read_dir(path).unwrap();
699
700 for entry in dir {
701 let entry = entry.unwrap();
702
703 if let Ok(mut fh) = std::fs::File::open(entry.path()) {
704 let mut magic = [0; 4];
705
706 if let Ok(size) = fh.read(&mut magic) {
707 if size == 4 && (magic == MACHO_UNIVERSAL_MAGIC || magic == MACHO_64BIT_MAGIC) {
708 res.push(entry.path());
709 }
710 }
711 }
712 }
713
714 res
715 }
716
717 fn find_apple_embedded_signature<'a>(macho: &'a MachOBinary) -> Option<EmbeddedSignature<'a>> {
718 if let Ok(Some(signature)) = macho.code_signature() {
719 Some(signature)
720 } else {
721 None
722 }
723 }
724
725 fn validate_macho(path: &Path, macho: &MachOBinary) {
726 if let Some(signature) = find_apple_embedded_signature(macho) {
728 for blob in &signature.blobs {
730 match blob.clone().into_parsed_blob() {
731 Ok(parsed) => {
732 match parsed.blob.to_blob_bytes() {
734 Ok(serialized) => {
735 if serialized != blob.data {
736 println!("blob serialization roundtrip failure on {}: index {}, magic {:?}",
737 path.display(),
738 blob.index,
739 blob.magic,
740 );
741 }
742 }
743 Err(e) => {
744 println!(
745 "blob serialization failure on {}; index {}, magic {:?}: {:?}",
746 path.display(),
747 blob.index,
748 blob.magic,
749 e
750 );
751 }
752 }
753 }
754 Err(e) => {
755 println!(
756 "blob parse failure on {}; index {}, magic {:?}: {:?}",
757 path.display(),
758 blob.index,
759 blob.magic,
760 e
761 );
762 }
763 }
764 }
765
766 if matches!(signature.signature_data(), Ok(Some(_))) {
768 match signature.signed_data() {
769 Ok(Some(signed_data)) => {
770 for signer in signed_data.signers() {
771 if let Err(e) = signer.verify_signature_with_signed_data(&signed_data) {
772 println!(
773 "signature verification failed for {}: {}",
774 path.display(),
775 e
776 );
777 }
778
779 if let Ok(()) =
780 signer.verify_message_digest_with_signed_data(&signed_data)
781 {
782 println!(
783 "message digest verification unexpectedly correct for {}",
784 path.display()
785 );
786 }
787 }
788 }
789 Ok(None) => {
790 eprintln!(
793 "{} has a signature blob without CMS data; weird",
794 path.display()
795 );
796 }
797 Err(e) => {
798 println!("error performing CMS parse of {}: {:?}", path.display(), e);
799 }
800 }
801 }
802 }
803 }
804
805 fn validate_macho_in_dir(dir: &Path) {
806 for path in find_likely_macho_files(dir).into_iter() {
807 if let Ok(file_data) = std::fs::read(&path) {
808 if let Ok(mach) = MachFile::parse(&file_data) {
809 for macho in mach.into_iter() {
810 validate_macho(&path, &macho);
811 }
812 }
813 }
814 }
815 }
816
817 #[test]
818 fn parse_applications_macho_signatures() {
819 if let Ok(dir) = std::fs::read_dir("/Applications") {
823 for entry in dir {
824 let entry = entry.unwrap();
825
826 let search_dir = entry.path().join("Contents").join("MacOS");
827
828 if search_dir.exists() {
829 validate_macho_in_dir(&search_dir);
830 }
831 }
832 }
833
834 for dir in &["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
835 let dir = PathBuf::from(dir);
836
837 if dir.exists() {
838 validate_macho_in_dir(&dir);
839 }
840 }
841 }
842
843 #[test]
844 fn version_nibbles() {
845 assert_eq!(
846 parse_version_nibbles(12 << 16 | 1 << 8 | 2),
847 semver::Version::new(12, 1, 2)
848 );
849 assert_eq!(
850 parse_version_nibbles(11 << 16 | 10 << 8 | 15),
851 semver::Version::new(11, 10, 15)
852 );
853 assert_eq!(
854 semver_to_macho_target_version(&semver::Version::new(12, 1, 2)),
855 12 << 16 | 1 << 8 | 2
856 );
857 }
858}