Skip to content

Commit

Permalink
Merge pull request #2 from zznop/1-te-loader
Browse files Browse the repository at this point in the history
Loader for Terse Executables
  • Loading branch information
Brandon Miller authored May 2, 2021
2 parents e19a203 + d1553b8 commit aa7e839
Show file tree
Hide file tree
Showing 7 changed files with 765 additions and 231 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Helper plugin for analyzing UEFI firmware. This plugin contains the following fe
* Locate known protocol GUIDs and assign the GUID type and a symbol
* Locate global assigments in entry and initialization functions and assign types
* `EFI_SYSTEM_TABLE`, `EFI_RUNTIME_SERVICES`, `EFI_BOOT_SERVICES`, etc...
* Loader for Terse Executables

![demo bn-uefi-helper](screen.gif)

Expand Down
234 changes: 5 additions & 229 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,231 +1,7 @@
"""
Binary Ninja plugin that aids in analysis of UEFI PEI and DXE modules
"""

import os
import csv
import glob
import uuid
from binaryninja import (PluginCommand, BackgroundTaskThread, SegmentFlag, SectionSemantics, BinaryReader, Symbol,
SymbolType, HighLevelILOperation, BinaryView)
from binaryninja.highlevelil import HighLevelILInstruction
from binaryninja.types import (Type, FunctionParameter)

class UEFIHelper(BackgroundTaskThread):
"""Class for analyzing UEFI firmware to automate GUID annotation, segment fixup, type imports, and more
"""

def __init__(self, bv: BinaryView):
BackgroundTaskThread.__init__(self, '', False)
self.bv = bv
self.br = BinaryReader(self.bv)
self.dirname = os.path.dirname(os.path.abspath(__file__))
self.guids = self._load_guids()

def _fix_segments(self):
"""UEFI modules run during boot, without page protections. Everything is RWX despite that the PE is built with
the segments not being writable. It needs to be RWX so calls through global function pointers are displayed
properly.
"""

for seg in self.bv.segments:
# Make segment RWX
self.bv.add_user_segment(seg.start, seg.data_length, seg.data_offset, seg.data_length,
SegmentFlag.SegmentWritable|SegmentFlag.SegmentReadable|SegmentFlag.SegmentExecutable)

# Make section semantics ReadWriteDataSectionSemantics
for section in self.bv.get_sections_at(seg.start):
self.bv.add_user_section(section.name, section.end-section.start, SectionSemantics.ReadWriteDataSectionSemantics)

def _import_types_from_headers(self):
"""Parse EDKII types from header files
"""

hdrs_path = os.path.join(self.dirname, 'headers')
headers = glob.glob(os.path.join(hdrs_path, '*.h'))
for hdr in headers:
_types = self.bv.platform.parse_types_from_source_file(hdr)
for name, _type in _types.types.items():
self.bv.define_user_type(name, _type)

def _set_entry_point_prototype(self):
"""Apply correct prototype to the module entry point
"""

_start = self.bv.get_function_at(self.bv.entry_point)
_start.function_type = "EFI_STATUS ModuleEntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)"

def _load_guids(self):
"""Read known GUIDs from CSV and convert string GUIDs to bytes
:return: Dictionary containing GUID bytes and associated names
"""

guids_path = os.path.join(self.dirname, 'guids.csv')
with open(guids_path) as f:
reader = csv.reader(f, skipinitialspace=True)
guids = dict(reader)

# Convert to bytes for faster lookup
guid_bytes = dict()
for guid, name in guids.items():
guid_bytes[name] = uuid.UUID(guid).bytes_le

return guid_bytes

def _apply_guid_name_if_data(self, name: str, address: int):
"""Check if there is a function at the address. If not, then apply the EFI_GUID type and name it
:param name: Name/symbol to apply to the GUID
:param address: Address of the GUID
"""

print(f'Found {name} at 0x{hex(address)} ({uuid.UUID(bytes_le=self.guids[name])})')

# Just to avoid a unlikely false positive and screwing up disassembly
if self.bv.get_functions_at(address) != []:
print(f'There is code at {address}, not applying GUID type and name')
return

self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, address, 'g'+name))
t = self.bv.parse_type_string("EFI_GUID")
self.bv.define_user_data_var(address, t[0])

def _find_known_guids(self):
"""Search for known GUIDs and apply names to matches not within a function
"""

names_list = list(self.guids.keys())
guids_list = list(self.guids.values())
def _check_guid_and_get_name(guid):
try:
return names_list[guids_list.index(guid)]
except ValueError:
return None

for seg in self.bv.segments:
for i in range(seg.start, seg.end):
self.br.seek(i)
data = self.br.read(16)
if not data or len(data) != 16:
continue

found_name = _check_guid_and_get_name(data)
if found_name:
self._apply_guid_name_if_data(found_name, i)

def _set_if_uefi_core_type(self, instr: HighLevelILInstruction):
"""Using HLIL, scrutinize the instruction to determine if it's a move of a local variable to a global variable.
If it is, check if the source operand type is a UEFI core type and apply the type to the destination global
variable.
:param instr: High level IL instruction object
"""

if instr.operation != HighLevelILOperation.HLIL_ASSIGN:
return

if instr.dest.operation != HighLevelILOperation.HLIL_DEREF:
return

if instr.dest.src.operation != HighLevelILOperation.HLIL_CONST_PTR:
return

if instr.src.operation != HighLevelILOperation.HLIL_VAR:
return

_type = instr.src.var.type
if len(_type.tokens) == 1 and str(_type.tokens[0]) == 'EFI_HANDLE':
self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gImageHandle'))
elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_BOOT_SERVICES':
self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gBS'))
elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_RUNTIME_SERVICES':
self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gRS'))
elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_SYSTEM_TABLE':
self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gST'))
else:
return

self.bv.define_user_data_var(instr.dest.src.constant, instr.src.var.type)
print(f'Found global assignment - offset:0x{hex(instr.dest.src.constant)} type:{instr.src.var.type}')

def _check_and_prop_types_on_call(self, instr: HighLevelILInstruction):
"""Most UEFI modules don't assign globals in the entry function and instead call a initialization routine and
pass the system table to it where global assignments are made. This function ensures that the types are applied
to the initialization function params so that we can catch global assignments outside of the module entry
:param instr: High level IL instruction object
"""

if instr.operation not in [HighLevelILOperation.HLIL_TAILCALL, HighLevelILOperation.HLIL_CALL]:
return

if instr.dest.operation != HighLevelILOperation.HLIL_CONST_PTR:
return

argv_is_passed = False
for arg in instr.params:
if 'ImageHandle' in str(arg) or 'SystemTable' in str(arg):
argv_is_passed = True
break

if not argv_is_passed:
return

func = self.bv.get_function_at(instr.dest.constant)
old = func.function_type
call_args = instr.params
new_params = []
for arg, param in zip(call_args, old.parameters):
if hasattr(arg, 'var'):
new_type = arg.var.type
else:
new_type = param.type
new_type.confidence = 256
new_params.append(FunctionParameter(new_type, param.name))

# TODO: this is a hack to account for odd behavior. func.function_type should be able to set directly to
# Type.Function(...). However, during testing this isn't the case. I am only able to get it to work if I
# set function_type to a string and update analysis.
gross_hack = str(
Type.function(old.return_value, new_params, old.calling_convention, old.has_variable_arguments, old.stack_adjustment)
).replace('(', '{}('.format(func.name))
func.function_type = gross_hack
self.bv.update_analysis_and_wait()

def _set_global_variables(self):
"""On entry, UEFI modules usually set global variables for EFI_BOOT_SERVICES, EFI_RUNTIME_SERIVCES, and
EFI_SYSTEM_TABLE. This function attempts to identify these assignments and apply types.
"""

func = self.bv.get_function_at(self.bv.entry_point)
for block in func.high_level_il:
for instr in block:
self._check_and_prop_types_on_call(instr)

for func in self.bv.functions:
for block in func.high_level_il:
for instr in block:
self._set_if_uefi_core_type(instr)

def run(self):
"""Run the task in the background
"""

self.progress = "UEFI Helper: Fixing up segments, applying types, and searching for known GUIDs ..."
self._fix_segments()
self._import_types_from_headers()
self._set_entry_point_prototype()
self._find_known_guids()
self.progress = "UEFI Helper: searching for global assignments for UEFI core services ..."
self._set_global_variables()
print('UEFI Helper completed successfully!')

def run_uefi_helper(bv: BinaryView):
"""Run UEFI helper utilities in the background
"""

task = UEFIHelper(bv)
task.start()
from binaryninja import PluginCommand
from .helper import run_uefi_helper
from .teloader import TerseExecutableView

PluginCommand.register('UEFI Helper', 'Run UEFI Helper analysis', run_uefi_helper)
TerseExecutableView.register()

Loading

0 comments on commit aa7e839

Please sign in to comment.