summaryrefslogtreecommitdiffstats
path: root/src/common/impl/binary_apple.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common/impl/binary_apple.c
Add the files
Diffstat (limited to 'src/common/impl/binary_apple.c')
-rw-r--r--src/common/impl/binary_apple.c303
1 files changed, 303 insertions, 0 deletions
diff --git a/src/common/impl/binary_apple.c b/src/common/impl/binary_apple.c
new file mode 100644
index 0000000..8478169
--- /dev/null
+++ b/src/common/impl/binary_apple.c
@@ -0,0 +1,303 @@
+#include "common/binary.h"
+#include "common/io.h"
+#include "common/strutil.h"
+
+#include <string.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <mach-o/loader.h>
+#include <mach-o/swap.h>
+#include <mach-o/fat.h>
+
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // swap_fat_arch
+
+// Ref: https://github.com/AlexDenisov/segment_dumper/blob/master/main.c
+
+typedef struct {
+ const uint8_t* data;
+ size_t length;
+} FFMemoryMapping;
+
+static inline void wrapMunmap(FFMemoryMapping* mapping) {
+ assert(mapping);
+ if (mapping->data == NULL || mapping->data == MAP_FAILED) {
+ return;
+ }
+ munmap((void*) mapping->data, mapping->length);
+}
+
+/**
+ * Helper function to access data from a memory-mapped file at a specific offset
+ */
+static inline const void* readData(const FFMemoryMapping* mapping, size_t size, off_t offset) {
+ if (offset < 0) {
+ return NULL;
+ }
+
+ size_t start = (size_t) offset;
+ if (start > mapping->length || size > mapping->length - start) {
+ return NULL;
+ }
+
+ return mapping->data + start;
+}
+
+/**
+ * Handles a Mach-O section by extracting strings from the __cstring section
+ *
+ * @param mapping Memory mapping of the Mach-O object file
+ * @param name Section name to check
+ * @param offset Offset of the section in the file
+ * @param size Size of the section
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return true to continue processing, false to stop
+ */
+static 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) {
+ if (!ffStrEquals(name, "__cstring")) {
+ return true;
+ }
+
+ const char* data = readData(mapping, size, offset);
+ if (!data) {
+ return true;
+ }
+
+ for (size_t off = 0; off < size; ++off) {
+ const char* p = data + off;
+ if (*p == '\0') {
+ continue;
+ }
+ uint32_t len = (uint32_t) strnlen(p, size - off);
+ if (len < minLength) {
+ off += len; // Skip short strings
+ continue;
+ }
+ if (*p >= ' ' && *p <= '~') { // Ignore control characters
+ if (!cb(p, len, userdata)) {
+ return false;
+ }
+ }
+ off += len;
+ }
+ return true;
+}
+
+/**
+ * Processes a Mach-O header (32-bit or 64-bit)
+ *
+ * This function parses the load commands in a Mach-O header, looking for
+ * LC_SEGMENT or LC_SEGMENT_64 commands that contain the __TEXT segment.
+ * It then processes the sections within that segment to extract strings.
+ *
+ * @param mapping Memory mapping of the Mach-O object file
+ * @param offset Offset of the Mach header in the file
+ * @param is_64 Whether this is a 64-bit Mach-O header
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return NULL on success, error message on failure
+ */
+static 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) {
+ uint32_t ncmds;
+ off_t loadCommandsOffset = offset;
+
+ if (is_64) {
+ const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
+ if (!header) {
+ return "read mach header failed";
+ }
+
+ ncmds = header->ncmds;
+ loadCommandsOffset += sizeof(*header);
+ } else {
+ const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
+ if (!header) {
+ return "read mach header failed";
+ }
+
+ ncmds = header->ncmds;
+ loadCommandsOffset += sizeof(*header);
+ }
+
+ off_t commandOffset = loadCommandsOffset;
+ const struct load_command* cmd = NULL;
+ for (uint32_t i = 0U; i < ncmds; i++, commandOffset += cmd->cmdsize) {
+ cmd = readData(mapping, sizeof(*cmd), commandOffset);
+ if (!cmd) {
+ break;
+ }
+
+ if (cmd->cmdsize < sizeof(*cmd)) {
+ break;
+ }
+
+ if (cmd->cmd == LC_SEGMENT_64) {
+ const struct segment_command_64* segment = readData(mapping, sizeof(struct segment_command_64), commandOffset);
+ if (!segment) {
+ continue;
+ }
+
+ if (!ffStrEquals(segment->segname, "__TEXT")) {
+ continue;
+ }
+
+ for (uint32_t j = 0U; j < segment->nsects; j++) {
+ off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section_64));
+ const struct section_64* section = readData(mapping, sizeof(struct section_64), sectionOffset);
+ if (!section) {
+ continue;
+ }
+
+ if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
+ return NULL;
+ }
+ }
+ } else if (cmd->cmd == LC_SEGMENT) {
+ const struct segment_command* segment = readData(mapping, sizeof(struct segment_command), commandOffset);
+ if (!segment) {
+ continue;
+ }
+
+ if (!ffStrEquals(segment->segname, "__TEXT")) {
+ continue;
+ }
+
+ for (uint32_t j = 0; j < segment->nsects; j++) {
+ off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section));
+ const struct section* section = readData(mapping, sizeof(struct section), sectionOffset);
+ if (!section) {
+ continue;
+ }
+
+ if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
+ return NULL;
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Processes a Fat binary header (Universal binary)
+ *
+ * This function handles the fat header of a universal binary, which can contain
+ * multiple Mach-O binaries for different architectures. It extracts and processes
+ * each embedded Mach-O file.
+ *
+ * @param mapping Memory mapping of the universal binary
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return NULL on success, error message on failure
+ */
+static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
+ if (!headerRaw) {
+ return "read fat header failed";
+ }
+
+ struct fat_header header = *headerRaw;
+
+ bool needSwap = header.magic == FAT_CIGAM || header.magic == FAT_CIGAM_64;
+
+ if (needSwap) {
+ swap_fat_header(&header, NX_UnknownByteOrder);
+ }
+
+ for (uint32_t i = 0U; i < header.nfat_arch; i++) {
+ off_t machHeaderOffset = 0;
+ if (header.magic == FAT_MAGIC) {
+ off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch));
+ const struct fat_arch* archRaw = readData(mapping, sizeof(struct fat_arch), archOffset);
+ if (!archRaw) {
+ continue;
+ }
+
+ struct fat_arch arch = *archRaw;
+
+ if (needSwap) {
+ swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
+ }
+ machHeaderOffset = (off_t) arch.offset;
+ } else {
+ off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch_64));
+ const struct fat_arch_64* archRaw = readData(mapping, sizeof(struct fat_arch_64), archOffset);
+ if (!archRaw) {
+ continue;
+ }
+
+ struct fat_arch_64 arch = *archRaw;
+
+ if (needSwap) {
+ swap_fat_arch_64(&arch, 1, NX_UnknownByteOrder);
+ }
+
+ machHeaderOffset = (off_t) arch.offset;
+ }
+
+ const uint32_t* magic = readData(mapping, sizeof(uint32_t), machHeaderOffset);
+ if (!magic) {
+ continue;
+ }
+
+ if (*magic == MH_MAGIC_64 || *magic == MH_MAGIC) {
+ return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
+ }
+ }
+ return "Unsupported fat header";
+}
+
+/**
+ * Extracts string literals from a Mach-O (Apple) binary file
+ *
+ * This function supports both single-architecture Mach-O files and
+ * universal binaries (fat binaries) containing multiple architectures.
+ * It locates the __cstring section in the __TEXT segment which contains
+ * the string literals used in the program.
+ */
+const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ return "File could not be opened";
+ }
+
+ struct stat st;
+ if (fstat(fd, &st) != 0 || st.st_size <= 0) {
+ return "Failed to stat file";
+ }
+
+ FF_A_CLEANUP(wrapMunmap) FFMemoryMapping mapping = {
+ .data = mmap(NULL, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0),
+ .length = (size_t) st.st_size,
+ };
+ if (mapping.data == MAP_FAILED) {
+ return "mmap failed";
+ }
+
+ // Read the magic number to determine the type of binary
+ const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
+ if (!magic) {
+ return "read magic number failed";
+ }
+
+ // Check for supported formats
+ // MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
+ // However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
+ if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
+ return "Unsupported format or big endian mach-o file";
+ }
+
+ // Process either a fat binary or a regular Mach-O binary
+ if (*magic == FAT_MAGIC || *magic == FAT_MAGIC_64 || *magic == FAT_CIGAM || *magic == FAT_CIGAM_64) {
+ return dumpFatHeader(&mapping, cb, userdata, minLength);
+ } else {
+ return dumpMachHeader(&mapping, 0, *magic == MH_MAGIC_64, cb, userdata, minLength);
+ }
+}