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