|
1 |
| -""" |
2 |
| -Binary Ninja plugin that aids in analysis of UEFI PEI and DXE modules |
3 |
| -""" |
4 |
| - |
5 |
| -import os |
6 |
| -import csv |
7 |
| -import glob |
8 |
| -import uuid |
9 |
| -from binaryninja import (PluginCommand, BackgroundTaskThread, SegmentFlag, SectionSemantics, BinaryReader, Symbol, |
10 |
| - SymbolType, HighLevelILOperation, BinaryView) |
11 |
| -from binaryninja.highlevelil import HighLevelILInstruction |
12 |
| -from binaryninja.types import (Type, FunctionParameter) |
13 |
| - |
14 |
| -class UEFIHelper(BackgroundTaskThread): |
15 |
| - """Class for analyzing UEFI firmware to automate GUID annotation, segment fixup, type imports, and more |
16 |
| - """ |
17 |
| - |
18 |
| - def __init__(self, bv: BinaryView): |
19 |
| - BackgroundTaskThread.__init__(self, '', False) |
20 |
| - self.bv = bv |
21 |
| - self.br = BinaryReader(self.bv) |
22 |
| - self.dirname = os.path.dirname(os.path.abspath(__file__)) |
23 |
| - self.guids = self._load_guids() |
24 |
| - |
25 |
| - def _fix_segments(self): |
26 |
| - """UEFI modules run during boot, without page protections. Everything is RWX despite that the PE is built with |
27 |
| - the segments not being writable. It needs to be RWX so calls through global function pointers are displayed |
28 |
| - properly. |
29 |
| - """ |
30 |
| - |
31 |
| - for seg in self.bv.segments: |
32 |
| - # Make segment RWX |
33 |
| - self.bv.add_user_segment(seg.start, seg.data_length, seg.data_offset, seg.data_length, |
34 |
| - SegmentFlag.SegmentWritable|SegmentFlag.SegmentReadable|SegmentFlag.SegmentExecutable) |
35 |
| - |
36 |
| - # Make section semantics ReadWriteDataSectionSemantics |
37 |
| - for section in self.bv.get_sections_at(seg.start): |
38 |
| - self.bv.add_user_section(section.name, section.end-section.start, SectionSemantics.ReadWriteDataSectionSemantics) |
39 |
| - |
40 |
| - def _import_types_from_headers(self): |
41 |
| - """Parse EDKII types from header files |
42 |
| - """ |
43 |
| - |
44 |
| - hdrs_path = os.path.join(self.dirname, 'headers') |
45 |
| - headers = glob.glob(os.path.join(hdrs_path, '*.h')) |
46 |
| - for hdr in headers: |
47 |
| - _types = self.bv.platform.parse_types_from_source_file(hdr) |
48 |
| - for name, _type in _types.types.items(): |
49 |
| - self.bv.define_user_type(name, _type) |
50 |
| - |
51 |
| - def _set_entry_point_prototype(self): |
52 |
| - """Apply correct prototype to the module entry point |
53 |
| - """ |
54 |
| - |
55 |
| - _start = self.bv.get_function_at(self.bv.entry_point) |
56 |
| - _start.function_type = "EFI_STATUS ModuleEntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)" |
57 |
| - |
58 |
| - def _load_guids(self): |
59 |
| - """Read known GUIDs from CSV and convert string GUIDs to bytes |
60 |
| -
|
61 |
| - :return: Dictionary containing GUID bytes and associated names |
62 |
| - """ |
63 |
| - |
64 |
| - guids_path = os.path.join(self.dirname, 'guids.csv') |
65 |
| - with open(guids_path) as f: |
66 |
| - reader = csv.reader(f, skipinitialspace=True) |
67 |
| - guids = dict(reader) |
68 |
| - |
69 |
| - # Convert to bytes for faster lookup |
70 |
| - guid_bytes = dict() |
71 |
| - for guid, name in guids.items(): |
72 |
| - guid_bytes[name] = uuid.UUID(guid).bytes_le |
73 |
| - |
74 |
| - return guid_bytes |
75 |
| - |
76 |
| - def _apply_guid_name_if_data(self, name: str, address: int): |
77 |
| - """Check if there is a function at the address. If not, then apply the EFI_GUID type and name it |
78 |
| -
|
79 |
| - :param name: Name/symbol to apply to the GUID |
80 |
| - :param address: Address of the GUID |
81 |
| - """ |
82 |
| - |
83 |
| - print(f'Found {name} at 0x{hex(address)} ({uuid.UUID(bytes_le=self.guids[name])})') |
84 |
| - |
85 |
| - # Just to avoid a unlikely false positive and screwing up disassembly |
86 |
| - if self.bv.get_functions_at(address) != []: |
87 |
| - print(f'There is code at {address}, not applying GUID type and name') |
88 |
| - return |
89 |
| - |
90 |
| - self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, address, 'g'+name)) |
91 |
| - t = self.bv.parse_type_string("EFI_GUID") |
92 |
| - self.bv.define_user_data_var(address, t[0]) |
93 |
| - |
94 |
| - def _find_known_guids(self): |
95 |
| - """Search for known GUIDs and apply names to matches not within a function |
96 |
| - """ |
97 |
| - |
98 |
| - names_list = list(self.guids.keys()) |
99 |
| - guids_list = list(self.guids.values()) |
100 |
| - def _check_guid_and_get_name(guid): |
101 |
| - try: |
102 |
| - return names_list[guids_list.index(guid)] |
103 |
| - except ValueError: |
104 |
| - return None |
105 |
| - |
106 |
| - for seg in self.bv.segments: |
107 |
| - for i in range(seg.start, seg.end): |
108 |
| - self.br.seek(i) |
109 |
| - data = self.br.read(16) |
110 |
| - if not data or len(data) != 16: |
111 |
| - continue |
112 |
| - |
113 |
| - found_name = _check_guid_and_get_name(data) |
114 |
| - if found_name: |
115 |
| - self._apply_guid_name_if_data(found_name, i) |
116 |
| - |
117 |
| - def _set_if_uefi_core_type(self, instr: HighLevelILInstruction): |
118 |
| - """Using HLIL, scrutinize the instruction to determine if it's a move of a local variable to a global variable. |
119 |
| - If it is, check if the source operand type is a UEFI core type and apply the type to the destination global |
120 |
| - variable. |
121 |
| -
|
122 |
| - :param instr: High level IL instruction object |
123 |
| - """ |
124 |
| - |
125 |
| - if instr.operation != HighLevelILOperation.HLIL_ASSIGN: |
126 |
| - return |
127 |
| - |
128 |
| - if instr.dest.operation != HighLevelILOperation.HLIL_DEREF: |
129 |
| - return |
130 |
| - |
131 |
| - if instr.dest.src.operation != HighLevelILOperation.HLIL_CONST_PTR: |
132 |
| - return |
133 |
| - |
134 |
| - if instr.src.operation != HighLevelILOperation.HLIL_VAR: |
135 |
| - return |
136 |
| - |
137 |
| - _type = instr.src.var.type |
138 |
| - if len(_type.tokens) == 1 and str(_type.tokens[0]) == 'EFI_HANDLE': |
139 |
| - self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gImageHandle')) |
140 |
| - elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_BOOT_SERVICES': |
141 |
| - self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gBS')) |
142 |
| - elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_RUNTIME_SERVICES': |
143 |
| - self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gRS')) |
144 |
| - elif len(_type.tokens) > 2 and str(_type.tokens[2]) == 'EFI_SYSTEM_TABLE': |
145 |
| - self.bv.define_user_symbol(Symbol(SymbolType.DataSymbol, instr.dest.src.constant, 'gST')) |
146 |
| - else: |
147 |
| - return |
148 |
| - |
149 |
| - self.bv.define_user_data_var(instr.dest.src.constant, instr.src.var.type) |
150 |
| - print(f'Found global assignment - offset:0x{hex(instr.dest.src.constant)} type:{instr.src.var.type}') |
151 |
| - |
152 |
| - def _check_and_prop_types_on_call(self, instr: HighLevelILInstruction): |
153 |
| - """Most UEFI modules don't assign globals in the entry function and instead call a initialization routine and |
154 |
| - pass the system table to it where global assignments are made. This function ensures that the types are applied |
155 |
| - to the initialization function params so that we can catch global assignments outside of the module entry |
156 |
| -
|
157 |
| - :param instr: High level IL instruction object |
158 |
| - """ |
159 |
| - |
160 |
| - if instr.operation not in [HighLevelILOperation.HLIL_TAILCALL, HighLevelILOperation.HLIL_CALL]: |
161 |
| - return |
162 |
| - |
163 |
| - if instr.dest.operation != HighLevelILOperation.HLIL_CONST_PTR: |
164 |
| - return |
165 |
| - |
166 |
| - argv_is_passed = False |
167 |
| - for arg in instr.params: |
168 |
| - if 'ImageHandle' in str(arg) or 'SystemTable' in str(arg): |
169 |
| - argv_is_passed = True |
170 |
| - break |
171 |
| - |
172 |
| - if not argv_is_passed: |
173 |
| - return |
174 |
| - |
175 |
| - func = self.bv.get_function_at(instr.dest.constant) |
176 |
| - old = func.function_type |
177 |
| - call_args = instr.params |
178 |
| - new_params = [] |
179 |
| - for arg, param in zip(call_args, old.parameters): |
180 |
| - if hasattr(arg, 'var'): |
181 |
| - new_type = arg.var.type |
182 |
| - else: |
183 |
| - new_type = param.type |
184 |
| - new_type.confidence = 256 |
185 |
| - new_params.append(FunctionParameter(new_type, param.name)) |
186 |
| - |
187 |
| - # TODO: this is a hack to account for odd behavior. func.function_type should be able to set directly to |
188 |
| - # Type.Function(...). However, during testing this isn't the case. I am only able to get it to work if I |
189 |
| - # set function_type to a string and update analysis. |
190 |
| - gross_hack = str( |
191 |
| - Type.function(old.return_value, new_params, old.calling_convention, old.has_variable_arguments, old.stack_adjustment) |
192 |
| - ).replace('(', '{}('.format(func.name)) |
193 |
| - func.function_type = gross_hack |
194 |
| - self.bv.update_analysis_and_wait() |
195 |
| - |
196 |
| - def _set_global_variables(self): |
197 |
| - """On entry, UEFI modules usually set global variables for EFI_BOOT_SERVICES, EFI_RUNTIME_SERIVCES, and |
198 |
| - EFI_SYSTEM_TABLE. This function attempts to identify these assignments and apply types. |
199 |
| - """ |
200 |
| - |
201 |
| - func = self.bv.get_function_at(self.bv.entry_point) |
202 |
| - for block in func.high_level_il: |
203 |
| - for instr in block: |
204 |
| - self._check_and_prop_types_on_call(instr) |
205 |
| - |
206 |
| - for func in self.bv.functions: |
207 |
| - for block in func.high_level_il: |
208 |
| - for instr in block: |
209 |
| - self._set_if_uefi_core_type(instr) |
210 |
| - |
211 |
| - def run(self): |
212 |
| - """Run the task in the background |
213 |
| - """ |
214 |
| - |
215 |
| - self.progress = "UEFI Helper: Fixing up segments, applying types, and searching for known GUIDs ..." |
216 |
| - self._fix_segments() |
217 |
| - self._import_types_from_headers() |
218 |
| - self._set_entry_point_prototype() |
219 |
| - self._find_known_guids() |
220 |
| - self.progress = "UEFI Helper: searching for global assignments for UEFI core services ..." |
221 |
| - self._set_global_variables() |
222 |
| - print('UEFI Helper completed successfully!') |
223 |
| - |
224 |
| -def run_uefi_helper(bv: BinaryView): |
225 |
| - """Run UEFI helper utilities in the background |
226 |
| - """ |
227 |
| - |
228 |
| - task = UEFIHelper(bv) |
229 |
| - task.start() |
| 1 | +from binaryninja import PluginCommand |
| 2 | +from .helper import run_uefi_helper |
| 3 | +from .teloader import TerseExecutableView |
230 | 4 |
|
231 | 5 | PluginCommand.register('UEFI Helper', 'Run UEFI Helper analysis', run_uefi_helper)
|
| 6 | +TerseExecutableView.register() |
| 7 | + |
0 commit comments