Playing with Mach-O binaries and dyld
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.
Prerequisites
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 */
#define DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT 8
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)];
#else
uintptr_t reserved[12-DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT];
#endif
};
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_,
TASK_DYLD_INFO,
(task_info_t)&dyld_info,
&count);
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
[...]
LC 05: LC_SYMTAB
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.
DYLD SHARED CACHE MAPPINGS ON YOSEMITE *
========================================
(*): 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
memcmp
ing more memory.
With this, we can find the symtab, iterate over each of them and
check the corresponding strings, looking for _printf
.
Conclusion
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!
Links
- Code for calling printf
- Slides of the corresponding Lightning talk
- Dynamic Symbol table duel - ELF vs Mach-O
- Mach-O executables
/usr/include/mach-o/*