[
  {
    "path": "README.md",
    "content": "# IDA iBoot Loader\n\nIDA loader for Apple's iBoot, SecureROM and AVPBooter. \n\n![Capture](https://user-images.githubusercontent.com/8758978/134245891-c458bcb1-632e-445b-9ace-2e8b798cba5e.PNG)\n\n\n### Support\n\nThis loader supports IDA 7.5 to IDA 8.4 and works on all Apple ARM64 bootloaders even M1+.\n\n### Installation\n\nCopy the plugin file `iboot-loader.py` to your user plugins directory:\n\nOS      | User Plugins Directory\n--------|-------------------------------------\nWindows | `%APPDATA%\\Hex-Rays\\IDA Pro\\loaders`\nLinux   | `~/.idapro/loaders`\nmacOS   | `~/.idapro/loaders`\n\n\n### Usage\n\nOpen a decrypted 64 bits iBoot image or a [SecureROM](https://securerom.fun) file with IDA. IDA should ask to open with this loader.\n\n![Capture](https://user-images.githubusercontent.com/8758978/134242135-299bd5d0-cc62-44f0-8c8b-329361196942.PNG)\n\n### Credits\n\n* This code is based on argp's [iBoot64helper](https://github.com/argp/iBoot64helper)\n* [iBoot-Binja-Loader](https://github.com/EliseZeroTwo/iBoot-Binja-Loader)\n"
  },
  {
    "path": "iboot-loader.py",
    "content": "import idautils\r\nimport idaapi\r\nimport ida_idaapi\r\nimport ida_search\r\nimport ida_funcs\r\nimport ida_bytes\r\nimport ida_kernwin\r\nimport ida_segment\r\nimport ida_idp\r\nimport idc\r\n\r\nPROLOGUES = [\"7F 23 03 D5\", \"BD A9\", \"BF A9\"]\r\n\r\n\r\ndef set_name_from_str_xref(base_addr, name, string):\r\n    \"\"\"Set function name based on a string xref.\"\"\"\r\n    string_offset = ida_search.find_text(\r\n        base_addr, 1, 1, string, ida_search.SEARCH_DOWN\r\n    )\r\n    if string_offset == ida_idaapi.BADADDR:\r\n        return ida_idaapi.BADADDR\r\n\r\n    xref = list(idautils.XrefsTo(string_offset))\r\n    if len(xref) == 0:\r\n        return ida_idaapi.BADADDR\r\n\r\n    function = idaapi.get_func(xref[0].frm)\r\n    if function is None:\r\n        return ida_idaapi.BADADDR\r\n\r\n    idc.set_name(function.start_ea, name, idc.SN_CHECK)\r\n    print(f\"[+] {name} : {hex(function.start_ea)}\")\r\n    return function.start_ea\r\n\r\n\r\ndef set_name_from_pattern_xref(base_addr, end, name, pattern):\r\n    \"\"\"Set function name based on a specific bytes pattern.\"\"\"\r\n    pattern_offset = ida_bytes.find_bytes(pattern, base_addr)\r\n    if pattern_offset == ida_idaapi.BADADDR or pattern_offset is None:\r\n        return ida_idaapi.BADADDR\r\n\r\n    xref = list(idautils.XrefsTo(pattern_offset))\r\n    if len(xref) == 0:\r\n        return ida_idaapi.BADADDR\r\n\r\n    function = idaapi.get_func(xref[0].frm)\r\n    if function is None:\r\n        return ida_idaapi.BADADDR\r\n    idc.set_name(function.start_ea, name, idc.SN_CHECK)\r\n    print(f\"[+] {name} : {hex(function.start_ea)}\")\r\n    return function.start_ea\r\n\r\n\r\ndef set_name_from_func_xref(base_addr, name, function_addr):\r\n    \"\"\"Set function name based on a function xref.\"\"\"\r\n    if function_addr == ida_idaapi.BADADDR:\r\n        return ida_idaapi.BADADDR\r\n\r\n    xref_list = list(idautils.XrefsTo(function_addr))\r\n    if len(xref_list) == 0:\r\n        return ida_idaapi.BADADDR\r\n\r\n    function = ida_funcs.get_func(xref_list[0].frm)\r\n    if function is None:\r\n        return ida_idaapi.BADADDR\r\n\r\n    idc.set_name(function.start_ea, name, idc.SN_CHECK)\r\n    print(f\"[+] {name} : {hex(function.start_ea)}\")\r\n    return function.start_ea\r\n\r\n\r\ndef set_name_on_str_before_bl(name: str, string: str):\r\n    \"\"\"Set name according to string before BL inst.\r\n    Example with printf, we look for \"USB_SERIAL_NUMBER:\" then find the next BL.\r\n    It branches to printf.\r\n    ADR             X0, aUsbSerialNumbe ; \"::\\tUSB_SERIAL_NUMBER: %s\\n\"\r\n    NOP\r\n    BL              sub_1800F4980 <- printf\r\n\r\n    TODO: maybe find a better name\r\n    \"\"\"\r\n    string_offset = ida_search.find_text(0, 1, 1, string, ida_search.SEARCH_DOWN)\r\n\r\n    if string_offset == ida_idaapi.BADADDR:\r\n        return ida_idaapi.BADADDR\r\n\r\n    xref = list(idautils.XrefsTo(string_offset))\r\n    if len(xref) == 0:\r\n        return ida_idaapi.BADADDR\r\n\r\n    function = idaapi.get_func(xref[0].frm)\r\n    for addr in range(xref[0].frm, idc.find_func_end(function.start_ea)):\r\n        insn = idc.print_insn_mnem(addr)\r\n        if \"BL\" in insn:\r\n            function_addr = f\"0x{idc.print_operand(addr, 0).split('_')[1]}\"\r\n            function = idaapi.get_func(int(function_addr, 16))\r\n            print(f\"[+] {name} : {hex(function.start_ea)}\")\r\n            idc.set_name(function.start_ea, name, idc.SN_CHECK)\r\n            return function.start_ea\r\n\r\n\r\ndef set_name_on_xref_asserts(functions_list: list) -> list:\r\n    \"\"\"In A12+ dev iBoots we have strings like 'ASSERT (%s:%d)\\n'\r\n    at xref_addr-8 you can find the name of the function used by assert. Eg:\r\n    ADR             X0, aArchTaskFreeSt ; \"arch_task_free_stack\"\r\n    NOP\r\n    ADR             X1, aAssertSD ; \"ASSERT (%s:%d)\\n\"\r\n    \"\"\"\r\n    assert_str = idc.get_name_ea_simple(\"aAssertSD\")\r\n    xrefs = idautils.XrefsTo(assert_str)\r\n    for xref in xrefs:\r\n        if ida_kernwin.user_cancelled():\r\n            break\r\n\r\n        addr = xref.frm\r\n        function = ida_funcs.get_func(xref.frm)\r\n        if function is None or \"sub_\" not in ida_funcs.get_func_name(xref.frm):\r\n            continue\r\n        dis = idc.GetDisasm(addr - 8)\r\n        if \"X0, a\" in dis:\r\n            operand = idc.print_operand(addr - 8, 1)\r\n            string_name_addr = idc.get_name_ea_simple(operand)\r\n            name = idc.get_strlit_contents(string_name_addr).decode()\r\n\r\n            # if name already exists, continue\r\n            if f\"_{name}\" in functions_list:\r\n                continue\r\n            print(f\"[+] _{name} : {hex(function.start_ea)}\")\r\n            idc.set_name(function.start_ea, f\"_{name}\", idc.SN_NOWARN)\r\n            # use idc.SN_NOWARN if there are to many warnings\r\n            functions_list.append(f\"_{name}\")\r\n    return functions_list\r\n\r\n\r\ndef set_name_on_xref_heap_malloc(heap_malloc: int):\r\n    \"\"\"Debug iBoots use heap_malloc(size_t size, const char *caller_name).\r\n    We can use it to get the name of the function which calls it.\r\n    Only tested on one debug iBoot (from A10/iOS10), it may not be 100% accurate.\r\n    \"\"\"\r\n    xrefs = idautils.XrefsTo(heap_malloc)\r\n    for xref in xrefs:\r\n        if ida_kernwin.user_cancelled():\r\n            break\r\n\r\n        addr = xref.frm\r\n        function = ida_funcs.get_func(addr)\r\n        # check that the function hasn't already a name\r\n        if function is None or \"sub_\" not in ida_funcs.get_func_name(xref.frm):\r\n            continue\r\n\r\n        # find the name of heap_malloc caller\r\n        for i in range(addr, addr - 20, -4):\r\n            dis = idc.GetDisasm(i)\r\n            if \"BL\" in dis and i != addr:\r\n                break\r\n\r\n            if \"ADRX1,a\" in dis.replace(\" \", \"\"):\r\n                operand = idc.print_operand(i, 1)\r\n                string_name_addr = idc.get_name_ea_simple(operand)\r\n                name = idc.get_strlit_contents(string_name_addr).decode()\r\n                print(f\"[+] _{name} : {hex(function.start_ea)}\")\r\n                idc.set_name(function.start_ea, f\"_{name}\")\r\n\r\n\r\ndef set_name_on_xref_panics(panic) -> list:\r\n    \"\"\"Same as previous function but for panic xrefs.\"\"\"\r\n    xrefs = idautils.XrefsTo(panic)\r\n    functions_list = []\r\n    for xref in xrefs:\r\n        if ida_kernwin.user_cancelled():\r\n            break\r\n\r\n        addr = xref.frm\r\n        function = ida_funcs.get_func(xref.frm)\r\n        if function is None or \"sub_\" not in ida_funcs.get_func_name(xref.frm):\r\n            continue\r\n\r\n        expected_nop = idc.print_insn_mnem(addr - 4)\r\n        dis = idc.GetDisasm(addr - 16)\r\n        if expected_nop == \"NOP\" and (\"X0, a\" in dis and \"#0\" not in dis[-2:]):\r\n            # if we have a line like this : \"ADR X0, aPlatformQuiesc\"\r\n            # it returns \"aPlatformQuiesc\"\r\n            operand = idc.print_operand(addr - 16, 1)\r\n            string_name_addr = idc.get_name_ea_simple(operand)\r\n            name = idc.get_strlit_contents(string_name_addr).decode()\r\n\r\n            if f\"_{name}\" in functions_list:\r\n                continue\r\n\r\n            print(f\"[+] _{name} : {hex(function.start_ea)}\")\r\n            idc.set_name(function.start_ea, f\"_{name}\")\r\n            functions_list.append(f\"_{name}\")\r\n    return functions_list\r\n\r\n\r\ndef accept_file(fd, fname):\r\n    \"\"\"Make sure file is valid.\"\"\"\r\n    fd.seek(0x200)\r\n    try:\r\n        image_type = fd.read(0x30).decode()\r\n    except UnicodeDecodeError:\r\n        return 0\r\n    except AttributeError:\r\n        # When file is small, IDA will report error\r\n        # AttributeError: 'NoneType' object has no attribute 'decode'\r\n        return 0\r\n\r\n    if image_type[:5] == \"iBoot\" or image_type[:4] in [\"iBEC\", \"iBSS\"]:\r\n        return {\"format\": \"iBoot (AArch64)\", \"processor\": \"arm\"}\r\n\r\n    if image_type[:9] in [\"SecureROM\", \"AVPBooter\"]:\r\n        return {\"format\": \"SecureROM (AArch64)\", \"processor\": \"arm\"}\r\n    return 0\r\n\r\n\r\ndef is_bootrom(fd) -> bool:\r\n    \"\"\"Check if image is rom type. Purely aesthetic.\"\"\"\r\n    fd.seek(0x200)\r\n    image_type = fd.read(0x30).decode()\r\n    if image_type[:9] in [\"SecureROM\", \"AVPBooter\"]:\r\n        return True\r\n    return False\r\n\r\n\r\ndef is_bootloader_release(fd) -> [bool, str]:\r\n    \"\"\"Check if bootloader is type release.\"\"\"\r\n    tags = [b\"RELEASE\", b\"ROMRELEASE\", b\"RESEARCH_RELEASE\", b\"DEBUG\", b\"DEVELOPMENT\"]\r\n    fd.seek(0x240)\r\n    data = fd.read(16)\r\n    for tag in tags:\r\n        tag_len = len(tag)\r\n        data_ = data[:tag_len]\r\n        if data_ == tag and data_ in tags[:3]:\r\n            return True, tag.decode()\r\n        elif data_ == tag and data not in tags[:3]:\r\n            return False, tag.decode()\r\n    return False, None\r\n\r\n\r\nBASIC_STR_XREFS = {\r\n    \"_do_printf\": \"<null>\",\r\n    \"_platform_get_usb_serial_number_string\": \"CPID:\",\r\n    \"_platform_get_usb_more_other_string\": \" NONC:\",\r\n    \"_UpdateDeviceTree\": \"fuse-revision\",\r\n    \"_main_task\": \"debug-uarts\",\r\n    \"_platform_init_display\": \"backlight-level\",\r\n    \"_do_printf\": \"<null>\",\r\n    \"_do_memboot\": \"Combo image too large\",\r\n    \"_do_go\": \"Memory image not valid\",\r\n    \"_task_init\": \"idle task\",\r\n    \"_sys_setup_default_environment\": \"/System/Library/Caches/com.apple.kernelcaches/kernelcache\",\r\n    \"_check_autoboot\": \"aborting autoboot due to user intervention.\",\r\n    \"_do_setpict\": \"picture too large: size:%zu\",\r\n    \"_arm_exception_abort\": \"ARM %s abort at 0x%016llx:\",\r\n    \"_do_devicetree\": \"Device Tree image not valid\",\r\n    \"_do_ramdisk\": \"Ramdisk image not valid\",\r\n    \"_nvme_bdev_create\": \"Couldn't construct blockdev for namespace %d\",\r\n    \"_record_memory_range\": \"chosen/memory-map\",\r\n    \"_boot_upgrade_system\": \"/boot/kernelcache\",\r\n    \"_target_pass_boot_manifest\": \"chosen/manifest-properties\",\r\n    \"_image4_validate_property_callback_interposer\": \"Unknown ASN1 type %llu\",\r\n    \"_platform_handoff_update_devicetree\": \"iboot-handoff\",\r\n    \"_prepare_and_jump\": \"======== End of %s serial output. ========\",\r\n}\r\n\r\n\r\ndef post_process(use_panic_strings: bool) -> None:\r\n    prompt = (\r\n        \"Autoanalysis is complete.\\n\\nDo you want to search for known iBoot functions?\"\r\n    )\r\n    if ida_kernwin.ask_yn(ida_kernwin.ASKBTN_YES, prompt) != ida_kernwin.ASKBTN_YES:\r\n        return\r\n\r\n    # The loader only creates one segment, so we can easily get that segment\r\n    # and its bounds like this.\r\n    main_segm = ida_segment.get_first_seg()\r\n    base_addr = main_segm.start_ea\r\n    segment_end = main_segm.end_ea\r\n\r\n    ida_kernwin.show_wait_box(\"Searching for known functions...\")\r\n\r\n    # find IMG4 string as byte\r\n    set_name_from_pattern_xref(\r\n        base_addr, segment_end, \"_image4_get_partial\", \"49 4d 47 34\"\r\n    )\r\n\r\n    panic = set_name_from_str_xref(base_addr, \"_panic\", \"double panic in\")\r\n    heap_malloc = set_name_from_str_xref(\r\n        base_addr, \"_heap_malloc\", \"heap_malloc must allocate at least one byte\"\r\n    )\r\n    img4_register = set_name_from_str_xref(\r\n        base_addr,\r\n        \"_image4_register_property_capture_callbacks\",\r\n        \"image4_register_property_capture_callbacks\",\r\n    )\r\n\r\n    # Handle the bulk of the basic string-to-name patterns in a loop for both\r\n    # organizational purposes and the ability to cancel the operation while it\r\n    # is in progress.\r\n    i = 0\r\n    count = len(BASIC_STR_XREFS)\r\n    for name, string in BASIC_STR_XREFS.items():\r\n        if ida_kernwin.user_cancelled():\r\n            ida_kernwin.hide_wait_box()\r\n            return\r\n\r\n        i += 1\r\n        ida_kernwin.replace_wait_box(\r\n            f\"Analyzing basic string references... ({i}/{count})\"\r\n        )\r\n\r\n        set_name_from_str_xref(base_addr, name, string)\r\n\r\n    # If the user wants to cancel here, they will just have to suffer...\r\n    usb_vendor_id = set_name_from_pattern_xref(\r\n        base_addr, segment_end, \"_platform_get_usb_vendor_id\", \"80 b5 80 52\"\r\n    )\r\n    usb_core_init = set_name_from_func_xref(base_addr, \"_usb_core_init\", usb_vendor_id)\r\n    set_name_from_func_xref(base_addr, \"_usb_init_with_controller\", usb_core_init)\r\n    set_name_from_func_xref(base_addr, \"_target_init_boot_manifest\", img4_register)\r\n\r\n    set_name_on_str_before_bl(\"_printf\", \"USB_SERIAL_NUMBER:\")\r\n    set_name_on_str_before_bl(\"_der_expect_ia5string\", \"IM4P\")\r\n\r\n    functions = []\r\n    if use_panic_strings:\r\n        ida_kernwin.replace_wait_box(\"Analyzing panic strings...\")\r\n\r\n        # All of these functions below check for the \"user cancelled\" signal\r\n        # inside and will return early accordingly.\r\n        functions = set_name_on_xref_panics(panic)\r\n        set_name_on_xref_asserts(functions)\r\n\r\n        if heap_malloc != ida_idaapi.BADADDR:\r\n            set_name_on_xref_heap_malloc(heap_malloc)\r\n\r\n    ida_kernwin.hide_wait_box()\r\n\r\n\r\nclass post_processing_hook_t(ida_idp.IDB_Hooks):\r\n    use_panic_strings: bool\r\n\r\n    def __init__(self, use_panic_strings: bool = False):\r\n        super().__init__()\r\n        self.use_panic_strings = use_panic_strings\r\n\r\n    def auto_empty_finally(self, *args):\r\n        post_process(self.use_panic_strings)\r\n\r\n\r\nPOST_PROCESS_HOOK = None\r\n\r\n\r\ndef load_file(fd, neflags, format):\r\n    \"\"\"Function to load file.\"\"\"\r\n    size = 0\r\n    base_addr = 0\r\n\r\n    idaapi.set_processor_type(\"arm\", ida_idp.SETPROC_LOADER_NON_FATAL)\r\n    idc.set_inf_attr(idc.INF_LFLAGS, idc.get_inf_attr(idc.INF_LFLAGS) | idc.LFLG_64BIT)\r\n\r\n    if (neflags & idaapi.NEF_RELOAD) != 0:\r\n        return 1\r\n\r\n    fd.seek(0, idaapi.SEEK_END)\r\n    size = fd.tell()\r\n\r\n    segm = idaapi.segment_t()\r\n    segm.bitness = 2  # 64-bit\r\n    segm.start_ea = 0\r\n    segm.end_ea = size\r\n\r\n    if is_bootrom(fd):\r\n        idaapi.add_segm_ex(segm, \"SecureROM\", \"CODE\", idaapi.ADDSEG_OR_DIE)\r\n    else:\r\n        idaapi.add_segm_ex(segm, \"iBoot\", \"CODE\", idaapi.ADDSEG_OR_DIE)\r\n\r\n    bl_data = is_bootloader_release(fd)\r\n    print(f\"[i] bootloader : {bl_data[1]}\")\r\n\r\n    global POST_PROCESS_HOOK\r\n    POST_PROCESS_HOOK = post_processing_hook_t(bl_data[0] == False)\r\n    POST_PROCESS_HOOK.hook()\r\n\r\n    fd.seek(0)\r\n    fd.file2base(0, 0, size, False)\r\n\r\n    idaapi.add_entry(0, 0, \"start\", 1)\r\n\r\n    for addr in range(0, 0x200, 4):\r\n        insn = idc.GetDisasm(addr)\r\n        if \"LDR\" in insn:\r\n            base_str = idc.print_operand(addr, 1)\r\n            base_addr = int(base_str.split(\"=\")[1], 16)\r\n            break\r\n\r\n    if base_addr == 0:\r\n        print(\"[!] Failed to find base address, it's now set to 0x0\")\r\n\r\n    print(f\"[+] Rebasing to address {hex(base_addr)}\")\r\n    idaapi.rebase_program(base_addr, idc.MSF_NOFIX)\r\n\r\n    segment_end = idc.get_segm_attr(base_addr, idc.SEGATTR_END)\r\n\r\n    for prologue in PROLOGUES:\r\n        while addr != ida_idaapi.BADADDR:\r\n            addr = ida_bytes.find_bytes(prologue, addr)\r\n            if addr != ida_idaapi.BADADDR:\r\n                if len(prologue) < 8:\r\n                    addr = addr - 2\r\n\r\n                if (addr % 4) == 0 and ida_bytes.get_full_flags(addr) < 0x200:\r\n                    ida_funcs.add_func(addr)\r\n                addr += 4\r\n    return 1\r\n"
  }
]