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
8Arranges for the executable at |executable_path| to have its data (heap)
9pages protected to prevent execution on Mac OS X 10.7 ("Lion"), and to have
10the PIE (position independent executable) bit set to enable ASLR (address
11space layout randomization). With --executable-heap or --no-pie, the
12respective bits are cleared instead of set, making the heap executable or
13disabling PIE/ASLR.
14
15This script is able to operate on thin (single-architecture) Mach-O files
16and fat (universal, multi-architecture) files. When operating on fat files,
17it will set or clear the bits for each architecture contained therein.
18
19NON-EXECUTABLE HEAP
20
21Traditionally in Mac OS X, 32-bit processes did not have data pages set to
22prohibit execution. Although user programs could call mprotect and
23mach_vm_protect to deny execution of code in data pages, the kernel would
24silently ignore such requests without updating the page tables, and the
25hardware would happily execute code on such pages. 64-bit processes were
26always given proper hardware protection of data pages. This behavior was
27controllable on a system-wide level via the vm.allow_data_exec sysctl, which
28is set by default to 1. The bit with value 1 (set by default) allows code
29execution 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
32In Mac OS X 10.7, executables can "opt in" to having hardware protection
33against code execution on data pages applied. This is done by setting a new
34bit in the |flags| field of an executable's |mach_header|. When
35MH_NO_HEAP_EXECUTION is set, proper protections will be applied, regardless
36of the setting of vm.allow_data_exec. See xnu-1699.22.73/osfmk/vm/vm_map.c
37override_nx and xnu-1699.22.73/bsd/kern/mach_loader.c load_machfile.
38
39The Apple toolchain has been revised to set the MH_NO_HEAP_EXECUTION when
40producing executables, provided that -allow_heap_execute is not specified
41at link time. Only linkers shipping with Xcode 4.0 and later (ld64-123.2 and
42later) have this ability. See ld64-123.2.1/src/ld/Options.cpp
43Options::reconfigureDefaults() and
44ld64-123.2.1/src/ld/HeaderAndLoadCommands.hpp
45HeaderAndLoadCommandsAtom<A>::flags().
46
47This script sets the MH_NO_HEAP_EXECUTION bit on Mach-O executables. It is
48intended for use with executables produced by a linker that predates Apple's
49modifications to set this bit itself. It is also useful for setting this bit
50for non-i386 executables, including x86_64 executables. Apple's linker only
51sets it for 32-bit i386 executables, presumably under the assumption that
52the value of vm.allow_data_exec is set in stone. However, if someone were to
53change vm.allow_data_exec to 2 or 3, 64-bit x86_64 executables would run
54without hardware protection against code execution on data pages. This
55script can set the bit for x86_64 executables, guaranteeing that they run
56with appropriate protection even when vm.allow_data_exec has been tampered
57with.
58
59POSITION-INDEPENDENT EXECUTABLES/ADDRESS SPACE LAYOUT RANDOMIZATION
60
61This script sets or clears the MH_PIE bit in an executable's Mach-O header,
62enabling or disabling position independence on Mac OS X 10.5 and later.
63Processes running position-independent executables have varying levels of
64ASLR protection depending on the OS release. The main executable's load
65address, shared library load addresess, and the heap and stack base
66addresses may be randomized. Position-independent executables are produced
67by supplying the -pie flag to the linker (or defeated by supplying -no_pie).
68Executables linked with a deployment target of 10.7 or higher have PIE on
69by default.
70
71This script is never strictly needed during the build to enable PIE, as all
72linkers used are recent enough to support -pie. However, it's used to
73disable the PIE bit as needed on already-linked executables.
74"""
75
76import optparse
77import os
78import struct
79import sys
80
81
82# <mach-o/fat.h>
83FAT_MAGIC = 0xcafebabe
84FAT_CIGAM = 0xbebafeca
85
86# <mach-o/loader.h>
87MH_MAGIC = 0xfeedface
88MH_CIGAM = 0xcefaedfe
89MH_MAGIC_64 = 0xfeedfacf
90MH_CIGAM_64 = 0xcffaedfe
91MH_EXECUTE = 0x2
92MH_PIE = 0x00200000
93MH_NO_HEAP_EXECUTION = 0x01000000
94
95
96class MachOError(Exception):
97  """A class for exceptions thrown by this module."""
98
99  pass
100
101
102def 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
113def 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
125def 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
137def 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
151def 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
164def 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
175def 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
221def 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
242def 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
272if __name__ == '__main__':
273  sys.exit(main(sys.argv[0], sys.argv[1:]))
274