
In a
previous article , the call pickup method for shared
ELF libraries was described. And now we will look at how to do the same with the
Mach-O libraries.
Briefly recall the situation. We have a program for Mac OS X, which uses many third-party dynamically-composable libraries, which, in turn, also use each other's functions.
The task is as follows: intercept the call of a function from one library to another, and in the handler call the original.
')
As usual, the impatient can all
download and try right now.
For clarity, an imaginary example: we have a program called “test” in the C language (file test.c) and a shared library (file libtest.c), with constant content, compiled in advance. This library provides one function: libtest (). In their implementation, each of them uses the puts () function from the standard C language library (supplied with Mac OS, contained in libSystem.B.dylib). Let's look at a schematic representation of the described situation:

The task is as follows:
- It is necessary to replace the call of the puts () function for libtest.dylib with the call of the hooked_puts () function implemented in the main program (file test.c), which, in turn, can use the original has ();

- Cancel the changes made, that is, make the repeated call to libtest () result in a call to the original puts ().

In this case, changing the code or recompiling the libraries themselves is not allowed, only the main program. Call forwarding itself should be carried out only for a specific library and on the fly, without restarting the program.
Mach-O in brief
The best way to understand Mach-O is to look at the image below.

It seems that humanity has not yet managed to portray its structure more clearly. In the first approximation, it looks like this:
- Title - information about the target architecture and various options for further interpretation of the file contents are stored here.
- Load commands - tell you how and where to load Mach-O parts: segments (see below), symbol tables, and also - which libraries this file depends on in order to load them first
- Segments - describe the regions of memory where to load sections with code or data.
Parser Utilities
For the second approximation you'll have to get acquainted with some utilities:
- otool - is a console program that comes with the system. It is capable of displaying the contents of various parts of a file: headers, load commands, segments, sections, and so on. It is especially useful to add the -v (verbose) switch when calling.
- MachOView - distributed under the GPL, has a GUI, works only on Mac OS 10.6 and higher. Allows you to view the full content of Mach-O, complements the information on some sections, based on data from other parts, which is very convenient.

By and large, in order for an ordinary user to deal with Mach-O, it’s enough to play with MachOView on various examples. But this is not enough for Mach-O programming, since the exact structures of the headers, load commands, segments, sections, symbol tables and the exact description of their fields are unknown. But, it is not a big problem, if there is a specification. And it is always
available on the official Apple website. And if you have installed development tools, you can look at the header files from / usr / include / mach-o (especially loader.h).
In addition, it is worth remembering that, although the contents of the file are located in memory in exactly the same order as it is on the disk, but at boot time the linker can delete some parts of the symbol table, the entire table of rows and put down the values ​​of real memory offsets where necessary, while in the file, these values ​​can generally be set to zero or correspond to the offset on the disk.
The header structure is simple (for a 32-bit architecture, the 64-bit architecture is not much different):
struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; };
Everything starts with a magical value (0xFEEDFACE or vice versa, depending on the agreement regarding the
order of bytes in machine words). It then indicates the type of processor architecture, the number and size of boot commands, and flags describing other features.
For example:

The essential download commands are listed below:
- LC_SEGMENT - contains various information about a certain segment: size, number of sections, offset in the file and, after loading, in memory
- LC_SYMTAB - loads character table and strings
- LC_DYSYMTAB - creates an import table, character data is taken from the symbol table
- LC_LOAD_DYLIB - indicates dependency on some third-party library.
For example (32-bit and 64-bit versions, respectively):
The most important segments are as follows:
- __TEXT - executable code and other read-only data
- __DATA - data available for writing; including import tables that tend to be changed by the dynamic loader during late binding
- __OBJC - various information of the standard runtime Objective-C language library
- __IMPORT - import table exclusively for 32-bit architecture (I only generated on Mac OS 10.5)
- __LINKEDIT - here the dynamic loader places its data for already loaded modules: character tables, strings, etc.
Any download command starts with the following fields:
struct load_command { uint32_t cmd;
After which there may be many more different fields, depending on the type of team.
For example:

The most interesting sections in these segments are:
- __TEXT, __ text - the actual code
- __TEXT, __ cstring - constant strings (in double quotes)
- __TEXT, __ const - various constants
- __DATA, __ data - initialized variables (strings and arrays)
- __DATA, __ la_symbol_ptr - a table of pointers to imported functions
- __DATA, __ bss - uninitialized static variables
- __IMPORT, __ jump_table - stubs for calls to imported functions
Looking ahead, I’ll note that in a single Mach-O, the import table can be either __IMPORT, __ jump_table (32 bits, Mac OS 10.5), or __DATA, __ la_symbol_ptr (64 bits, or Mac OS 10.6 and later).
Sections in the segments have the following structure:
struct section { char sectname[16]; char segname[16]; uint32_t addr; uint32_t size; uint32_t offset; uint32_t align; uint32_t reloff; uint32_t nreloc; uint32_t flags; uint32_t reserved1; uint32_t reserved2; };
We have the name of the segment and the section itself, the size, the offset in the file and the address in memory for which the dynamic loader has placed it. In addition, there is another, section-specific information.
For example:

Fat binary
Of course, it is worth mentioning that, as a result of Apple's repeated smooth change of its target architectures (Motorola -> IBM -> Intel), executable files and libraries “learned” to store several versions of executable code at once. In general, such files are called
fat binary . In fact, these are several Mach-O, collected in one file, but its title is special. It contains information on the number and type of supported architectures and the offset to each of them. At this offset are the usual Mach-O with the structure described above.
Here’s what it looks like in C:
struct fat_header { uint32_t magic; uint32_t nfat_arch; };
Where under magic lies 0xCAFEBABE (or vice versa - we remember about the different order of bytes in machine words on different processors). And after, immediately follows exactly nfat_arch type structures:
struct fat_arch { cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t offset; uint32_t size; uint32_t align; };
Actually, the field names speak for themselves: the type of processor, the offset in the file of a particular Mach-O, the size and alignment.
Experimental program
To study the work of a call to an imported function, we take the following files in C:
File test.c
void libtest(); //from libtest.dylib int main() { libtest(); //calls puts() from libSystem.B.dylib return 0; }
File libtest.c
#include <stdio.h> void libtest()
Explore dynamic layout
We confine ourselves to Intel processors. Let us have Mac OS 10.5. Add these files to the new Xcode project, compile it (32-bit version) and launch it in debug mode, stopping at the line where the function lib (() libtest.dylib function calls the puts () function. Here is the assembler listing for libtest ():

Perform another instruction:

And look at her in memory:

This is the cell of the import table (in this case, the __IMPORT cell, __jump_table), which serves as a springboard for calling the dynamic loader (__dyld_stub_binding_helper_interface function) if late binding (lazy binding) is used, or jumps directly to the objective function. This is confirmed by a subsequent call to puts ():

And in memory:

So, we see that the dynamic loader has replaced the indirect call instruction CALL (0xE8) with the indirect jump instruction JMP (0xE9). Therefore, to redirect the __jump_table elements, it will be enough for us to write instead of their initial content an instruction for indirectly switching to the beginning of the substitution function.
Another interesting point. Why is JMP not used to go to the dynamic bootloader (also known as linker)? Because CALL, which stores the return address on the stack, will help the linker to determine which element of the import table has caused it. And, therefore, calculate what it was for the symbol and resolve it by changing the CALL to itself for an indirect JMP for the required function.
Now let's move the project to Mac OS 10.6 and compile the fat binary for 32-bit and 64-bit architectures. Just in case, in Xcode you can do it like this:

We compile, run the 64-bit version (just for example; the import table on Snow Leopard will be the same for 32-bit) and stops again at the call to (():

And again simple CALL. Look further:

Here there is already a noticeable difference with the usual __IMPORT, __jump_table.
Welcome to __TEXT, __symbol_stub1. This table is a set of JMP instructions for each function being imported. In our case, there is only one such instruction presented above. Each such instruction jumps to the address specified in the corresponding cell of the __DATA table, __la_symbol_ptr. The latter is the import table for this Mach-O.
But let's continue the research. If you look at the address to which the transition is going to occur:

Then we will see the following:

We fall into the section __TEXT, __stub_helper. In essence, this is a PLT (Procedure Linkage Table) for Mach-O. The first instruction (in this case, this is the LEA in conjunction with R11, and there could be a simple PUSH) dynamic linker remembers what character needs to be relocated, the second instruction always leads to the same address — the beginning of the function __dyld_stub_binding_helper, which will bind :

After the dynamic linker performs the relocation for puts (), the corresponding cell in __DATA, __la_symbol_ptr will look like this:

And this is already the address of the puts () function from the libSystem.B.dylib module. That is, replacing it with some address, we get the desired effect of call forwarding.
So. At this stage, we used a concrete example to find out how dynamic linking takes place, what import tables there are in Mach-O and what elements they consist of. Now we proceed to the analysis of Mach-O!
Search item in import table
It is necessary to find the corresponding cell in the import table by the symbol name. The algorithm of this action is somewhat non-trivial.
First, you need to find the character itself in the symbol table. The latter is an array of the following structures:
struct nlist { union { int32_t n_strx; } n_un; uint8_t n_type; uint8_t n_sect; int16_t n_desc; uint32_t n_value; };
Where n_un.n_strx is the offset in bytes from the beginning of the string table of the name of this character. The rest concerns the type of character, the section in which it is located, and so on. In short, here are some of its last elements for our experimental library libtest.dylib (32-bit version):

The string table is a chain of names, each of which is terminated by zero. However, it is worth noting that for each name the compiler adds an underscore "_" to the beginning, so for example the name "puts" will appear in the string table as "_puts".
Here is an example:

You can find the location of the symbol and string table from the corresponding load command (LC_SYMTAB):

However, the symbol table is not uniform. There are several sections in it. One of them is especially interesting to us - these are undefined symbols, that is, those that are dynamically linked. By the way, MachOView highlights any bluish background. In order to determine which part of the symbol table reflects a subset of undefined symbols, you need to look into the dynamic character loading command (LC_DYSYMTAB):

Here is its presentation in C:
struct dysymtab_command { uint32_t cmd; uint32_t cmdsize; uint32_t ilocalsym; uint32_t nlocalsym; uint32_t iextdefsym; uint32_t nextdefsym; uint32_t iundefsym; uint32_t nundefsym; uint32_t tocoff; uint32_t ntoc; uint32_t modtaboff; uint32_t nmodtab; uint32_t extrefsymoff; uint32_t nextrefsyms; uint32_t indirectsymoff; uint32_t nindirectsyms; uint32_t extreloff; uint32_t nextrel; uint32_t locreloff; uint32_t nlocrel; };
Here dysymtab_command.iundefsym is the index in the symbol table, from which the subset of undefined symbols begins. dysymtab_command.nundefsym - the number of undefined characters. Since what we are looking for is a deliberately undefined symbol, then you only need to search for it in the symbol table in this subset.
And now, a very important point: having found a symbol by its name, the most important thing for us is to remember its index in the symbol table from its beginning. Because of the numerical values ​​of these indices is another important table - a table of indirect (indirect) characters. You can find it by dysymtab_command.indirectsymoff, and the number of indices is determined by dysymtab_command.nindirectsyms.
In our trivial case, this table consists of only one element (in real life there are much more):

And finally, let's look at the __IMPORT section, __jump_table, some element of which we need to find in the end. She looks like this:

The section.reserved1 field for this section is very important (MachOView called it Indirect Sym Index). It means the index in the table of indirect symbols, from which one-to-one correspondence begins with the __jump_table elements. And we remember that the elements in the table of indirect symbols are indices in the symbol table. Catch what I'm getting at?
But, before finally gathering all the pieces of knowledge together, for completeness, we’ll have a quick look at the situation in Snow Leopard, where __DATA, __la_symbol_ptr plays the role of the import table. In fact, the differences are not very noticeable.
Here is the character load command:

And here are its last elements:

Two undefined symbols are visible on the bluish background, which corresponds to the data from the dynamic symbol loading command (LC_DYSYMTAB):

Yes, and in the table of indirect symbols, there is no longer one element, but four:

However, if you look at the reserved1 field of the cherished section __la_symbol_ptr, you can find that the one-to-one reflection of its elements on the table of indirect symbols starts not from the beginning of the last, but from the fourth element (the index is 3):

The very contents of the import table, which the __la_symbol_ptr section describes, will be:

Having learned about all these intricacies of Mach-O, we can formulate an algorithm for finding the desired item in the import table.
Redirection algorithm
We describe all actions in words, since the code, despite the abundance of comments, may not be so clear:
- We look for a table of characters and strings from the data from the LC_SYMTAB download command.
- We learn from the download command LC_DYSYMTAB with which element of the symbol table the subset of undefined symbols begins (iundefsym field).
- We are looking for a target symbol by name among a subset of undefined symbols in the symbol table.
- Remember the index of the target character from the beginning of the symbol table.
- We look for a table of indirect characters from the data from the LC_DYSYMTAB download command (field indirectsymoff).
- We recognize the index from which the mapping of the import table begins (the contents of the __DATA section, __la_symbol_ptr (or __IMPORT, __jump_table - there will be one thing)) on the indirect symbol table (reserved1 field).
- Starting from this index, we look through the table of indirect symbols and look for the value corresponding to the index of the target symbol in the symbol table.
- We remember how the target character got into the account from the beginning of the mapping of the import table to the table of indirect symbols. The stored value is the index of the desired item in the import table.
- According to the data from the __la_symbol_ptr section (or __jump_table) we find the import table (field offset).
- Having the index of the target element in it, we rewrite the address (for __la_symbol_ptr) to the value we need (or change the CALL / JMP instruction to JMP with the operand — the address of the function we need (for __jump_table)).
I note that working with tables of characters, strings and indirect characters is only necessary by loading them from a file. And to read the contents of the sections describing import tables, and, of course, to make the redirect itself, already in memory. This is due to the fact that the tables of characters and strings may be missing or do not display the actual state of affairs in the target Mach-O. After all, a dynamic loader worked there before us and safely saved all the necessary data about the characters, without placing the tables themselves.
Redirect implementation
It is time to turn the thoughts into code. To optimize the search for the necessary Mach-O elements at each redirection, we divide the whole operation into three steps:
void *mach_hook_init(char const *library_filename, void const *library_address);
Based on the Mach-O file itself and its memory mapping, this function returns an opaque descriptor, which hides offsets to the import table, a table of characters, strings, and the display of indirect (indirect) characters from the table of dynamic symbols, as well as a number of useful indices for this module. Here is this descriptor:
struct mach_hook_handle { void const *library_address; //base address of a library in memory char const *string_table; //buffer to read string_table table from file struct nlist const *symbol_table; //buffer to read symbol table from file uint32_t const *indirect_table; //buffer to read the indirect symbol table in dynamic symbol table from file uint32_t undefined_symbols_count; //number of undefined symbols in the symbol table uint32_t undefined_symbols_index; //position of undefined symbols in the symbol table uint32_t indirect_symbols_count; //number of indirect symbols in the indirect symbol table of DYSYMTAB uint32_t indirect_symbols_index; //index of the first imported symbol in the indirect symbol table of DYSYMTAB uint32_t import_table_offset; //the offset of (__DATA, __la_symbol_ptr) or (__IMPORT, __jump_table) uint32_t jump_table_present; //special flag to show if we work with (__IMPORT, __jump_table) };
mach_substitution mach_hook(void const *handle, char const *function_name, mach_substitution substitution);
This function, according to the library descriptor, the name of the target character and the interceptor address performs the redirection itself according to the algorithm described above.
void mach_hook_free(void *handle);
This is the way to clear any handle that returned mach_hook_init ().
Given these prototypes, the test program will have to be rewritten:
#include <stdio.h> #include <dlfcn.h> #include "mach_hook.h" #define LIBTEST_PATH "libtest.dylib" void libtest(); //from libtest.dylib int hooked_puts(char const *s) { puts(s); //calls the original puts() from libSystem.B.dylib, because our main executable module called "test" remains intact return puts("HOOKED!"); } int main() { void *handle = 0; //handle to store hook-related info mach_substitution original; //original data for restoration Dl_info info; if (!dladdr((void const *)libtest, &info)) //gets an address of a library which contains libtest() function { fprintf(stderr, "Failed to get the base address of a library!\n", LIBTEST_PATH); goto end; } handle = mach_hook_init(LIBTEST_PATH, info.dli_fbase); if (!handle) { fprintf(stderr, "Redirection init failed!\n"); goto end; } libtest(); //calls puts() from libSystem.B.dylib puts("-----------------------------"); original = mach_hook(handle, "puts", (mach_substitution)hooked_puts); if (!original) { fprintf(stderr, "Redirection failed!\n"); goto end; } libtest(); //calls hooked_puts() puts("-----------------------------"); original = mach_hook(handle, "puts", original); //restores the original relocation if (!original) { fprintf(stderr, "Restoration failed!\n"); goto end; } libtest(); //again calls puts() from libSystem.B.dylib end: mach_hook_free(handle); handle = 0; //no effect here, but just a good advice to prevent double freeing return 0; }
The full implementation of the test case along with the redirection algorithm and the project file is available for
download .
Test run
and try something like this:
user@mac$ arch -i386 ./test libtest: calls the original puts()
user@mac$ arch -x86_64 ./test libtest: calls the original puts()
The output of the program indicates the complete implementation of the task set at the very beginning.
useful links
Good luck!