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;
12
13const MAGIC: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
14
15#[derive(Clone, Debug)]
23pub struct FatMacho<'a> {
24 pub binaries: Vec<Macho<'a>>,
26
27 pub has_overlay: Option<bool>,
29
30 pub contents: &'a [u8],
32}
33
34impl<'a> FatMacho<'a> {
35 #[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 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 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
84impl ExecutableFile for FatMacho<'_> {
86 fn architecture(&self) -> Option<Architecture> {
87 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}