Skip to main content

malwaredb_types/exec/macho/
fat.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::exec::{macho::Macho, Architecture, ExecutableFile, OperatingSystem, Sections};
4use crate::utils::{bytes_offset_match, u32_from_offset, EntropyCalc};
5use crate::{Ordering, SpecimenFile};
6
7use std::fmt::{Display, Formatter};
8
9use anyhow::{anyhow, bail, Result};
10use chrono::{DateTime, Utc};
11use tracing::instrument;
12use uuid::Uuid;
13
14const MAGIC: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
15
16/// Fat Mach-O files contain executable code for more than one architecture, allowing the
17/// same binary to be run on different hardware, such as the same file working on
18/// Power PC, Intel, and Apple Silicon machines.
19///
20/// This format is an array of Mach-O files. However, the magic number is also used for Java
21/// class files, so we need to make sure the amount of stored binaries makes sense. Too high, and
22/// it's probably the Java class version and not the number of contained Mach Objects.
23#[derive(Clone, Debug)]
24pub struct FatMacho<'a> {
25    /// The embedded Mach-O files within
26    pub binaries: Vec<Macho<'a>>,
27
28    /// If the binary has extra data after the last section, could be used to hide something
29    pub has_overlay: Option<bool>,
30
31    /// The array containing the raw bytes used to parse this program
32    pub contents: &'a [u8],
33}
34
35impl<'a> FatMacho<'a> {
36    /// Fat Mach-O parsed from a sequence of bytes
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if parsing fails.
41    #[instrument(name = "Fat Mach-O parser", skip(contents))]
42    pub fn from(contents: &'a [u8]) -> Result<Self> {
43        if !bytes_offset_match(contents, 0, &MAGIC) {
44            bail!("Not a Fat Mach-O file");
45        }
46
47        let contained_binaries = u32_from_offset(contents, 4, Ordering::BigEndian).ok_or(
48            anyhow!("Fat Mach-O too small for contained binaries integer"),
49        )? as usize;
50        if contained_binaries > 0x20 {
51            // Might be a Java .class file
52            // https://stackoverflow.com/questions/73546728/magic-value-collision-between-macho-fat-binaries-and-java-class-files
53            bail!("Not a Fat Mach-O file; probably a Java class");
54        }
55
56        let mut binaries = Vec::with_capacity(contained_binaries);
57        let mut offset_counter = 8;
58        let mut has_overlay = None;
59        for contained_binary_offset in 0..contained_binaries {
60            let offset = u32_from_offset(contents, offset_counter + 8, Ordering::BigEndian)
61                .unwrap_or_default() as usize;
62            let size = u32_from_offset(contents, offset_counter + 12, Ordering::BigEndian)
63                .unwrap_or_default() as usize;
64            if size == 0 || offset == 0 {
65                continue;
66            }
67            binaries.push(Macho::from(&contents[offset..offset + size])?);
68
69            if contained_binary_offset == contained_binaries - 1 {
70                // See if there is extra space in the binary after the last section
71                has_overlay = Some(offset + size < contents.len());
72            }
73
74            offset_counter += 20;
75        }
76
77        Ok(Self {
78            binaries,
79            has_overlay,
80            contents,
81        })
82    }
83}
84
85// TODO: Fix up `ExecutableFile` for `FatMacho`
86impl ExecutableFile for FatMacho<'_> {
87    fn architecture(&self) -> Option<Architecture> {
88        // TODO: Need something better
89        if let Some(first) = self.binaries.first() {
90            first.architecture()
91        } else {
92            None
93        }
94    }
95
96    fn pointer_size(&self) -> usize {
97        if let Some(first) = self.binaries.first() {
98            first.pointer_size()
99        } else {
100            0
101        }
102    }
103
104    fn operating_system(&self) -> OperatingSystem {
105        if let Some(first) = self.binaries.first() {
106            first.operating_system()
107        } else {
108            OperatingSystem::MacOS
109        }
110    }
111
112    fn compiled_timestamp(&self) -> Option<DateTime<Utc>> {
113        None
114    }
115
116    fn num_sections(&self) -> u32 {
117        self.binaries
118            .iter()
119            .map(crate::exec::ExecutableFile::num_sections)
120            .sum()
121    }
122
123    fn sections(&self) -> Option<&Sections<'_>> {
124        if let Some(contents) = self.binaries.first() {
125            contents.sections.as_ref()
126        } else {
127            None
128        }
129    }
130
131    fn import_hash(&self) -> Option<Uuid> {
132        None
133    }
134
135    fn fuzzy_imports(&self) -> Option<String> {
136        None
137    }
138}
139
140impl SpecimenFile for FatMacho<'_> {
141    const MAGIC: &'static [&'static [u8]] = &[&MAGIC];
142
143    fn type_name(&self) -> &'static str {
144        "Fat Mach-O"
145    }
146}
147
148impl Display for FatMacho<'_> {
149    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150        writeln!(
151            f,
152            "Fat Mach-O containing {} architectures:",
153            self.binaries.len()
154        )?;
155        for bin in &self.binaries {
156            writeln!(f, "{bin}")?;
157        }
158        if self.has_overlay == Some(true) {
159            writeln!(f, "\tHas extra bytes at the end (overlay).")?;
160        }
161        writeln!(f, "\tTotal Size: {}", self.contents.len())?;
162        writeln!(f, "\tEntropy: {:.4}", self.contents.entropy())
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    use rstest::rstest;
171
172    #[rstest]
173    #[case::three_architectures(include_bytes!("../../../testdata/macho/macho_fat_arm64_x86_64"), 2)]
174    #[case::four_architectures(include_bytes!("../../../testdata/macho/macho_fat_arm64_ppc_ppc64_x86_64"), 4)]
175    #[test]
176    fn multi_arch(#[case] bytes: &[u8], #[case] expected_architectures: usize) {
177        let macho = FatMacho::from(bytes).unwrap();
178        assert_eq!(macho.binaries.len(), expected_architectures);
179    }
180
181    #[test]
182    fn java() {
183        const BYTES: &[u8] = include_bytes!("../../../testdata/class/Hello.class");
184        assert!(FatMacho::from(BYTES).is_err());
185    }
186}