ohmu
·
2026-10-15
1#include "common/binary.h"
2#include "common/debug.h"
3#include "common/io.h"
4#include "common/strutil.h"
5
6#include <errno.h>
7#include <string.h>
8#include <fcntl.h>
9#include <sys/mman.h>
10#include <mach-o/loader.h>
11#include <mach-o/swap.h>
12#include <mach-o/fat.h>
13
14#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // swap_fat_arch
15
16// Ref: https://github.com/AlexDenisov/segment_dumper/blob/master/main.c
17
18typedef struct {
19 const uint8_t* data;
20 size_t length;
21} FFMemoryMapping;
22
23static inline void wrapMunmap(FFMemoryMapping* mapping) {
24 assert(mapping);
25 if (mapping->data == nullptr || mapping->data == MAP_FAILED) {
26 return;
27 }
28 munmap((void*) mapping->data, mapping->length);
29}
30
31/**
32 * Helper function to access data from a memory-mapped file at a specific offset
33 */
34static inline const void* readData(const FFMemoryMapping* mapping, size_t size, off_t offset) {
35 if (offset < 0) {
36 return nullptr;
37 }
38
39 size_t start = (size_t) offset;
40 if (start > mapping->length || size > mapping->length - start) {
41 return nullptr;
42 }
43
44 return mapping->data + start;
45}
46
47/**
48 * Handles a Mach-O section by extracting strings from the __cstring section
49 *
50 * @param mapping Memory mapping of the Mach-O object file
51 * @param name Section name to check
52 * @param offset Offset of the section in the file
53 * @param size Size of the section
54 * @param cb Callback function to process strings
55 * @param userdata User data for the callback
56 * @param minLength Minimum string length to extract
57 *
58 * @return true to continue processing, false to stop
59 */
60static bool handleMachSection(const FFMemoryMapping* mapping, const char* name, off_t offset, size_t size, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
61 if (!ffStrEquals(name, "__cstring")) {
62 return true;
63 }
64
65 const char* data = readData(mapping, size, offset);
66 if (!data) {
67 return true;
68 }
69
70 for (size_t off = 0; off < size; ++off) {
71 const char* p = data + off;
72 if (*p == '\0') {
73 continue;
74 }
75 uint32_t len = (uint32_t) strnlen(p, size - off);
76 if (len < minLength) {
77 off += len; // Skip short strings
78 continue;
79 }
80 if (*p >= ' ' && *p <= '~') { // Ignore control characters
81 if (!cb(p, len, userdata)) {
82 return false;
83 }
84 }
85 off += len;
86 }
87 return true;
88}
89
90/**
91 * Processes a Mach-O header (32-bit or 64-bit)
92 *
93 * This function parses the load commands in a Mach-O header, looking for
94 * LC_SEGMENT or LC_SEGMENT_64 commands that contain the __TEXT segment.
95 * It then processes the sections within that segment to extract strings.
96 *
97 * @param mapping Memory mapping of the Mach-O object file
98 * @param offset Offset of the Mach header in the file
99 * @param is_64 Whether this is a 64-bit Mach-O header
100 * @param cb Callback function to process strings
101 * @param userdata User data for the callback
102 * @param minLength Minimum string length to extract
103 *
104 * @return nullptr on success, error message on failure
105 */
106static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset, bool is_64, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
107 uint32_t ncmds;
108 off_t loadCommandsOffset = offset;
109
110 if (is_64) {
111 const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
112 if (!header) {
113 FF_DEBUG("Failed to read the 64-bit mach header at offset %lld", (long long) offset);
114 return "read mach header failed";
115 }
116
117 ncmds = header->ncmds;
118 loadCommandsOffset += sizeof(*header);
119 } else {
120 const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
121 if (!header) {
122 FF_DEBUG("Failed to read the mach header at offset %lld", (long long) offset);
123 return "read mach header failed";
124 }
125
126 ncmds = header->ncmds;
127 loadCommandsOffset += sizeof(*header);
128 }
129
130 off_t commandOffset = loadCommandsOffset;
131 const struct load_command* cmd = nullptr;
132 for (uint32_t i = 0U; i < ncmds; i++, commandOffset += cmd->cmdsize) {
133 cmd = readData(mapping, sizeof(*cmd), commandOffset);
134 if (!cmd) {
135 break;
136 }
137
138 if (cmd->cmdsize < sizeof(*cmd)) {
139 break;
140 }
141
142 if (cmd->cmd == LC_SEGMENT_64) {
143 const struct segment_command_64* segment = readData(mapping, sizeof(struct segment_command_64), commandOffset);
144 if (!segment) {
145 continue;
146 }
147
148 if (!ffStrEquals(segment->segname, "__TEXT")) {
149 continue;
150 }
151
152 for (uint32_t j = 0U; j < segment->nsects; j++) {
153 off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section_64));
154 const struct section_64* section = readData(mapping, sizeof(struct section_64), sectionOffset);
155 if (!section) {
156 continue;
157 }
158
159 if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
160 return nullptr;
161 }
162 }
163 } else if (cmd->cmd == LC_SEGMENT) {
164 const struct segment_command* segment = readData(mapping, sizeof(struct segment_command), commandOffset);
165 if (!segment) {
166 continue;
167 }
168
169 if (!ffStrEquals(segment->segname, "__TEXT")) {
170 continue;
171 }
172
173 for (uint32_t j = 0; j < segment->nsects; j++) {
174 off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section));
175 const struct section* section = readData(mapping, sizeof(struct section), sectionOffset);
176 if (!section) {
177 continue;
178 }
179
180 if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
181 return nullptr;
182 }
183 }
184 }
185 }
186
187 return nullptr;
188}
189
190/**
191 * Processes a Fat binary header (Universal binary)
192 *
193 * This function handles the fat header of a universal binary, which can contain
194 * multiple Mach-O binaries for different architectures. It extracts and processes
195 * each embedded Mach-O file.
196 *
197 * @param mapping Memory mapping of the universal binary
198 * @param cb Callback function to process strings
199 * @param userdata User data for the callback
200 * @param minLength Minimum string length to extract
201 *
202 * @return nullptr on success, error message on failure
203 */
204static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
205 const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
206 if (!headerRaw) {
207 FF_DEBUG("Failed to read the fat header");
208 return "read fat header failed";
209 }
210
211 struct fat_header header = *headerRaw;
212
213 bool needSwap = header.magic == FAT_CIGAM || header.magic == FAT_CIGAM_64;
214
215 if (needSwap) {
216 swap_fat_header(&header, NX_UnknownByteOrder);
217 }
218
219 for (uint32_t i = 0U; i < header.nfat_arch; i++) {
220 off_t machHeaderOffset = 0;
221 if (header.magic == FAT_MAGIC) {
222 off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch));
223 const struct fat_arch* archRaw = readData(mapping, sizeof(struct fat_arch), archOffset);
224 if (!archRaw) {
225 continue;
226 }
227
228 struct fat_arch arch = *archRaw;
229
230 if (needSwap) {
231 swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
232 }
233 machHeaderOffset = (off_t) arch.offset;
234 } else {
235 off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch_64));
236 const struct fat_arch_64* archRaw = readData(mapping, sizeof(struct fat_arch_64), archOffset);
237 if (!archRaw) {
238 continue;
239 }
240
241 struct fat_arch_64 arch = *archRaw;
242
243 if (needSwap) {
244 swap_fat_arch_64(&arch, 1, NX_UnknownByteOrder);
245 }
246
247 machHeaderOffset = (off_t) arch.offset;
248 }
249
250 const uint32_t* magic = readData(mapping, sizeof(uint32_t), machHeaderOffset);
251 if (!magic) {
252 continue;
253 }
254
255 if (*magic == MH_MAGIC_64 || *magic == MH_MAGIC) {
256 return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
257 }
258 }
259 FF_DEBUG("No supported Mach-O slice was found in the fat binary");
260 return "Unsupported fat header";
261}
262
263/**
264 * Extracts string literals from a Mach-O (Apple) binary file
265 *
266 * This function supports both single-architecture Mach-O files and
267 * universal binaries (fat binaries) containing multiple architectures.
268 * It locates the __cstring section in the __TEXT segment which contains
269 * the string literals used in the program.
270 */
271const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
272 FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
273 if (fd < 0) {
274 FF_DEBUG("open(\"%s\") failed: %s", machoFile, strerror(errno));
275 return "File could not be opened";
276 }
277
278 struct stat st;
279 if (fstat(fd, &st) != 0 || st.st_size <= 0) {
280 FF_DEBUG("fstat(\"%s\") failed or the file is empty", machoFile);
281 return "Failed to stat file";
282 }
283
284 [[gnu::cleanup(wrapMunmap)]] FFMemoryMapping mapping = {
285 .data = mmap(nullptr, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0),
286 .length = (size_t) st.st_size,
287 };
288 if (mapping.data == MAP_FAILED) {
289 FF_DEBUG("mmap(\"%s\", %lld bytes) failed: %s", machoFile, (long long) st.st_size, strerror(errno));
290 return "mmap failed";
291 }
292
293 // Read the magic number to determine the type of binary
294 const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
295 if (!magic) {
296 FF_DEBUG("Failed to read the magic number");
297 return "read magic number failed";
298 }
299
300 // Check for supported formats
301 // MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
302 // However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
303 if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
304 FF_DEBUG("Unsupported magic number 0x%08x", (unsigned) *magic);
305 return "Unsupported format or big endian mach-o file";
306 }
307
308 // Process either a fat binary or a regular Mach-O binary
309 if (*magic == FAT_MAGIC || *magic == FAT_MAGIC_64 || *magic == FAT_CIGAM || *magic == FAT_CIGAM_64) {
310 return dumpFatHeader(&mapping, cb, userdata, minLength);
311 } else {
312 return dumpMachHeader(&mapping, 0, *magic == MH_MAGIC_64, cb, userdata, minLength);
313 }
314}