diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d2a5f30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..39ed95c --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# EFI Resolver Plugin for Binary Ninja + +EFI Resolver is a Binary Ninja plugin developed to enhance your UEFI reverse engineering workflow. The plugin automatically resolves type information for EFI protocol usage, making it easier to understand and analyze EFI binaries. + +## Features + +* **Automatic EFI Protocol Typing**: EFI Resolver intelligently identifies instances where EFI protocols are used and automatically applies the appropriate type information. EFI Resolver looks for references to the boot services protocol functions and applies type information according to the GUID passed to these functions. +* **Global Variable Propagation**: The plugin propagates pointers to the system table, boot services, and runtime services to any global variables where they are stored. This streamlines the process of tracking these vital system components across a binary. +* **Comprehensive UEFI Specification Support**: The plugin fully supports all core protocols within the UEFI specification. However, please note that vendor-specific protocols are not currently supported. + +## Usage + +To use the EFI Resolver plugin, open a UEFI binary in Binary Ninja. Then, navigate to the `Plugins` menu, and choose `Resolve EFI Protocols`. The plugin will automatically analyze the binary and apply type information. + +Please note that this process might take a few moments to complete, depending on the size and complexity of the binary. + +## Limitations + +The current version of EFI Resolver does not support vendor-specific protocols. It is focused on the core protocols defined within the UEFI specification. + +## License + +This project is licensed under the terms of the Apache 2.0 license. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0791032 --- /dev/null +++ b/__init__.py @@ -0,0 +1,41 @@ +from binaryninja import PluginCommand, BinaryView, BackgroundTaskThread, log_alert +from .protocols import init_protocol_mapping, define_handle_protocol_types, define_open_protocol_types, define_locate_protocol_types +from .system_table import propagate_system_table_pointer + +def resolve_efi(bv: BinaryView): + class Task(BackgroundTaskThread): + def __init__(self, bv: BinaryView): + super().__init__("Initializing EFI protocol mappings...", True) + self.bv = bv + + def run(self): + if not init_protocol_mapping(): + return + + if "EFI_SYSTEM_TABLE" not in self.bv.types: + log_alert("This binary is not using the EFI platform. Use Open with Options when loading the binary to select the EFI platform.") + return + + self.bv.begin_undo_actions() + try: + self.progress = "Propagating EFI system table pointers..." + if not propagate_system_table_pointer(self.bv, self): + return + + self.progress = "Defining types for uses of HandleProtocol..." + if not define_handle_protocol_types(self.bv, self): + return + + self.progress = "Defining types for uses of OpenProtocol..." + if not define_open_protocol_types(self.bv, self): + return + + self.progress = "Defining types for uses of LocateProtocol..." + if not define_locate_protocol_types(self.bv, self): + return + finally: + self.bv.commit_undo_actions() + + Task(bv).start() + +PluginCommand.register("Resolve EFI Protocols", "Automatically resolve usage of EFI protocols", resolve_efi) diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..34a5119 --- /dev/null +++ b/plugin.json @@ -0,0 +1,30 @@ +{ + "pluginmetadataversion": 2, + "name": "EFI Resolver", + "type": [ + "platform" + ], + "api": [ + "python3" + ], + "description": "A Binary Ninja plugin that automatically resolves type information for EFI protocol usage.", + "longdescription": "EFI Resolver is a Binary Ninja plugin that automates the task of resolving EFI protocol type information. It propagates pointers to system table, boot services, and runtime services to any global variables where they are stored. The plugin also identifies references to the boot services protocol functions and applies type information according to the GUID passed to these functions. The plugin supports all of the core UEFI specification, but does not support vendor protocols.", + "license": { + "name": "Apache-2.0", + "text": "Copyright 2023 Vector 35 Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License." + }, + "platforms": [ + "Darwin", + "Linux", + "Windows" + ], + "installinstructions": { + "Darwin": "no special instructions, package manager is recommended", + "Linux": "no special instructions, package manager is recommended", + "Windows": "no special instructions, package manager is recommended" + }, + "dependencies": {}, + "version": "1.0.0", + "author": "Vector 35 Inc", + "minimumbinaryninjaversion": 4333 +} \ No newline at end of file diff --git a/protocols.py b/protocols.py new file mode 100644 index 0000000..8859ab7 --- /dev/null +++ b/protocols.py @@ -0,0 +1,264 @@ +from binaryninja import (BinaryView, BackgroundTask, HighLevelILCall, RegisterValueType, HighLevelILAddressOf, + HighLevelILVar, Constant, Function, HighLevelILVarSsa, HighLevelILVarInitSsa, + TypeFieldReference, bundled_plugin_path, log_info, log_warn, log_alert) +from typing import Optional, Tuple +import os +import sys +import struct + +protocols = None + +def init_protocol_mapping(): + # Parse EFI definitions only once + global protocols + if protocols is not None: + return True + + # Find the EFI type definition file within the Binary Ninja installation + if sys.platform == "darwin": + efi_def_path = os.path.join(bundled_plugin_path(), "..", "..", "Resources", "types", "efi.c") + else: + efi_def_path = os.path.join(bundled_plugin_path(), "..", "types", "efi.c") + + # Try to read the EFI type definitions. This may not exist on older versions of Binary Ninja. + try: + efi_defs = open(efi_def_path, "r").readlines() + except: + log_alert(f"Could not open EFI type definition file at '{efi_def_path}'. Your version of Binary Ninja may be out of date. Please update to version 3.5.4331 or higher.") + return False + + protocols = {} + + # Parse the GUID to protocol structure mappings out of the type definition source + guids = [] + for line in efi_defs: + if line.startswith("///@protocol"): + guid = line.split("///@protocol")[1].replace("{", "").replace("}", "").strip().split(",") + guid = [int(x, 16) for x in guid] + guid = struct.pack(" Optional[Tuple[str, str]]: + global protocols + if guid in protocols: + return protocols[guid] + return (None, None) + +def variable_name_for_protocol(protocol: str) -> str: + name = protocol + if name.startswith("EFI_"): + name = name[4:] + if name.endswith("_GUID"): + name = name[:-5] + if name.endswith("_PROTOCOL"): + name = name[:-9] + case_str = "" + first = True + for c in name: + if c == "_": + first = True + continue + elif first: + case_str += c.upper() + first = False + else: + case_str += c.lower() + return case_str + +def nonconflicting_variable_name(func: Function, base_name: str) -> str: + idx = 0 + name = base_name + while True: + ok = True + for var in func.vars: + if var.name == name: + ok = False + break + if ok: + break + idx += 1 + name = f"{base_name}_{idx}" + return name + +def define_protocol_types_for_refs(bv: BinaryView, func_name: str, refs, guid_param: int, interface_param: int, task: BackgroundTask) -> bool: + refs = list(refs) + for ref in refs: + if task.cancelled: + return False + + if isinstance(ref, TypeFieldReference): + func = ref.func + else: + func = ref.function + + llil = func.get_llil_at(ref.address, ref.arch) + if not llil: + continue + for hlil in llil.hlils: + if isinstance(hlil, HighLevelILCall): + # Check for status transform wrapper function + if len(hlil.params) == 1 and isinstance(hlil.params[0], HighLevelILCall): + hlil = hlil.params[0] + + # Found call to target field + if len(hlil.params) <= max(guid_param, interface_param): + continue + + # Get GUID parameter and read it from the binary or the stack + guid_addr = hlil.params[guid_param].value + guid = None + if guid_addr.type in [RegisterValueType.ConstantValue, RegisterValueType.ConstantPointerValue]: + guid = bv.read(guid_addr.value, 16) + if not guid or len(guid) < 16: + continue + elif guid_addr.type == RegisterValueType.StackFrameOffset: + mlil = hlil.mlil + if mlil is None: + continue + low = mlil.get_stack_contents(guid_addr.value, 8) + high = mlil.get_stack_contents(guid_addr.value + 8, 8) + if low.type in [RegisterValueType.ConstantValue, RegisterValueType.ConstantPointerValue]: + low = low.value + else: + continue + if high.type in [RegisterValueType.ConstantValue, RegisterValueType.ConstantPointerValue]: + high = high.value + else: + continue + guid = struct.pack(" bool: + boot_services = bv.types["EFI_BOOT_SERVICES"] + offset = None + for member in boot_services.members: + if member.name == field: + offset = member.offset + break + if offset is None: + log_warn(f"Could not find {field} member in EFI_BOOT_SERVICES") + return True + return define_protocol_types_for_refs(bv, field, bv.get_code_refs_for_type_field("EFI_BOOT_SERVICES", offset), + guid_param, interface_param, task) + +def define_handle_protocol_types(bv: BinaryView, task: BackgroundTask) -> bool: + return define_protocol_types(bv, "HandleProtocol", 1, 2, task) + +def define_open_protocol_types(bv: BinaryView, task: BackgroundTask) -> bool: + return define_protocol_types(bv, "OpenProtocol", 1, 2, task) + +def define_locate_protocol_types(bv: BinaryView, task: BackgroundTask) -> bool: + return define_protocol_types(bv, "LocateProtocol", 0, 2, task) diff --git a/system_table.py b/system_table.py new file mode 100644 index 0000000..6ff9bd8 --- /dev/null +++ b/system_table.py @@ -0,0 +1,140 @@ +from binaryninja import (BinaryView, BackgroundTask, PointerType, NamedTypeReferenceType, HighLevelILCallSsa, + SSAVariable, Constant, HighLevelILAssign, HighLevelILAssignMemSsa, HighLevelILDerefSsa, + Function, Variable, HighLevelILDerefFieldSsa, HighLevelILVarInitSsa, HighLevelILVarSsa, + StructureType, log_info, log_warn) +from typing import List + +types_to_propagate = ["EFI_SYSTEM_TABLE", "EFI_RUNTIME_SERVICES", "EFI_BOOT_SERVICES"] +var_name_for_type = {"EFI_SYSTEM_TABLE": "SystemTable", "EFI_RUNTIME_SERVICES": "RuntimeServices", + "EFI_BOOT_SERVICES": "BootServices"} + +def propagate_variable_uses(bv: BinaryView, func: Function, var: SSAVariable, func_queue: List[Function]) -> bool: + global types_to_propagate, var_name_for_type + updates = False + + for use in func.hlil.ssa_form.get_ssa_var_uses(var): + instr = use.parent + if isinstance(instr, HighLevelILCallSsa): + # Function call, propagate the variable type to the function call target + target = instr.dest + if not isinstance(target, Constant): + continue + target = bv.get_function_at(target.constant) + if not target: + continue + + for param_idx in range(len(instr.params)): + if instr.params[param_idx] == use: + log_info(f"Propagating {var.type.target.name} pointer to parameter #{param_idx + 1} of {target.name}") + if param_idx >= len(target.parameter_vars): + continue + target.parameter_vars[param_idx].type = var.type + target.parameter_vars[param_idx].name = var_name_for_type[var.type.target.name] + if target not in func_queue: + func_queue.append(target) + updates = True + elif isinstance(instr, HighLevelILAssignMemSsa): + # Assignment, propagate the variable type if it is assigning to a global variable + target = instr.dest + if not isinstance(target, HighLevelILDerefSsa): + continue + target = target.src + if not isinstance(target, Constant): + continue + + log_info(f"Propagating {var.type.target.name} pointer to data variable at {hex(target.constant)}") + bv.define_user_data_var(target.constant, var.type, var_name_for_type[var.type.target.name]) + updates = True + elif isinstance(instr, HighLevelILDerefFieldSsa): + # Dereferencing field, see if it is a field for a type we want to propagate + expr_type = instr.expr_type + if not isinstance(expr_type, PointerType): + continue + if not isinstance(expr_type.target, StructureType): + continue + if expr_type.target.registered_name.name not in types_to_propagate: + continue + + # See if this is an assignment to a variable, and propagate that variable if so + deref_parent = instr.parent + if isinstance(deref_parent, HighLevelILVarInitSsa): + target = deref_parent.dest + elif isinstance(deref_parent, HighLevelILAssign): + target = deref_parent.dest + if not isinstance(target, HighLevelILVarSsa): + continue + target = deref_parent.var + elif isinstance(deref_parent, HighLevelILAssignMemSsa): + # Assignment to memory, if assigning to a global variable, propagate directly + target = deref_parent.dest + if not isinstance(target, HighLevelILDerefSsa): + continue + target = target.src + if not isinstance(target, Constant): + continue + + log_info(f"Propagating {expr_type.target.registered_name.name} pointer to data variable at {hex(target.constant)}") + bv.define_user_data_var(target.constant, expr_type, var_name_for_type[expr_type.target.registered_name.name]) + updates = True + continue + else: + continue + + func.create_user_var(target.var, expr_type, var_name_for_type[expr_type.target.registered_name.name]) + propagate_variable_uses(bv, func, target, func_queue) + updates = True + + return updates + +def propagate_system_table_pointer(bv: BinaryView, task: BackgroundTask): + # Add entry function to the list of functions in which to propagate. + func_queue = [] + entry_func = bv.entry_function + if entry_func: + func_queue.append(entry_func) + + # Propagate system table and services tables + + # Process functions until there is no more propagation to be done + while len(func_queue) > 0: + if task.cancelled: + return False + + func = func_queue.pop() + + # Go through the list of parameter variables to see if there are any that need to be propagated + parameter_vars = func.parameter_vars + updates = False + for param_idx in range(len(parameter_vars)): + param = parameter_vars[param_idx] + if not isinstance(param.type, PointerType): + continue + if not isinstance(param.type.target, NamedTypeReferenceType): + continue + if param.type.target.name not in types_to_propagate: + continue + updates |= propagate_variable_uses(bv, func, SSAVariable(param, 0), func_queue) + + if updates: + bv.update_analysis_and_wait() + + # Set types of known Windows bootloader pointers, as these go through several translation layers + # before arriving at the global variables. + sym = bv.get_symbol_by_raw_name("EfiST") + if sym is not None: + bv.define_user_data_var(sym.address, "EFI_SYSTEM_TABLE*", "EfiST") + sym = bv.get_symbol_by_raw_name("EfiBS") + if sym is not None: + bv.define_user_data_var(sym.address, "EFI_BOOT_SERVICES*", "EfiBS") + sym = bv.get_symbol_by_raw_name("EfiRT") + if sym is not None: + bv.define_user_data_var(sym.address, "EFI_RUNTIME_SERVICES*", "EfiRT") + sym = bv.get_symbol_by_raw_name("EfiConOut") + if sym is not None: + bv.define_user_data_var(sym.address, "EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL*", "EfiConOut") + sym = bv.get_symbol_by_raw_name("EfiConIn") + if sym is not None: + bv.define_user_data_var(sym.address, "EFI_SIMPLE_TEXT_INPUT_PROTOCOL*", "EfiConIn") + + bv.update_analysis_and_wait() + return True