Skip to main content

crashrustler/unwind/
macho.rs

1//! Mach-O section finder operating on byte slices from MemoryReader.
2//!
3//! Parses the Mach-O header and load commands to locate key sections:
4//! `__TEXT,__unwind_info`, `__TEXT,__eh_frame`, and `__TEXT,__text`.
5
6use super::{MemoryReader, SectionRef};
7
8/// Results of Mach-O section lookup.
9pub struct MachOSections {
10    pub unwind_info: Option<SectionRef>,
11    pub eh_frame: Option<SectionRef>,
12    pub text: Option<SectionRef>,
13}
14
15// Mach-O constants
16const MH_MAGIC_64: u32 = 0xFEED_FACF;
17const MH_MAGIC: u32 = 0xFEED_FACE;
18const LC_SEGMENT_64: u32 = 0x19;
19const LC_SEGMENT: u32 = 0x01;
20const LC_UUID: u32 = 0x1B;
21
22/// Finds key sections in a Mach-O binary at the given load address.
23pub fn find_sections(
24    reader: &dyn MemoryReader,
25    load_address: u64,
26    is_64_bit: bool,
27) -> MachOSections {
28    let mut result = MachOSections {
29        unwind_info: None,
30        eh_frame: None,
31        text: None,
32    };
33
34    if is_64_bit {
35        find_sections_64(reader, load_address, &mut result);
36    } else {
37        find_sections_32(reader, load_address, &mut result);
38    }
39
40    result
41}
42
43/// Extracts the LC_UUID from a Mach-O binary.
44pub fn find_uuid(
45    reader: &dyn MemoryReader,
46    load_address: u64,
47    is_64_bit: bool,
48) -> Option<[u8; 16]> {
49    let magic = reader.read_u32(load_address)?;
50
51    let (header_size, expected_magic, lc_segment) = if is_64_bit {
52        (32u64, MH_MAGIC_64, LC_SEGMENT_64)
53    } else {
54        (28u64, MH_MAGIC, LC_SEGMENT)
55    };
56
57    if magic != expected_magic {
58        return None;
59    }
60
61    let ncmds = reader.read_u32(load_address + 16)? as u64;
62    let _ = lc_segment; // used in find_sections, not needed here
63
64    let mut offset = header_size;
65    for _ in 0..ncmds {
66        let cmd = reader.read_u32(load_address + offset)?;
67        let cmd_size = reader.read_u32(load_address + offset + 4)? as u64;
68
69        if cmd == LC_UUID && cmd_size >= 24 {
70            let uuid_bytes = reader.read_memory(load_address + offset + 8, 16)?;
71            let mut uuid = [0u8; 16];
72            uuid.copy_from_slice(&uuid_bytes);
73            return Some(uuid);
74        }
75
76        offset += cmd_size;
77    }
78
79    None
80}
81
82fn find_sections_64(reader: &dyn MemoryReader, load_address: u64, result: &mut MachOSections) {
83    let Some(magic) = reader.read_u32(load_address) else {
84        return;
85    };
86    if magic != MH_MAGIC_64 {
87        return;
88    }
89
90    let Some(ncmds) = reader.read_u32(load_address + 16) else {
91        return;
92    };
93
94    // 64-bit header is 32 bytes
95    let mut offset = 32u64;
96
97    for _ in 0..ncmds {
98        let Some(cmd) = reader.read_u32(load_address + offset) else {
99            return;
100        };
101        let Some(cmd_size) = reader.read_u32(load_address + offset + 4) else {
102            return;
103        };
104
105        if cmd == LC_SEGMENT_64 {
106            // Read segment name (16 bytes at offset+8)
107            let Some(seg_name_bytes) = reader.read_memory(load_address + offset + 8, 16) else {
108                offset += cmd_size as u64;
109                continue;
110            };
111            let seg_name = bytes_to_name(&seg_name_bytes);
112
113            if seg_name == "__TEXT" {
114                // Parse sections within this segment
115                // nsects is at offset+48 (after segname(16)+vmaddr(8)+vmsize(8)+fileoff(8)+filesize(8))
116                let Some(nsects) = reader.read_u32(load_address + offset + 64) else {
117                    offset += cmd_size as u64;
118                    continue;
119                };
120
121                // Section headers start at offset+72 (segment_command_64 size)
122                let mut sect_offset = offset + 72;
123                for _ in 0..nsects {
124                    parse_section_64(reader, load_address, sect_offset, result);
125                    sect_offset += 80; // sizeof(section_64)
126                }
127            }
128        }
129
130        offset += cmd_size as u64;
131    }
132}
133
134fn find_sections_32(reader: &dyn MemoryReader, load_address: u64, result: &mut MachOSections) {
135    let Some(magic) = reader.read_u32(load_address) else {
136        return;
137    };
138    if magic != MH_MAGIC {
139        return;
140    }
141
142    let Some(ncmds) = reader.read_u32(load_address + 16) else {
143        return;
144    };
145
146    // 32-bit header is 28 bytes
147    let mut offset = 28u64;
148
149    for _ in 0..ncmds {
150        let Some(cmd) = reader.read_u32(load_address + offset) else {
151            return;
152        };
153        let Some(cmd_size) = reader.read_u32(load_address + offset + 4) else {
154            return;
155        };
156
157        if cmd == LC_SEGMENT {
158            let Some(seg_name_bytes) = reader.read_memory(load_address + offset + 8, 16) else {
159                offset += cmd_size as u64;
160                continue;
161            };
162            let seg_name = bytes_to_name(&seg_name_bytes);
163
164            if seg_name == "__TEXT" {
165                let Some(nsects) = reader.read_u32(load_address + offset + 48) else {
166                    offset += cmd_size as u64;
167                    continue;
168                };
169
170                let mut sect_offset = offset + 56; // sizeof(segment_command)
171                for _ in 0..nsects {
172                    parse_section_32(reader, load_address, sect_offset, result);
173                    sect_offset += 68; // sizeof(section)
174                }
175            }
176        }
177
178        offset += cmd_size as u64;
179    }
180}
181
182fn parse_section_64(
183    reader: &dyn MemoryReader,
184    load_address: u64,
185    sect_offset: u64,
186    result: &mut MachOSections,
187) {
188    // section_64: sectname(16) + segname(16) + addr(8) + size(8)
189    let Some(sect_name_bytes) = reader.read_memory(load_address + sect_offset, 16) else {
190        return;
191    };
192    let sect_name = bytes_to_name(&sect_name_bytes);
193
194    let Some(addr) = reader.read_u64(load_address + sect_offset + 32) else {
195        return;
196    };
197    let Some(size) = reader.read_u64(load_address + sect_offset + 40) else {
198        return;
199    };
200
201    match sect_name.as_str() {
202        "__unwind_info" => {
203            result.unwind_info = Some(SectionRef {
204                vm_addr: addr,
205                size,
206            });
207        }
208        "__eh_frame" => {
209            result.eh_frame = Some(SectionRef {
210                vm_addr: addr,
211                size,
212            });
213        }
214        "__text" => {
215            result.text = Some(SectionRef {
216                vm_addr: addr,
217                size,
218            });
219        }
220        _ => {}
221    }
222}
223
224fn parse_section_32(
225    reader: &dyn MemoryReader,
226    load_address: u64,
227    sect_offset: u64,
228    result: &mut MachOSections,
229) {
230    let Some(sect_name_bytes) = reader.read_memory(load_address + sect_offset, 16) else {
231        return;
232    };
233    let sect_name = bytes_to_name(&sect_name_bytes);
234
235    // section: sectname(16) + segname(16) + addr(4) + size(4)
236    let Some(addr) = reader.read_u32(load_address + sect_offset + 32) else {
237        return;
238    };
239    let Some(size) = reader.read_u32(load_address + sect_offset + 36) else {
240        return;
241    };
242
243    match sect_name.as_str() {
244        "__unwind_info" => {
245            result.unwind_info = Some(SectionRef {
246                vm_addr: addr as u64,
247                size: size as u64,
248            });
249        }
250        "__eh_frame" => {
251            result.eh_frame = Some(SectionRef {
252                vm_addr: addr as u64,
253                size: size as u64,
254            });
255        }
256        "__text" => {
257            result.text = Some(SectionRef {
258                vm_addr: addr as u64,
259                size: size as u64,
260            });
261        }
262        _ => {}
263    }
264}
265
266/// Converts a null-padded byte array to a string.
267fn bytes_to_name(bytes: &[u8]) -> String {
268    let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
269    String::from_utf8_lossy(&bytes[..len]).to_string()
270}
271
272#[cfg(test)]
273mod tests {
274    use super::super::SliceMemoryReader;
275    use super::*;
276
277    /// Builds a minimal 64-bit Mach-O with one LC_SEGMENT_64 (__TEXT)
278    /// containing __unwind_info, __eh_frame, and __text sections.
279    fn build_test_macho_64() -> Vec<u8> {
280        let mut data = Vec::new();
281
282        // Mach-O header (32 bytes)
283        data.extend_from_slice(&MH_MAGIC_64.to_le_bytes()); // magic
284        data.extend_from_slice(&0u32.to_le_bytes()); // cputype
285        data.extend_from_slice(&0u32.to_le_bytes()); // cpusubtype
286        data.extend_from_slice(&0u32.to_le_bytes()); // filetype
287        data.extend_from_slice(&1u32.to_le_bytes()); // ncmds = 1
288        let sizeofcmds = 72 + 80 * 3; // segment_command_64 + 3 sections
289        data.extend_from_slice(&(sizeofcmds as u32).to_le_bytes()); // sizeofcmds
290        data.extend_from_slice(&0u32.to_le_bytes()); // flags
291        data.extend_from_slice(&0u32.to_le_bytes()); // reserved
292        assert_eq!(data.len(), 32);
293
294        // LC_SEGMENT_64 (72 bytes base + 80 * nsects)
295        let cmd_size = 72 + 80 * 3;
296        data.extend_from_slice(&LC_SEGMENT_64.to_le_bytes()); // cmd
297        data.extend_from_slice(&(cmd_size as u32).to_le_bytes()); // cmdsize
298        // segname: "__TEXT\0..."
299        let mut segname = [0u8; 16];
300        segname[..6].copy_from_slice(b"__TEXT");
301        data.extend_from_slice(&segname);
302        data.extend_from_slice(&0x1000u64.to_le_bytes()); // vmaddr
303        data.extend_from_slice(&0x3000u64.to_le_bytes()); // vmsize
304        data.extend_from_slice(&0u64.to_le_bytes()); // fileoff
305        data.extend_from_slice(&0u64.to_le_bytes()); // filesize
306        data.extend_from_slice(&0u32.to_le_bytes()); // maxprot
307        data.extend_from_slice(&0u32.to_le_bytes()); // initprot
308        data.extend_from_slice(&3u32.to_le_bytes()); // nsects = 3
309        data.extend_from_slice(&0u32.to_le_bytes()); // flags
310        assert_eq!(data.len(), 104); // 32 + 72
311
312        // Section 1: __text
313        write_section_64(&mut data, "__text", "__TEXT", 0x1100, 0x500);
314        // Section 2: __unwind_info
315        write_section_64(&mut data, "__unwind_info", "__TEXT", 0x2000, 0x200);
316        // Section 3: __eh_frame
317        write_section_64(&mut data, "__eh_frame", "__TEXT", 0x2200, 0x400);
318
319        data
320    }
321
322    fn write_section_64(data: &mut Vec<u8>, sectname: &str, segname: &str, addr: u64, size: u64) {
323        // section_64 is 80 bytes
324        let mut sn = [0u8; 16];
325        let bytes = sectname.as_bytes();
326        sn[..bytes.len().min(16)].copy_from_slice(&bytes[..bytes.len().min(16)]);
327        data.extend_from_slice(&sn);
328
329        let mut sg = [0u8; 16];
330        let bytes = segname.as_bytes();
331        sg[..bytes.len().min(16)].copy_from_slice(&bytes[..bytes.len().min(16)]);
332        data.extend_from_slice(&sg);
333
334        data.extend_from_slice(&addr.to_le_bytes()); // addr
335        data.extend_from_slice(&size.to_le_bytes()); // size
336        data.extend_from_slice(&0u32.to_le_bytes()); // offset
337        data.extend_from_slice(&0u32.to_le_bytes()); // align
338        data.extend_from_slice(&0u32.to_le_bytes()); // reloff
339        data.extend_from_slice(&0u32.to_le_bytes()); // nreloc
340        data.extend_from_slice(&0u32.to_le_bytes()); // flags
341        data.extend_from_slice(&0u32.to_le_bytes()); // reserved1
342        data.extend_from_slice(&0u32.to_le_bytes()); // reserved2
343        data.extend_from_slice(&0u32.to_le_bytes()); // reserved3 (padding to 80 bytes)
344    }
345
346    #[test]
347    fn find_sections_64bit() {
348        let macho_data = build_test_macho_64();
349        let reader = SliceMemoryReader {
350            data: macho_data,
351            base_address: 0,
352        };
353
354        let sections = find_sections(&reader, 0, true);
355
356        let text = sections.text.unwrap();
357        assert_eq!(text.vm_addr, 0x1100);
358        assert_eq!(text.size, 0x500);
359
360        let unwind = sections.unwind_info.unwrap();
361        assert_eq!(unwind.vm_addr, 0x2000);
362        assert_eq!(unwind.size, 0x200);
363
364        let eh = sections.eh_frame.unwrap();
365        assert_eq!(eh.vm_addr, 0x2200);
366        assert_eq!(eh.size, 0x400);
367    }
368
369    #[test]
370    fn find_sections_bad_magic() {
371        let reader = SliceMemoryReader {
372            data: vec![0u8; 64],
373            base_address: 0,
374        };
375        let sections = find_sections(&reader, 0, true);
376        assert!(sections.text.is_none());
377        assert!(sections.unwind_info.is_none());
378        assert!(sections.eh_frame.is_none());
379    }
380
381    #[test]
382    fn bytes_to_name_with_null() {
383        let b = b"__TEXT\0\0\0\0\0\0\0\0\0\0\0";
384        assert_eq!(bytes_to_name(b), "__TEXT");
385    }
386
387    #[test]
388    fn bytes_to_name_full() {
389        let b = b"__longerthan16ch";
390        assert_eq!(bytes_to_name(b), "__longerthan16ch");
391    }
392}