From a73cd3b5e20ec119e82a8564e3303f964e40d795 Mon Sep 17 00:00:00 2001 From: Jon Palmisciano Date: Sun, 11 Jan 2026 08:17:34 +0000 Subject: [PATCH 1/4] Plugins: Mark panic as `noreturn`, name functions using strings - Finds `panic` via the panic string, marks as `noreturn` for improved analysis - Other known functions are also named using unique string references - Formatting of reported exceptions has been improved - Binja: Fixed issue where exceptions were incorrectly silenced - Updated README roadmap Resolves: https://github.com/jonpalmisc/ibis/issues/2 Resolves: https://github.com/jonpalmisc/ibis/issues/17 --- README.md | 3 +- plugin/binja/view.py | 65 +++++++++++++++---- plugin/ida/ibis.py | 145 +++++++++++++++++++++++++++++++++++-------- src/ibis/plugins.py | 7 ++- src/ibis/strings.py | 25 ++++++++ 5 files changed, 204 insertions(+), 41 deletions(-) create mode 100644 src/ibis/strings.py diff --git a/README.md b/README.md index 33d1eca..4948107 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,7 @@ planned features are detailed as follows: segments with correct boundaries and permissions. - [x] Function prologue detection (helps prevent run-on functions when disassemblers fail to identify `noreturn` functions). -- [ ] Automatic known function identification via string reference heuristics. - (https://github.com/jonpalmisc/ibis/issues/2) +- [x] Automatic known function identification via string reference heuristics. - [ ] Automatic detection & marking of outlined functions. (https://github.com/jonpalmisc/ibis/issues/4) - [ ] πŸ”₯πŸŒΈβ‰οΈ diff --git a/plugin/binja/view.py b/plugin/binja/view.py index abfea1f..fba2043 100644 --- a/plugin/binja/view.py +++ b/plugin/binja/view.py @@ -4,10 +4,12 @@ from binaryninja import ( Architecture, BinaryView, + Function, MessageBoxIcon, Platform, SectionSemantics, SegmentFlag, + StringReference, Symbol, SymbolType, log_error_for_exception, @@ -25,10 +27,32 @@ from ibis.layout import FALLBACK_BSS_SIZE, Layout # noqa: E402 from ibis.plugins import ( # noqa: E402 ANALYZE_FAIL_MESSAGE, - ANALYZE_FAIL_TITLE, + ERROR_HEADER, + ERROR_TITLE, ISSUES_URL, MISSING_BSS_BOUNDS, + PLEASE_REPORT_GUI_MESSAGE, + PLEASE_REPORT_MESSAGE, ) +from ibis.strings import UNIQUE_STR_XREFS # noqa: E402 + + +def report_exception(message: str): + log_error(f"{ERROR_HEADER}") + log_error(" ") # Newlines & empty lines are stripped, so we have to do this. + + # The exception here is always the current exception. A clickable "details" + # button will be added to the end of the logged message. + log_error_for_exception(message) + + log_error(" ") + log_error(f"{PLEASE_REPORT_MESSAGE} ({ISSUES_URL})") + + show_message_box( + ERROR_TITLE, + f"{message}\n\n{PLEASE_REPORT_GUI_MESSAGE}\n\n{ISSUES_URL}", + icon=MessageBoxIcon.WarningIcon, + ) class BinjaDriver(Driver): @@ -118,6 +142,30 @@ def _apply_layout(self, layout: Layout): SectionSemantics.ReadWriteDataSectionSemantics, ) + def _first_string_matching(self, pattern: str) -> StringReference | None: + return next((s for s in self.strings if pattern in s.value), None) + + def _set_name_from_str_xref(self, name: str, pattern: str) -> Function | None: + if not (needle := self._first_string_matching(pattern)): + return None + + if not (ref := next(self.get_code_refs(needle.start))): + return None + + if not (funcs := self.get_functions_containing(ref.address)): + return None + + funcs[0].name = name + return funcs[0] + + def _post_process(self): + panic = self._set_name_from_str_xref("_panic", "double panic in") + if panic: + panic.can_return = False + + for func_name, needle in UNIQUE_STR_XREFS.items(): + self._set_name_from_str_xref(func_name, needle) + @classmethod def is_valid_for_data(cls, data: BinaryView) -> bool: try: @@ -158,18 +206,11 @@ def init(self): start = layout.text.start - except Exception as e: - if self.parse_only: - log_error_for_exception(e) + self.add_analysis_completion_event(self._post_process) - show_message_box( - ANALYZE_FAIL_TITLE, - f"{ANALYZE_FAIL_MESSAGE}\n\nPlease report this bug!\n\n{ISSUES_URL}", - icon=MessageBoxIcon.WarningIcon, - ) - - log_error(ANALYZE_FAIL_MESSAGE) - log_error(f"Please report this bug! ({ISSUES_URL})") + except Exception: + if self.parse_only: + report_exception(ANALYZE_FAIL_MESSAGE) self._add_segment( "APP", diff --git a/plugin/ida/ibis.py b/plugin/ida/ibis.py index bb323b5..ae528e1 100644 --- a/plugin/ida/ibis.py +++ b/plugin/ida/ibis.py @@ -1,6 +1,9 @@ import sys +import traceback +from collections.abc import Iterator from pathlib import Path +import ida_auto import ida_bytes import ida_entry import ida_funcs @@ -9,7 +12,10 @@ import ida_idp import ida_kernwin import ida_loader +import ida_name import ida_segment +import ida_strlist +import ida_xref IBIS_PATH = Path(__file__).resolve().parent.parent.parent / "src" @@ -21,35 +27,99 @@ from ibis.layout import FALLBACK_BSS_SIZE, Layout # noqa: E402 from ibis.plugins import ( # noqa: E402 ANALYZE_FAIL_MESSAGE, + ERROR_HEADER, ISSUES_URL, MISSING_BSS_BOUNDS, + PLEASE_REPORT_GUI_MESSAGE, + PLEASE_REPORT_MESSAGE, + POST_ANALYZE_FAIL_MESSAGE, ) +from ibis.strings import UNIQUE_STR_XREFS # noqa: E402 -class IDADriver(Driver): - def __init__(self, fd) -> None: +def report_exception(message: str): + print(f"\n{ERROR_HEADER}\n") + print(f"{message}\n") + print(traceback.format_exc()) + print(f"{PLEASE_REPORT_MESSAGE} ({ISSUES_URL})") + + ida_kernwin.warning(f"{message}\n\n{PLEASE_REPORT_GUI_MESSAGE}\n\n{ISSUES_URL}") + + +def string_info_decode(info: ida_strlist.string_info_t) -> str | None: + content = ida_bytes.get_strlit_contents(info.ea, info.length, info.type) + + if content: + try: + return content.decode() + except Exception: + pass + + return None + + +def strings_containing(pattern: str) -> Iterator[ida_strlist.string_info_t]: + info = ida_strlist.string_info_t() + + for i in range(ida_strlist.get_strlist_qty()): + if not ida_strlist.get_strlist_item(info, i): + continue + + decoded = string_info_decode(info) + if decoded and pattern in decoded: + yield info + + info = ida_strlist.string_info_t() # New info object for next iteration. + + +def xrefs_to(ea: int) -> Iterator[ida_xref.xrefblk_t]: + xref = ida_xref.xrefblk_t() + + # This might bite us later if we don't limit it to code references. + if xref.first_to(ea, ida_xref.XREF_ALL): + yield xref + + while xref.next_to(): + yield xref + + +def set_name_from_str_xref(name: str, pattern: str) -> ida_funcs.func_t | None: + if not (needle := next(strings_containing(pattern), None)): + return None + + if not (ref := next(xrefs_to(needle.ea), None)): + return None + + if not (func := ida_funcs.get_func(ref.frm)): + return None + + ida_name.set_name(func.start_ea, name) + return func + + +class PostProcessHook(ida_idp.IDB_Hooks): + def __init__(self): super().__init__() - self.fd = fd - # @override - def read(self, offset: int, size: int) -> bytes: - self.fd.seek(offset) - return self.fd.read(size) + def auto_empty_finally(self, *args): + try: + panic = set_name_from_str_xref("_panic", "double panic in") + if not panic: + raise ValueError("Failed to find panic function") - def size(self) -> int: - self.fd.seek(0, ida_idaapi.SEEK_END) - return self.fd.tell() + panic.flags |= ida_funcs.FUNC_NORET + ida_funcs.update_func(panic) + for name, needle in UNIQUE_STR_XREFS.items(): + set_name_from_str_xref(name, needle) -def accept_file(fd, _): - try: - ctx = IDADriver(fd).detect_context() + except Exception: + report_exception(POST_ANALYZE_FAIL_MESSAGE) - return {"format": f"{ctx.app.value}", "processor": "arm"} - except Exception as e: - print(e) + ida_auto.auto_wait() - return 0 + +POST_PROCESS_HOOK: PostProcessHook | None = None input_size = None # XXX: Global, set in `load_file`. @@ -121,6 +191,32 @@ def apply_layout(fd, layout: Layout): ) +class IDADriver(Driver): + def __init__(self, fd) -> None: + super().__init__() + self.fd = fd + + # @override + def read(self, offset: int, size: int) -> bytes: + self.fd.seek(offset) + return self.fd.read(size) + + def size(self) -> int: + self.fd.seek(0, ida_idaapi.SEEK_END) + return self.fd.tell() + + +def accept_file(fd, _): + try: + ctx = IDADriver(fd).detect_context() + + return {"format": f"{ctx.app.value}", "processor": "arm"} + except Exception as e: + print(e) + + return 0 + + def load_file(fd, neflags: int, _): ida_idp.set_processor_type("arm", ida_idp.SETPROC_LOADER) ida_ida.inf_set_app_bitness(64) @@ -147,15 +243,8 @@ def load_file(fd, neflags: int, _): start = layout.text.start - except Exception as e: - print(e) - - ida_kernwin.warning( - f"{ANALYZE_FAIL_MESSAGE}\n\nPlease report this bug!\n\n{ISSUES_URL}" - ) - - print(ANALYZE_FAIL_MESSAGE) - print(f"Please report this bug! ({ISSUES_URL})") + except Exception: + report_exception(ANALYZE_FAIL_MESSAGE) add_segment( fd, @@ -173,4 +262,8 @@ def load_file(fd, neflags: int, _): ida_entry.add_entry(0, start, "_start", True) + global POST_PROCESS_HOOK + POST_PROCESS_HOOK = PostProcessHook() + POST_PROCESS_HOOK.hook() + return 1 diff --git a/src/ibis/plugins.py b/src/ibis/plugins.py index 17d83e7..57ee29b 100644 --- a/src/ibis/plugins.py +++ b/src/ibis/plugins.py @@ -1,7 +1,12 @@ -ANALYZE_FAIL_TITLE = "Failed to Analyze Memory Layout" +ERROR_TITLE = "Ibis Error" ANALYZE_FAIL_MESSAGE = "Ibis couldn't determine the memory layout for this file; a single RWX segment will be used." +POST_ANALYZE_FAIL_MESSAGE = "An error ocurred during post-analysis." +PLEASE_REPORT_MESSAGE = "Please report this bug!" +PLEASE_REPORT_GUI_MESSAGE = "Please check the console and report this bug!" ISSUES_URL = "https://github.com/jonpalmisc/ibis/issues" +ERROR_HEADER = ("*" * 34) + " Ibis Error " + ("*" * 34) + MISSING_BSS_BOUNDS = ( "WARNING: Couldn't determine BSS segment bounds; using best guess..." ) diff --git a/src/ibis/strings.py b/src/ibis/strings.py new file mode 100644 index 0000000..95f18c1 --- /dev/null +++ b/src/ibis/strings.py @@ -0,0 +1,25 @@ +# https://github.com/matteyeux/ida-iboot-loader +UNIQUE_STR_XREFS = { + "_do_printf": "", + "_platform_get_usb_serial_number_string": "CPID:", + "_platform_get_usb_more_other_string": " NONC:", + "_UpdateDeviceTree": "fuse-revision", + "_main_task": "debug-uarts", + "_platform_init_display": "backlight-level", + "_do_memboot": "Combo image too large", + "_do_go": "Memory image not valid", + "_task_init": "idle task", + "_sys_setup_default_environment": "/System/Library/Caches/com.apple.kernelcaches/kernelcache", + "_check_autoboot": "aborting autoboot due to user intervention.", + "_do_setpict": "picture too large: size:%zu", + "_arm_exception_abort": "ARM %s abort at 0x%016llx:", + "_do_devicetree": "Device Tree image not valid", + "_do_ramdisk": "Ramdisk image not valid", + "_nvme_bdev_create": "Couldn't construct blockdev for namespace %d", + "_record_memory_range": "chosen/memory-map", + "_boot_upgrade_system": "/boot/kernelcache", + "_target_pass_boot_manifest": "chosen/manifest-properties", + "_image4_validate_property_callback_interposer": "Unknown ASN1 type %llu", + "_platform_handoff_update_devicetree": "iboot-handoff", + "_prepare_and_jump": "======== End of %s serial output. ========", +} From 600606c213ef2bf6668261ee72019537661787cf Mon Sep 17 00:00:00 2001 From: Jon Palmisciano Date: Tue, 13 Jan 2026 18:55:56 +0000 Subject: [PATCH 2/4] Plugins: Search for PACIBSP backwards for better results --- plugin/binja/view.py | 2 +- plugin/ida/ibis.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/binja/view.py b/plugin/binja/view.py index fba2043..1e79119 100644 --- a/plugin/binja/view.py +++ b/plugin/binja/view.py @@ -200,7 +200,7 @@ def init(self): # detect no-return functions. A cheap hack is to create functions # starting at every PACIBSP, which shouldn't ever appear in the middle # of a function. - for addr in range(layout.text.start, layout.text.end, 4): + for addr in range(layout.text.end - 4, layout.text.start - 4, -4): if self.read_int(addr, 4, False) == 0xD503237F: # pacibsp self.add_function(addr) diff --git a/plugin/ida/ibis.py b/plugin/ida/ibis.py index ae528e1..37f3dbf 100644 --- a/plugin/ida/ibis.py +++ b/plugin/ida/ibis.py @@ -236,7 +236,7 @@ def load_file(fd, neflags: int, _): # Analysis can get confused about function bounds when if it fails to detect # no-return functions. A cheap hack is to create functions starting at every # PACIBSP, which shouldn't ever appear in the middle of a function. - for addr in range(layout.text.start, layout.text.end, 4): + for addr in range(layout.text.end - 4, layout.text.start - 4, -4): if ida_bytes.get_dword(addr) == 0xD503237F: # pacibsp print(f"Adding function @ {addr:#x}...") ida_funcs.add_func(addr) From a6047657c8d9023525c1ba412ff016acfe292d19 Mon Sep 17 00:00:00 2001 From: Jon Palmisciano Date: Wed, 14 Jan 2026 16:31:02 +0000 Subject: [PATCH 3/4] Core: Add `boot-command` string for `CONST` boundary detection --- src/ibis/analyzer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ibis/analyzer.py b/src/ibis/analyzer.py index 596e982..401d46d 100644 --- a/src/ibis/analyzer.py +++ b/src/ibis/analyzer.py @@ -151,6 +151,7 @@ def _detect_layout_v6823(context: Context, driver: Driver) -> Layout: [b"virt_firmware\x00", b"double panic in\x00"] if context.app == App.AVP_BOOTER else [ + b"boot-command\x00", b"%llx:%d\x00", b"anc_firmware\x00", b"nor0\x00", From 708dcd8cf373a8a3e9f5eb3d78df59b843870372 Mon Sep 17 00:00:00 2001 From: Jon Palmisciano Date: Mon, 12 Jan 2026 08:51:01 +0000 Subject: [PATCH 4/4] Bump version to 1.4.0 --- plugin/binja/plugin.json | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/binja/plugin.json b/plugin/binja/plugin.json index 602325a..81a95b6 100644 --- a/plugin/binja/plugin.json +++ b/plugin/binja/plugin.json @@ -2,7 +2,7 @@ "pluginmetadataversion": 2, "name": "Ibis", "description": "Segment-accurate iBoot/SecureROM loader", - "version": "1.3.1", + "version": "1.4.0", "author": "jonpalmisc", "type": ["binaryview"], "api": ["python3"], diff --git a/pyproject.toml b/pyproject.toml index a4d520a..980f206 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ibis" -version = "1.3.1" +version = "1.4.0" description = "Library and plugins for mapping 64-bit iBoot-like binaries" readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index 873b3df..233b570 100644 --- a/uv.lock +++ b/uv.lock @@ -129,7 +129,7 @@ wheels = [ [[package]] name = "ibis" -version = "1.3.1" +version = "1.4.0" source = { editable = "." } [package.dev-dependencies]