malwaredb_types/exec/macho/
fat.rs1use 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#[derive(Clone, Debug)]
24pub struct FatMacho<'a> {
25 pub binaries: Vec<Macho<'a>>,
27
28 pub has_overlay: Option<bool>,
30
31 pub contents: &'a [u8],
33}
34
35impl<'a> FatMacho<'a> {
36 #[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 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 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
85impl ExecutableFile for FatMacho<'_> {
87 fn architecture(&self) -> Option<Architecture> {
88 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}