From ab83f6d8f4999ae299c47d284456257c09e75211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Tue, 13 Jun 2023 18:10:02 +0200 Subject: [PATCH 01/10] added uefi plugin --- src/plugins/analysis/uefi/code/__init__.py | 0 src/plugins/analysis/uefi/code/uefi.py | 67 +++++++++++ src/plugins/analysis/uefi/docker/Dockerfile | 22 ++++ src/plugins/analysis/uefi/docker/scan.py | 117 ++++++++++++++++++++ src/plugins/analysis/uefi/install.py | 31 ++++++ src/plugins/analysis/uefi/view/uefi.html | 50 +++++++++ 6 files changed, 287 insertions(+) create mode 100644 src/plugins/analysis/uefi/code/__init__.py create mode 100644 src/plugins/analysis/uefi/code/uefi.py create mode 100644 src/plugins/analysis/uefi/docker/Dockerfile create mode 100755 src/plugins/analysis/uefi/docker/scan.py create mode 100755 src/plugins/analysis/uefi/install.py create mode 100644 src/plugins/analysis/uefi/view/uefi.html diff --git a/src/plugins/analysis/uefi/code/__init__.py b/src/plugins/analysis/uefi/code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py new file mode 100644 index 000000000..d6ef13feb --- /dev/null +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import json +import logging +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +from analysis.PluginBase import AnalysisBasePlugin +from helperFunctions.docker import run_docker_container + +from docker.types import Mount + +if TYPE_CHECKING: + from objects.file import FileObject + +DOCKER_IMAGE = 'fact/uefi' + + +class AnalysisPlugin(AnalysisBasePlugin): + NAME = 'uefi' + DESCRIPTION = 'find vulnerabilities in UEFI modules using the tool FwHunt' + DEPENDENCIES = ['file_type'] + MIME_WHITELIST = ['application/x-dosexec'] + VERSION = '0.0.1' + FILE = __file__ + + def process_object(self, file_object: FileObject): + file_object.processed_analysis.setdefault(self.NAME, {}) + + type_result = file_object.processed_analysis['file_type'].get('result', {}).get('full', '') + if 'EFI boot service driver' not in type_result: + # only EFI modules are analyzed + return file_object + + data = self._analyze_uefi_module(file_object.file_path) + + file_object.processed_analysis[self.NAME].update(data) + file_object.processed_analysis[self.NAME]['summary'] = self._get_summary(data) + return file_object + + def _analyze_uefi_module(self, path: str) -> dict[str, dict]: + with TemporaryDirectory() as tmp_dir: + output_file = Path(tmp_dir) / 'output.json' + output_file.write_text('{}') + run_docker_container( + DOCKER_IMAGE, + combine_stderr_stdout=True, + timeout=self.TIMEOUT, + mounts=[ + Mount('/input/file', path, type='bind'), + Mount('/output/file', str(output_file), type='bind'), + ], + ) + data = json.loads(output_file.read_text()) + logging.warning(f'{data=}') + return data + + def _get_summary(self, data: dict[str, dict]) -> list[str]: + summary = set() + for category, category_data in data.items(): + for rule_results in category_data.values(): + for variant_result in rule_results['variants'].values(): + if variant_result['match']: + summary.add(category) + continue + return sorted(summary) diff --git a/src/plugins/analysis/uefi/docker/Dockerfile b/src/plugins/analysis/uefi/docker/Dockerfile new file mode 100644 index 000000000..3f03613f0 --- /dev/null +++ b/src/plugins/analysis/uefi/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11 + +# install rizin +ARG rizin_version="v0.5.2" +RUN wget https://github.com/rizinorg/rizin/releases/download/${rizin_version}/rizin-${rizin_version}-static-x86_64.tar.xz && \ + tar xf rizin-${rizin_version}-static-x86_64.tar.xz && \ + rm rizin-${rizin_version}-static-x86_64.tar.xz + +# clone FwHunt rules +WORKDIR /work/FwHunt +ARG fwhunt_sha="8906b6bc48dbed3f6005d8e842474f9b1e709003" +RUN git init && \ + git remote add origin https://github.com/binarly-io/fwhunt && \ + git fetch --depth 1 origin ${fwhunt_sha} && \ + git checkout FETCH_HEAD + +# install fwhunt-scan +RUN python3 -m pip install --no-cache-dir fwhunt-scan + +COPY scan.py . + +ENTRYPOINT ["/work/FwHunt/scan.py"] diff --git a/src/plugins/analysis/uefi/docker/scan.py b/src/plugins/analysis/uefi/docker/scan.py new file mode 100755 index 000000000..67b3da08f --- /dev/null +++ b/src/plugins/analysis/uefi/docker/scan.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import sys +import yaml +from pathlib import Path +from shlex import split +from subprocess import run + +RULE_SUFFIXES = ['.yml', '.yaml'] +RULES = Path(__file__).parent / 'rules' +INPUT_FILE = Path('/input/file') +OUTPUT_FILE = Path('/output/file') +BLACKLIST = [ + 'RsbStuffingCheck.yml', # too many false positives +] +CLI_COLOR_REGEX = re.compile(rb'\x1b\[\d{1,3}m') +RESULT_PARSING_REGEX = re.compile(r'Scanner result (\w+?) \(variant: (\w+?)\) ([\w ]+?) \(') +NO_MATCH_STR = 'No threat detected' + + +def main(): + _validate_setup() + rule_files = _find_rule_files() + _scan_file(_load_rules(rule_files), rule_files) + + +def _validate_setup(): + if not INPUT_FILE.is_file(): + print('error: input file not found') + sys.exit(1) + if not RULES.is_dir(): + print('error: rules dir not found') + sys.exit(2) + + +def _find_rule_files() -> list[Path]: + return [file for file in RULES.glob('**/*') if _is_rule_file(file) and file.name not in BLACKLIST] + + +def _load_rules(rule_files: list[Path]) -> dict[str, dict]: + """ + Rule structure should look something like this: + { + "": { + "meta": { + "author": "...", + "name": "...", + "namespace": "", + "description": "...", + "url": "...", + ... + }, + "variants": { + "": { + "": {...} + }, + ... + } + } + } + """ + rules = {} + for file in rule_files: + with file.open('rb') as fp: + rule_data = yaml.safe_load(fp) + for rule_dict in rule_data.values(): + rules[rule_dict['meta']['name']] = rule_dict + return rules + + +def _scan_file(rules: dict[str, dict], rule_files: list[Path]): + rules_str = ' '.join(f'-r {file}' for file in rule_files) + proc = run( + split(f'fwhunt_scan_analyzer.py scan-module {INPUT_FILE} {rules_str}'), + capture_output=True, + ) + if proc.returncode != 0: + print(f'warning: Scan exited with return code {proc.returncode}: {proc.stderr}') + else: + output = CLI_COLOR_REGEX.sub(b'', proc.stdout).decode(errors='replace') + result = _parse_output(output, rules) + OUTPUT_FILE.write_text(json.dumps(result)) + + +def _parse_output(output: str, rules: dict[str, dict]) -> dict[str, dict]: + result = {} + for rule_name, variant, detected in RESULT_PARSING_REGEX.findall(output): + rule_data = rules.get(rule_name) + if rule_data is None: + print(f'error: rule {rule_name} not found') + sys.exit(3) + category = rule_data['meta']['namespace'] + result.setdefault(category, {}).setdefault( + rule_name, + { + 'description': rule_data['meta'].get('description', ''), + 'author': rule_data['meta'].get('author', ''), + 'url': rule_data['meta'].get('url', ''), + 'variants': {}, + }, + ) + result[category][rule_name]['variants'][variant] = { + 'output': detected, + 'match': NO_MATCH_STR not in detected, + } + return result + + +def _is_rule_file(rule: Path) -> bool: + return rule.is_file() and rule.suffix in RULE_SUFFIXES + + +if __name__ == '__main__': + main() diff --git a/src/plugins/analysis/uefi/install.py b/src/plugins/analysis/uefi/install.py new file mode 100755 index 000000000..66f11b8de --- /dev/null +++ b/src/plugins/analysis/uefi/install.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import logging +from pathlib import Path + +try: + from helperFunctions.install import run_cmd_with_logging + from plugins.installer import AbstractPluginInstaller +except ImportError: + import sys + + SRC_PATH = Path(__file__).absolute().parent.parent.parent.parent + sys.path.append(str(SRC_PATH)) + + from helperFunctions.install import run_cmd_with_logging + from plugins.installer import AbstractPluginInstaller + + +class UefiInstaller(AbstractPluginInstaller): + base_path = Path(__file__).resolve().parent + + def install_docker_images(self): + run_cmd_with_logging('docker build -t fact/uefi ./docker') + + +# Alias for generic use +Installer = UefiInstaller + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + Installer().install() diff --git a/src/plugins/analysis/uefi/view/uefi.html b/src/plugins/analysis/uefi/view/uefi.html new file mode 100644 index 000000000..ef0c91b1a --- /dev/null +++ b/src/plugins/analysis/uefi/view/uefi.html @@ -0,0 +1,50 @@ +{% extends "analysis_plugins/general_information.html" %} + +{% block analysis_result_details %} + + {% for category, category_results in analysis_result.items() %} + + {{ category | safe }} + + + {% for rule, rule_results in category_results.items() %} + + + + + {% endfor %} +
{{ rule | safe }} + + + + + + + + + + + + + +
url{{ rule_results.url | safe }}
author{{ rule_results.author | safe }}
variants + + {% for variant, variant_data in rule_results.variants.items() %} + + + {% if variant_data.match %} + + {% else %} + + {% endif %} + + {% endfor %} +
{{ variant | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}
+
+
+ + + {% endfor %} + +{% endblock %} + From 6c1b276ee7f5d9dc9ee34752373a0632fc9725a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 15 Jun 2023 14:29:47 +0200 Subject: [PATCH 02/10] uefi plugin: improved template spacing --- src/plugins/analysis/uefi/view/uefi.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/analysis/uefi/view/uefi.html b/src/plugins/analysis/uefi/view/uefi.html index ef0c91b1a..5fa7c8ce3 100644 --- a/src/plugins/analysis/uefi/view/uefi.html +++ b/src/plugins/analysis/uefi/view/uefi.html @@ -9,12 +9,12 @@ {% for rule, rule_results in category_results.items() %} - +
{{ rule | safe }}{{ rule | safe }} - - + + @@ -28,9 +28,9 @@ {% if variant_data.match %} - + {% else %} - + {% endif %} {% endfor %} From c801c5d25a90ab2094218fa73dafc50cb45b6ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 15 Jun 2023 18:13:54 +0200 Subject: [PATCH 03/10] uefi plugin: added scanning of uefi containers + optimized template/regex --- src/plugins/analysis/uefi/code/uefi.py | 25 +++++++++++++++--------- src/plugins/analysis/uefi/docker/scan.py | 6 ++++-- src/plugins/analysis/uefi/view/uefi.html | 12 +++++++----- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py index d6ef13feb..d6b02ba81 100644 --- a/src/plugins/analysis/uefi/code/uefi.py +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import logging from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING @@ -21,25 +20,26 @@ class AnalysisPlugin(AnalysisBasePlugin): NAME = 'uefi' DESCRIPTION = 'find vulnerabilities in UEFI modules using the tool FwHunt' DEPENDENCIES = ['file_type'] - MIME_WHITELIST = ['application/x-dosexec'] + MIME_WHITELIST = ['application/x-dosexec', 'firmware/uefi'] VERSION = '0.0.1' FILE = __file__ def process_object(self, file_object: FileObject): file_object.processed_analysis.setdefault(self.NAME, {}) + mime = file_object.processed_analysis['file_type'].get('result', {}).get('mime', '') type_result = file_object.processed_analysis['file_type'].get('result', {}).get('full', '') - if 'EFI boot service driver' not in type_result: - # only EFI modules are analyzed + if _is_no_uefi_module(mime, type_result): + # only EFI modules are analyzed, not regular PE files return file_object - data = self._analyze_uefi_module(file_object.file_path) + data = self._analyze_uefi_module(file_object.file_path, _get_analysis_mode(mime)) file_object.processed_analysis[self.NAME].update(data) file_object.processed_analysis[self.NAME]['summary'] = self._get_summary(data) return file_object - def _analyze_uefi_module(self, path: str) -> dict[str, dict]: + def _analyze_uefi_module(self, path: str, mode: str) -> dict[str, dict]: with TemporaryDirectory() as tmp_dir: output_file = Path(tmp_dir) / 'output.json' output_file.write_text('{}') @@ -51,10 +51,9 @@ def _analyze_uefi_module(self, path: str) -> dict[str, dict]: Mount('/input/file', path, type='bind'), Mount('/output/file', str(output_file), type='bind'), ], + environment={'UEFI_ANALYSIS_MODE': mode}, ) - data = json.loads(output_file.read_text()) - logging.warning(f'{data=}') - return data + return json.loads(output_file.read_text()) def _get_summary(self, data: dict[str, dict]) -> list[str]: summary = set() @@ -65,3 +64,11 @@ def _get_summary(self, data: dict[str, dict]) -> list[str]: summary.add(category) continue return sorted(summary) + + +def _is_no_uefi_module(mime: str, type_result: str) -> bool: + return mime == 'application/x-dosexec' and 'EFI boot service driver' not in type_result + + +def _get_analysis_mode(mime: str) -> str: + return 'firmware' if mime == 'firmware/uefi' else 'module' diff --git a/src/plugins/analysis/uefi/docker/scan.py b/src/plugins/analysis/uefi/docker/scan.py index 67b3da08f..3f24ad8bf 100755 --- a/src/plugins/analysis/uefi/docker/scan.py +++ b/src/plugins/analysis/uefi/docker/scan.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import os import re import sys import yaml @@ -17,7 +18,7 @@ 'RsbStuffingCheck.yml', # too many false positives ] CLI_COLOR_REGEX = re.compile(rb'\x1b\[\d{1,3}m') -RESULT_PARSING_REGEX = re.compile(r'Scanner result (\w+?) \(variant: (\w+?)\) ([\w ]+?) \(') +RESULT_PARSING_REGEX = re.compile(r'Scanner result ([^\n]+?) \(variant: ([^\n]+?)\) ([^(]+?)(?: \(|\n|$)') NO_MATCH_STR = 'No threat detected' @@ -73,8 +74,9 @@ def _load_rules(rule_files: list[Path]) -> dict[str, dict]: def _scan_file(rules: dict[str, dict], rule_files: list[Path]): rules_str = ' '.join(f'-r {file}' for file in rule_files) + mode = os.environ.get('UEFI_ANALYSIS_MODE', default='module') proc = run( - split(f'fwhunt_scan_analyzer.py scan-module {INPUT_FILE} {rules_str}'), + split(f'fwhunt_scan_analyzer.py scan-{mode} {INPUT_FILE} {rules_str}'), capture_output=True, ) if proc.returncode != 0: diff --git a/src/plugins/analysis/uefi/view/uefi.html b/src/plugins/analysis/uefi/view/uefi.html index 5fa7c8ce3..b3f258b09 100644 --- a/src/plugins/analysis/uefi/view/uefi.html +++ b/src/plugins/analysis/uefi/view/uefi.html @@ -12,12 +12,14 @@
url{{ rule_results.url | safe }}url{{ rule_results.url | safe }}
author
{{ variant | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}
{{ rule | safe }} + {% if rule_results.url %} + + + + + {% endif %} - - - - - + From a81f880c775a66c6db1b4b03d7a88ff1e0ad9d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 16 Oct 2023 17:57:54 +0200 Subject: [PATCH 04/10] uefi plugin: convert to plugin v0 --- src/plugins/analysis/uefi/code/uefi.py | 151 ++++++++++++++++++----- src/plugins/analysis/uefi/docker/scan.py | 12 +- src/plugins/analysis/uefi/view/uefi.html | 11 +- 3 files changed, 128 insertions(+), 46 deletions(-) diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py index d6b02ba81..4fa189973 100644 --- a/src/plugins/analysis/uefi/code/uefi.py +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -3,46 +3,79 @@ import json from pathlib import Path from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING -from analysis.PluginBase import AnalysisBasePlugin +from pydantic import BaseModel, Field + +from analysis.plugin import AnalysisPluginV0, Tag +from analysis.plugin.compat import AnalysisBasePluginAdapterMixin from helperFunctions.docker import run_docker_container from docker.types import Mount +from helperFunctions.tag import TagColor + if TYPE_CHECKING: - from objects.file import FileObject + from io import FileIO DOCKER_IMAGE = 'fact/uefi' -class AnalysisPlugin(AnalysisBasePlugin): - NAME = 'uefi' - DESCRIPTION = 'find vulnerabilities in UEFI modules using the tool FwHunt' - DEPENDENCIES = ['file_type'] - MIME_WHITELIST = ['application/x-dosexec', 'firmware/uefi'] - VERSION = '0.0.1' - FILE = __file__ +class Variant(BaseModel): + name: str = Field(description='The name of the vulnerability variant') + match: bool = Field(description='Whether there was a match for this vulnerability') + output: str = Field(description='The output of FwHunt') - def process_object(self, file_object: FileObject): - file_object.processed_analysis.setdefault(self.NAME, {}) - mime = file_object.processed_analysis['file_type'].get('result', {}).get('mime', '') - type_result = file_object.processed_analysis['file_type'].get('result', {}).get('full', '') - if _is_no_uefi_module(mime, type_result): - # only EFI modules are analyzed, not regular PE files - return file_object +class Rule(BaseModel): + name: str = Field(description='The name of the rule') + category: str = Field(description='The rule category (e.g. vulnerabilities or mitigation failures)') + author: Optional[str] = Field(None, description='The Author of the rule') + description: Optional[str] = Field(None, description='The description of the rule/vulnerability') + url: Optional[str] = Field(None, description='A link with more information for this rule/vulnerability') + variants: List[Variant] = Field(description='The list of variants with matching information') + + +class Schema(BaseModel): + vulnerabilities: List[Rule] = Field(description='A list of UEFI vulnerabilities') + + +class UefiPluginError(Exception): + pass - data = self._analyze_uefi_module(file_object.file_path, _get_analysis_mode(mime)) - file_object.processed_analysis[self.NAME].update(data) - file_object.processed_analysis[self.NAME]['summary'] = self._get_summary(data) - return file_object +class AnalysisPlugin(AnalysisPluginV0, AnalysisBasePluginAdapterMixin): + def __init__(self): + super().__init__( + metadata=AnalysisPluginV0.MetaData( + name='uefi', + description='find vulnerabilities in UEFI modules using the tool FwHunt', + dependencies=['file_type'], + version='0.1.0', + Schema=Schema, + mime_whitelist=['application/x-dosexec', 'firmware/uefi'], + ), + ) - def _analyze_uefi_module(self, path: str, mode: str) -> dict[str, dict]: + def analyze( + self, + file_handle: FileIO, + virtual_file_path: dict[str, list[str]], + analyses: dict[str, BaseModel], + ) -> Schema | None: + del virtual_file_path + + type_analysis = analyses['file_type'] + if _is_no_uefi_module(type_analysis): + # only EFI modules are analyzed, not regular PE files + return None + + return self._analyze_uefi_module(file_handle.name, _get_analysis_mode(type_analysis.mime)) + + def _analyze_uefi_module(self, path: str, mode: str) -> Schema | None: with TemporaryDirectory() as tmp_dir: output_file = Path(tmp_dir) / 'output.json' - output_file.write_text('{}') + output_file.touch() run_docker_container( DOCKER_IMAGE, combine_stderr_stdout=True, @@ -53,21 +86,71 @@ def _analyze_uefi_module(self, path: str, mode: str) -> dict[str, dict]: ], environment={'UEFI_ANALYSIS_MODE': mode}, ) - return json.loads(output_file.read_text()) + try: + return _convert_json_to_schema(json.loads(output_file.read_text())) + except json.JSONDecodeError as error: + raise UefiPluginError('Could not load container output') from error - def _get_summary(self, data: dict[str, dict]) -> list[str]: + def summarize(self, result: Schema) -> list[str]: summary = set() - for category, category_data in data.items(): - for rule_results in category_data.values(): - for variant_result in rule_results['variants'].values(): - if variant_result['match']: - summary.add(category) - continue + for rule in result.vulnerabilities: + for variant in rule.variants: + if variant.match: + summary.add(rule.category) + continue return sorted(summary) - -def _is_no_uefi_module(mime: str, type_result: str) -> bool: - return mime == 'application/x-dosexec' and 'EFI boot service driver' not in type_result + def get_tags(self, result: Schema, summary: list[str]) -> list[Tag]: + del result + return [ + Tag( + name=category, + value='UEFI vulnerability', + color=TagColor.ORANGE, + propagate=True, + ) + for category in summary + ] + + +def _convert_json_to_schema(fw_hunt_data: dict[str, dict]) -> Schema: + """ + The output of the docker container has the following structure: + { + : { + category: ..., + author: ..., + description: ..., + [url: ...,] + variants: { + : { + output: ..., + match: ... + }, + ... + }, + }, + ... + } + """ + vulnerabilities = [ + Rule( + name=rule_name, + category=data['category'], + author=data['author'], + description=data['description'], + url=data.get('url') or None, # fix for empty strings + variants=[ + Variant(name=variant_name, **variant_data) for variant_name, variant_data in data['variants'].items() + ], + ) + for rule_name, data in fw_hunt_data.items() + ] + return Schema(vulnerabilities=vulnerabilities) + + +def _is_no_uefi_module(type_analysis: BaseModel) -> bool: + return type_analysis.mime == 'application/x-dosexec' and 'EFI boot service driver' not in type_analysis.full def _get_analysis_mode(mime: str) -> str: diff --git a/src/plugins/analysis/uefi/docker/scan.py b/src/plugins/analysis/uefi/docker/scan.py index 3f24ad8bf..84550bbf4 100755 --- a/src/plugins/analysis/uefi/docker/scan.py +++ b/src/plugins/analysis/uefi/docker/scan.py @@ -94,17 +94,17 @@ def _parse_output(output: str, rules: dict[str, dict]) -> dict[str, dict]: if rule_data is None: print(f'error: rule {rule_name} not found') sys.exit(3) - category = rule_data['meta']['namespace'] - result.setdefault(category, {}).setdefault( + result.setdefault( rule_name, { - 'description': rule_data['meta'].get('description', ''), - 'author': rule_data['meta'].get('author', ''), - 'url': rule_data['meta'].get('url', ''), + 'category': rule_data['meta']['namespace'], + 'description': rule_data['meta'].get('description'), + 'author': rule_data['meta'].get('author'), + 'url': rule_data['meta'].get('url'), 'variants': {}, }, ) - result[category][rule_name]['variants'][variant] = { + result[rule_name]['variants'][variant] = { 'output': detected, 'match': NO_MATCH_STR not in detected, } diff --git a/src/plugins/analysis/uefi/view/uefi.html b/src/plugins/analysis/uefi/view/uefi.html index b3f258b09..7fb7e7256 100644 --- a/src/plugins/analysis/uefi/view/uefi.html +++ b/src/plugins/analysis/uefi/view/uefi.html @@ -1,15 +1,14 @@ {% extends "analysis_plugins/general_information.html" %} {% block analysis_result_details %} - - {% for category, category_results in analysis_result.items() %} + {% for category, result_list in analysis_result.vulnerabilities | groupby("category") %}
url{{ rule_results.url | safe }}
url{{ rule_results.url | safe }}
authorauthor {{ rule_results.author | safe }}
{{ category | safe }} - {% for rule, rule_results in category_results.items() %} + {% for rule_results in result_list %} - +
{{ rule | safe }}{{ rule_results.name | safe }} {% if rule_results.url %} @@ -26,9 +25,9 @@
variants - {% for variant, variant_data in rule_results.variants.items() %} + {% for variant_data in rule_results.variants %} - + {% if variant_data.match %} {% else %} From 9ac59186755535e4d8323a68c9f1c591b7f5c50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 16 Oct 2023 17:59:26 +0200 Subject: [PATCH 05/10] uefi plugin: made more data fields optional --- src/plugins/analysis/uefi/code/uefi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py index 4fa189973..fd0b8277f 100644 --- a/src/plugins/analysis/uefi/code/uefi.py +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -119,8 +119,8 @@ def _convert_json_to_schema(fw_hunt_data: dict[str, dict]) -> Schema: { : { category: ..., - author: ..., - description: ..., + [author: ...,] + [description: ...,] [url: ...,] variants: { : { @@ -137,9 +137,9 @@ def _convert_json_to_schema(fw_hunt_data: dict[str, dict]) -> Schema: Rule( name=rule_name, category=data['category'], - author=data['author'], - description=data['description'], - url=data.get('url') or None, # fix for empty strings + author=data.get('author'), + description=data.get('description'), + url=data.get('url'), variants=[ Variant(name=variant_name, **variant_data) for variant_name, variant_data in data['variants'].items() ], From 5c446d0884a307a82b382396efd9f58ff55497f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Tue, 17 Oct 2023 11:12:17 +0200 Subject: [PATCH 06/10] uefi plugin: switched base image to alpine --- src/plugins/analysis/uefi/docker/Dockerfile | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/plugins/analysis/uefi/docker/Dockerfile b/src/plugins/analysis/uefi/docker/Dockerfile index 3f03613f0..3d838517f 100644 --- a/src/plugins/analysis/uefi/docker/Dockerfile +++ b/src/plugins/analysis/uefi/docker/Dockerfile @@ -1,21 +1,26 @@ -FROM python:3.11 +FROM alpine:3.18 # install rizin -ARG rizin_version="v0.5.2" -RUN wget https://github.com/rizinorg/rizin/releases/download/${rizin_version}/rizin-${rizin_version}-static-x86_64.tar.xz && \ - tar xf rizin-${rizin_version}-static-x86_64.tar.xz && \ - rm rizin-${rizin_version}-static-x86_64.tar.xz +ARG rizin_version="v0.6.2" +ARG ARCHIVE="rizin-${rizin_version}-static-x86_64.tar.xz" +RUN wget https://github.com/rizinorg/rizin/releases/download/${rizin_version}/${ARCHIVE} && \ + tar xf ${ARCHIVE} && \ + rm ${ARCHIVE} # clone FwHunt rules WORKDIR /work/FwHunt -ARG fwhunt_sha="8906b6bc48dbed3f6005d8e842474f9b1e709003" -RUN git init && \ +ARG fwhunt_sha="1f684f1d0d38ba061988c39e0ac4d43eaeec0e50" +RUN apk add --virtual --no-cache git && \ + git init && \ git remote add origin https://github.com/binarly-io/fwhunt && \ git fetch --depth 1 origin ${fwhunt_sha} && \ - git checkout FETCH_HEAD + git checkout FETCH_HEAD && \ + apk del git -# install fwhunt-scan -RUN python3 -m pip install --no-cache-dir fwhunt-scan +# install fwhunt-scan & python +RUN apk add --virtual --no-cache python3 py3-pip && \ + python3 -m pip install --no-cache-dir fwhunt-scan && \ + apk del py3-pip COPY scan.py . From 7e9315b2b3104e472f742db1026a8a10f73a883e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Tue, 17 Oct 2023 11:13:41 +0200 Subject: [PATCH 07/10] uefi plugin: added more metadata fields and also sorted results in the template by rule name --- src/plugins/analysis/uefi/code/uefi.py | 4 ++++ src/plugins/analysis/uefi/docker/scan.py | 6 +++++- src/plugins/analysis/uefi/view/uefi.html | 24 ++++++++++++++---------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py index fd0b8277f..9857b65d4 100644 --- a/src/plugins/analysis/uefi/code/uefi.py +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -33,6 +33,8 @@ class Rule(BaseModel): author: Optional[str] = Field(None, description='The Author of the rule') description: Optional[str] = Field(None, description='The description of the rule/vulnerability') url: Optional[str] = Field(None, description='A link with more information for this rule/vulnerability') + cve: Optional[str] = Field(None, description='A list of related CVEs') + architecture: Optional[str] = Field(None, description='The affected architecture') variants: List[Variant] = Field(description='The list of variants with matching information') @@ -140,6 +142,8 @@ def _convert_json_to_schema(fw_hunt_data: dict[str, dict]) -> Schema: author=data.get('author'), description=data.get('description'), url=data.get('url'), + architecture=data.get('architecture'), + cve=data.get('CVE'), variants=[ Variant(name=variant_name, **variant_data) for variant_name, variant_data in data['variants'].items() ], diff --git a/src/plugins/analysis/uefi/docker/scan.py b/src/plugins/analysis/uefi/docker/scan.py index 84550bbf4..c11dd8bf6 100755 --- a/src/plugins/analysis/uefi/docker/scan.py +++ b/src/plugins/analysis/uefi/docker/scan.py @@ -52,6 +52,8 @@ def _load_rules(rule_files: list[Path]) -> dict[str, dict]: "namespace": "", "description": "...", "url": "...", + "CVE number": "...", + "advisory": "...", ... }, "variants": { @@ -100,7 +102,9 @@ def _parse_output(output: str, rules: dict[str, dict]) -> dict[str, dict]: 'category': rule_data['meta']['namespace'], 'description': rule_data['meta'].get('description'), 'author': rule_data['meta'].get('author'), - 'url': rule_data['meta'].get('url'), + 'url': rule_data['meta'].get('url', rule_data['meta'].get('advisory')), + 'CVE': rule_data['meta'].get('CVE number'), + 'architecture': rule_data['meta'].get('architecture'), 'variants': {}, }, ) diff --git a/src/plugins/analysis/uefi/view/uefi.html b/src/plugins/analysis/uefi/view/uefi.html index 7fb7e7256..d247ae7d1 100644 --- a/src/plugins/analysis/uefi/view/uefi.html +++ b/src/plugins/analysis/uefi/view/uefi.html @@ -5,33 +5,37 @@
{{ variant | safe }}{{ variant_data.name | safe }}{{ variant_data.output | safe }}
{{ category | safe }} - - {% for rule_results in result_list %} +
+ {% for rule_results in result_list | sort(attribute="name") %}
{{ rule_results.name | safe }} - +
{% if rule_results.url %} {% endif %} - - - - + {% for key in ["author", "description", "cve", "architecture"] %} + {% if rule_results.get(key) %} + + + + + {% endif %} + {% endfor %}
url {{ rule_results.url | safe }}
author{{ rule_results.author | safe }}
{{ key }}{{ rule_results.get(key) | link_cve | safe }}
variants - +
{% for variant_data in rule_results.variants %} {% if variant_data.match %} - + {% else %} - + {% endif %} {% endfor %} From a90d08fe5059e6e5a2d680038b59f1000860e2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Tue, 17 Oct 2023 14:38:03 +0200 Subject: [PATCH 08/10] uefi plugin: added test --- src/plugins/analysis/uefi/__init__.py | 0 src/plugins/analysis/uefi/test/__init__.py | 0 .../analysis/uefi/test/data/test_file.pe | Bin 0 -> 372 bytes .../analysis/uefi/test/test_plugin_uefi.py | 36 ++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 src/plugins/analysis/uefi/__init__.py create mode 100644 src/plugins/analysis/uefi/test/__init__.py create mode 100644 src/plugins/analysis/uefi/test/data/test_file.pe create mode 100644 src/plugins/analysis/uefi/test/test_plugin_uefi.py diff --git a/src/plugins/analysis/uefi/__init__.py b/src/plugins/analysis/uefi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/analysis/uefi/test/__init__.py b/src/plugins/analysis/uefi/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/analysis/uefi/test/data/test_file.pe b/src/plugins/analysis/uefi/test/data/test_file.pe new file mode 100644 index 0000000000000000000000000000000000000000..34925606cfebc4fe4256274a0cab05c8ef9dc95e GIT binary patch literal 372 zcmeZ`VjvsrAX_Kp8UtJz7*g7pQ4IRPprpXf#K~{~MU-?7H&BbmaaRULApJkU!=v>; z34=$ktAMNV0gp}>6$YPPUj~nER*=CSy`ezN3t|FQ0!b%^M1~R|Ol2rysALER^3s6p Q5+K``A)g@y$Yx*w0EFln>Hq)$ literal 0 HcmV?d00001 diff --git a/src/plugins/analysis/uefi/test/test_plugin_uefi.py b/src/plugins/analysis/uefi/test/test_plugin_uefi.py new file mode 100644 index 000000000..699e47dbb --- /dev/null +++ b/src/plugins/analysis/uefi/test/test_plugin_uefi.py @@ -0,0 +1,36 @@ +from io import FileIO +from pathlib import Path + +import pytest + +from ..code.uefi import AnalysisPlugin, Schema +from plugins.analysis.file_type.code.file_type import AnalysisPlugin as FileType + +TEST_FILE = Path(__file__).parent / 'data' / 'test_file.pe' + + +@pytest.mark.AnalysisPluginTestConfig(plugin_class=AnalysisPlugin) +class TestFileSystemMetadata: + def test_analyze_summarize_and_tag(self, analysis_plugin): + assert TEST_FILE.is_file(), 'test file is missing' + dependencies = { + 'file_type': FileType.Schema( + mime='application/x-dosexec', + full='MS-DOS executable PE32+ executable (DLL) (EFI boot service driver) x86-64, for MS Windows', + ) + } + result = analysis_plugin.analyze(FileIO(str(TEST_FILE)), {}, dependencies) + assert isinstance(result, Schema) + assert len(result.vulnerabilities) > 0 + + rules_by_name = {r.name: r for r in result.vulnerabilities} + assert 'BRLY-2021-007' in rules_by_name + matching_rule = rules_by_name['BRLY-2021-007'] + assert matching_rule.variants[0].match is True, 'rule did not match' + + summary = analysis_plugin.summarize(result) + assert summary == [matching_rule.category] + + tags = analysis_plugin.get_tags(result, summary) + assert len(tags) == 1 + assert tags[0].name == matching_rule.category From 914c3636db748d2f7412148eea572b7d31224928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Tue, 17 Oct 2023 14:38:45 +0200 Subject: [PATCH 09/10] uefi plugin: added to mandatory tags + added config --- src/config/fact-core-config.toml | 4 ++++ src/storage/db_interface_common.py | 1 + 2 files changed, 5 insertions(+) diff --git a/src/config/fact-core-config.toml b/src/config/fact-core-config.toml index 6a878149c..45c057266 100644 --- a/src/config/fact-core-config.toml +++ b/src/config/fact-core-config.toml @@ -103,6 +103,10 @@ delay = 0.0 name = "cpu_architecture" processes = 4 +[[backend.plugin]] +name = "uefi" +processes = 4 + [[backend.plugin]] name = "cve_lookup" processes = 4 diff --git a/src/storage/db_interface_common.py b/src/storage/db_interface_common.py index 70815cc73..8a968de09 100644 --- a/src/storage/db_interface_common.py +++ b/src/storage/db_interface_common.py @@ -32,6 +32,7 @@ 'known_vulnerabilities', 'qemu_exec', 'software_components', + 'uefi', 'users_and_passwords', ] Summary = Dict[str, List[str]] From e9f105ddac7f6bf6374a659fad1e01c7f9656f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 19 Oct 2023 10:53:22 +0200 Subject: [PATCH 10/10] uefi plugin: timeout bug fix --- src/plugins/analysis/uefi/code/uefi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/analysis/uefi/code/uefi.py b/src/plugins/analysis/uefi/code/uefi.py index 9857b65d4..4e4eb517c 100644 --- a/src/plugins/analysis/uefi/code/uefi.py +++ b/src/plugins/analysis/uefi/code/uefi.py @@ -81,7 +81,7 @@ def _analyze_uefi_module(self, path: str, mode: str) -> Schema | None: run_docker_container( DOCKER_IMAGE, combine_stderr_stdout=True, - timeout=self.TIMEOUT, + timeout=self.metadata.timeout, mounts=[ Mount('/input/file', path, type='bind'), Mount('/output/file', str(output_file), type='bind'),
{{ variant_data.name | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}{{ variant_data.output | safe }}