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.
6 """Usage: change_mach_o_flags.py [--executable-heap] [--no-pie] <executablepath>
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
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.
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.
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.
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().
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
59 POSITION-INDEPENDENT EXECUTABLES/ADDRESS SPACE LAYOUT RANDOMIZATION
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
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.
83 FAT_MAGIC
= 0xcafebabe
84 FAT_CIGAM
= 0xbebafeca
89 MH_MAGIC_64
= 0xfeedfacf
90 MH_CIGAM_64
= 0xcffaedfe
93 MH_NO_HEAP_EXECUTION
= 0x01000000
96 class MachOError(Exception):
97 """A class for exceptions thrown by this module."""
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."""
106 file.seek(offset
, os
.SEEK_SET
)
107 new_offset
= file.tell()
108 if new_offset
!= offset
:
110 'seek: expected offset %d, observed %d' % (offset
, new_offset
)
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."""
117 bytes
= file.read(count
)
118 if len(bytes
) != count
:
120 'read: expected length %d, observed %d' % (count
, len(bytes
))
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|."""
131 bytes
= CheckedRead(file, 4)
133 (uint32
,) = struct
.unpack(endian
+ 'I', bytes
)
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
144 bytes
= CheckedRead(file, 28)
146 magic
, cputype
, cpusubtype
, filetype
, ncmds
, sizeofcmds
, flags
= \
147 struct
.unpack(endian
+ '7I', bytes
)
148 return magic
, cputype
, cpusubtype
, filetype
, ncmds
, sizeofcmds
, flags
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
158 bytes
= CheckedRead(file, 20)
160 cputype
, cpusubtype
, offset
, size
, align
= struct
.unpack('>5I', bytes
)
161 return cputype
, cpusubtype
, offset
, size
, align
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
169 bytes
= struct
.pack(endian
+ 'I', uint32
)
170 assert len(bytes
) == 4
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."""
185 CheckedSeek(file, offset
)
186 magic
= ReadUInt32(file, '<')
187 if magic
== MH_MAGIC
or magic
== MH_MAGIC_64
:
189 elif magic
== MH_CIGAM
or magic
== MH_CIGAM_64
:
193 'Mach-O file at offset %d has illusion of magic' % offset
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
:
201 'Mach-O file at offset %d is type 0x%x, expected MH_EXECUTE' % \
204 original_flags
= flags
206 if options
.no_heap_execution
:
207 flags |
= MH_NO_HEAP_EXECUTION
209 flags
&= ~MH_NO_HEAP_EXECUTION
216 if flags
!= original_flags
:
217 CheckedSeek(file, offset
+ 24)
218 WriteUInt32(file, flags
, endian
)
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."""
225 CheckedSeek(file, fat_offset
)
226 magic
= ReadUInt32(file, '>')
227 assert magic
== FAT_MAGIC
229 nfat_arch
= ReadUInt32(file, '>')
231 for index
in xrange(0, nfat_arch
):
232 cputype
, cpusubtype
, offset
, size
, align
= ReadFatArch(file)
235 # HandleMachOFile will seek around. Come back here after calling it, in
237 fat_arch_offset
= file.tell()
238 HandleMachOFile(file, options
, offset
)
239 CheckedSeek(file, fat_arch_offset
)
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:
255 executable_path
= loose_args
[0]
256 executable_file
= open(executable_path
, 'rb+')
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
)
266 raise MachOError
, '%s is not a Mach-O or fat file' % executable_file
268 executable_file
.close()
272 if __name__
== '__main__':
273 sys
.exit(main(sys
.argv
[0], sys
.argv
[1:]))