Home | History | Annotate | Download | only in mac
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Usage: change_mach_o_flags.py [--executable-heap] [--no-pie] <executablepath>
      7 
      8 Arranges for the executable at |executable_path| to have its data (heap)
      9 pages protected to prevent execution on Mac OS X 10.7 ("Lion"), and to have
     10 the PIE (position independent executable) bit set to enable ASLR (address
     11 space layout randomization). With --executable-heap or --no-pie, the
     12 respective bits are cleared instead of set, making the heap executable or
     13 disabling PIE/ASLR.
     14 
     15 This script is able to operate on thin (single-architecture) Mach-O files
     16 and fat (universal, multi-architecture) files. When operating on fat files,
     17 it will set or clear the bits for each architecture contained therein.
     18 
     19 NON-EXECUTABLE HEAP
     20 
     21 Traditionally in Mac OS X, 32-bit processes did not have data pages set to
     22 prohibit execution. Although user programs could call mprotect and
     23 mach_vm_protect to deny execution of code in data pages, the kernel would
     24 silently ignore such requests without updating the page tables, and the
     25 hardware would happily execute code on such pages. 64-bit processes were
     26 always given proper hardware protection of data pages. This behavior was
     27 controllable on a system-wide level via the vm.allow_data_exec sysctl, which
     28 is set by default to 1. The bit with value 1 (set by default) allows code
     29 execution on data pages for 32-bit processes, and the bit with value 2
     30 (clear by default) does the same for 64-bit processes.
     31 
     32 In Mac OS X 10.7, executables can "opt in" to having hardware protection
     33 against code execution on data pages applied. This is done by setting a new
     34 bit in the |flags| field of an executable's |mach_header|. When
     35 MH_NO_HEAP_EXECUTION is set, proper protections will be applied, regardless
     36 of the setting of vm.allow_data_exec. See xnu-1699.22.73/osfmk/vm/vm_map.c
     37 override_nx and xnu-1699.22.73/bsd/kern/mach_loader.c load_machfile.
     38 
     39 The Apple toolchain has been revised to set the MH_NO_HEAP_EXECUTION when
     40 producing executables, provided that -allow_heap_execute is not specified
     41 at link time. Only linkers shipping with Xcode 4.0 and later (ld64-123.2 and
     42 later) have this ability. See ld64-123.2.1/src/ld/Options.cpp
     43 Options::reconfigureDefaults() and
     44 ld64-123.2.1/src/ld/HeaderAndLoadCommands.hpp
     45 HeaderAndLoadCommandsAtom<A>::flags().
     46 
     47 This script sets the MH_NO_HEAP_EXECUTION bit on Mach-O executables. It is
     48 intended for use with executables produced by a linker that predates Apple's
     49 modifications to set this bit itself. It is also useful for setting this bit
     50 for non-i386 executables, including x86_64 executables. Apple's linker only
     51 sets it for 32-bit i386 executables, presumably under the assumption that
     52 the value of vm.allow_data_exec is set in stone. However, if someone were to
     53 change vm.allow_data_exec to 2 or 3, 64-bit x86_64 executables would run
     54 without hardware protection against code execution on data pages. This
     55 script can set the bit for x86_64 executables, guaranteeing that they run
     56 with appropriate protection even when vm.allow_data_exec has been tampered
     57 with.
     58 
     59 POSITION-INDEPENDENT EXECUTABLES/ADDRESS SPACE LAYOUT RANDOMIZATION
     60 
     61 This script sets or clears the MH_PIE bit in an executable's Mach-O header,
     62 enabling or disabling position independence on Mac OS X 10.5 and later.
     63 Processes running position-independent executables have varying levels of
     64 ASLR protection depending on the OS release. The main executable's load
     65 address, shared library load addresess, and the heap and stack base
     66 addresses may be randomized. Position-independent executables are produced
     67 by supplying the -pie flag to the linker (or defeated by supplying -no_pie).
     68 Executables linked with a deployment target of 10.7 or higher have PIE on
     69 by default.
     70 
     71 This script is never strictly needed during the build to enable PIE, as all
     72 linkers used are recent enough to support -pie. However, it's used to
     73 disable the PIE bit as needed on already-linked executables.
     74 """
     75 
     76 import optparse
     77 import os
     78 import struct
     79 import sys
     80 
     81 
     82 # <mach-o/fat.h>
     83 FAT_MAGIC = 0xcafebabe
     84 FAT_CIGAM = 0xbebafeca
     85 
     86 # <mach-o/loader.h>
     87 MH_MAGIC = 0xfeedface
     88 MH_CIGAM = 0xcefaedfe
     89 MH_MAGIC_64 = 0xfeedfacf
     90 MH_CIGAM_64 = 0xcffaedfe
     91 MH_EXECUTE = 0x2
     92 MH_PIE = 0x00200000
     93 MH_NO_HEAP_EXECUTION = 0x01000000
     94 
     95 
     96 class MachOError(Exception):
     97   """A class for exceptions thrown by this module."""
     98 
     99   pass
    100 
    101 
    102 def CheckedSeek(file, offset):
    103   """Seeks the file-like object at |file| to offset |offset| and raises a
    104   MachOError if anything funny happens."""
    105 
    106   file.seek(offset, os.SEEK_SET)
    107   new_offset = file.tell()
    108   if new_offset != offset:
    109     raise MachOError, \
    110           'seek: expected offset %d, observed %d' % (offset, new_offset)
    111 
    112 
    113 def CheckedRead(file, count):
    114   """Reads |count| bytes from the file-like |file| object, raising a
    115   MachOError if any other number of bytes is read."""
    116 
    117   bytes = file.read(count)
    118   if len(bytes) != count:
    119     raise MachOError, \
    120           'read: expected length %d, observed %d' % (count, len(bytes))
    121 
    122   return bytes
    123 
    124 
    125 def ReadUInt32(file, endian):
    126   """Reads an unsinged 32-bit integer from the file-like |file| object,
    127   treating it as having endianness specified by |endian| (per the |struct|
    128   module), and returns it as a number. Raises a MachOError if the proper
    129   length of data can't be read from |file|."""
    130 
    131   bytes = CheckedRead(file, 4)
    132 
    133   (uint32,) = struct.unpack(endian + 'I', bytes)
    134   return uint32
    135 
    136 
    137 def ReadMachHeader(file, endian):
    138   """Reads an entire |mach_header| structure (<mach-o/loader.h>) from the
    139   file-like |file| object, treating it as having endianness specified by
    140   |endian| (per the |struct| module), and returns a 7-tuple of its members
    141   as numbers. Raises a MachOError if the proper length of data can't be read
    142   from |file|."""
    143 
    144   bytes = CheckedRead(file, 28)
    145 
    146   magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = \
    147       struct.unpack(endian + '7I', bytes)
    148   return magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags
    149 
    150 
    151 def ReadFatArch(file):
    152   """Reads an entire |fat_arch| structure (<mach-o/fat.h>) from the file-like
    153   |file| object, treating it as having endianness specified by |endian|
    154   (per the |struct| module), and returns a 5-tuple of its members as numbers.
    155   Raises a MachOError if the proper length of data can't be read from
    156   |file|."""
    157 
    158   bytes = CheckedRead(file, 20)
    159 
    160   cputype, cpusubtype, offset, size, align = struct.unpack('>5I', bytes)
    161   return cputype, cpusubtype, offset, size, align
    162 
    163 
    164 def WriteUInt32(file, uint32, endian):
    165   """Writes |uint32| as an unsinged 32-bit integer to the file-like |file|
    166   object, treating it as having endianness specified by |endian| (per the
    167   |struct| module)."""
    168 
    169   bytes = struct.pack(endian + 'I', uint32)
    170   assert len(bytes) == 4
    171 
    172   file.write(bytes)
    173 
    174 
    175 def HandleMachOFile(file, options, offset=0):
    176   """Seeks the file-like |file| object to |offset|, reads its |mach_header|,
    177   and rewrites the header's |flags| field if appropriate. The header's
    178   endianness is detected. Both 32-bit and 64-bit Mach-O headers are supported
    179   (mach_header and mach_header_64). Raises MachOError if used on a header that
    180   does not have a known magic number or is not of type MH_EXECUTE. The
    181   MH_PIE and MH_NO_HEAP_EXECUTION bits are set or cleared in the |flags| field
    182   according to |options| and written to |file| if any changes need to be made.
    183   If already set or clear as specified by |options|, nothing is written."""
    184 
    185   CheckedSeek(file, offset)
    186   magic = ReadUInt32(file, '<')
    187   if magic == MH_MAGIC or magic == MH_MAGIC_64:
    188     endian = '<'
    189   elif magic == MH_CIGAM or magic == MH_CIGAM_64:
    190     endian = '>'
    191   else:
    192     raise MachOError, \
    193           'Mach-O file at offset %d has illusion of magic' % offset
    194 
    195   CheckedSeek(file, offset)
    196   magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = \
    197       ReadMachHeader(file, endian)
    198   assert magic == MH_MAGIC or magic == MH_MAGIC_64
    199   if filetype != MH_EXECUTE:
    200     raise MachOError, \
    201           'Mach-O file at offset %d is type 0x%x, expected MH_EXECUTE' % \
    202               (offset, filetype)
    203 
    204   original_flags = flags
    205 
    206   if options.no_heap_execution:
    207     flags |= MH_NO_HEAP_EXECUTION
    208   else:
    209     flags &= ~MH_NO_HEAP_EXECUTION
    210 
    211   if options.pie:
    212     flags |= MH_PIE
    213   else:
    214     flags &= ~MH_PIE
    215 
    216   if flags != original_flags:
    217     CheckedSeek(file, offset + 24)
    218     WriteUInt32(file, flags, endian)
    219 
    220 
    221 def HandleFatFile(file, options, fat_offset=0):
    222   """Seeks the file-like |file| object to |offset| and loops over its
    223   |fat_header| entries, calling HandleMachOFile for each."""
    224 
    225   CheckedSeek(file, fat_offset)
    226   magic = ReadUInt32(file, '>')
    227   assert magic == FAT_MAGIC
    228 
    229   nfat_arch = ReadUInt32(file, '>')
    230 
    231   for index in xrange(0, nfat_arch):
    232     cputype, cpusubtype, offset, size, align = ReadFatArch(file)
    233     assert size >= 28
    234 
    235     # HandleMachOFile will seek around. Come back here after calling it, in
    236     # case it sought.
    237     fat_arch_offset = file.tell()
    238     HandleMachOFile(file, options, offset)
    239     CheckedSeek(file, fat_arch_offset)
    240 
    241 
    242 def main(me, args):
    243   parser = optparse.OptionParser('%prog [options] <executable_path>')
    244   parser.add_option('--executable-heap', action='store_false',
    245                     dest='no_heap_execution', default=True,
    246                     help='Clear the MH_NO_HEAP_EXECUTION bit')
    247   parser.add_option('--no-pie', action='store_false',
    248                     dest='pie', default=True,
    249                     help='Clear the MH_PIE bit')
    250   (options, loose_args) = parser.parse_args(args)
    251   if len(loose_args) != 1:
    252     parser.print_usage()
    253     return 1
    254 
    255   executable_path = loose_args[0]
    256   executable_file = open(executable_path, 'rb+')
    257 
    258   magic = ReadUInt32(executable_file, '<')
    259   if magic == FAT_CIGAM:
    260     # Check FAT_CIGAM and not FAT_MAGIC because the read was little-endian.
    261     HandleFatFile(executable_file, options)
    262   elif magic == MH_MAGIC or magic == MH_CIGAM or \
    263       magic == MH_MAGIC_64 or magic == MH_CIGAM_64:
    264     HandleMachOFile(executable_file, options)
    265   else:
    266     raise MachOError, '%s is not a Mach-O or fat file' % executable_file
    267 
    268   executable_file.close()
    269   return 0
    270 
    271 
    272 if __name__ == '__main__':
    273   sys.exit(main(sys.argv[0], sys.argv[1:]))
    274