├── README.md └── iboot-loader.py /README.md: -------------------------------------------------------------------------------- 1 | # IDA iBoot Loader 2 | 3 | IDA loader for Apple's iBoot, SecureROM and AVPBooter. 4 | 5 | ![Capture](https://user-images.githubusercontent.com/8758978/134245891-c458bcb1-632e-445b-9ace-2e8b798cba5e.PNG) 6 | 7 | 8 | ### Support 9 | 10 | This loader supports IDA 7.5 to IDA 8.4 and works on all Apple ARM64 bootloaders even M1+. 11 | 12 | ### Installation 13 | 14 | Copy the plugin file `iboot-loader.py` to your user plugins directory: 15 | 16 | OS | User Plugins Directory 17 | --------|------------------------------------- 18 | Windows | `%APPDATA%\Hex-Rays\IDA Pro\loaders` 19 | Linux | `~/.idapro/loaders` 20 | macOS | `~/.idapro/loaders` 21 | 22 | 23 | ### Usage 24 | 25 | Open a decrypted 64 bits iBoot image or a [SecureROM](https://securerom.fun) file with IDA. IDA should ask to open with this loader. 26 | 27 | ![Capture](https://user-images.githubusercontent.com/8758978/134242135-299bd5d0-cc62-44f0-8c8b-329361196942.PNG) 28 | 29 | ### Credits 30 | 31 | * This code is based on argp's [iBoot64helper](https://github.com/argp/iBoot64helper) 32 | * [iBoot-Binja-Loader](https://github.com/EliseZeroTwo/iBoot-Binja-Loader) 33 | -------------------------------------------------------------------------------- /iboot-loader.py: -------------------------------------------------------------------------------- 1 | import idautils 2 | import idaapi 3 | import ida_idaapi 4 | import ida_search 5 | import ida_funcs 6 | import ida_bytes 7 | import ida_kernwin 8 | import ida_segment 9 | import ida_idp 10 | import idc 11 | 12 | PROLOGUES = ["7F 23 03 D5", "BD A9", "BF A9"] 13 | 14 | 15 | def set_name_from_str_xref(base_addr, name, string): 16 | """Set function name based on a string xref.""" 17 | string_offset = ida_search.find_text( 18 | base_addr, 1, 1, string, ida_search.SEARCH_DOWN 19 | ) 20 | if string_offset == ida_idaapi.BADADDR: 21 | return ida_idaapi.BADADDR 22 | 23 | xref = list(idautils.XrefsTo(string_offset)) 24 | if len(xref) == 0: 25 | return ida_idaapi.BADADDR 26 | 27 | function = idaapi.get_func(xref[0].frm) 28 | if function is None: 29 | return ida_idaapi.BADADDR 30 | 31 | idc.set_name(function.start_ea, name, idc.SN_CHECK) 32 | print(f"[+] {name} : {hex(function.start_ea)}") 33 | return function.start_ea 34 | 35 | 36 | def set_name_from_pattern_xref(base_addr, end, name, pattern): 37 | """Set function name based on a specific bytes pattern.""" 38 | pattern_offset = ida_bytes.find_bytes(pattern, base_addr) 39 | if pattern_offset == ida_idaapi.BADADDR or pattern_offset is None: 40 | return ida_idaapi.BADADDR 41 | 42 | xref = list(idautils.XrefsTo(pattern_offset)) 43 | if len(xref) == 0: 44 | return ida_idaapi.BADADDR 45 | 46 | function = idaapi.get_func(xref[0].frm) 47 | if function is None: 48 | return ida_idaapi.BADADDR 49 | idc.set_name(function.start_ea, name, idc.SN_CHECK) 50 | print(f"[+] {name} : {hex(function.start_ea)}") 51 | return function.start_ea 52 | 53 | 54 | def set_name_from_func_xref(base_addr, name, function_addr): 55 | """Set function name based on a function xref.""" 56 | if function_addr == ida_idaapi.BADADDR: 57 | return ida_idaapi.BADADDR 58 | 59 | xref_list = list(idautils.XrefsTo(function_addr)) 60 | if len(xref_list) == 0: 61 | return ida_idaapi.BADADDR 62 | 63 | function = ida_funcs.get_func(xref_list[0].frm) 64 | if function is None: 65 | return ida_idaapi.BADADDR 66 | 67 | idc.set_name(function.start_ea, name, idc.SN_CHECK) 68 | print(f"[+] {name} : {hex(function.start_ea)}") 69 | return function.start_ea 70 | 71 | 72 | def set_name_on_str_before_bl(name: str, string: str): 73 | """Set name according to string before BL inst. 74 | Example with printf, we look for "USB_SERIAL_NUMBER:" then find the next BL. 75 | It branches to printf. 76 | ADR X0, aUsbSerialNumbe ; "::\tUSB_SERIAL_NUMBER: %s\n" 77 | NOP 78 | BL sub_1800F4980 <- printf 79 | 80 | TODO: maybe find a better name 81 | """ 82 | string_offset = ida_search.find_text(0, 1, 1, string, ida_search.SEARCH_DOWN) 83 | 84 | if string_offset == ida_idaapi.BADADDR: 85 | return ida_idaapi.BADADDR 86 | 87 | xref = list(idautils.XrefsTo(string_offset)) 88 | if len(xref) == 0: 89 | return ida_idaapi.BADADDR 90 | 91 | function = idaapi.get_func(xref[0].frm) 92 | for addr in range(xref[0].frm, idc.find_func_end(function.start_ea)): 93 | insn = idc.print_insn_mnem(addr) 94 | if "BL" in insn: 95 | function_addr = f"0x{idc.print_operand(addr, 0).split('_')[1]}" 96 | function = idaapi.get_func(int(function_addr, 16)) 97 | print(f"[+] {name} : {hex(function.start_ea)}") 98 | idc.set_name(function.start_ea, name, idc.SN_CHECK) 99 | return function.start_ea 100 | 101 | 102 | def set_name_on_xref_asserts(functions_list: list) -> list: 103 | """In A12+ dev iBoots we have strings like 'ASSERT (%s:%d)\n' 104 | at xref_addr-8 you can find the name of the function used by assert. Eg: 105 | ADR X0, aArchTaskFreeSt ; "arch_task_free_stack" 106 | NOP 107 | ADR X1, aAssertSD ; "ASSERT (%s:%d)\n" 108 | """ 109 | assert_str = idc.get_name_ea_simple("aAssertSD") 110 | xrefs = idautils.XrefsTo(assert_str) 111 | for xref in xrefs: 112 | if ida_kernwin.user_cancelled(): 113 | break 114 | 115 | addr = xref.frm 116 | function = ida_funcs.get_func(xref.frm) 117 | if function is None or "sub_" not in ida_funcs.get_func_name(xref.frm): 118 | continue 119 | dis = idc.GetDisasm(addr - 8) 120 | if "X0, a" in dis: 121 | operand = idc.print_operand(addr - 8, 1) 122 | string_name_addr = idc.get_name_ea_simple(operand) 123 | name = idc.get_strlit_contents(string_name_addr).decode() 124 | 125 | # if name already exists, continue 126 | if f"_{name}" in functions_list: 127 | continue 128 | print(f"[+] _{name} : {hex(function.start_ea)}") 129 | idc.set_name(function.start_ea, f"_{name}", idc.SN_NOWARN) 130 | # use idc.SN_NOWARN if there are to many warnings 131 | functions_list.append(f"_{name}") 132 | return functions_list 133 | 134 | 135 | def set_name_on_xref_heap_malloc(heap_malloc: int): 136 | """Debug iBoots use heap_malloc(size_t size, const char *caller_name). 137 | We can use it to get the name of the function which calls it. 138 | Only tested on one debug iBoot (from A10/iOS10), it may not be 100% accurate. 139 | """ 140 | xrefs = idautils.XrefsTo(heap_malloc) 141 | for xref in xrefs: 142 | if ida_kernwin.user_cancelled(): 143 | break 144 | 145 | addr = xref.frm 146 | function = ida_funcs.get_func(addr) 147 | # check that the function hasn't already a name 148 | if function is None or "sub_" not in ida_funcs.get_func_name(xref.frm): 149 | continue 150 | 151 | # find the name of heap_malloc caller 152 | for i in range(addr, addr - 20, -4): 153 | dis = idc.GetDisasm(i) 154 | if "BL" in dis and i != addr: 155 | break 156 | 157 | if "ADRX1,a" in dis.replace(" ", ""): 158 | operand = idc.print_operand(i, 1) 159 | string_name_addr = idc.get_name_ea_simple(operand) 160 | name = idc.get_strlit_contents(string_name_addr).decode() 161 | print(f"[+] _{name} : {hex(function.start_ea)}") 162 | idc.set_name(function.start_ea, f"_{name}") 163 | 164 | 165 | def set_name_on_xref_panics(panic) -> list: 166 | """Same as previous function but for panic xrefs.""" 167 | xrefs = idautils.XrefsTo(panic) 168 | functions_list = [] 169 | for xref in xrefs: 170 | if ida_kernwin.user_cancelled(): 171 | break 172 | 173 | addr = xref.frm 174 | function = ida_funcs.get_func(xref.frm) 175 | if function is None or "sub_" not in ida_funcs.get_func_name(xref.frm): 176 | continue 177 | 178 | expected_nop = idc.print_insn_mnem(addr - 4) 179 | dis = idc.GetDisasm(addr - 16) 180 | if expected_nop == "NOP" and ("X0, a" in dis and "#0" not in dis[-2:]): 181 | # if we have a line like this : "ADR X0, aPlatformQuiesc" 182 | # it returns "aPlatformQuiesc" 183 | operand = idc.print_operand(addr - 16, 1) 184 | string_name_addr = idc.get_name_ea_simple(operand) 185 | name = idc.get_strlit_contents(string_name_addr).decode() 186 | 187 | if f"_{name}" in functions_list: 188 | continue 189 | 190 | print(f"[+] _{name} : {hex(function.start_ea)}") 191 | idc.set_name(function.start_ea, f"_{name}") 192 | functions_list.append(f"_{name}") 193 | return functions_list 194 | 195 | 196 | def accept_file(fd, fname): 197 | """Make sure file is valid.""" 198 | fd.seek(0x200) 199 | try: 200 | image_type = fd.read(0x30).decode() 201 | except UnicodeDecodeError: 202 | return 0 203 | except AttributeError: 204 | # When file is small, IDA will report error 205 | # AttributeError: 'NoneType' object has no attribute 'decode' 206 | return 0 207 | 208 | if image_type[:5] == "iBoot" or image_type[:4] in ["iBEC", "iBSS"]: 209 | return {"format": "iBoot (AArch64)", "processor": "arm"} 210 | 211 | if image_type[:9] in ["SecureROM", "AVPBooter"]: 212 | return {"format": "SecureROM (AArch64)", "processor": "arm"} 213 | return 0 214 | 215 | 216 | def is_bootrom(fd) -> bool: 217 | """Check if image is rom type. Purely aesthetic.""" 218 | fd.seek(0x200) 219 | image_type = fd.read(0x30).decode() 220 | if image_type[:9] in ["SecureROM", "AVPBooter"]: 221 | return True 222 | return False 223 | 224 | 225 | def is_bootloader_release(fd) -> [bool, str]: 226 | """Check if bootloader is type release.""" 227 | tags = [b"RELEASE", b"ROMRELEASE", b"RESEARCH_RELEASE", b"DEBUG", b"DEVELOPMENT"] 228 | fd.seek(0x240) 229 | data = fd.read(16) 230 | for tag in tags: 231 | tag_len = len(tag) 232 | data_ = data[:tag_len] 233 | if data_ == tag and data_ in tags[:3]: 234 | return True, tag.decode() 235 | elif data_ == tag and data not in tags[:3]: 236 | return False, tag.decode() 237 | return False, None 238 | 239 | 240 | BASIC_STR_XREFS = { 241 | "_do_printf": "", 242 | "_platform_get_usb_serial_number_string": "CPID:", 243 | "_platform_get_usb_more_other_string": " NONC:", 244 | "_UpdateDeviceTree": "fuse-revision", 245 | "_main_task": "debug-uarts", 246 | "_platform_init_display": "backlight-level", 247 | "_do_printf": "", 248 | "_do_memboot": "Combo image too large", 249 | "_do_go": "Memory image not valid", 250 | "_task_init": "idle task", 251 | "_sys_setup_default_environment": "/System/Library/Caches/com.apple.kernelcaches/kernelcache", 252 | "_check_autoboot": "aborting autoboot due to user intervention.", 253 | "_do_setpict": "picture too large: size:%zu", 254 | "_arm_exception_abort": "ARM %s abort at 0x%016llx:", 255 | "_do_devicetree": "Device Tree image not valid", 256 | "_do_ramdisk": "Ramdisk image not valid", 257 | "_nvme_bdev_create": "Couldn't construct blockdev for namespace %d", 258 | "_record_memory_range": "chosen/memory-map", 259 | "_boot_upgrade_system": "/boot/kernelcache", 260 | "_target_pass_boot_manifest": "chosen/manifest-properties", 261 | "_image4_validate_property_callback_interposer": "Unknown ASN1 type %llu", 262 | "_platform_handoff_update_devicetree": "iboot-handoff", 263 | "_prepare_and_jump": "======== End of %s serial output. ========", 264 | } 265 | 266 | 267 | def post_process(use_panic_strings: bool) -> None: 268 | prompt = ( 269 | "Autoanalysis is complete.\n\nDo you want to search for known iBoot functions?" 270 | ) 271 | if ida_kernwin.ask_yn(ida_kernwin.ASKBTN_YES, prompt) != ida_kernwin.ASKBTN_YES: 272 | return 273 | 274 | # The loader only creates one segment, so we can easily get that segment 275 | # and its bounds like this. 276 | main_segm = ida_segment.get_first_seg() 277 | base_addr = main_segm.start_ea 278 | segment_end = main_segm.end_ea 279 | 280 | ida_kernwin.show_wait_box("Searching for known functions...") 281 | 282 | # find IMG4 string as byte 283 | set_name_from_pattern_xref( 284 | base_addr, segment_end, "_image4_get_partial", "49 4d 47 34" 285 | ) 286 | 287 | panic = set_name_from_str_xref(base_addr, "_panic", "double panic in") 288 | heap_malloc = set_name_from_str_xref( 289 | base_addr, "_heap_malloc", "heap_malloc must allocate at least one byte" 290 | ) 291 | img4_register = set_name_from_str_xref( 292 | base_addr, 293 | "_image4_register_property_capture_callbacks", 294 | "image4_register_property_capture_callbacks", 295 | ) 296 | 297 | # Handle the bulk of the basic string-to-name patterns in a loop for both 298 | # organizational purposes and the ability to cancel the operation while it 299 | # is in progress. 300 | i = 0 301 | count = len(BASIC_STR_XREFS) 302 | for name, string in BASIC_STR_XREFS.items(): 303 | if ida_kernwin.user_cancelled(): 304 | ida_kernwin.hide_wait_box() 305 | return 306 | 307 | i += 1 308 | ida_kernwin.replace_wait_box( 309 | f"Analyzing basic string references... ({i}/{count})" 310 | ) 311 | 312 | set_name_from_str_xref(base_addr, name, string) 313 | 314 | # If the user wants to cancel here, they will just have to suffer... 315 | usb_vendor_id = set_name_from_pattern_xref( 316 | base_addr, segment_end, "_platform_get_usb_vendor_id", "80 b5 80 52" 317 | ) 318 | usb_core_init = set_name_from_func_xref(base_addr, "_usb_core_init", usb_vendor_id) 319 | set_name_from_func_xref(base_addr, "_usb_init_with_controller", usb_core_init) 320 | set_name_from_func_xref(base_addr, "_target_init_boot_manifest", img4_register) 321 | 322 | set_name_on_str_before_bl("_printf", "USB_SERIAL_NUMBER:") 323 | set_name_on_str_before_bl("_der_expect_ia5string", "IM4P") 324 | 325 | functions = [] 326 | if use_panic_strings: 327 | ida_kernwin.replace_wait_box("Analyzing panic strings...") 328 | 329 | # All of these functions below check for the "user cancelled" signal 330 | # inside and will return early accordingly. 331 | functions = set_name_on_xref_panics(panic) 332 | set_name_on_xref_asserts(functions) 333 | 334 | if heap_malloc != ida_idaapi.BADADDR: 335 | set_name_on_xref_heap_malloc(heap_malloc) 336 | 337 | ida_kernwin.hide_wait_box() 338 | 339 | 340 | class post_processing_hook_t(ida_idp.IDB_Hooks): 341 | use_panic_strings: bool 342 | 343 | def __init__(self, use_panic_strings: bool = False): 344 | super().__init__() 345 | self.use_panic_strings = use_panic_strings 346 | 347 | def auto_empty_finally(self, *args): 348 | post_process(self.use_panic_strings) 349 | 350 | 351 | POST_PROCESS_HOOK = None 352 | 353 | 354 | def load_file(fd, neflags, format): 355 | """Function to load file.""" 356 | size = 0 357 | base_addr = 0 358 | 359 | idaapi.set_processor_type("arm", ida_idp.SETPROC_LOADER_NON_FATAL) 360 | idc.set_inf_attr(idc.INF_LFLAGS, idc.get_inf_attr(idc.INF_LFLAGS) | idc.LFLG_64BIT) 361 | 362 | if (neflags & idaapi.NEF_RELOAD) != 0: 363 | return 1 364 | 365 | fd.seek(0, idaapi.SEEK_END) 366 | size = fd.tell() 367 | 368 | segm = idaapi.segment_t() 369 | segm.bitness = 2 # 64-bit 370 | segm.start_ea = 0 371 | segm.end_ea = size 372 | 373 | if is_bootrom(fd): 374 | idaapi.add_segm_ex(segm, "SecureROM", "CODE", idaapi.ADDSEG_OR_DIE) 375 | else: 376 | idaapi.add_segm_ex(segm, "iBoot", "CODE", idaapi.ADDSEG_OR_DIE) 377 | 378 | bl_data = is_bootloader_release(fd) 379 | print(f"[i] bootloader : {bl_data[1]}") 380 | 381 | global POST_PROCESS_HOOK 382 | POST_PROCESS_HOOK = post_processing_hook_t(bl_data[0] == False) 383 | POST_PROCESS_HOOK.hook() 384 | 385 | fd.seek(0) 386 | fd.file2base(0, 0, size, False) 387 | 388 | idaapi.add_entry(0, 0, "start", 1) 389 | 390 | for addr in range(0, 0x200, 4): 391 | insn = idc.GetDisasm(addr) 392 | if "LDR" in insn: 393 | base_str = idc.print_operand(addr, 1) 394 | base_addr = int(base_str.split("=")[1], 16) 395 | break 396 | 397 | if base_addr == 0: 398 | print("[!] Failed to find base address, it's now set to 0x0") 399 | 400 | print(f"[+] Rebasing to address {hex(base_addr)}") 401 | idaapi.rebase_program(base_addr, idc.MSF_NOFIX) 402 | 403 | segment_end = idc.get_segm_attr(base_addr, idc.SEGATTR_END) 404 | 405 | for prologue in PROLOGUES: 406 | while addr != ida_idaapi.BADADDR: 407 | addr = ida_bytes.find_bytes(prologue, addr) 408 | if addr != ida_idaapi.BADADDR: 409 | if len(prologue) < 8: 410 | addr = addr - 2 411 | 412 | if (addr % 4) == 0 and ida_bytes.get_full_flags(addr) < 0x200: 413 | ida_funcs.add_func(addr) 414 | addr += 4 415 | return 1 416 | --------------------------------------------------------------------------------