change_mach_o_flags.py 10 KB

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