One cool way to get your hands dirty when discovering something is to try to make it do simple stuff in some stupid/overkill way.

When I first had “fun” with the Linux ELF format, I was told to call printf without using it directly, by finding which address to call from inside the binary. For this, one would start from the mapped program header, find the r_debug structure which would give the program’s link map containing the mapped libc’s base address. From it, one would find printf by iterating over the library’s symbol table and find where it is, before calling it. No syscall’s allowed, so everything must come from the process’s own memory and structures.

Recently I wanted to give a closer look at macOS, and decided to try the same thing with Mach-O binaries. This post will be a sum-up for me to remember, and for anybody that might want to learn anything about macOS in general. I will not re-explain what already exists on other websites, I’ll just link them instead.


First things first, we are looking for printf, from the libc. To find it, just write a simple program, and open gdb. There are multiple ways to determine which library we are looking for, but using info sharedlibrary and determining in which range falls printf is one of the simplest. In our case, we care about /usr/lib/system/libsystem_c.dylib.

This binary is what file calls a Mach-O universal binary, which in fact is a wrapper around multiple Mach-Os. Also called Fat binaries in the old days, they were used to mix x86 and PPC binaries in a single blob. Now, it ships libraries for both 32 and 64 bits architectures.

p1kachu@OrangeLabOfSun:osx$ file libsystem_c.dylib
help/libsystem_c.dylib: Mach-O universal binary with 2 architectures: [x86_64: Mach-O 64-bit x86_64 dynamically linked shared library, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|NO_REEXPORTED_DYLIBS|APP_EXTENSION_SAFE>] [i386: Mach-O i386 dynamically linked shared library, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|NO_REEXPORTED_DYLIBS|APP_EXTENSION_SAFE>]

A universal binary consists of a fat binary header, and multiple Mach-Os. So we’ll only take a look at one of the Mach-O, the one used by our system (in our case, the first one). Here is, however, an overview of the format:


Finding the libc

In memory will only be mapped the corresponding Mach-O, so that’s what we are going to look for in our process’s address space. We first need to understand how the dynamic linker maps it. Let’s take a look at /usr/include/mach-o/* to try to find some informations. The interesting stuff lies into dyld_images.h and loader.h. We see that the structure dyld_images.h:dyld_all_image_infos has two interesting fields: a pointer (infoArray) to an array of struct dyld_image_info, which gives us every mapped binary in memory, and infoArrayCount which gives the number of binaries in the array. We can thus iterate over these structures to find the libsystem_c.dylib address in memory.

Here are the important parts from dyld_images.h (macOS Sierra). Comments have been moved/reduced for more readability.

struct dyld_image_info {
        /* base address image is mapped into */
        const struct mach_header* imageLoadAddress;
        /* path dyld used to load the image */
        const char*               imageFilePath;
        /* time_t of image file */
        uintptr_t                 imageFileModDate;
// ...

// ...

/* internal limit */

struct dyld_all_image_infos {
        uint32_t                      version;  /* 1 in Mac OS X 10.4 and 10.5 */
        uint32_t                      infoArrayCount;
        const struct dyld_image_info* infoArray;
        dyld_image_notifier           notification;
        bool                          processDetachedFromSharedRegion;
        /* Mac OS X 10.6, iPhoneOS 2.0 and later */
        bool                          libSystemInitialized;
        const struct mach_header*     dyldImageLoadAddress;
        /* Mac OS X 10.6, iPhoneOS 3.0 and later */
        void*                         jitInfo;
        /* Mac OS X 10.6, iPhoneOS 3.0 and later */
        const char*                   dyldVersion;
        const char*                   errorMessage;
        uintptr_t                     terminationFlags;
        /* Mac OS X 10.6, iPhoneOS 3.1 and later */
        void*                         coreSymbolicationShmPage;
        /* Mac OS X 10.6, iPhoneOS 3.1 and later */
        uintptr_t                     systemOrderFlag;
        /* Mac OS X 10.7, iPhoneOS 3.1 and later */
        uintptr_t                     uuidArrayCount;
        const struct dyld_uuid_info*  uuidArray; /* only images not in dyld shared cache */
        /* Mac OS X 10.7, iOS 4.0 and later */
        struct dyld_all_image_infos*  dyldAllImageInfosAddress;
        /* Mac OS X 10.7, iOS 4.2 and later */
        uintptr_t                     initialImageCount;
        /* Mac OS X 10.7, iOS 4.2 and later */
        uintptr_t                     errorKind;
        const char*                   errorClientOfDylibPath;
        const char*                   errorTargetDylibPath;
        const char*                   errorSymbol;
        /* Mac OS X 10.7, iOS 4.3 and later */
        uintptr_t                     sharedCacheSlide;
        /* Mac OS X 10.9, iOS 7.0 and later */
        uint8_t                       sharedCacheUUID[16];
        /* (macOS 10.12, iOS 10.0 and later */
        uintptr_t                     sharedCacheBaseAddress;
        uint64_t                      infoArrayChangeTimestamp;
        const char*                   dyldPath;
        mach_port_t                   notifyPorts[DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT];
#if __LP64__
        uintptr_t                     reserved[13-(DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT/2)];
        uintptr_t                     reserved[12-DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT];

By looking at this structure afterwards, one can notice that other fields from this structure could have been quite useful for our purpose !

However, we first have to find its address in memory. The function /usr/include/mach/task.h:task_info does exactly this, but uses a mach port, which is a kernel-provided inter-process communication mechanism. It’s not exactly a syscall, but still, it’s a little bit like cheating. I don’t think there is any reliable way of doing it without (as of Yosemite at least).

Phew! We are now able to get the base address of libsystem_c.dylib:

static char *find_libc(void)
        // Get DYLD task infos
        struct task_dyld_info dyld_info;
        mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
        kern_return_t ret;
        ret = task_info(mach_task_self_,
        if (ret != KERN_SUCCESS) {
                return NULL;

        // Get image array's size and address
        mach_vm_address_t image_infos = dyld_info.all_image_info_addr;
        struct dyld_all_image_infos *infos;
        infos = (struct dyld_all_image_infos *)image_infos;
        uint32_t image_count = infos->infoArrayCount;
        struct dyld_image_info *image_array = infos->infoArray;

        // Find libsystem_c.dylib among them
        struct dyld_image_info *image;
        for (int i = 0; i < image_count; ++i) {
                image = image_array + i;

                // Find libsystem_c.dylib's load address
                if (strstr(image->imageFilePath, "libsystem_c.dylib")) {
                        return (char*)image->imageLoadAddress;

Getting printf

Right. So now we have the binary in memory, let’s finally take a look at the Mach-O format. A good introduction has already been written here, so let’s not dive in too deep and directly look for what interests us, accessing the symbol table. Thus, we are looking for the LC_SYMTAB command, which will give us the strtab and symtab offsets on which we will iterate to find printf.

The process of looking up the name of a given entry in the lazy or non-lazy pointer tables looks like this:


Analysing it with jtool gives us an overview on what we are supposed to find:

p1kachu@OrangeLabOfSun:osx$ ./jtool.ELF64 -arch x86_64 -l libsystem_c.dylib
        Symbol table is at offset 0x9da70 (645744), 2372 entries
        String table is at offset 0xa7708 (685832), 32264 bytes

However, the values recovered from memory are quite different:

P1kachu@GreyLabOfSteel:~/D/L/W/c/osx$ ./get_symcmd
symoff: 0x134596ef
stroff: 0x141ad9f4

And then began the SIGSEGV ballet. Something was definitely off.

The shared cache

Let’s take a step back in late 2009, with iOS 3.1. One change in the way iOS handled libraries was introduced by the mean of the Dyld shared cache, which combines all system (private and public) libraries into a big cache file to improve performance. On macOS, the same thing happened. The shared caches live in /private/var/db/dyld/ and regroups a lot of libraries (~400 for Yosemite and ~670 for Sierra, as for the x86_64 versions). The file format isn’t documented and changes between versions, so we must trick a little bit. Some informations about it can be retrieved using jtool again:

p1kachu@OrangeLabOfSun:osx$ ./jtool.ELF64 -h dyld_shared_cache_x86_64h_yosemite
File is a shared cache containing 414 images (use -l to list)
Header size: 0x70 bytes
Got gap of -8 bytes:
3 mappings starting from 0x68. 414 Images starting from 0xc8
mapping r-x/r-x  251MB     7fff80000000 -> 7fff8fb31000      (0-fb31000)
mapping rw-/rw-   38MB     7fff70000000 -> 7fff72604000      (fb31000-12135000)
mapping r--/r--   75MB     7fff8fb31000 -> 7fff9466d000      (12135000-16c71000)
DYLD base address: 7fff5fc00000
Local Symbols:  0x0-0x0 (0 bytes)
Code Signature: 0x16c71000-0x16e38a07 (1866247 bytes)
Slide info:     0x16ba7000-0x16c71000 (827392 bytes)
        Slide Info version 1, TOC offset: 24, count 9732, entries: 6309 of size 128
p1kachu@OrangeLabOfSun:osx$ ./jtool.ELF64 -h dyld_shared_cache_x86_64h_sierra
File is a shared cache containing 675 images (use -l to list)
Header size: 0x70 bytes
Got gap of 40 bytes: 0xf8 0x00 0x00 0x00 0x00 0x00 0x5790 0x00 0x29d 0x00
3 mappings starting from 0x98. 675 Images starting from 0xf8
mapping r-x/r-x  424MB     7fff70000000 -> 7fff8a824000      (0-1a824000)
mapping rw-/rw-   75MB     7fff8e824000 -> 7fff933a7000      (1a824000-1f3a7000)
mapping r--/r--  118MB     7fff973a7000 -> 7fff9ea3c000      (1f3a7000-26a3c000)
DYLD base address: 0
Local Symbols:  0x0-0x0 (0 bytes)
Code Signature: 0x26a3c000-0x26f14000 (5079040 bytes)
Slide info:     0x1f3a7000-0x1f3b1000 (40960 bytes)
        Slide Info version 2, TOC offset: 4096, count 40, entries: 38702 of size 0

Memory layout subtlety

On Yosemite (and probably other versions that I didn’t look at), the cache memory mapping differs from the file layout: as can be seen using jtool’s output above, the TEXT mapping is after the DATA, while it is the opposite in the file layout. This was put back to normal between Yosemite and Sierra.


(*): Without ASLR slide

  ----------------------  0x7fff70000000
 |                      |
 |                      |
 |                      |
 |                      |
 |         RW-          |
 |                      |
 |                      |
 |                      |
 |----------------------| 0x7fff70000000 + [RW-].size
 |         Junk         |
 |----------------------| 0x7fff80000000
 |     Cache Header     |
 |                      |
 |         R-X          |
 |                      |
 |         ...          |
 |  libsystem_c.dylib   |
 |         ...          |
 |                      |
 |                      |
 |----------------------| 0x7fff80000000 + [R-X].size
 |                      |
 |                      |
 |                      |
 |         R--          |
 |                      |
 |                      |
 |                      |
 |                      |
  ----------------------  0x7fff80000000 + [R-X].size + [R--].size

  cache.base = [R-X].address + [R-X].size - [R--].offset

Among these cached libraries is our libsystem_c, and thus we simply understand that the {str,sym}tabs offsets are from the beginning of the cache file.

Finding it on Yosemite was not trivial without issuing syscalls, and I thus went for the stupid way: I first found the loaded library with the smallest load address (the first one contained in the shared cache), and got back into memory until finding the shared cache magic string (dyld_v1 x86_64\0).

On Sierra, however, one can observe that the dyld_all_image_infos structure contains a nice field named sharedCacheBaseAddress. I used it to avoid memcmping more memory.

With this, we can find the symtab, iterate over each of them and check the corresponding strings, looking for _printf.


The final code, compatible with at least Yosemite and Sierra, is available here.

I may have skipped some informations. I read way too much from different sources to be able to put everything down. If anything is unclear, feel free to ping me by mail or twitter.

Interesting auxilliary stuff

Shared cache and ASLR

The shared cache is loaded in memory at boot and is the same for every process. Even if affected by ASLR, it will not be re-randomized on a per program basis, and thus any program leaking addresses from it actually leaks system-wide addresses, which is nice!