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