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