Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- [ ] 🔥🌸⁉️
Expand Down
2 changes: 1 addition & 1 deletion plugin/binja/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
67 changes: 54 additions & 13 deletions plugin/binja/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from binaryninja import (
Architecture,
BinaryView,
Function,
MessageBoxIcon,
Platform,
SectionSemantics,
SegmentFlag,
StringReference,
Symbol,
SymbolType,
log_error_for_exception,
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -152,24 +200,17 @@ 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)

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",
Expand Down
147 changes: 120 additions & 27 deletions plugin/ida/ibis.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"

Expand All @@ -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`.
Expand Down Expand Up @@ -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)
Expand All @@ -140,22 +236,15 @@ 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)

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,
Expand All @@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
1 change: 1 addition & 0 deletions src/ibis/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion src/ibis/plugins.py
Original file line number Diff line number Diff line change
@@ -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..."
)
25 changes: 25 additions & 0 deletions src/ibis/strings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# https://github.com/matteyeux/ida-iboot-loader
UNIQUE_STR_XREFS = {
"_do_printf": "<null>",
"_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. ========",
}
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.