Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
D0ntPanic committed Jun 14, 2023
0 parents commit 0a2c9ba
Show file tree
Hide file tree
Showing 7 changed files with 512 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 41 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -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)
30 changes: 30 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -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
}
264 changes: 264 additions & 0 deletions protocols.py
Original file line number Diff line number Diff line change
@@ -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("<IHHBBBBBBBB", *guid)
guids.append((guid, None))
elif line.startswith("///@binding"):
guid_name = line.split(" ")[1]
guid = line.split(" ")[2].replace("{", "").replace("}", "").strip().split(",")
guid = [int(x, 16) for x in guid]
guid = struct.pack("<IHHBBBBBBBB", *guid)
guids.append((guid, guid_name))
elif line.startswith("struct"):
name = line.split(" ")[1].strip()
for guid_info in guids:
guid, guid_name = guid_info
if guid_name is None:
protocols[guid] = (name, f"{name}_GUID")
else:
protocols[guid] = (name, guid_name)
else:
guids = []

return True

def lookup_protocol_guid(guid: bytes) -> 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("<QQ", low, high)
elif isinstance(hlil.params[guid_param], HighLevelILVar):
# See if GUID variable is an incoming parameter
ssa = hlil.params[guid_param].ssa_form
if ssa is None or not isinstance(ssa, HighLevelILVarSsa):
continue
if ssa.var.version != 0:
incoming_def = func.hlil.get_ssa_var_definition(ssa.var)
if incoming_def is None:
continue
incoming_def = incoming_def.ssa_form
if not isinstance(incoming_def, HighLevelILVarInitSsa):
continue
if not isinstance(incoming_def.src, HighLevelILVarSsa):
continue
if incoming_def.src.var.version != 0:
continue
ssa = incoming_def.src

# Find index of incoming parameter
incoming_guid_param_idx = None
for i in range(len(func.parameter_vars)):
if func.parameter_vars[i] == ssa.var.var:
incoming_guid_param_idx = i
break
if incoming_guid_param_idx is None:
continue

# See if output interface variable is an incoming parameter
ssa = hlil.params[interface_param].ssa_form
if ssa is None or not isinstance(ssa, HighLevelILVarSsa):
continue
if ssa.var.version != 0:
incoming_def = func.hlil.get_ssa_var_definition(ssa.var)
if incoming_def is None:
continue
incoming_def = incoming_def.ssa_form
if not isinstance(incoming_def, HighLevelILVarInitSsa):
continue
if not isinstance(incoming_def.src, HighLevelILVarSsa):
continue
if incoming_def.src.var.version != 0:
continue
ssa = incoming_def.src

# Find index of incoming parameter
incoming_interface_param_idx = None
for i in range(len(func.parameter_vars)):
if func.parameter_vars[i] == ssa.var.var:
incoming_interface_param_idx = i
break
if incoming_interface_param_idx is None:
continue

# This function is a wrapper, resolve protocols for calls to this function
log_info(f"Found EFI protocol wrapper {func_name} at {hex(ref.address)}, checking references to wrapper function")
if not define_protocol_types_for_refs(bv, func.name, bv.get_code_refs(func.start),
incoming_guid_param_idx, incoming_interface_param_idx, task):
return False
continue

if guid is None:
continue

# Get the protocol from the GUID
protocol, guid_name = lookup_protocol_guid(guid)
if protocol is None:
log_warn(f"Unknown EFI protocol {guid.hex()} referenced at {hex(ref.address)}")
continue

# Rename the GUID with the protocol name
sym = bv.get_symbol_at(guid_addr.value)
name = guid_name
if sym is not None:
name = sym.name
bv.define_user_data_var(guid_addr.value, "EFI_GUID", name)

# Get interface pointer parameter and set it to the type of the protocol
dest = hlil.params[interface_param]
if isinstance(dest, HighLevelILAddressOf):
dest = dest.src
if isinstance(dest, HighLevelILVar):
dest = dest.var
log_info(f"Setting type {protocol}* for local variable in {func_name} call at {hex(ref.address)}")
name = nonconflicting_variable_name(func, variable_name_for_protocol(guid_name))
func.create_user_var(dest, f"{protocol}*", name)
elif isinstance(dest, Constant):
dest = dest.constant
log_info(f"Setting type {protocol}* for global variable at {hex(dest)} in {func_name} call at {hex(ref.address)}")
sym = bv.get_symbol_at(dest)
name = f"{variable_name_for_protocol(guid_name)}_{dest:x}"
if sym is not None:
name = sym.name
bv.define_user_data_var(dest, f"{protocol}*", name)

bv.update_analysis_and_wait()
return True

def define_protocol_types(bv: BinaryView, field: str, guid_param: int, interface_param: int, task: BackgroundTask) -> 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)
Loading

0 comments on commit 0a2c9ba

Please sign in to comment.