On MacOS 10.7 dyld randomization

First article for the blog, let’s talk about something I had in mind for a while.
There has been a lot of talk about the introduced full ASLR on MacOS X Lion, so as soon as I had my hands on the OS I wanted to check which were the changes introduced.

Let’s start from the very beginning, Mach-O. In order to understand what are the differences introduced in Lion, we need to first give a look at a Mach-O built on two different OSes, we will take as a reference Snow Leopard. Let’s build this simple code for test:

1
2
3
4
5
int main()
{
  while (1) {}
  return 0;
}

If we compile that code on Lion, no specific option passed to gcc, we will notice a difference from the very same code compiled on Snow Leopard. The difference is the presence of the flag MH_PIE (Position Independent):

/usr/include/mach-o/loader.h

1
2
3
4
5
          /* When this bit is set, the OS will
             load the main executable at a
             random address.  Only used in
             MH_EXECUTE filetypes. */

#define MH_PIE 0x200000

The MH_PIE flag has been sitting there since MacOS 10.5 and the linker on Lion is now defaulting to it.
The funny thing is that with MH_PIE enabled, the image base will always be at a fixed offset from dyld, 32bit or 64bit, no difference (just a bigger displacement).

Let’s give a look at the base addresses for the main executable and dyld on both cases [Lion, SL]:
NOTE: If you execute the binary through gdb you will have to set disable-aslr off (in gdb, before run) in order to enable dyld randomization, but even then you won’t have PIE enabled. That’s why I have added that otherwise inexplicable while (1) {}

Compiled on Lion (64bit), executed on Lion:

0x000000010b397f44 in main ()
(gdb) info shared
The DYLD shared library state has not yet been initialized.
                                       Requested State Current State
Num Basename             Type Address         Reason | | Source    
  | |                       | |                    | | | |          
  1 lion                    - 0x10b397000        exec Y Y /tmp/lion at 0x10b397000 (offset 0xb397000)
  2 dyld                    - 0x7fff6af97000        dyld Y Y /usr/lib/dyld at 0x7fff6af97000 (offset 0xb397000) with prefix "__dyld_"

Compiled on SL (64bit), executed on Lion:

0x0000000100000f3c in main ()
(gdb) info shared
The DYLD shared library state has not yet been initialized.
                                       Requested State Current State
Num Basename             Type Address         Reason | | Source    
  | |                       | |                    | | | |          
  1 snow                    - 0x100000000        exec Y Y /tmp/snow (offset 0x0)
  2 dyld                    - 0x7fff66058000        dyld Y Y /usr/lib/dyld at 0x7fff66058000 (offset 0x6458000) with prefix "__dyld_"

After hundreds of executions you can notice that dyld will always be at 0x1000 from the image base on 32bit, and 0x400000 on 64bit:

image base: 0x10b397000
dyld      : 0x7fff6af97000

image base & 0xFFFFFFF = 0xb397000
dyld_base  & 0xFFFFFFF = 0xaf97000

0xb397000 - 0xaf97000  = 0x400000

Since image bases are page aligned we can remove 1 byte and a nibble from the address, this leaves us with 2 bytes randomization on dyld (in this case 0xaf97). We’re actually working on 64bit! On 32bit we have, uhm, the incredible amount of 1 byte.

(gdb) info shared
The DYLD shared library state has not yet been initialized.
                                       Requested State Current State
Num Basename             Type Address         Reason | | Source    
  | |                       | |                    | | | |          
  1 lion32                  - 0x65000           exec Y Y /tmp/lion32 at 0x65000 (offset 0x64000)
  2 dyld                    - 0x8fe64000        dyld Y Y /usr/lib/dyld at 0x8fe64000 (offset 0x64000) with prefix "__dyld_"

On Lion the userspace has been changed quite a bit, now there’s no big fat libSystem anymore (there still is but it’s not that fat :)), instead there are several different libsystem_something located at /usr/lib/system. This ensures logical separation and randomization (libraries were already randomized in 10.5). Now there is also a libdyld.dylib which has a lot of symbols that were once in /usr/lib/dyld.

The problem with dyld was that it was loaded at a fixed address providing loads of go-go-gadgets for ROP exploitation. Now they say it’s randomized. And several different symbols have been moved to libdyld which is randomized.

(gdb) info symbol _dyld_image_count
_dyld_image_count in section LC_SEGMENT.__TEXT.__text of /usr/lib/system/libdyld.dylib
(gdb) info symbol _dyld_get_image_header
_dyld_get_image_header in section LC_SEGMENT.__TEXT.__text of /usr/lib/system/libdyld.dylib

Funny thing is that /usr/lib/dyld is still working as it was. dlsym() will resolve symbols in libdyld, but if you resolve symbols on the mapped dyld image (e.g. with your own implementation of dlsym()) you’ll see that everything works as it was in the past.

In conclusion, it looks like the big changes introduced in Lion (regarding dyld randomization) were actually a 2 bytes randomization on 64bit and 1 byte randomization on 32bit, always at a fixed offset from the main executable image base (with MH_PIE flag set).

Well, that’s it :) Hope you guys enjoyed and stay tuned!