├── MANIFEST.in ├── fwhunt_scan ├── py.typed ├── __init__.py ├── test_internal.py ├── uefi_utils.py ├── uefi_te.py ├── uefi_types.py ├── uefi_tables.py ├── uefi_extractor.py ├── uefi_smm.py ├── uefi_analyzer.py └── uefi_scanner.py ├── pics └── fwhunt_logo.png ├── pyproject.toml ├── setup.cfg ├── requirements.txt ├── mypy.ini ├── .gitignore ├── Makefile ├── Dockerfile ├── .github └── workflows │ ├── ci.yml │ └── docker-release.yml ├── setup.py ├── README.md ├── fwhunt_scan_docker.py ├── fwhunt_scan_analyzer.py └── LICENSE /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /fwhunt_scan/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The mypy package uses inline types. 2 | -------------------------------------------------------------------------------- /pics/fwhunt_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binarly-io/fwhunt-scan/HEAD/pics/fwhunt_logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E402,W503,E203 3 | exclude = 4 | .git, 5 | env, 6 | dist, 7 | build, 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=7 2 | rzpipe==0.4.0 3 | pyyaml~=6.0.1 4 | shared_memory38; python_version >= "3.6" and python_version < "3.8" 5 | uefi_firmware>=1.10 6 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_unused_configs = True 3 | disallow_incomplete_defs = True 4 | no_implicit_optional = True 5 | disallow_untyped_calls = True 6 | [mypy-r2pipe.*] 7 | ignore_missing_imports = True 8 | [mypy-rzpipe.*] 9 | ignore_missing_imports = True 10 | [mypy-setuptools.*] 11 | ignore_missing_imports = True 12 | [mypy-shared_memory.*] 13 | ignore_missing_imports = True 14 | [mypy-uefi_firmware.*] 15 | ignore_missing_imports = True 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .eggs/ 11 | .installed.cfg 12 | .mypy_cache/ 13 | .pytest_cache/ 14 | .Python 15 | *.egg 16 | *.egg-info/ 17 | *.pyi 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | lib/ 24 | lib64/ 25 | MANIFEST 26 | parts/ 27 | sdist/ 28 | share/python-wheels/ 29 | var/ 30 | wheels/ 31 | 32 | # Directories 33 | .ruff_cache 34 | .vscode/ 35 | env/ 36 | rules/ 37 | test/ 38 | 39 | # Files 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /fwhunt_scan/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | """ 4 | Tools for analyzing UEFI firmware and checking UEFI modules with FwHunt rules 5 | """ 6 | 7 | from .uefi_analyzer import UefiAnalyzer, UefiAnalyzerError 8 | from .uefi_extractor import UefiBinary, UefiExtractor 9 | from .uefi_scanner import UefiRule, UefiScanner, UefiScannerError 10 | from .uefi_te import TerseExecutableParser 11 | 12 | __all__ = [ 13 | "UefiAnalyzer", 14 | "UefiRule", 15 | "UefiScanner", 16 | "UefiScannerError", 17 | "TerseExecutableParser", 18 | "UefiAnalyzerError", 19 | "UefiBinary", 20 | "UefiExtractor", 21 | ] 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Richard Hughes 2 | # SPDX-License-Identifier: GPL-3.0+ 3 | 4 | VENV=./env 5 | PYTHON=$(VENV)/bin/python 6 | PYTEST=$(VENV)/bin/pytest 7 | BLACK=$(VENV)/bin/black 8 | FLAKE8=$(VENV)/bin/flake8 9 | MYPY=$(VENV)/bin/mypy 10 | STUBGEN=$(VENV)/bin/stubgen 11 | 12 | setup: requirements.txt 13 | $(VENV)/bin/pip install -r requirements.txt 14 | 15 | clean: 16 | rm -rf ./build 17 | rm -rf ./htmlcov 18 | rm -rf ./dist 19 | 20 | blacken: 21 | find fwhunt_scan -name '*.py' -exec $(BLACK) {} \; 22 | 23 | check: $(PYTEST) 24 | $(PYTEST) 25 | $(MYPY) fwhunt_scan 26 | $(FLAKE8) 27 | 28 | pkg: $(STUBGEN) 29 | $(STUBGEN) --output . --package fwhunt_scan 30 | $(PYTHON) setup.py sdist bdist_wheel 31 | -------------------------------------------------------------------------------- /fwhunt_scan/test_internal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # SPDX-License-Identifier: GPL-3.0+ 4 | 5 | """ 6 | Simple self tests for fwhunt_scan 7 | """ 8 | 9 | import unittest 10 | 11 | from .uefi_protocols import GUID_FROM_VALUE, PROTOCOLS_GUIDS 12 | 13 | 14 | class TestInternal(unittest.TestCase): 15 | """internal tests of privaet API""" 16 | 17 | def test_guid_convert(self): 18 | """convert to GUID by index and value""" 19 | 20 | self.assertEqual( 21 | PROTOCOLS_GUIDS[144].value, 22 | "5C6FA2C9-9768-45F6-8E645AECCADAB481", 23 | ) 24 | self.assertEqual( 25 | PROTOCOLS_GUIDS[144].bytes, 26 | b"\xc9\xa2o\\h\x97\xf6E\x8edZ\xec\xca\xda\xb4\x81", 27 | ) 28 | self.assertEqual( 29 | GUID_FROM_VALUE["C2702B74-800C-4131-87468FB5B89CE4AC"].name, 30 | "EFI_SMM_ACCESS2_PROTOCOL_GUID", 31 | ) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | LABEL org.opencontainers.image.source=https://github.com/binarly-io/fwhunt-scan 4 | 5 | ARG rz_version=v0.6.2 6 | 7 | # add library paths 8 | ENV LD_LIBRARY_PATH=/tmp/rizin-$rz_version/build/librz/core 9 | 10 | RUN apt-get update 11 | RUN apt-get install -y ninja-build parallel 12 | RUN pip install meson==1.0.0 13 | 14 | # add fwhunt_scan unprivileged user 15 | RUN useradd -u 1001 -m fwhunt_scan 16 | 17 | # install rizin from source code 18 | WORKDIR /tmp 19 | RUN wget https://github.com/rizinorg/rizin/releases/download/$rz_version/rizin-src-$rz_version.tar.xz 20 | RUN tar -xvf rizin-src-$rz_version.tar.xz 21 | 22 | WORKDIR /tmp/rizin-$rz_version 23 | RUN meson build 24 | RUN ninja -C build install 25 | 26 | # install fwhunt_scan 27 | COPY fwhunt_scan_analyzer.py /home/fwhunt_scan/app/ 28 | COPY requirements.txt /home/fwhunt_scan/app/ 29 | COPY fwhunt_scan /home/fwhunt_scan/app/fwhunt_scan 30 | 31 | WORKDIR /home/fwhunt_scan/app/ 32 | RUN pip install -r requirements.txt 33 | 34 | USER fwhunt_scan 35 | 36 | ENTRYPOINT ["python3", "fwhunt_scan_analyzer.py"] 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: fwhunt_scan 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-24.04 13 | permissions: 14 | contents: read 15 | 16 | strategy: 17 | matrix: 18 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Lint with flake8 27 | run: | 28 | pip install flake8 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | - name: Test with pytest 32 | run: | 33 | pip install -r requirements.txt 34 | pip install pytest 35 | pytest 36 | -------------------------------------------------------------------------------- /.github/workflows/docker-release.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: ["release"] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | VERSION: ${{ github.sha }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ secrets.ACTOR }} 28 | password: ${{ secrets.TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 38 | with: 39 | context: . 40 | push: true 41 | tags: ghcr.io/binarly-io/fwhunt-scan:${{ env.VERSION }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("requirements.txt") as f: 4 | REQUIRED = f.readlines() 5 | 6 | with open("README.md", "r") as f: 7 | README = f.read() 8 | 9 | setup( 10 | name="fwhunt_scan", 11 | version="2.3.8", 12 | author="FwHunt team", 13 | author_email="fwhunt@binarly.io", 14 | packages=["fwhunt_scan"], 15 | license="GPL-3.0", 16 | url="https://github.com/binarly-io/fwhunt-scan", 17 | install_requires=REQUIRED, 18 | description="Tools for analyzing UEFI firmware and checking UEFI modules with FwHunt rules", 19 | long_description=README, 20 | long_description_content_type="text/markdown", 21 | platforms=["Platform Independent"], 22 | classifiers=[ 23 | "Development Status :: 3 - Alpha", 24 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python :: 3", 27 | ], 28 | include_package_data=True, 29 | zip_safe=False, 30 | package_data={ 31 | "fwhunt_scan": [ 32 | "py.typed", 33 | "uefi_analyzer.pyi", 34 | "uefi_extractor.pyi", 35 | "uefi_protocols.pyi", 36 | "uefi_scanner.pyi", 37 | "uefi_smm.pyi", 38 | "uefi_tables.pyi", 39 | "uefi_te.pyi", 40 | "uefi_types.pyi", 41 | "uefi_utils.pyi", 42 | "__init__.pyi", 43 | ] 44 | }, 45 | scripts=["fwhunt_scan_analyzer.py", "fwhunt_scan_docker.py"], 46 | ) 47 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | import binascii 4 | from typing import Any, Dict, List, Optional 5 | 6 | import rzpipe 7 | 8 | from fwhunt_scan.uefi_protocols import UefiGuid 9 | 10 | 11 | def get_int(item: str) -> Optional[int]: 12 | res = None 13 | for base in [10, 16]: 14 | try: 15 | value = int(item, base) 16 | return value 17 | except ValueError: 18 | continue 19 | return res 20 | 21 | 22 | def get_xrefs_to_guids(rz: rzpipe.open, guids: List[UefiGuid]) -> List[int]: 23 | code_addrs = list() # xrefs from code 24 | for guid in guids: 25 | guid_bytes = binascii.hexlify(guid.bytes).decode() 26 | json_addrs = rz.cmdj(f"/xj {guid_bytes}") 27 | for element in json_addrs: 28 | if "offset" not in element: 29 | continue 30 | offset = element["offset"] 31 | 32 | # xrefs = rz.cmd(f"axtj @ {offset:x}") 33 | # this doesn't work in rizin, so needs to be split into two separate steps (seek + axtj) 34 | 35 | # seek to GUID location in .data segment 36 | rz.cmd(f"s {offset:#x}") 37 | 38 | # get xrefs 39 | xrefs = rz.cmdj("axtj") 40 | 41 | for xref in xrefs: 42 | if "from" not in xref: 43 | continue 44 | 45 | # get code address 46 | addr = xref["from"] 47 | code_addrs.append(addr) 48 | 49 | return code_addrs 50 | 51 | 52 | def get_xrefs_to_data(rz: rzpipe.open, addr: int) -> List[int]: 53 | code_addrs = list() # xrefs from code 54 | 55 | rz.cmd(f"s {addr:#x}") 56 | 57 | # get xrefs 58 | xrefs = rz.cmdj("axtj") 59 | 60 | for xref in xrefs: 61 | if "from" not in xref: 62 | continue 63 | 64 | # get code address 65 | addr = xref["from"] 66 | if addr not in code_addrs: 67 | code_addrs.append(addr) 68 | 69 | return code_addrs 70 | 71 | 72 | def get_current_insn_index( 73 | insns: Optional[List[Dict[str, Any]]], code_addr: int 74 | ) -> Optional[int]: 75 | 76 | if insns is None: 77 | return None 78 | 79 | current_insn = None 80 | 81 | for insn in insns: 82 | if insn.get("offset", None) == code_addr: 83 | current_insn = insn 84 | break 85 | 86 | if current_insn is None: 87 | return None 88 | 89 | return insns.index(current_insn) 90 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_te.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | import os 4 | import struct 5 | from typing import Optional 6 | 7 | 8 | class TerseExecutableError(Exception): 9 | """Generic TE format error exception.""" 10 | 11 | def __init__(self, value: str) -> None: 12 | self.value = value 13 | 14 | def __str__(self): 15 | return repr(self.value) 16 | 17 | 18 | class TerseExecutableParser: 19 | """Terse Executable header parser""" 20 | 21 | def __init__( 22 | self, image_path: Optional[str] = None, blob: Optional[bytes] = None 23 | ) -> None: 24 | 25 | self._data: bytes = bytes() 26 | 27 | if blob is not None: 28 | self._data = blob 29 | 30 | elif image_path is not None: 31 | 32 | # check file path 33 | if not os.path.join(image_path): 34 | raise TerseExecutableError("Wrong file path") 35 | 36 | self._data = bytes() 37 | with open(image_path, "rb") as f: 38 | self._data = f.read() 39 | 40 | self._signature: bytes = self._data[:2] 41 | if self._signature != b"VZ": 42 | raise TerseExecutableError("Wrong signature") 43 | 44 | self._parse() 45 | 46 | def _parse(self) -> None: 47 | format = "<2sHBBHIIQ" 48 | data = self._data[: struct.calcsize(format)] 49 | if len(data) != struct.calcsize(format): 50 | raise TerseExecutableError("Can't parse header, file is too small") 51 | ( 52 | self._signature, 53 | self._machine, 54 | self._number_of_sections, 55 | self._subsystem, 56 | self._stripped_size, 57 | self._address_of_entry_point, 58 | self._base_of_code, 59 | self._image_base, 60 | ) = struct.unpack(format, data) 61 | 62 | @property 63 | def signature(self) -> str: 64 | """Get Signature""" 65 | 66 | return self._signature.decode() 67 | 68 | @property 69 | def machine(self) -> int: 70 | """Get Machine""" 71 | 72 | return self._machine 73 | 74 | @property 75 | def number_of_sections(self) -> int: 76 | """Get NumberOfSections""" 77 | 78 | return self._number_of_sections 79 | 80 | @property 81 | def subsystem(self) -> int: 82 | """Get Subsystem""" 83 | 84 | return self._subsystem 85 | 86 | @property 87 | def stripped_size(self) -> int: 88 | """Get StrippedSize""" 89 | 90 | return self._stripped_size 91 | 92 | @property 93 | def address_of_entry_point(self) -> int: 94 | """Get AddressOfEntryPoint""" 95 | 96 | return self._address_of_entry_point 97 | 98 | @property 99 | def base_of_code(self) -> int: 100 | """Get BaseOfCode""" 101 | 102 | return self._base_of_code 103 | 104 | @property 105 | def image_base(self) -> int: 106 | """Get ImageBase""" 107 | 108 | return self._image_base 109 | 110 | def __str__(self): 111 | return "\n".join( 112 | [ 113 | f"Machine: {self.machine:#x}", 114 | f"NumberOfSections = {self.number_of_sections:#x}", 115 | f"Subsystem = {self.subsystem:#x}", 116 | f"StrippedSize = {self.stripped_size:#x}", 117 | f"AddressOfEntryPoint = {self.address_of_entry_point:#x}", 118 | f"BaseOfCode = {self.base_of_code:#x}", 119 | f"ImageBase = {self.image_base:#x}", 120 | ] 121 | ) 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0) 2 | [![fwhunt-scan CI](https://github.com/binarly-io/fwhunt-scan/actions/workflows/ci.yml/badge.svg)](https://github.com/binarly-io/fwhunt-scan/actions) 3 | [![fwhunt-scan pypi](https://img.shields.io/pypi/v/fwhunt-scan.svg)](https://pypi.org/project/fwhunt-scan) 4 | 5 |

6 | fwhunt Logo 7 |

8 | 9 | # FwHunt Community Scanner 10 | 11 | Tools for analyzing UEFI firmware and checking UEFI modules with [FwHunt rules](https://github.com/binarly-io/fwhunt). 12 | 13 | # Dependencies 14 | 15 | rizin (v0.6.2) 16 | 17 | # Installation 18 | 19 | Install with `pip` (tested on `python3.6` and above): 20 | 21 | ``` 22 | $ python -m pip install fwhunt-scan 23 | ``` 24 | 25 | Install manually: 26 | 27 | ``` 28 | $ git clone https://github.com/binarly-io/fwhunt-scan.git && cd fwhunt-scan 29 | $ python setup.py install 30 | ``` 31 | 32 | # Example 33 | 34 | ### With script 35 | 36 | Analyze/scan separate module: 37 | 38 | ``` 39 | $ python3 fwhunt_scan_analyzer.py analyze-module {image_path} -o out.json 40 | $ python3 fwhunt_scan_analyzer.py scan-module --rule {rule_path} {image_path} 41 | ``` 42 | 43 | Scan the entire firmware image: 44 | 45 | ``` 46 | $ python3 fwhunt_scan_analyzer.py scan-firmware -r rules/BRLY-2021-001.yml -r rules/BRLY-2021-004.yml -r rules/RsbStuffingCheck.yml test/fw.bin 47 | ``` 48 | 49 | ### With docker 50 | 51 | To avoid installing dependencies, you can use the docker image. 52 | 53 | You can build a docker image locally as follows: 54 | 55 | ``` 56 | docker build -t fwhunt_scan . 57 | ``` 58 | 59 | Or pull the latest image from [ghcr](https://github.com/binarly-io/fwhunt-scan/pkgs/container/fwhunt-scan). 60 | 61 | Example of use: 62 | 63 | ``` 64 | docker run --rm -it -v {module_path}:/tmp/image:ro \ 65 | fwhunt_scan analyze-module /tmp/image # to analyze EFI module 66 | 67 | docker run --rm -it -v {module_path}:/tmp/image:ro -v {rule_path}:/tmp/rule.yml:ro \ 68 | fwhunt_scan scan-module /tmp/image -r /tmp/rule.yml # to scan EFI module with specified FwHunt rule 69 | 70 | docker run --rm -it -v {module_path}:/tmp/image:ro -v {rule_path}:/tmp/rule.yml:ro \ 71 | fwhunt_scan scan-firmware /tmp/image -r /tmp/rule.yml # to scan firmware image with specified FwHunt rule 72 | 73 | docker run --rm -it -v {module_path}:/tmp/image:ro -v {rules_directory}:/tmp/rules:ro \ 74 | fwhunt_scan scan-firmware /tmp/image --rules_dir /tmp/rules # to scan firmware image with specified rules directory 75 | ``` 76 | 77 | All these steps are automated in the `fwhunt_scan_docker.py` script: 78 | 79 | ``` 80 | python3 fwhunt_scan_docker.py analyze-module {module_path} # to analyze EFI module 81 | 82 | python3 fwhunt_scan_docker.py scan-module -r {rule_path} {module_path} # to scan EFI module with specified FwHunt rule 83 | 84 | python3 fwhunt_scan_docker.py scan-firmware -r {rule_path} {firmware_path} # to scan firmware image with specified FwHunt rule 85 | 86 | python3 fwhunt_scan_docker.py scan-firmware --rules_dir {rules_directory} {firmware_path} # to scan firmware image with specified rules directory 87 | ``` 88 | 89 | ### From code 90 | 91 | #### UefiAnalyzer 92 | 93 | Basic usage examples: 94 | 95 | ```python 96 | from fwhunt_scan import UefiAnalyzer 97 | 98 | ... 99 | uefi_analyzer = UefiAnalyzer(image_path=module_path) 100 | print(uefi_analyzer.get_summary()) 101 | uefi_analyzer.close() 102 | ``` 103 | 104 | ```python 105 | from fwhunt_scan import UefiAnalyzer 106 | 107 | ... 108 | with UefiAnalyzer(image_path=module_path) as uefi_analyzer: 109 | print(uefi_analyzer.get_summary()) 110 | ``` 111 | 112 | On Linux platforms, you can pass blob for analysis instead of file: 113 | 114 | ```python 115 | from fwhunt_scan import UefiAnalyzer 116 | 117 | ... 118 | with UefiAnalyzer(blob=data) as uefi_analyzer: 119 | print(uefi_analyzer.get_summary()) 120 | ``` 121 | 122 | #### UefiScanner 123 | 124 | ```python 125 | from fwhunt_scan import UefiAnalyzer, UefiRule, UefiScanner 126 | 127 | ... 128 | uefi_analyzer = UefiAnalyzer(module_path) 129 | 130 | # rule1 and rule2 - contents of the rules on YAML format 131 | uefi_rules = [UefiRule(rule1), UefiRule(rule2)] 132 | 133 | scanner = UefiScanner(uefi_analyzer, uefi_rules) 134 | result = scanner.result 135 | ``` 136 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_types.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | import uuid 4 | from enum import Enum 5 | from typing import Optional 6 | 7 | 8 | class SmiKind(Enum): 9 | CHILD_SW_SMI = 0 10 | SW_SMI = 1 11 | USB_SMI = 2 12 | SX_SMI = 3 13 | IO_TRAP_SMI = 4 14 | GPI_SMI = 5 15 | TCO_SMI = 6 16 | STANDBY_BUTTON_SMI = 7 17 | PERIODIC_TIMER_SMI = 8 18 | POWER_BUTTON_SMI = 9 19 | ICHN_SMI = 10 20 | PCH_TCO_SMI = 11 21 | PCH_PCIE_SMI = 12 22 | PCH_ACPI_SMI = 13 23 | PCH_GPIO_UNLOCK_SMI = 14 24 | PCH_SMI = 15 25 | PCH_ESPI_SMI = 16 26 | ACPI_EN_SMI = 17 27 | ACPI_DIS_SMI = 18 28 | 29 | 30 | class UefiService: 31 | """A UEFI service""" 32 | 33 | def __init__(self, name: str, address: int) -> None: 34 | self.name: str = name 35 | self.address: int = address 36 | 37 | @property 38 | def __dict__(self): 39 | val = dict() 40 | if self.name: 41 | val["name"] = self.name 42 | if self.address: 43 | val["address"] = self.address 44 | return val 45 | 46 | 47 | class UefiGuid: 48 | """A UEFI GUID""" 49 | 50 | def __init__(self, value: str, name: str) -> None: 51 | self.value: str = value 52 | self.name: str = name 53 | self._bytes: bytes = b"" 54 | 55 | @property 56 | def bytes(self) -> bytes: 57 | """Convert guid structure to array of bytes""" 58 | if not self._bytes: 59 | self._bytes = uuid.UUID(self.value).bytes_le 60 | return self._bytes 61 | 62 | @property 63 | def __dict__(self): 64 | return {"value": self.value, "name": self.name} 65 | 66 | def __str__(self): 67 | return "{} ({})".format(self.value, self.name) 68 | 69 | 70 | class UefiProtocol(UefiGuid): 71 | """A UEFI protocol""" 72 | 73 | def __init__( 74 | self, name: str, address: int, value: str, guid_address: int, service: str 75 | ) -> None: 76 | super().__init__(name=name, value=value) 77 | self.address: int = address 78 | self.guid_address: int = guid_address 79 | self.service: str = service 80 | 81 | @property 82 | def __dict__(self): 83 | val = super().__dict__ 84 | if self.address: 85 | val["address"] = self.address 86 | if self.guid_address: 87 | val["guid_address"] = self.guid_address 88 | if self.service: 89 | val["service"] = self.service 90 | return val 91 | 92 | 93 | class UefiProtocolGuid(UefiGuid): 94 | """A UEFI protocol GUID""" 95 | 96 | def __init__(self, name: str, address: int, value: str) -> None: 97 | super().__init__(name=name, value=value) 98 | self.address: int = address 99 | 100 | @property 101 | def __dict__(self): 102 | val = super().__dict__ 103 | if self.address: 104 | val["address"] = self.address 105 | return val 106 | 107 | 108 | class NvramVariable: 109 | """A UEFI NVRAM variable""" 110 | 111 | def __init__(self, name: str, guid: str, service: UefiService) -> None: 112 | self.name: str = name 113 | self.guid: str = guid 114 | self.service: UefiService = service 115 | 116 | @property 117 | def __dict__(self): 118 | val = dict() 119 | if self.name: 120 | val["name"] = self.name 121 | if self.guid: 122 | val["guid"] = self.guid 123 | if self.service: 124 | val["service"] = { 125 | "name": self.service.name, 126 | "address": self.service.address, 127 | } 128 | return val 129 | 130 | 131 | class SmiHandler: 132 | """SMI handler basic class""" 133 | 134 | def __init__(self, address: int, kind: SmiKind) -> None: 135 | self.address = address 136 | self.kind = kind 137 | self._place: Optional[str] = None 138 | 139 | def _get_place(self): 140 | return f"{self.kind.name.lower()}_handlers" 141 | 142 | @property 143 | def place(self): 144 | if self._place is None: 145 | self._place = self._get_place() 146 | return self._place 147 | 148 | @property 149 | def __dict__(self): 150 | val = dict() 151 | if self.address: 152 | val["address"] = self.address 153 | if self.kind: 154 | val["kind"] = self.kind.name 155 | return val 156 | 157 | 158 | class ChildSwSmiHandler(SmiHandler): 159 | """Child software SMI handler""" 160 | 161 | def __init__(self, handler_guid: Optional[str], address: int) -> None: 162 | super().__init__(address=address, kind=SmiKind.CHILD_SW_SMI) 163 | self.handler_guid = handler_guid 164 | 165 | @property 166 | def __dict__(self): 167 | val = dict() 168 | if self.address: 169 | val["address"] = self.address 170 | if self.handler_guid: 171 | val["handler_guid"] = self.handler_guid 172 | if self.kind: 173 | val["kind"] = self.kind.name 174 | return val 175 | -------------------------------------------------------------------------------- /fwhunt_scan_docker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # SPDX-License-Identifier: GPL-3.0+ 4 | 5 | import logging 6 | import os 7 | from typing import List 8 | 9 | import click 10 | 11 | logging.basicConfig( 12 | format="%(name)s %(asctime)s %(levelname)s: %(message)s", 13 | datefmt="%m/%d/%Y %I:%M:%S %p", 14 | level=os.environ.get("PYTHON_LOG", "INFO"), 15 | ) 16 | logger = logging.getLogger("fwhunt_scan") 17 | 18 | TAG = "fwhunt_scan" 19 | 20 | 21 | @click.group() 22 | def cli(): 23 | pass 24 | 25 | 26 | @click.command() 27 | def build(): 28 | """Build docker image.""" 29 | 30 | os.system(" ".join(["docker", "build", "-t", TAG, "."])) 31 | 32 | 33 | @click.command() 34 | @click.argument("path") 35 | def analyze(path: str) -> bool: 36 | """Analyze single EFI file.""" 37 | 38 | if not os.path.isfile(path): 39 | click.echo( 40 | "{} check module path".format(click.style("ERROR", fg="red", bold=True)) 41 | ) 42 | return False 43 | 44 | fpath = os.path.realpath(path) 45 | 46 | cmdstr = " ".join( 47 | [ 48 | "docker", 49 | "run", 50 | "--rm", 51 | "-it", 52 | "-v", 53 | f"{fpath}:/tmp/module:ro", 54 | TAG, 55 | "analyze", 56 | "/tmp/module", 57 | ] 58 | ) 59 | 60 | logger.debug(f"Command: {cmdstr}") 61 | 62 | os.system(cmdstr) 63 | 64 | return True 65 | 66 | 67 | @click.command() 68 | @click.argument("path") 69 | @click.option("-r", "--rule", help="The path to the rule.", multiple=True) 70 | def scan(path: str, rule: List[str]) -> bool: 71 | """Scan singe EFI file.""" 72 | 73 | rules = rule 74 | 75 | if not os.path.isfile(path): 76 | click.echo( 77 | "{} check module path".format(click.style("ERROR", fg="red", bold=True)) 78 | ) 79 | return False 80 | if not all(rule and os.path.isfile(rule) for rule in rules): 81 | click.echo( 82 | "{} check rule(s) path".format(click.style("ERROR", fg="red", bold=True)) 83 | ) 84 | return False 85 | 86 | cmd = [ 87 | "docker", 88 | "run", 89 | "--rm", 90 | "-it", 91 | "-v", 92 | f"{os.path.realpath(path)}:/tmp/module:ro", 93 | ] 94 | rules_cmd = [TAG, "scan", "/tmp/module"] 95 | for r in rules: 96 | _, name = os.path.split(r) 97 | cmd += ["-v", f"{os.path.realpath(r)}:/tmp/{name}:ro"] 98 | rules_cmd += ["-r", f"/tmp/{name}"] 99 | 100 | cmd += rules_cmd 101 | cmdstr = " ".join(cmd) 102 | 103 | logger.debug(f"Command: {cmdstr}") 104 | 105 | os.system(cmdstr) 106 | 107 | return True 108 | 109 | 110 | @click.command() 111 | @click.argument("image_path") 112 | @click.option("-r", "--rule", help="The path to the rule.", multiple=True) 113 | @click.option("-d", "--rules_dir", help="The path to the rules directory.") 114 | @click.option( 115 | "-f", 116 | "--force", 117 | help="Enforcing the use of rules without specified volume guids.", 118 | is_flag=True, 119 | ) 120 | def scan_firmware( 121 | image_path: str, rule: List[str], rules_dir: str, force: bool 122 | ) -> bool: 123 | """Scan UEFI firmware image.""" 124 | 125 | rules = rule 126 | 127 | if not os.path.isfile(image_path): 128 | click.echo( 129 | "{} check image path".format(click.style("ERROR", fg="red", bold=True)) 130 | ) 131 | return False 132 | if not (rules_dir or rule): 133 | click.echo("{} check command".format(click.style("ERROR", fg="red", bold=True))) 134 | return False 135 | if rules_dir and not os.path.isdir(rules_dir): 136 | click.echo( 137 | "{} check rules directory path".format( 138 | click.style("ERROR", fg="red", bold=True) 139 | ) 140 | ) 141 | return False 142 | if rule and not all(rule and os.path.isfile(rule) for rule in rules): 143 | click.echo( 144 | "{} check rule(s) path".format(click.style("ERROR", fg="red", bold=True)) 145 | ) 146 | return False 147 | 148 | cmd = [ 149 | "docker", 150 | "run", 151 | "--rm", 152 | "-it", 153 | "-v", 154 | f"{os.path.realpath(image_path)}:/tmp/image:ro", 155 | ] 156 | rules_cmd = [TAG, "scan-firmware", "/tmp/image"] 157 | 158 | if force: 159 | rules_cmd += ["-f"] 160 | 161 | for r in rules: 162 | _, name = os.path.split(r) 163 | cmd += ["-v", f"{os.path.realpath(r)}:/tmp/{name}:ro"] 164 | rules_cmd += ["-r", f"/tmp/{name}"] 165 | 166 | if rules_dir: 167 | cmd += ["-v", f"{os.path.realpath(rules_dir)}:/tmp/rules:ro"] 168 | rules_cmd += ["-d", f"/tmp/rules"] 169 | 170 | cmd += rules_cmd 171 | cmdstr = " ".join(cmd) 172 | 173 | logger.debug(f"Command: {cmdstr}") 174 | 175 | os.system(cmdstr) 176 | 177 | return True 178 | 179 | 180 | cli.add_command(build) 181 | cli.add_command(analyze) 182 | cli.add_command(analyze, "analyze-module") 183 | cli.add_command(analyze, "analyze-bootloader") 184 | cli.add_command(scan) 185 | cli.add_command(scan, "scan-module") 186 | cli.add_command(scan, "scan-bootloader") 187 | cli.add_command(scan_firmware) 188 | 189 | if __name__ == "__main__": 190 | cli() 191 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_tables.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | # 3 | # pylint: disable=consider-using-dict-comprehension 4 | 5 | EFI_BOOT_SERVICES_64_BIT = { 6 | 0x00000018: "RaiseTPL", 7 | 0x00000020: "RestoreTPL", 8 | 0x00000028: "AllocatePages", 9 | 0x00000030: "FreePages", 10 | 0x00000038: "GetMemoryMap", 11 | 0x00000040: "AllocatePool", 12 | 0x00000048: "FreePool", 13 | 0x00000050: "CreateEvent", 14 | 0x00000058: "SetTimer", 15 | 0x00000060: "WaitForEvent", 16 | 0x00000068: "SignalEvent", 17 | 0x00000070: "CloseEvent", 18 | 0x00000078: "CheckEvent", 19 | 0x00000080: "InstallProtocolInterface", 20 | 0x00000088: "ReinstallProtocolInterface", 21 | 0x00000090: "UninstallProtocolInterface", 22 | 0x00000098: "HandleProtocol", 23 | 0x000000A0: "Reserved", 24 | 0x000000A8: "RegisterProtocolNotify", 25 | 0x000000B0: "LocateHandle", 26 | 0x000000B8: "LocateDevicePath", 27 | 0x000000C0: "InstallConfigurationTable", 28 | 0x000000C8: "LoadImage", 29 | 0x000000D0: "StartImage", 30 | 0x000000D8: "Exit", 31 | 0x000000E0: "UnloadImage", 32 | 0x000000E8: "ExitBootServices", 33 | 0x000000F0: "GetNextMonotonicCount", 34 | 0x000000F8: "Stall", 35 | 0x00000100: "SetWatchdogTimer", 36 | 0x00000108: "ConnectController", 37 | 0x00000110: "DisconnectController", 38 | 0x00000118: "OpenProtocol", 39 | 0x00000120: "CloseProtocol", 40 | 0x00000128: "OpenProtocolInformation", 41 | 0x00000130: "ProtocolsPerHandle", 42 | 0x00000138: "LocateHandleBuffer", 43 | 0x00000140: "LocateProtocol", 44 | 0x00000148: "InstallMultipleProtocolInterfaces", 45 | 0x00000150: "UninstallMultipleProtocolInterfaces", 46 | 0x00000158: "CalculateCrc32", 47 | 0x00000160: "CopyMem", 48 | 0x00000168: "SetMem", 49 | 0x00000170: "CreateEventEx", 50 | } 51 | 52 | EFI_RUNTIME_SERVICES_64_BIT = { 53 | 0x00000018: "GetTime", 54 | 0x00000020: "SetTime", 55 | 0x00000028: "GetWakeupTime", 56 | 0x00000030: "SetWakeupTime", 57 | 0x00000038: "SetVirtualAddressMap", 58 | 0x00000040: "ConvertPointer", 59 | 0x00000048: "GetVariable", 60 | 0x00000050: "GetNextVariableName", 61 | 0x00000058: "SetVariable", 62 | 0x00000060: "GetNextHighMonotonicCount", 63 | 0x00000068: "ResetSystem", 64 | 0x00000070: "UpdateCapsule", 65 | 0x00000078: "QueryCapsuleCapabilities", 66 | 0x00000080: "QueryVariableInfo", 67 | } 68 | 69 | EFI_SMM_SYSTEM_TABLE2_64_BIT = { 70 | 0x00000028: "SmmInstallConfigurationTable", 71 | 0x00000030: "SmmIo", 72 | 0x00000050: "SmmAllocatePool", 73 | 0x00000058: "SmmFreePool", 74 | 0x00000060: "SmmAllocatePages", 75 | 0x00000068: "SmmFreePages", 76 | 0x00000070: "SmmStartupThisAp", 77 | 0x00000078: "CurrentlyExecutingCpu", 78 | 0x00000080: "NumberOfCpus", 79 | 0x00000088: "CpuSaveStateSize", 80 | 0x00000090: "CpuSaveState", 81 | 0x00000098: "NumberOfTableEntries", 82 | 0x000000A0: "SmmConfigurationTable", 83 | 0x000000A8: "SmmInstallProtocolInterface", 84 | 0x000000B0: "SmmUninstallProtocolInterface", 85 | 0x000000B8: "SmmHandleProtocol", 86 | 0x000000C0: "SmmRegisterProtocolNotify", 87 | 0x000000C8: "SmmLocateHandle", 88 | 0x000000D0: "SmmLocateProtocol", 89 | 0x000000D8: "SmiManage", 90 | 0x000000E0: "SmiHandlerRegister", 91 | 0x000000E8: "SmiHandlerUnRegister", 92 | } 93 | 94 | BS_PROTOCOLS = [ 95 | "InstallProtocolInterface", 96 | "ReinstallProtocolInterface", 97 | "UninstallProtocolInterface", 98 | "HandleProtocol", 99 | "RegisterProtocolNotify", 100 | "OpenProtocol", 101 | "CloseProtocol", 102 | "OpenProtocolInformation", 103 | "ProtocolsPerHandle", 104 | "LocateHandleBuffer", 105 | "LocateProtocol", 106 | "InstallMultipleProtocolInterfaces", 107 | "UninstallMultipleProtocolInterfaces", 108 | ] 109 | 110 | BS_PROTOCOLS_INFO_64_BIT = { 111 | "InstallProtocolInterface": {"offset": 0x80, "reg": "rdx"}, 112 | "ReinstallProtocolInterface": {"offset": 0x88, "reg": "rdx"}, 113 | "UninstallProtocolInterface": {"offset": 0x90, "reg": "rdx"}, 114 | "HandleProtocol": {"offset": 0x98, "reg": "rdx"}, 115 | "RegisterProtocolNotify": {"offset": 0xA8, "reg": "rcx"}, 116 | "OpenProtocol": {"offset": 0x118, "reg": "rdx"}, 117 | "CloseProtocol": {"offset": 0x120, "reg": "rdx"}, 118 | "OpenProtocolInformation": {"offset": 0x128, "reg": "rdx"}, 119 | "ProtocolsPerHandle": {"offset": 0x130, "reg": "rdx"}, 120 | "LocateHandleBuffer": {"offset": 0x138, "reg": "rdx"}, 121 | "LocateProtocol": {"offset": 0x140, "reg": "rcx"}, 122 | "InstallMultipleProtocolInterfaces": {"offset": 0x148, "reg": "rdx"}, 123 | "UninstallMultipleProtocolInterfaces": {"offset": 0x150, "reg": "rdx"}, 124 | } 125 | 126 | OFFSET_TO_SERVICE = dict( 127 | [(BS_PROTOCOLS_INFO_64_BIT[s]["offset"], s) for s in BS_PROTOCOLS_INFO_64_BIT] 128 | ) 129 | 130 | EFI_PEI_SERVICES_32_BIT = { 131 | 0x00000018: {"name": "InstallPpi", "arg_num": 2}, 132 | 0x0000001C: {"name": "ReInstallPpi", "arg_num": 3}, 133 | 0x00000020: {"name": "LocatePpi", "arg_num": 5}, 134 | 0x00000024: {"name": "NotifyPpi", "arg_num": 2}, 135 | 0x00000028: {"name": "GetBootMode", "arg_num": 2}, 136 | 0x0000002C: {"name": "SetBootMode", "arg_num": 2}, 137 | 0x00000030: {"name": "GetHobList", "arg_num": 2}, 138 | 0x00000034: {"name": "CreateHob", "arg_num": 4}, 139 | 0x00000038: {"name": "FfsFindNextVolume", "arg_num": 3}, 140 | 0x0000003C: {"name": "FfsFindNextFile", "arg_num": 4}, 141 | 0x00000040: {"name": "FfsFindSectionData", "arg_num": 4}, 142 | 0x00000044: {"name": "InstallPeiMemory", "arg_num": 3}, 143 | 0x00000048: {"name": "AllocatePages", "arg_num": 4}, 144 | 0x0000004C: {"name": "AllocatePool", "arg_num": 3}, 145 | 0x00000050: {"name": "CopyMem", "arg_num": 3}, 146 | 0x00000054: {"name": "SetMem", "arg_num": 3}, 147 | 0x00000058: {"name": "ReportStatusCode", "arg_num": 6}, 148 | 0x0000005C: {"name": "ResetSystem", "arg_num": 1}, 149 | 0x00000060: {"name": "CpuIo", "arg_num": 1}, 150 | 0x00000064: {"name": "PciCfg", "arg_num": 1}, 151 | } 152 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_extractor.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | from typing import Any, Dict, List, Optional 4 | 5 | import uefi_firmware 6 | 7 | 8 | class UefiBinary: 9 | KINDS = [ 10 | ".sec", 11 | ".pei.core", 12 | ".dxe.core", 13 | ".peim", 14 | ".dxe", 15 | ".peim.dxe", 16 | ".app", 17 | ".smm", 18 | ".smm.dxe", 19 | ".smm.core", 20 | ] 21 | 22 | def __init__( 23 | self, 24 | content: Optional[bytes], 25 | name: Optional[str], 26 | guid: str, 27 | ext: Optional[str], 28 | ) -> None: 29 | self.guid: str = guid.lower() 30 | self._content: Optional[bytes] = content 31 | self._name: Optional[str] = name 32 | self._ext: Optional[str] = ext 33 | 34 | @property 35 | def content(self) -> bytes: 36 | if self._content is None: 37 | self._content = bytes() 38 | return self._content 39 | 40 | @property 41 | def name(self) -> str: 42 | if self._name is None: 43 | self._name = self.guid 44 | return self._name 45 | 46 | @property 47 | def ext(self) -> str: 48 | if self._ext is None: 49 | self._ext = ".bin" 50 | return self._ext 51 | 52 | @property 53 | def is_ok(self) -> bool: 54 | return self.guid and len(self.content) and self.ext in UefiBinary.KINDS 55 | 56 | 57 | class UefiExtractor: 58 | FILE_TYPES = { 59 | 0x01: ("raw", "raw", "RAW"), 60 | 0x02: ("freeform", "freeform", "FREEFORM"), 61 | 0x03: ("security core", "sec", "SEC"), 62 | 0x04: ("pei core", "pei.core", "PEI_CORE"), 63 | 0x05: ("dxe core", "dxe.core", "DXE_CORE"), 64 | 0x06: ("pei module", "peim", "PEIM"), 65 | 0x07: ("driver", "dxe", "DRIVER"), 66 | 0x08: ("combined pei module/driver", "peim.dxe", "COMBO_PEIM_DRIVER"), 67 | 0x09: ("application", "app", "APPLICATION"), 68 | 0x0A: ("system management", "smm", "SMM"), 69 | 0x0C: ("combined smm/driver", "smm.dxe", "COMBO_SMM_DRIVER"), 70 | 0x0D: ("smm core", "smm.core", "SMM_CORE"), 71 | } 72 | SECTION_TYPES = { 73 | 0x10: ("PE32 image", "pe", "PE32"), 74 | 0x12: ("Terse executable (TE)", "te", "TE"), 75 | } 76 | UI = {0x15: ("User interface name", "ui", "UI")} 77 | 78 | def __init__(self, firmware_data: bytes, file_guids: List[str]): 79 | offset = firmware_data.find(b"_FVH") 80 | if offset >= 0: 81 | firmware_data = firmware_data[offset - 40 :] 82 | self._firmware_data: bytes = firmware_data 83 | self._file_guids: List[str] = [g.lower() for g in file_guids] 84 | self._parsers: List[uefi_firmware.AutoParser] = list() 85 | self._info: Dict[str, Any] = dict() 86 | self.binaries: List[UefiBinary] = list() 87 | 88 | def _compressed_search(self, object: Any, root_guid: str) -> None: 89 | if object is None: 90 | return 91 | 92 | for component in object.iterate_objects(): 93 | attrs = component.get("attrs", None) 94 | if attrs is not None: 95 | section_type = attrs.get("type", None) 96 | if section_type in UefiExtractor.UI: 97 | self._info[root_guid]["name"] = component["label"] 98 | if section_type in UefiExtractor.SECTION_TYPES: 99 | self._info[root_guid]["content"] = component["_self"].content 100 | self._compressed_search(component["_self"], root_guid) 101 | 102 | def _compressed_handle(self, object: Any, root_guid: str) -> None: 103 | if object is None: 104 | return 105 | 106 | for obj in object.iterate_objects(): 107 | if ( 108 | obj.get("attrs", None) is not None 109 | and obj["attrs"].get("attrs", None) == 0x01 110 | ): # if compressed 111 | self._compressed_search(obj["_self"], root_guid) 112 | 113 | def _append_binaries(self, object: Any) -> None: 114 | if object is None: 115 | return 116 | 117 | for component in object.iterate_objects(): 118 | guid = component.get("guid", None) 119 | attrs = component.get("attrs", None) 120 | if guid is not None and attrs is not None: 121 | if guid not in self._info: 122 | self._info[guid] = {"name": None, "ext": None, "content": None} 123 | section_type = attrs.get("type", None) 124 | if section_type in UefiExtractor.UI: 125 | self._info[guid]["name"] = component["label"] 126 | if section_type in UefiExtractor.FILE_TYPES: 127 | if self._info[guid]["ext"] is None: 128 | ext = UefiExtractor.FILE_TYPES[section_type][1] 129 | self._info[guid]["ext"] = f".{ext}" 130 | self._compressed_handle(component["_self"], guid) 131 | if section_type in UefiExtractor.SECTION_TYPES: 132 | self._info[guid]["content"] = component["_self"].content 133 | self._append_binaries(component["_self"]) 134 | 135 | def _extract(self) -> bool: 136 | potencial_volumes = uefi_firmware.search_firmware_volumes(self._firmware_data) 137 | for offset in potencial_volumes: 138 | parser = uefi_firmware.AutoParser(self._firmware_data[offset - 40 :]) 139 | if parser is None or parser.type() == "unknown": 140 | continue 141 | self._parsers.append(parser) 142 | 143 | if not len(self._parsers): 144 | return False 145 | 146 | for parser in self._parsers: 147 | firmware = parser.parse() 148 | self._append_binaries(firmware) 149 | 150 | return True 151 | 152 | def extract_all(self, ignore_guid: bool = False) -> None: 153 | with open(os.devnull, "w") as devnull: 154 | with contextlib.redirect_stderr(devnull): 155 | self._extract() 156 | for guid in self._info: 157 | if ignore_guid or ( 158 | self._info[guid]["content"] is not None 159 | and (guid in self._file_guids) 160 | ): 161 | self.binaries.append( 162 | UefiBinary( 163 | content=self._info[guid]["content"], 164 | name=self._info[guid]["name"], 165 | guid=guid, 166 | ext=self._info[guid]["ext"], 167 | ) 168 | ) 169 | -------------------------------------------------------------------------------- /fwhunt_scan_analyzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # SPDX-License-Identifier: GPL-3.0+ 4 | # 5 | # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring 6 | 7 | # fwhunt_scan: tools for analyzing UEFI firmware and checking UEFI modules with FwHunt rules 8 | 9 | import json 10 | import logging 11 | import os 12 | import pathlib 13 | import tempfile 14 | from typing import Dict, List 15 | 16 | import click 17 | 18 | from fwhunt_scan import UefiAnalyzer, UefiExtractor, UefiRule, UefiScanner 19 | 20 | logging.basicConfig( 21 | format="%(name)s %(asctime)s %(levelname)s: %(message)s", 22 | datefmt="%m/%d/%Y %I:%M:%S %p", 23 | ) 24 | logger = logging.getLogger("fwhunt_scan") 25 | 26 | 27 | @click.group() 28 | def cli(): 29 | pass 30 | 31 | 32 | @click.command() 33 | @click.argument("path") 34 | @click.option("-o", "--out", help="Output JSON file.") 35 | def analyze(path: str, out: str) -> bool: 36 | """Analyze single EFI file.""" 37 | 38 | if not os.path.isfile(path): 39 | click.echo( 40 | "{} check module path".format(click.style("ERROR", fg="red", bold=True)) 41 | ) 42 | return False 43 | 44 | # on linux platforms you can pass blob via shm:// 45 | # uefi_analyzer = UefiAnalyzer(blob=data) 46 | 47 | summary = None 48 | with UefiAnalyzer(image_path=path) as uefi_analyzer: 49 | summary = uefi_analyzer.get_summary() 50 | 51 | if out: 52 | with open(out, "w") as f: 53 | json.dump(summary, f, indent=4) 54 | else: 55 | click.echo(json.dumps(summary, indent=4)) 56 | 57 | return True 58 | 59 | 60 | @click.command() 61 | @click.argument("path") 62 | @click.option("-r", "--rule", help="The path to the rule.", multiple=True) 63 | def scan(path: str, rule: List[str]) -> bool: 64 | """Scan single EFI file.""" 65 | 66 | rules = rule 67 | 68 | if not os.path.isfile(path): 69 | click.echo( 70 | "{} check module path".format(click.style("ERROR", fg="red", bold=True)) 71 | ) 72 | return False 73 | if not all(rule and os.path.isfile(rule) for rule in rules): 74 | click.echo( 75 | "{} check rule(s) path".format(click.style("ERROR", fg="red", bold=True)) 76 | ) 77 | return False 78 | 79 | # on linux platforms you can pass blob via shm:// 80 | # uefi_analyzer = UefiAnalyzer(blob=data) 81 | 82 | uefi_analyzer = UefiAnalyzer(image_path=path) 83 | 84 | uefi_rules: List[UefiRule] = list() 85 | 86 | for r in rules: 87 | with open(r, "r") as f: 88 | rule_content = f.read() 89 | uefi_rule = UefiRule(rule_content=rule_content) 90 | uefi_rules.append(uefi_rule) 91 | 92 | scanner = UefiScanner(uefi_analyzer, uefi_rules) 93 | prefix = click.style("Scanner result", fg="green") 94 | 95 | no_threat = click.style("No threat detected", fg="green") 96 | threat = click.style( 97 | "FwHunt rule has been triggered and threat detected!", fg="red" 98 | ) 99 | 100 | for result in scanner.results: 101 | msg = threat if result.res else no_threat 102 | click.echo( 103 | f"{prefix} {result.rule.name} (variant: {result.variant_label}) {msg} ({path})" 104 | ) 105 | 106 | uefi_analyzer.close() 107 | 108 | return True 109 | 110 | 111 | @click.command() 112 | @click.argument("image_path") 113 | @click.option("-r", "--rule", help="The path to the rule.", multiple=True) 114 | @click.option("-d", "--rules_dir", help="The path to the rules directory.") 115 | @click.option( 116 | "-f", 117 | "--force", 118 | help="Enforcing the use of rules without specified volume guids.", 119 | is_flag=True, 120 | ) 121 | def scan_firmware( 122 | image_path: str, rule: List[str], rules_dir: str, force: bool 123 | ) -> bool: 124 | """Scan UEFI firmware image.""" 125 | 126 | rules = list(rule) 127 | error_prefix = click.style("ERROR", fg="red", bold=True) 128 | if not rules_dir: 129 | if not all(rules and os.path.isfile(rule) for rule in rules): 130 | click.echo(f"{error_prefix} check rule(s) path") 131 | return False 132 | else: 133 | rules += list(map(str, pathlib.Path(rules_dir).rglob("*.yml"))) 134 | 135 | if not os.path.isfile(image_path): 136 | click.echo(f"{error_prefix} check image path") 137 | return False 138 | 139 | prefix = click.style("Scanner result", fg="green") 140 | no_threat = click.style("No threat detected", fg="green") 141 | threat = click.style( 142 | "FwHunt rule has been triggered and threat detected!", fg="red" 143 | ) 144 | 145 | # on linux platforms you can pass blob via shm:// 146 | # uefi_analyzer = UefiAnalyzer(blob=data) 147 | 148 | uefi_rules: List[UefiRule] = list() 149 | 150 | for r in rules: 151 | with open(r, "r") as f: 152 | uefi_rules.append(UefiRule(rule_content=f.read())) 153 | 154 | # Select the rules with `target: firmware` 155 | uefi_rules_fw: List[UefiRule] = list( 156 | filter(lambda rule: rule.target == "firmware", uefi_rules) 157 | ) 158 | if len(uefi_rules_fw): 159 | with UefiAnalyzer(image_path=image_path) as uefi_analyzer_fw: 160 | scanner_fw = UefiScanner(uefi_analyzer_fw, uefi_rules_fw) 161 | for result in scanner_fw.results: 162 | msg = threat if result.res else no_threat 163 | click.echo( 164 | f"{prefix} {result.rule.name} (variant: {result.variant_label}) {msg}" 165 | ) 166 | 167 | # Group rules by guids 168 | rules_guids: Dict[str, List[UefiRule]] = dict() 169 | rules_universal: List[UefiRule] = list() 170 | for uefi_rule in set(uefi_rules) - set(uefi_rules_fw): 171 | if uefi_rule.target not in (None, "module"): 172 | logger.debug( 173 | f"The rule {uefi_rule.name} incompatible with scan-firmware command (target: {uefi_rule.target})" 174 | ) 175 | continue 176 | if not len(uefi_rule.volume_guids) and not force: 177 | logger.warning( 178 | f"Specify volume_guids in {uefi_rule.name} or run command with --force flag" 179 | ) 180 | continue 181 | elif not len(uefi_rule.volume_guids): 182 | rules_universal.append(uefi_rule) 183 | for guid in [g.lower() for g in uefi_rule.volume_guids]: 184 | lower_guid = guid.lower() 185 | if lower_guid not in rules_guids: 186 | rules_guids[lower_guid] = list() 187 | rules_guids[lower_guid].append(uefi_rule) 188 | 189 | if not rules_guids.keys() and not force: 190 | click.echo( 191 | f"{error_prefix} None of the rules specify volume_guids (use scan-module command)" 192 | ) 193 | return False 194 | 195 | with open(image_path, "rb") as f: 196 | firmware_data = f.read() 197 | 198 | extractor = UefiExtractor( 199 | firmware_data, list(rules_guids.keys()) if not force else list() 200 | ) 201 | extractor.extract_all(ignore_guid=force) 202 | 203 | if not len(extractor.binaries): 204 | click.echo("No modules were found for scanning") 205 | return False 206 | 207 | for binary in extractor.binaries: 208 | if not binary.is_ok: 209 | continue 210 | 211 | rules_scan = rules_universal + ( 212 | rules_guids[binary.guid] if binary.guid in rules_guids else list() 213 | ) 214 | 215 | if not len(rules_scan): 216 | continue 217 | 218 | fpath = os.path.join(tempfile.gettempdir(), f"{binary.name}{binary.ext}") 219 | with open(fpath, "wb") as f: 220 | f.write(binary.content) 221 | 222 | logger.debug(f"Scanning the module {binary.name}{binary.ext}") 223 | 224 | with UefiAnalyzer(image_path=fpath) as uefi_analyzer: 225 | scanner = UefiScanner(uefi_analyzer, rules_scan) 226 | for result in scanner.results: 227 | msg = threat if result.res else no_threat 228 | click.echo( 229 | f"{prefix} {result.rule.name} (variant: {result.variant_label}) {msg} ({binary.name})" 230 | ) 231 | 232 | os.remove(fpath) 233 | 234 | return True 235 | 236 | 237 | @click.command() 238 | @click.argument( 239 | "image_path", type=click.Path(exists=True, dir_okay=False, file_okay=True) 240 | ) 241 | @click.argument("extract_path", type=click.Path(dir_okay=True, file_okay=False)) 242 | def extract(image_path: str, extract_path: str) -> bool: 243 | """Extract all modules from UEFI firmware image.""" 244 | 245 | if not os.path.isdir(extract_path): 246 | os.mkdir(extract_path) 247 | 248 | with open(image_path, "rb") as f: 249 | firmware_data = f.read() 250 | 251 | extractor = UefiExtractor(firmware_data, list()) 252 | extractor.extract_all(ignore_guid=True) 253 | 254 | if not len(extractor.binaries): 255 | click.echo("No modules found", err=True) 256 | return False 257 | 258 | for binary in extractor.binaries: 259 | if not binary.is_ok: 260 | continue 261 | fpath = os.path.join(extract_path, f"{binary.name}-{binary.guid}{binary.ext}") 262 | with open(fpath, "wb") as f: 263 | f.write(binary.content) 264 | 265 | click.echo(f"{binary.guid}: {fpath}") 266 | 267 | return True 268 | 269 | 270 | cli.add_command(analyze) 271 | cli.add_command(analyze, "analyze-module") 272 | cli.add_command(analyze, "analyze-bootloader") 273 | cli.add_command(scan) 274 | cli.add_command(scan, "scan-module") 275 | cli.add_command(scan, "scan-bootloader") 276 | cli.add_command(scan_firmware) 277 | cli.add_command(extract) 278 | 279 | if __name__ == "__main__": 280 | cli() 281 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_smm.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | import json 4 | import uuid 5 | from typing import Any, Dict, List, Optional 6 | 7 | import rzpipe 8 | 9 | from fwhunt_scan.uefi_protocols import UefiGuid 10 | from fwhunt_scan.uefi_types import ChildSwSmiHandler, SmiHandler, SmiKind 11 | from fwhunt_scan.uefi_utils import ( 12 | get_current_insn_index, 13 | get_int, 14 | get_xrefs_to_data, 15 | get_xrefs_to_guids, 16 | ) 17 | 18 | SMI_KINDS = { 19 | SmiKind.SW_SMI: [ 20 | UefiGuid( 21 | "18A3C6DC-5EEA-48C8-A1C1B53389F98999", 22 | name="EFI_SMM_SW_DISPATCH2_PROTOCOL_GUID", 23 | ), 24 | UefiGuid( 25 | "E541B773-DD11-420C-B026DF993653F8BF", 26 | name="EFI_SMM_SW_DISPATCH_PROTOCOL_GUID", 27 | ), 28 | ], 29 | SmiKind.USB_SMI: [ 30 | UefiGuid( 31 | "EE9B8D90-C5A6-40A2-BDE252558D33CCA1", 32 | name="EFI_SMM_USB_DISPATCH2_PROTOCOL_GUID", 33 | ), 34 | UefiGuid( 35 | "A05B6FFD-87AF-4E42-95C96228B63CF3F3", 36 | name="EFI_SMM_USB_DISPATCH_PROTOCOL_GUID", 37 | ), 38 | ], 39 | SmiKind.SX_SMI: [ 40 | UefiGuid( 41 | "456D2859-A84B-4E47-A2EE3276D886997D", 42 | name="EFI_SMM_SX_DISPATCH2_PROTOCOL_GUID", 43 | ), 44 | UefiGuid( 45 | "14FC52BE-01DC-426C-91AEA23C3E220AE8", 46 | name="EFI_SMM_SX_DISPATCH_PROTOCOL_GUID", 47 | ), 48 | ], 49 | SmiKind.IO_TRAP_SMI: [ 50 | UefiGuid( 51 | "58DC368D-7BFA-4E77-ABBC0E29418DF930", 52 | name="EFI_SMM_IO_TRAP_DISPATCH2_PROTOCOL_GUID", 53 | ), 54 | UefiGuid( 55 | "DB7F536B-EDE4-4714-A5C8E346EBAA201D", 56 | name="EFI_SMM_IO_TRAP_DISPATCH_PROTOCOL_GUID", 57 | ), 58 | ], 59 | SmiKind.GPI_SMI: [ 60 | UefiGuid( 61 | "25566B03-B577-4CBF-958CED663EA24380", 62 | name="EFI_SMM_GPI_DISPATCH2_PROTOCOL_GUID", 63 | ), 64 | UefiGuid( 65 | "E0744B81-9513-49CD-8CEAE9245E7039DA", 66 | name="EFI_SMM_GPI_DISPATCH_PROTOCOL_GUID", 67 | ), 68 | ], 69 | SmiKind.STANDBY_BUTTON_SMI: [ 70 | UefiGuid( 71 | "7300C4A1-43F2-4017-A51BC81A7F40585B", 72 | name="EFI_SMM_STANDBY_BUTTON_DISPATCH2_PROTOCOL_GUID", 73 | ), 74 | UefiGuid( 75 | "78965B98-B0BF-449E-8B22D2914E498A98", 76 | name="EFI_SMM_STANDBY_BUTTON_DISPATCH_PROTOCOL_GUID", 77 | ), 78 | ], 79 | SmiKind.PERIODIC_TIMER_SMI: [ 80 | UefiGuid( 81 | "4CEC368E-8E8E-4D71-8BE1958C45FC8A53", 82 | name="EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL_GUID", 83 | ), 84 | UefiGuid( 85 | "9CCA03FC-4C9E-4A19-9B06ED7B479BDE55", 86 | name="EFI_SMM_PERIODIC_TIMER_DISPATCH_PROTOCOL_GUID", 87 | ), 88 | ], 89 | SmiKind.POWER_BUTTON_SMI: [ 90 | UefiGuid( 91 | "1B1183FA-1823-46A7-88729C578755409D", 92 | name="EFI_SMM_POWER_BUTTON_DISPATCH2_PROTOCOL_GUID", 93 | ), 94 | UefiGuid( 95 | "B709EFA0-47A6-4B41-B93112ECE7A8EE56", 96 | name="EFI_SMM_POWER_BUTTON_DISPATCH_PROTOCOL_GUID", 97 | ), 98 | ], 99 | SmiKind.ICHN_SMI: [ 100 | UefiGuid( 101 | "C50B323E-9075-4F2A-AC8ED2596A1085CC", 102 | name="EFI_SMM_ICHN_DISPATCH_PROTOCOL_GUID", 103 | ), 104 | UefiGuid( 105 | "ADF3A128-416D-4060-8DDF-30A1D7AAB699", 106 | name="EFI_SMM_ICHN_DISPATCH2_PROTOCOL_GUID", 107 | ), 108 | ], 109 | SmiKind.TCO_SMI: [ 110 | UefiGuid( 111 | "0E2D6BB1-C624-446D-9982693CD181A607", 112 | name="EFI_SMM_TCO_DISPATCH_PROTOCOL_GUID", 113 | ), 114 | ], 115 | SmiKind.STANDBY_BUTTON_SMI: [ 116 | UefiGuid( 117 | "7300C4A1-43F2-4017-A51BC81A7F40585B", 118 | name="EFI_SMM_STANDBY_BUTTON_DISPATCH2_PROTOCOL_GUID", 119 | ), 120 | UefiGuid( 121 | "78965B98-B0BF-449E-8B22D2914E498A98", 122 | name="EFI_SMM_STANDBY_BUTTON_DISPATCH_PROTOCOL_GUID", 123 | ), 124 | ], 125 | SmiKind.PERIODIC_TIMER_SMI: [ 126 | UefiGuid( 127 | "4CEC368E-8E8E-4D71-8BE1958C45FC8A53", 128 | name="EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL_GUID", 129 | ), 130 | UefiGuid( 131 | "9CCA03FC-4C9E-4A19-9B06ED7B479BDE55", 132 | name="EFI_SMM_PERIODIC_TIMER_DISPATCH_PROTOCOL_GUID", 133 | ), 134 | ], 135 | SmiKind.POWER_BUTTON_SMI: [ 136 | UefiGuid( 137 | "1B1183FA-1823-46A7-88729C578755409D", 138 | name="EFI_SMM_POWER_BUTTON_DISPATCH2_PROTOCOL_GUID", 139 | ), 140 | UefiGuid( 141 | "B709EFA0-47A6-4B41-B93112ECE7A8EE56", 142 | name="EFI_SMM_POWER_BUTTON_DISPATCH_PROTOCOL_GUID", 143 | ), 144 | ], 145 | SmiKind.PCH_TCO_SMI: [ 146 | UefiGuid( 147 | "9E71D609-6D24-47FD-B572-6140F8D9C2A4", 148 | name="PCH_TCO_SMI_DISPATCH_PROTOCOL_GUID", 149 | ), 150 | ], 151 | SmiKind.PCH_PCIE_SMI: [ 152 | UefiGuid( 153 | "3e7d2b56-3f47-42aa-8f6b-22f519818dab", 154 | name="PCH_PCIE_SMI_DISPATCH_PROTOCOL_GUID", 155 | ), 156 | ], 157 | SmiKind.PCH_ACPI_SMI: [ 158 | UefiGuid( 159 | "d52bb262-f022-49ec-86d2-7a293a7a054b", 160 | name="PCH_ACPI_SMI_DISPATCH_PROTOCOL_GUID", 161 | ), 162 | ], 163 | SmiKind.PCH_GPIO_UNLOCK_SMI: [ 164 | UefiGuid( 165 | "83339ef7-9392-4716-8d3a-d1fc67cd55db", 166 | name="PCH_GPIO_UNLOCK_SMI_DISPATCH_PROTOCOL_GUID", 167 | ), 168 | ], 169 | SmiKind.PCH_SMI: [ 170 | UefiGuid( 171 | "e6a81bbf-873d-47fd-b6be-61b3e5720993", 172 | name="PCH_SMI_DISPATCH_PROTOCOL_GUID", 173 | ), 174 | ], 175 | SmiKind.PCH_ESPI_SMI: [ 176 | UefiGuid( 177 | "b3c14ff3-bae8-456c-8631-27fe0ceb340c", 178 | name="PCH_ESPI_SMI_DISPATCH_PROTOCOL_GUID", 179 | ), 180 | ], 181 | SmiKind.ACPI_EN_SMI: [ 182 | UefiGuid( 183 | "bd88ec68-ebe4-4f7b-935a-4f666642e75f", 184 | name="EFI_ACPI_EN_DISPATCH_PROTOCOL_GUID", 185 | ), 186 | ], 187 | SmiKind.ACPI_DIS_SMI: [ 188 | UefiGuid( 189 | "9c939ba6-1fcc-46f6-b4e1-102dbe186567", 190 | name="EFI_ACPI_DIS_DISPATCH_PROTOCOL_GUID", 191 | ), 192 | ], 193 | } 194 | 195 | 196 | def get_interface_from_bb(insns: List[Dict[str, Any]], code_addr: int) -> Optional[int]: 197 | """Get the address of the interface 198 | (in the case of local variables, this will be the address on the stack)""" 199 | 200 | res = None 201 | 202 | index = get_current_insn_index(insns, code_addr) 203 | if index is None: 204 | return res 205 | 206 | # check all instructions from index to 0 207 | for i in range(index - 1, -1, -1): 208 | insn = insns[i] 209 | if "esil" not in insn: 210 | continue 211 | esil = insn["esil"].split(",") 212 | if not (esil[-1] == "=" and esil[-2] == "r8"): 213 | continue 214 | try: 215 | res = int(esil[0], 16) 216 | return res 217 | except ValueError: 218 | continue 219 | 220 | return res 221 | 222 | 223 | def get_interface_global(insns: List[Dict[str, Any]], code_addr: int) -> Optional[int]: 224 | """Get the address of the interface 225 | (in the case of local variables, this will be the address on the stack)""" 226 | 227 | res = None 228 | 229 | index = get_current_insn_index(insns, code_addr) 230 | if index is None: 231 | return res 232 | 233 | # check all instructions from index to 0 234 | for i in range(index - 1, -1, -1): 235 | insn = insns[i] 236 | if "esil" not in insn: 237 | continue 238 | esil = insn["esil"].split(",") 239 | if not (esil[-1] == "=" and esil[-2] == "r8"): 240 | continue 241 | res = insn.get("ptr", None) 242 | if res is not None: 243 | return res 244 | res = get_int(esil[0]) 245 | if res is not None: 246 | return res 247 | 248 | return res 249 | 250 | 251 | def get_handler(insns: List[Dict[str, Any]], kind: SmiKind) -> Optional[SmiHandler]: 252 | address = None 253 | 254 | for insn in insns: 255 | if "esil" not in insn: 256 | continue 257 | esil = insn["esil"].split(",") 258 | 259 | if ( 260 | len(esil) > 4 261 | and esil[-1] == "=" 262 | and esil[-2] == "rdx" 263 | # skip esil[-3] 264 | # (esil[-3] == "+" or esil[-3] == "-") 265 | and esil[-4] == "rip" 266 | ): 267 | # handler address 268 | address = insn.get("ptr", None) 269 | 270 | # found `EfiSmmXXXDispatch2Protocol->Register()` 271 | if ( 272 | esil == ["rax", "[8]", "rip", "8", "rsp", "-=", "rsp", "=[]", "rip", "="] 273 | and address is not None 274 | ): 275 | return SmiHandler(address=address, kind=kind) 276 | 277 | return None 278 | 279 | 280 | def get_handlers( 281 | rz: rzpipe.open, code_addr: int, interface: int, kind: SmiKind 282 | ) -> List[SmiHandler]: 283 | res: List[SmiHandler] = list() 284 | res_addrs: List[int] = list() 285 | 286 | func = rz.cmdj(f"pdfj @ {code_addr:#x}") 287 | insns = func.get("ops", None) 288 | if insns is None: 289 | return res 290 | 291 | index = get_current_insn_index(insns, code_addr) 292 | if index is None: 293 | return res 294 | 295 | # check all instructions from index to end of function 296 | for i in range(index + 1, len(insns), 1): 297 | insn = insns[i] 298 | if "esil" not in insn: 299 | continue 300 | esil = insn["esil"].split(",") 301 | 302 | # find `mov rax, [rbp+EfiSmmSwDispatch2Protocol]` instruction 303 | if not (esil[-1] == "=" and esil[-2] == "rax"): 304 | continue 305 | 306 | value = get_int(esil[0]) 307 | if value is None: 308 | continue 309 | 310 | if value == interface: 311 | offset = insn.get("offset", None) 312 | if offset is None: 313 | continue 314 | bb = rz.cmdj(f"pdbj @ {offset:#x}") 315 | handler = get_handler(bb, kind) 316 | if handler is not None: 317 | if handler.address not in res_addrs: 318 | res.append(handler) 319 | # prevent duplicates 320 | res_addrs.append(handler.address) 321 | 322 | return res 323 | 324 | 325 | def get_smst_bb(insns: List[Dict[str, Any]]) -> Optional[int]: 326 | res = None 327 | 328 | for insn in insns: 329 | if "esil" not in insn: 330 | continue 331 | esil = insn["esil"].split(",") 332 | 333 | # check esil insn ({offset},rip,+,rdx,=) 334 | if esil[-1] == "=" and esil[-2] == "rdx": 335 | res = insn.get("ptr", None) 336 | if res is not None: 337 | return res 338 | 339 | return res 340 | 341 | 342 | def get_smst_func(rz: rzpipe.open, code_addr: int, interface: int) -> List[int]: 343 | res: List[int] = list() 344 | 345 | func = rz.cmdj(f"pdfj @ {code_addr:#x}") 346 | insns = func.get("ops", None) 347 | if insns is None: 348 | return res 349 | 350 | index = get_current_insn_index(insns, code_addr) 351 | if index is None: 352 | return res 353 | 354 | # check all instructions from index to end of function 355 | for i in range(index + 1, len(insns), 1): 356 | insn = insns[i] 357 | if "esil" not in insn: 358 | continue 359 | esil = insn["esil"].split(",") 360 | if esil[-1] == "=" and esil[-2] == "rax" and esil[-3] == "[8]": 361 | # check both for global variable and local variable 362 | if insn.get("ptr", None) == interface or get_int(esil[0]) == interface: 363 | offset = insn.get("offset", None) 364 | if offset is None: 365 | continue 366 | bb = rz.cmdj(f"pdbj @ {offset:#x}") 367 | smst = get_smst_bb(bb) 368 | if smst is not None: 369 | res.append(smst) 370 | return res 371 | 372 | 373 | def get_smst_list(rz: rzpipe.open) -> List[int]: 374 | """Find SMST addresses""" 375 | 376 | res: List[int] = list() 377 | 378 | guids = [ 379 | UefiGuid( 380 | "F4CCBFB7-F6E0-47FD-9DD410A8F150C191", name="EFI_SMM_BASE2_PROTOCOL_GUID" 381 | ) 382 | ] 383 | code_addrs = get_xrefs_to_guids(rz, guids) 384 | for code_addr in code_addrs: 385 | bb = rz.cmdj(f"pdbj @ {code_addr:#x}") 386 | interface = get_interface_global(bb, code_addr) 387 | if interface is None: 388 | continue 389 | res += get_smst_func(rz, code_addr, interface) 390 | 391 | return res 392 | 393 | 394 | def find_handler_register_service(insns: List[Dict[str, Any]]) -> Optional[int]: 395 | for insn in insns[::-1]: 396 | if "esil" not in insn: 397 | continue 398 | esil = insn["esil"].split(",") 399 | 400 | if esil[-7:] != ["8", "rsp", "-=", "rsp", "=[]", "rip", "="]: 401 | continue 402 | 403 | offset = get_int(esil[0]) 404 | if offset == 0xE0: # SmiHandlerRegister 405 | return insns.index(insn) 406 | 407 | return None 408 | 409 | 410 | def get_child_sw_smi_handler_bb( 411 | rz: rzpipe.open, insns: List[Dict[str, Any]] 412 | ) -> Optional[ChildSwSmiHandler]: 413 | handler_address = None 414 | handler_guid = None 415 | 416 | end_index = find_handler_register_service(insns) 417 | if end_index is None: 418 | return None 419 | 420 | for insn in insns[:end_index][::-1]: 421 | if "esil" not in insn: 422 | continue 423 | esil = insn["esil"].split(",") 424 | 425 | # try to get handler address (Handler parameter) 426 | if esil[-1] == "=" and esil[-2] == "rcx": 427 | handler_address = insn.get("ptr", None) 428 | 429 | # try to get handler guid value (HandlerType parameter) 430 | if esil[-1] == "=" and esil[-2] == "rdx": 431 | guid_addr = insn.get("ptr", None) 432 | if guid_addr is not None: 433 | rz.cmd(f"s {guid_addr:#x}") 434 | guid_b = bytes(rz.cmdj("xj 16")) 435 | if len(guid_b) == 16: 436 | handler_guid = str(uuid.UUID(bytes_le=guid_b)).upper() 437 | 438 | if handler_address is not None and handler_guid is not None: 439 | return ChildSwSmiHandler(address=handler_address, handler_guid=handler_guid) 440 | 441 | if handler_address is not None: # handler_guid is Optional 442 | return ChildSwSmiHandler(address=handler_address, handler_guid=handler_guid) 443 | 444 | return None 445 | 446 | 447 | def get_child_sw_smi_handlers( 448 | rz: rzpipe.open, smst_list: List[int] 449 | ) -> List[ChildSwSmiHandler]: 450 | res: List[ChildSwSmiHandler] = list() 451 | 452 | haddrs = list() # addresses 453 | 454 | for smst in smst_list: 455 | code_addrs = get_xrefs_to_data(rz, smst) 456 | for addr in code_addrs: 457 | # analyze instructions and found gSmst->SmiHandlerRegister call 458 | result = rz.cmd(f"pdj 24 @ {addr:#x}") 459 | # prevent error messages to sys.stderr from rizin: 460 | # https://github.com/rizinorg/rz-pipe/blob/0f7ac66e6d679ebb03be26bf61a33f9ccf199f27/python/rzpipe/open_base.py#L261 461 | try: 462 | bb = json.loads(result) 463 | except (ValueError, KeyError, TypeError): 464 | continue 465 | handler = get_child_sw_smi_handler_bb(rz, bb) 466 | if handler is not None: 467 | if handler.address not in haddrs: 468 | res.append(handler) 469 | haddrs.append(handler.address) 470 | 471 | return res 472 | 473 | 474 | def get_smi_handlers(rz: rzpipe.open) -> List[SmiHandler]: 475 | """Find Software SMI Handlers""" 476 | 477 | res: List[SmiHandler] = list() 478 | 479 | for kind in SMI_KINDS: 480 | code_addrs = get_xrefs_to_guids(rz, SMI_KINDS[kind]) 481 | for code_addr in code_addrs: 482 | # get basic block information 483 | bb = rz.cmdj(f"pdbj @ {code_addr:#x}") 484 | interface = get_interface_from_bb(bb, code_addr) 485 | if interface is None: 486 | continue 487 | 488 | # need to check the use of this interface below code_addr 489 | res += get_handlers(rz, code_addr, interface, kind) 490 | 491 | return res 492 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_analyzer.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | # 3 | # pylint: disable=too-many-nested-blocks,invalid-name 4 | # pylint: disable=too-few-public-methods,too-many-arguments,too-many-instance-attributes 5 | 6 | """ 7 | Tools for analyzing UEFI firmware using radare2/rizin 8 | """ 9 | 10 | import json 11 | import string 12 | import sys 13 | import uuid 14 | from types import TracebackType 15 | from typing import Any, Dict, List, Optional, Type 16 | 17 | import rzpipe 18 | 19 | import fwhunt_scan.uefi_smm as uefi_smm 20 | from fwhunt_scan.uefi_protocols import GUID_FROM_BYTES 21 | from fwhunt_scan.uefi_tables import ( 22 | BS_PROTOCOLS_INFO_64_BIT, 23 | EFI_BOOT_SERVICES_64_BIT, 24 | EFI_PEI_SERVICES_32_BIT, 25 | EFI_RUNTIME_SERVICES_64_BIT, 26 | OFFSET_TO_SERVICE, 27 | ) 28 | from fwhunt_scan.uefi_te import TerseExecutableError, TerseExecutableParser 29 | from fwhunt_scan.uefi_types import ( 30 | ChildSwSmiHandler, 31 | NvramVariable, 32 | SmiHandler, 33 | UefiGuid, 34 | UefiProtocol, 35 | UefiProtocolGuid, 36 | UefiService, 37 | ) 38 | from fwhunt_scan.uefi_utils import get_current_insn_index, get_int 39 | 40 | if sys.version_info.major == 3 and sys.version_info.minor >= 8: 41 | from multiprocessing import shared_memory 42 | if sys.version_info.major == 3 and ( 43 | sys.version_info.minor >= 6 and sys.version_info.minor < 8 44 | ): 45 | import shared_memory # type: ignore # noqa: F811 46 | 47 | 48 | class UefiAnalyzerError(Exception): 49 | """Generic TE format error exception.""" 50 | 51 | def __init__(self, value: str) -> None: 52 | self.value = value 53 | 54 | def __str__(self): 55 | return repr(self.value) 56 | 57 | 58 | class UefiAnalyzer: 59 | """Helper object to analyze the EFI binary and provide properties""" 60 | 61 | def __init__( 62 | self, 63 | image_path: Optional[str] = None, 64 | blob: Optional[bytes] = None, 65 | rizinhome: Optional[str] = None, 66 | ): 67 | """UEFI analyzer initialization""" 68 | 69 | self._rz: rzpipe.open = None 70 | self._shm: Optional[shared_memory.SharedMemory] = None 71 | self._te: Optional[TerseExecutableParser] = None 72 | 73 | # init rizin 74 | if image_path: 75 | self._rz = rzpipe.open( 76 | filename=image_path, flags=["-2"], rizin_home=rizinhome 77 | ) 78 | # analyze image 79 | with open(image_path, "rb") as f: 80 | if f.read(2) in [b"MZ", b"VZ"]: 81 | self._rz.cmd("aaaa") 82 | try: 83 | self._te = TerseExecutableParser(image_path=image_path) 84 | except TerseExecutableError: 85 | self._te = None 86 | 87 | if blob and sys.platform in ["linux"]: 88 | blob_size = len(blob) 89 | self._shm = shared_memory.SharedMemory(create=True, size=blob_size) 90 | self._shm.buf[:] = blob[:] 91 | self._rz = rzpipe.open( 92 | filename=f"shm://{self._shm.name}/{blob_size:#d}", 93 | flags=["-2"], 94 | rizin_home=rizinhome, 95 | ) 96 | if blob[:2] in [b"MZ", b"VZ"]: 97 | self._rz.cmd("aaaa") 98 | try: 99 | self._te = TerseExecutableParser(blob=blob) 100 | except TerseExecutableError: 101 | self._te = None 102 | 103 | if self._rz is None: 104 | raise UefiAnalyzerError( 105 | "Failed to initialize radare2/rizin analyzing engine" 106 | ) 107 | 108 | # private cache 109 | self._bs_list_g_bs: Optional[List[UefiService]] = None 110 | self._bs_prot: Optional[List[UefiService]] = None 111 | self._rt_list: Optional[List[UefiService]] = None 112 | self._pei_services: Optional[List[UefiService]] = None 113 | self._ppi_list: Optional[List[UefiProtocol]] = None 114 | self._protocols: Optional[List[UefiProtocol]] = None 115 | self._protocol_guids: Optional[List[UefiProtocolGuid]] = None 116 | self._nvram_vars: Optional[List[NvramVariable]] = None 117 | self._info: Optional[Dict[Any, Any]] = None 118 | self._strings: Optional[List[Any]] = None 119 | self._sections: Optional[List[Any]] = None 120 | self._functions: Optional[List[Any]] = None 121 | self._insns: Optional[List[Any]] = None 122 | self._g_bs: Optional[List[int]] = None 123 | self._g_rt: Optional[List[int]] = None 124 | self._smst_list: Optional[List[int]] = None 125 | 126 | self._rt_filter_list: List[int] = list() 127 | self._bs_filter_list: List[int] = list() 128 | self._pei_filter_list: List[int] = list() 129 | 130 | # SMI handlers addresses 131 | self._smi_handlers: Optional[List[SmiHandler]] = None 132 | self._child_swsmi_handlers: Optional[List[ChildSwSmiHandler]] = None 133 | 134 | def __enter__(self): 135 | return self 136 | 137 | def _section_paddr(self, section_name: str) -> int: 138 | for section in self.sections: 139 | if section["name"] == section_name: 140 | return section["paddr"] 141 | return 0 142 | 143 | def _correct_addr(self, addr: int) -> int: 144 | if not self._te: 145 | return addr 146 | offset = ( 147 | self._te.image_base + self._te.base_of_code - self._section_paddr(".text") 148 | ) 149 | return addr + offset 150 | 151 | def _wrong_addr(self, addr: int) -> int: 152 | if not self._te: 153 | return addr 154 | offset = ( 155 | self._te.image_base + self._te.base_of_code - self._section_paddr(".text") 156 | ) 157 | return addr - offset 158 | 159 | def _pei_service_args_num(self, reg: str, addr: int) -> int: 160 | """Get number of arguments for specified PEI service call""" 161 | 162 | args_num = 0 163 | self._rz.cmd("s {:#x}".format(addr)) 164 | res = self._rz.cmdj("pdbj") 165 | if not res: 166 | return 0 167 | 168 | # find current call in block 169 | for index in range(len(res)): 170 | if res[index]["offset"] == addr: 171 | break 172 | 173 | # get number of arguments 174 | for i in range(index, -1, -1): 175 | esil = res[i]["esil"].split(",") 176 | if len(esil) < 4: # fix IndexError 177 | continue 178 | if esil[-3] == "4" and esil[-2] == "esp" and esil[-1] == "-=": 179 | args_num += 1 180 | if ( 181 | len(esil) == 4 182 | and esil[-1] == "=" 183 | and esil[-2] == reg 184 | and esil[-3] == "[4]" 185 | ): 186 | return args_num 187 | return 0 188 | 189 | @property 190 | def info(self) -> Dict[Any, Any]: 191 | """Get common image properties (parsed header)""" 192 | 193 | if self._info is None: 194 | self._info = self._rz.cmdj("ij") 195 | return self._info 196 | 197 | @property 198 | def strings(self) -> List[Any]: 199 | """Get common image properties (strings)""" 200 | 201 | if self._strings is None: 202 | self._strings = self._rz.cmdj("izzj") or [] 203 | return self._strings 204 | 205 | @property 206 | def sections(self) -> List[Any]: 207 | """Get common image properties (sections)""" 208 | 209 | if self._sections is None: 210 | self._sections = self._rz.cmdj("iSj") or [] 211 | return self._sections 212 | 213 | @property 214 | def functions(self) -> List[Any]: 215 | """Get common image properties (functions)""" 216 | 217 | if self._functions is None: 218 | self._functions = self._rz.cmdj("aflj") or [] 219 | return self._functions 220 | 221 | def _get_insns(self) -> List[Any]: 222 | insns = list() 223 | for function in self.functions: 224 | self._rz.cmd("s {:#x}".format(function["offset"])) 225 | insns += self._rz.cmdj("pDj {:#x}".format(function["size"])) 226 | return insns 227 | 228 | @property 229 | def insns(self) -> List[Any]: 230 | """Get instructions""" 231 | 232 | if self._insns is None: 233 | self._insns = self._get_insns() 234 | return self._insns 235 | 236 | def _get_bs_64bit(self) -> List[int]: 237 | res: List[int] = list() 238 | 239 | for func in self.functions: 240 | func_addr = func["offset"] 241 | func_insns = self._rz.cmdj("pdfj @ {:#x}".format(func_addr)) 242 | g_bs_reg = None 243 | for insn in func_insns["ops"]: 244 | if "esil" in insn: 245 | esil = insn["esil"].split(",") 246 | if ( 247 | get_int(esil[0]) == 0x60 248 | and (esil[2] == "+") 249 | and (esil[3] == "[8]") 250 | and (esil[-1] == "=") 251 | ): 252 | g_bs_reg = esil[-2] 253 | if not g_bs_reg: 254 | continue 255 | if ( 256 | (esil[0] == g_bs_reg) 257 | and (esil[-1] == "=[8]") 258 | and ("ptr" in insn) 259 | ): 260 | res.append(insn["ptr"]) 261 | break 262 | return res 263 | 264 | @property 265 | def g_bs(self) -> List[int]: 266 | """Find BootServices table global address""" 267 | 268 | if self._g_bs is None: 269 | self._g_bs = self._get_bs_64bit() 270 | return self._g_bs 271 | 272 | def _get_rt_64bit(self) -> List[int]: 273 | res: List[int] = list() 274 | 275 | for func in self.functions: 276 | func_addr = func["offset"] 277 | func_insns = self._rz.cmdj("pdfj @ {:#x}".format(func_addr)) 278 | g_rt_reg = None 279 | for insn in func_insns["ops"]: 280 | if "esil" in insn: 281 | esil = insn["esil"].split(",") 282 | if ( 283 | get_int(esil[0]) == 0x58 284 | and (esil[2] == "+") 285 | and (esil[3] == "[8]") 286 | and (esil[-1] == "=") 287 | ): 288 | g_rt_reg = esil[-2] 289 | if not g_rt_reg: 290 | continue 291 | if ( 292 | (esil[0] == g_rt_reg) 293 | and (esil[-1] == "=[8]") 294 | and ("ptr" in insn) 295 | ): 296 | res.append(insn["ptr"]) 297 | break 298 | return res 299 | 300 | @property 301 | def g_rt(self) -> List[int]: 302 | """Find RuntimeServices table global address""" 303 | 304 | if self._g_rt is None: 305 | self._g_rt = self._get_rt_64bit() 306 | return self._g_rt 307 | 308 | def _get_boot_services_bs_64bit(self) -> List[UefiService]: 309 | bs_list: List[UefiService] = list() 310 | addrs: List[int] = list() 311 | 312 | if not len(self.g_bs): 313 | return bs_list 314 | 315 | for insn in self.insns: 316 | # find "mov rax, qword [g_bs]" instruction 317 | found = False 318 | if "esil" not in insn: 319 | continue 320 | esil = insn["esil"].split(",") 321 | if ( 322 | (insn["type"] == "mov") 323 | and (esil[-1] == "=") 324 | and (esil[-3] == "[8]") 325 | and (esil[-4] == "+") 326 | ): 327 | if ("ptr" in insn) and (insn["ptr"] in self.g_bs): 328 | found = True 329 | if not found: 330 | continue 331 | 332 | # if current instriction is "mov rax, qword [g_bs]" 333 | index = self.insns.index(insn) 334 | for g_bs_area_insn in self.insns[index : index + 0x10]: 335 | if "esil" not in g_bs_area_insn.keys(): 336 | continue 337 | g_bs_area_esil = g_bs_area_insn["esil"].split(",") 338 | if not ( 339 | (g_bs_area_insn["type"] in ["ucall", "ircall", "ujmp", "irjmp"]) 340 | and (g_bs_area_esil[1] == "rax") 341 | and (g_bs_area_esil[2] == "+") 342 | and (g_bs_area_esil[3] == "[8]") 343 | and (g_bs_area_esil[-1] == "=") 344 | ): 345 | continue 346 | if "ptr" not in g_bs_area_insn: 347 | continue 348 | service_offset = g_bs_area_insn["ptr"] 349 | if service_offset in EFI_BOOT_SERVICES_64_BIT: 350 | if g_bs_area_insn["offset"] in addrs: 351 | break 352 | if g_bs_area_insn["offset"] not in self._bs_filter_list: 353 | bs_list.append( 354 | UefiService( 355 | address=g_bs_area_insn["offset"], 356 | name=EFI_BOOT_SERVICES_64_BIT[service_offset], 357 | ) 358 | ) 359 | self._bs_filter_list.append(g_bs_area_insn["offset"]) 360 | break 361 | 362 | return bs_list 363 | 364 | def _get_boot_services_prot_64bit(self) -> List[UefiService]: 365 | bs_prot: List[UefiService] = list() 366 | 367 | for insn in self.insns: 368 | if "esil" not in insn: 369 | continue 370 | esil = insn["esil"].split(",") 371 | if not ( 372 | (insn["type"] in ["ucall", "ircall", "ujmp", "irjmp"]) 373 | and (esil[1] == "rax") 374 | and (esil[2] == "+") 375 | and (esil[3] == "[8]") 376 | ): 377 | continue 378 | if "ptr" not in insn: 379 | continue 380 | service_offset = insn["ptr"] 381 | if service_offset in OFFSET_TO_SERVICE: 382 | name = OFFSET_TO_SERVICE[service_offset] 383 | # found boot service that work with protocol 384 | if insn["offset"] not in self._bs_filter_list: 385 | bs_prot.append(UefiService(address=insn["offset"], name=name)) 386 | self._bs_filter_list.append(insn["offset"]) 387 | return bs_prot 388 | 389 | @property 390 | def boot_services(self) -> List[UefiService]: 391 | """Find boot services using g_bs""" 392 | 393 | if self._bs_prot is None: 394 | self._bs_prot = self._get_boot_services_prot_64bit() 395 | if self._bs_list_g_bs is None: 396 | self._bs_list_g_bs = self._get_boot_services_bs_64bit() 397 | return self._bs_list_g_bs + self._bs_prot 398 | 399 | @property 400 | def boot_services_protocols(self) -> List[Any]: 401 | """Find boot service that work with protocols""" 402 | 403 | if self._bs_prot is None: 404 | self._bs_prot = self._get_boot_services_prot_64bit() 405 | return self._bs_prot 406 | 407 | def _get_runtime_services_64bit(self) -> List[UefiService]: 408 | rt_list: List[UefiService] = list() 409 | 410 | if not len(self.g_rt): 411 | return rt_list 412 | 413 | for insn in self.insns: 414 | # find "mov rax, qword [g_rt]" instruction 415 | found = False 416 | if "esil" not in insn: 417 | continue 418 | esil = insn["esil"].split(",") 419 | if ( 420 | (insn["type"] == "mov") 421 | and (esil[-1] == "=") 422 | and (esil[-3] == "[8]") 423 | and (esil[-4] == "+") 424 | ): 425 | if ("ptr" in insn) and (insn["ptr"] in self.g_rt): 426 | found = True 427 | if not found: 428 | continue 429 | 430 | index = self.insns.index(insn) 431 | for g_rt_area_insn in self.insns[index : index + 0x10]: 432 | g_rt_area_esil = g_rt_area_insn["esil"].split(",") 433 | if not ( 434 | (g_rt_area_insn["type"] in ["ucall", "ircall", "ujmp", "irjmp"]) 435 | and (g_rt_area_esil[1] == "rax") 436 | and (g_rt_area_esil[2] == "+") 437 | and (g_rt_area_esil[3] == "[8]") 438 | and (g_rt_area_esil[-1] == "=") 439 | ): 440 | continue 441 | if "ptr" not in g_rt_area_insn: 442 | continue 443 | service_offset = g_rt_area_insn["ptr"] 444 | if service_offset in EFI_RUNTIME_SERVICES_64_BIT: 445 | if g_rt_area_insn["offset"] not in self._rt_filter_list: 446 | rt_list.append( 447 | UefiService( 448 | address=g_rt_area_insn["offset"], 449 | name=EFI_RUNTIME_SERVICES_64_BIT[service_offset], 450 | ) 451 | ) 452 | self._rt_filter_list.append(g_rt_area_insn["offset"]) 453 | break 454 | return rt_list 455 | 456 | @property 457 | def runtime_services(self) -> List[UefiService]: 458 | """Find all runtime services""" 459 | 460 | if self._rt_list is None: 461 | self._rt_list = self._get_runtime_services_64bit() 462 | return self._rt_list 463 | 464 | def _get_protocols_64bit(self) -> List[UefiProtocol]: 465 | protocols = list() 466 | for bs in self.boot_services_protocols: 467 | block_insns = self._rz.cmd("pdbj @ {:#x}".format(bs.address)) 468 | try: 469 | block_insns = json.loads(block_insns) 470 | except (ValueError, KeyError, TypeError): 471 | continue 472 | for insn in block_insns: 473 | if "esil" in insn: 474 | esil = insn["esil"].split(",") 475 | if not ( 476 | (insn["type"] == "lea") 477 | and (esil[-1] == "=") 478 | and (esil[-2] == BS_PROTOCOLS_INFO_64_BIT[bs.name]["reg"]) 479 | and (esil[-3] == "+") 480 | ): 481 | continue 482 | if "ptr" not in insn: 483 | continue 484 | p_guid_addr = insn["ptr"] 485 | self._rz.cmd("s {:#x}".format(p_guid_addr)) 486 | p_guid_b = bytes(self._rz.cmdj("xj 16")) 487 | 488 | # look up in known list 489 | guid = GUID_FROM_BYTES.get(p_guid_b) 490 | if not guid: 491 | guid = UefiGuid( 492 | value=str(uuid.UUID(bytes_le=p_guid_b)).upper(), 493 | name="UNKNOWN_PROTOCOL", 494 | ) 495 | 496 | protocols.append( 497 | UefiProtocol( 498 | name=guid.name, 499 | value=guid.value, 500 | guid_address=p_guid_addr, 501 | address=insn["offset"], 502 | service=bs.name, 503 | ) 504 | ) 505 | return protocols 506 | 507 | @property 508 | def protocols(self) -> List[UefiProtocol]: 509 | """Find proprietary protocols""" 510 | 511 | if self._protocols is None: 512 | self._protocols = self._get_protocols_64bit() 513 | return self._protocols 514 | 515 | def _get_protocol_guids(self) -> List[UefiProtocolGuid]: 516 | protocol_guids = list() 517 | target_sections = [".data"] 518 | for section in self.sections: 519 | if section["name"] in target_sections: 520 | self._rz.cmd("s {:#x}".format(section["vaddr"])) 521 | section_data = bytes(self._rz.cmdj("xj {:#d}".format(section["vsize"]))) 522 | 523 | # find guids in section data: 524 | for i in range(len(section_data) - 15): 525 | chunk = section_data[i : i + 16] 526 | guid = GUID_FROM_BYTES.get(chunk) 527 | if not guid: 528 | continue 529 | if guid.value in ["00000000-0000-0000-0000000000000000"]: 530 | continue 531 | protocol_guids.append( 532 | UefiProtocolGuid( 533 | address=section["vaddr"] + i, 534 | name=guid.name, 535 | value=guid.value, 536 | ) 537 | ) 538 | return protocol_guids 539 | 540 | @property 541 | def protocol_guids(self) -> List[UefiProtocolGuid]: 542 | """Find protocols GUIDs""" 543 | 544 | if self._protocol_guids is None: 545 | self._protocol_guids = self._get_protocol_guids() 546 | return self._protocol_guids 547 | 548 | @staticmethod 549 | def _name_is_valid(name: str) -> bool: 550 | for c in name: 551 | if c not in string.printable: 552 | return False 553 | return True 554 | 555 | def r2_get_nvram_vars_64bit(self) -> List[NvramVariable]: 556 | # to fix FPs need to apply filtering 557 | # of the extracted Runtime Services (from self.runtime_services) 558 | 559 | nvram_vars = list() 560 | for service in self.runtime_services: 561 | if service.name not in ["GetVariable", "SetVariable"]: 562 | continue 563 | 564 | # disassemble current function 565 | func_insns = self._rz.cmdj("pdfj @ {:#x}".format(service.address)) 566 | if "ops" not in func_insns: 567 | continue 568 | 569 | insns = func_insns["ops"] 570 | index = get_current_insn_index(insns, service.address) 571 | if index is None: 572 | continue 573 | 574 | name: Optional[str] = None 575 | name_addr_reg: Optional[str] = None 576 | p_guid_b: Optional[bytes] = None 577 | stack_guid: bool = False 578 | for i in range(index - 1, -1, -1): 579 | insn = insns[i] 580 | if name is not None and p_guid_b is not None: 581 | break 582 | 583 | if "esil" not in insn: 584 | continue 585 | esil = insn["esil"].split(",") 586 | 587 | # handle case when we have: 588 | # NAME_ADDR,rip,+,REG,= 589 | # REG,rcx,= 590 | if ( 591 | name is None 592 | and name_addr_reg is None 593 | and len(esil) == 3 594 | and (esil[-1] == "=" and esil[-2] == "rcx") 595 | ): 596 | name_addr_reg = esil[0] 597 | 598 | ptr = insn.get("ptr", None) 599 | if ptr is None: 600 | continue 601 | 602 | if ( 603 | name is None 604 | and name_addr_reg 605 | and len(esil) == 5 606 | and esil[-1] == "=" 607 | and esil[-2] == name_addr_reg 608 | and esil[-3] == "+" 609 | and esil[-4] == "rip" 610 | ): 611 | tmp_name = self._rz.cmd("psw @ {:#x}".format(ptr))[:-1] 612 | if UefiAnalyzer._name_is_valid(tmp_name): 613 | name = tmp_name 614 | 615 | if ( 616 | name is None 617 | and len(esil) == 5 618 | and (esil[-1] == "=") 619 | and (esil[-2] == "rcx") 620 | and (esil[-3] == "+") 621 | and (esil[-4] == "rip") 622 | ): 623 | tmp_name = self._rz.cmd("psw @ {:#x}".format(ptr))[:-1] 624 | if UefiAnalyzer._name_is_valid(tmp_name): 625 | name = tmp_name 626 | 627 | if ( 628 | p_guid_b is None 629 | and len(esil) == 5 630 | and (esil[-1] == "=") 631 | and (esil[-2] == "rdx") 632 | and (esil[-4] in ["rsp", "rbp"]) 633 | ): 634 | stack_guid = True 635 | 636 | if ( 637 | not stack_guid 638 | and p_guid_b is None 639 | and len(esil) == 5 640 | and (esil[-1] == "=") 641 | and (esil[-2] == "rdx") 642 | and (esil[-3] == "+") 643 | and (esil[-4] == "rip") 644 | ): 645 | p_guid_b = bytes(self._rz.cmdj("xj 16 @ {:#x}".format(ptr))) 646 | 647 | if not name: 648 | name = "Unknown" 649 | 650 | if p_guid_b is not None: 651 | guid = str(uuid.UUID(bytes_le=p_guid_b)).upper() 652 | else: 653 | guid = "Unknown" 654 | 655 | nvram_vars.append(NvramVariable(name=name, guid=guid, service=service)) 656 | 657 | return nvram_vars 658 | 659 | @property 660 | def nvram_vars(self) -> List[NvramVariable]: 661 | """Find NVRAM variables passed to GetVariable and SetVariable services""" 662 | 663 | if self._nvram_vars is None: 664 | self._nvram_vars = self.r2_get_nvram_vars_64bit() 665 | return self._nvram_vars 666 | 667 | def _get_pei_services(self) -> List[UefiService]: 668 | pei_list: List[UefiService] = list() 669 | for func in self.functions: 670 | func_addr = func["offset"] 671 | func_insns = self._rz.cmdj("pdfj @ {:#x}".format(func_addr)) 672 | for insn in func_insns["ops"]: 673 | if "esil" not in insn: 674 | continue 675 | esil = insn["esil"].split(",") 676 | if esil[-1] == "=" and esil[-2] == "eip": 677 | offset = get_int(esil[0]) 678 | if offset is None: 679 | continue 680 | if offset not in EFI_PEI_SERVICES_32_BIT.keys(): 681 | continue 682 | reg = esil[1] 683 | service: Dict[str, Any] = EFI_PEI_SERVICES_32_BIT[offset] 684 | 685 | # found potential pei service, compare number of arguments 686 | arg_num: Any = self._pei_service_args_num(reg, insn["offset"]) 687 | if arg_num < service["arg_num"]: 688 | continue 689 | 690 | # wrong addresses in r2 in case of TE 691 | if insn["offset"] not in self._pei_filter_list: 692 | pei_list.append( 693 | UefiService(address=insn["offset"], name=service["name"]) 694 | ) 695 | self._pei_filter_list.append(insn["offset"]) 696 | 697 | return pei_list 698 | 699 | @property 700 | def pei_services(self) -> List[UefiService]: 701 | """Find all PEI services""" 702 | 703 | if self._pei_services is None: 704 | self._pei_services = self._get_pei_services() 705 | return self._pei_services 706 | 707 | def _get_ppi_list(self) -> List[UefiProtocol]: 708 | ppi_list: List[UefiProtocol] = list() 709 | for pei_service in self.pei_services: 710 | if pei_service.name != "LocatePpi": 711 | continue 712 | block_insns = self._rz.cmdj("pdj -16 @ {:#x}".format(pei_service.address)) 713 | for index in range(len(block_insns) - 1, -1, -1): 714 | esil = block_insns[index]["esil"].split(",") 715 | if not (esil[-1] == "-=" and esil[-2] == "esp" and esil[-3] == "4"): 716 | continue 717 | if "ptr" not in block_insns[index]: 718 | continue 719 | current_block = block_insns[index] 720 | p_guid_addr = current_block["ptr"] 721 | baddr = self.info["bin"]["baddr"] 722 | if p_guid_addr < baddr: 723 | continue 724 | # wrong addresses in r2 in case of TE 725 | p_guid_addr = self._wrong_addr(p_guid_addr) 726 | self._rz.cmd("s {:#x}".format(p_guid_addr)) 727 | p_guid_b = bytes(self._rz.cmdj("xj 16")) 728 | # look up in known list 729 | guid = GUID_FROM_BYTES.get(p_guid_b) 730 | if not guid: 731 | guid = UefiGuid( 732 | value=str(uuid.UUID(bytes_le=p_guid_b)).upper(), 733 | name="proprietary_ppi", 734 | ) 735 | ppi = UefiProtocol( 736 | name=guid.name, 737 | value=guid.value, 738 | guid_address=p_guid_addr, 739 | address=block_insns[index]["offset"], 740 | service=pei_service.name, 741 | ) 742 | if ppi not in ppi_list: 743 | ppi_list.append(ppi) 744 | 745 | return ppi_list 746 | 747 | @property 748 | def ppi_list(self) -> List[UefiProtocol]: 749 | """Find all PPIs""" 750 | 751 | if self._ppi_list is None: 752 | self._ppi_list = self._get_ppi_list() 753 | return self._ppi_list 754 | 755 | @property 756 | def smi_handlers(self) -> List[SmiHandler]: 757 | """Find software SMI handlers""" 758 | 759 | if self._smi_handlers is None: 760 | self._smi_handlers = uefi_smm.get_smi_handlers(self._rz) 761 | return self._smi_handlers 762 | 763 | @property 764 | def child_swsmi_handlers(self) -> List[ChildSwSmiHandler]: 765 | """Find child software SMI handlers""" 766 | 767 | if self._child_swsmi_handlers is None: 768 | self._child_swsmi_handlers = uefi_smm.get_child_sw_smi_handlers( 769 | self._rz, self.smst_list 770 | ) 771 | return self._child_swsmi_handlers 772 | 773 | @property 774 | def smst_list(self) -> List[int]: 775 | """Find list of SMST""" 776 | 777 | if self._smst_list is None: 778 | self._smst_list = uefi_smm.get_smst_list(self._rz) 779 | return self._smst_list 780 | 781 | def get_summary(self) -> Dict[str, Any]: 782 | """Collect all the information in a JSON object""" 783 | 784 | summary = dict() 785 | 786 | for key in self.info: 787 | summary[key] = self.info[key] 788 | 789 | if "bin" not in summary: 790 | return summary 791 | 792 | if not ( 793 | self.info["bin"]["class"].startswith("PE") 794 | or self.info["bin"]["class"].startswith("TE") 795 | ): 796 | return summary 797 | 798 | if self.info["bin"]["arch"] == "x86" and self.info["bin"]["bits"] == 32: 799 | summary["pei_list"] = [x.__dict__ for x in self.pei_services] 800 | summary["ppi_list"] = [x.__dict__ for x in self.ppi_list] 801 | summary["nvram_vars"] = [x.__dict__ for x in self.nvram_vars] 802 | 803 | elif self.info["bin"]["arch"] == "x86" and self.info["bin"]["bits"] == 64: 804 | summary["g_bs"] = self.g_bs 805 | summary["g_rt"] = self.g_rt 806 | summary["g_smst"] = self.smst_list 807 | summary["bs_list"] = [x.__dict__ for x in self.boot_services] 808 | summary["rt_list"] = [x.__dict__ for x in self.runtime_services] 809 | summary["protocols"] = [x.__dict__ for x in self.protocols] 810 | summary["nvram_vars"] = [x.__dict__ for x in self.nvram_vars] 811 | if len(self.smi_handlers) > 0: 812 | summary["smi_handlers"] = [x.__dict__ for x in self.smi_handlers] 813 | if len(self.child_swsmi_handlers) > 0: 814 | summary["child_swsmi_handlers"] = [ 815 | x.__dict__ for x in self.child_swsmi_handlers 816 | ] 817 | 818 | summary["p_guids"] = [x.__dict__ for x in self.protocol_guids] 819 | 820 | return summary 821 | 822 | def get_protocols_info(self) -> Dict[str, Any]: 823 | """Collect all the information in a JSON object""" 824 | 825 | summary = dict() 826 | for key in self.info: 827 | summary[key] = self.info[key] 828 | summary["g_bs"] = self.g_bs 829 | summary["bs_list"] = [x.__dict__ for x in self.boot_services] 830 | summary["protocols"] = [x.__dict__ for x in self.protocols] 831 | return summary 832 | 833 | def close(self) -> None: 834 | """Quits the r2 instance, releasing resources""" 835 | 836 | self._rz.quit() 837 | if self._shm is not None: 838 | self._shm.close() 839 | self._shm.unlink() 840 | 841 | def __exit__( 842 | self, 843 | exc_type: Optional[Type[BaseException]], 844 | exc_value: Optional[BaseException], 845 | traceback: Optional[TracebackType], 846 | ) -> None: 847 | self.close() 848 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /fwhunt_scan/uefi_scanner.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0+ 2 | 3 | """ 4 | Tools for analyzing UEFI firmware using radare2 5 | """ 6 | 7 | import binascii 8 | import json 9 | import logging 10 | import os 11 | from typing import Any, Dict, List, Optional, Tuple 12 | 13 | import yaml 14 | 15 | from fwhunt_scan.uefi_analyzer import ( 16 | NvramVariable, 17 | UefiAnalyzer, 18 | UefiGuid, 19 | UefiProtocol, 20 | UefiService, 21 | ) 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class CodePattern: 27 | """Code pattern""" 28 | 29 | def __init__(self, pattern: str, place: Optional[str]) -> None: 30 | self.pattern: str = str(pattern) 31 | self.place: Optional[str] = place 32 | # if True, scan the whole binary 33 | self.unspecified: bool = place is None 34 | 35 | @property 36 | def __dict__(self): 37 | return dict( 38 | { 39 | "pattern": self.pattern, 40 | "unspecified": self.unspecified, 41 | } 42 | ) 43 | 44 | 45 | class UefiRuleVariant: 46 | """A rule for scanning EFI image (content without meta and variants)""" 47 | 48 | def __init__(self, rule_content: Dict[str, Any]): 49 | self._uefi_rule: Dict[str, Any] = rule_content 50 | self._nvram_vars: Optional[Dict[str, List[NvramVariable]]] = None 51 | self._protocols: Optional[Dict[str, List[UefiProtocol]]] = None 52 | self._ppi_list: Optional[Dict[str, List[UefiProtocol]]] = None 53 | self._guids: Optional[Dict[str, List[UefiGuid]]] = None 54 | self._strings: Optional[Dict[str, List[str]]] = None 55 | self._wide_strings: Optional[Dict[str, List[Dict[str, str]]]] = None 56 | self._hex_strings: Optional[Dict[str, List[str]]] = None 57 | self._code: Optional[Dict[str, List[CodePattern]]] = None 58 | 59 | def _get_code(self) -> Dict[str, List[CodePattern]]: 60 | code: Dict[str, List[CodePattern]] = dict() 61 | if "code" not in self._uefi_rule: 62 | return code 63 | dict_items = dict() 64 | if isinstance(self._uefi_rule["code"], list): 65 | # if kind of matches is not specified 66 | dict_items["and"] = self._uefi_rule["code"] 67 | elif isinstance(self._uefi_rule["code"], dict): 68 | dict_items = self._uefi_rule["code"] 69 | else: 70 | return code 71 | for op in dict_items: 72 | code[op] = list() 73 | for c in dict_items[op]: 74 | cp = CodePattern( 75 | pattern=c.get("pattern", None), 76 | place=c.get("place", None), 77 | ) 78 | code[op].append(cp) 79 | return code 80 | 81 | @property 82 | def code(self) -> Dict[str, List[CodePattern]]: 83 | """Get code from rule""" 84 | 85 | if self._code is None: 86 | self._code = self._get_code() 87 | return self._code 88 | 89 | def _get_strings(self) -> Dict[str, List[str]]: 90 | strings: Dict[str, List[str]] = dict() 91 | if "strings" not in self._uefi_rule: 92 | return strings 93 | if isinstance(self._uefi_rule["strings"], list): 94 | # if kind of matches is not specified 95 | strings["and"] = self._uefi_rule["strings"] 96 | elif isinstance(self._uefi_rule["strings"], dict): 97 | strings = self._uefi_rule["strings"] 98 | else: 99 | return strings 100 | return strings 101 | 102 | @property 103 | def strings(self) -> Dict[str, List[str]]: 104 | """Get strings from rule""" 105 | 106 | if self._strings is None: 107 | self._strings = self._get_strings() 108 | return self._strings 109 | 110 | def _get_wide_strings(self) -> Dict[str, List[Dict[str, str]]]: 111 | wide_strings: Dict[str, List[Dict[str, str]]] = dict() 112 | if "wide_strings" not in self._uefi_rule: 113 | return wide_strings 114 | if isinstance(self._uefi_rule["wide_strings"], list): 115 | # if kind of matches is not specified 116 | wide_strings["and"] = self._uefi_rule["wide_strings"] 117 | elif isinstance(self._uefi_rule["wide_strings"], dict): 118 | wide_strings = self._uefi_rule["wide_strings"] 119 | else: 120 | return wide_strings 121 | return wide_strings 122 | 123 | @property 124 | def wide_strings(self) -> Dict[str, List[Dict[str, str]]]: 125 | """Get wide strings from rule""" 126 | 127 | if self._wide_strings is None: 128 | self._wide_strings = self._get_wide_strings() 129 | return self._wide_strings 130 | 131 | def _get_hex_strings(self) -> Dict[str, List[str]]: 132 | hex_strings: Dict[str, List[str]] = dict() 133 | if "hex_strings" not in self._uefi_rule: 134 | return hex_strings 135 | if isinstance(self._uefi_rule["hex_strings"], list): 136 | # if kind of matches is not specified 137 | hex_strings["and"] = self._uefi_rule["hex_strings"] 138 | elif isinstance(self._uefi_rule["hex_strings"], dict): 139 | hex_strings = self._uefi_rule["hex_strings"] 140 | else: 141 | return hex_strings 142 | return hex_strings 143 | 144 | @property 145 | def hex_strings(self) -> Dict[str, List[str]]: 146 | """Get hex strings from rule""" 147 | 148 | if self._hex_strings is None: 149 | self._hex_strings = self._get_hex_strings() 150 | return self._hex_strings 151 | 152 | def _get_nvram_vars(self) -> Dict[str, List[NvramVariable]]: 153 | nvram_vars: Dict[str, List[NvramVariable]] = dict() 154 | if "nvram" not in self._uefi_rule: 155 | return nvram_vars 156 | dict_items = dict() 157 | if isinstance(self._uefi_rule["nvram"], list): 158 | # if kind of matches is not specified 159 | dict_items["and"] = self._uefi_rule["nvram"] 160 | elif isinstance(self._uefi_rule["nvram"], dict): 161 | dict_items = self._uefi_rule["nvram"] 162 | else: 163 | return nvram_vars 164 | for op in dict_items: 165 | nvram_vars[op] = list() 166 | for element in dict_items[op]: 167 | nvram_vars[op].append( 168 | NvramVariable( 169 | name=element["name"], 170 | guid=element["guid"], 171 | service=UefiService( 172 | name=element["service"]["name"], address=0x0 173 | ), 174 | ) 175 | ) 176 | return nvram_vars 177 | 178 | @property 179 | def nvram_vars(self) -> Dict[str, List[NvramVariable]]: 180 | """Get NVRAM variables from rule""" 181 | 182 | if self._nvram_vars is None: 183 | self._nvram_vars = self._get_nvram_vars() 184 | return self._nvram_vars 185 | 186 | def _get_protocols(self) -> Dict[str, List[UefiProtocol]]: 187 | protocols: Dict[str, List[UefiProtocol]] = dict() 188 | if "protocols" not in self._uefi_rule: 189 | return protocols 190 | dict_items = dict() 191 | if isinstance(self._uefi_rule["protocols"], list): 192 | # if kind of matches is not specified 193 | dict_items["and"] = self._uefi_rule["protocols"] 194 | elif isinstance(self._uefi_rule["protocols"], dict): 195 | dict_items = self._uefi_rule["protocols"] 196 | else: 197 | return protocols 198 | for op in dict_items: 199 | protocols[op] = list() 200 | for element in dict_items[op]: 201 | protocols[op].append( 202 | UefiProtocol( 203 | name=element["name"], 204 | value=element["value"], 205 | service=element["service"]["name"], 206 | address=0x0, 207 | guid_address=0x0, 208 | ) 209 | ) 210 | return protocols 211 | 212 | @property 213 | def protocols(self) -> Dict[str, List[UefiProtocol]]: 214 | """Get protocols from rule""" 215 | 216 | if self._protocols is None: 217 | self._protocols = self._get_protocols() 218 | return self._protocols 219 | 220 | def _get_ppi_list(self) -> Dict[str, List[UefiProtocol]]: 221 | ppi_list: Dict[str, List[UefiProtocol]] = dict() 222 | if "ppi" not in self._uefi_rule: 223 | return ppi_list 224 | dict_items = dict() 225 | if isinstance(self._uefi_rule["ppi"], list): 226 | # if kind of matches is not specified 227 | dict_items["and"] = self._uefi_rule["ppi"] 228 | elif isinstance(self._uefi_rule["ppi"], dict): 229 | dict_items = self._uefi_rule["ppi"] 230 | else: 231 | return ppi_list 232 | for op in dict_items: 233 | ppi_list[op] = list() 234 | for element in dict_items[op]: 235 | ppi_list[op].append( 236 | UefiProtocol( 237 | name=element["name"], 238 | value=element["value"], 239 | service=element["service"]["name"], 240 | address=0x0, 241 | guid_address=0x0, 242 | ) 243 | ) 244 | return ppi_list 245 | 246 | @property 247 | def ppi_list(self) -> Dict[str, List[UefiProtocol]]: 248 | """Get PPI list from rule""" 249 | 250 | if self._ppi_list is None: 251 | self._ppi_list = self._get_ppi_list() 252 | return self._ppi_list 253 | 254 | def _get_guids(self) -> Dict[str, List[UefiGuid]]: 255 | guids: Dict[str, List[UefiGuid]] = dict() 256 | if "guids" not in self._uefi_rule: 257 | return guids 258 | dict_items = dict() 259 | if isinstance(self._uefi_rule["guids"], list): 260 | # if kind of matches is not specified 261 | dict_items["and"] = self._uefi_rule["guids"] 262 | elif isinstance(self._uefi_rule["guids"], dict): 263 | dict_items = self._uefi_rule["guids"] 264 | else: 265 | return guids 266 | for op in dict_items: 267 | guids[op] = list() 268 | for element in dict_items[op]: 269 | guids[op].append( 270 | UefiGuid( 271 | name=element["name"], 272 | value=element["value"], 273 | ) 274 | ) 275 | return guids 276 | 277 | @property 278 | def guids(self) -> Dict[str, List[UefiGuid]]: 279 | """Get GUIDs from rule""" 280 | 281 | if self._guids is None: 282 | self._guids = self._get_guids() 283 | return self._guids 284 | 285 | 286 | class UefiRule: 287 | """A rule for scanning EFI image""" 288 | 289 | def __init__( 290 | self, rule_path: Optional[str] = None, rule_content: Optional[str] = None 291 | ): 292 | self._rule: Optional[str] = rule_path 293 | self._rule_name: str = str() 294 | self._uefi_rule: Dict[str, Any] = dict() 295 | if self._rule is not None: 296 | if os.path.isfile(self._rule): 297 | try: 298 | with open(self._rule, "r") as f: 299 | self._uefi_rule = yaml.safe_load(f) 300 | except yaml.scanner.ScannerError as e: 301 | logger.error(repr(e)) 302 | elif rule_content is not None: 303 | try: 304 | self._uefi_rule = yaml.safe_load(rule_content) 305 | except yaml.scanner.ScannerError as e: 306 | logger.error(repr(e)) 307 | if self._uefi_rule: 308 | self._rule_name = list(self._uefi_rule.keys())[0] 309 | if self._rule_name: 310 | self._uefi_rule = self._uefi_rule[self._rule_name] 311 | self._variants: Optional[Dict[str, UefiRuleVariant]] = None 312 | 313 | def __str__(self): 314 | return json.dumps(self._uefi_rule, indent=2) 315 | 316 | @property 317 | def author(self) -> Optional[str]: 318 | """Get author from the metadata block""" 319 | 320 | try: 321 | return self._uefi_rule["meta"]["author"] 322 | except KeyError: 323 | return None 324 | 325 | @property 326 | def name(self) -> Optional[str]: 327 | """Get rule name from the metadata block""" 328 | 329 | try: 330 | return self._uefi_rule["meta"]["name"] 331 | except KeyError: 332 | return None 333 | 334 | @property 335 | def version(self) -> Optional[str]: 336 | """Get rule version from the metadata block""" 337 | 338 | try: 339 | return self._uefi_rule["meta"]["version"] 340 | except KeyError: 341 | return None 342 | 343 | @property 344 | def namespace(self) -> Optional[str]: 345 | """Get rule namespace from the metadata block""" 346 | 347 | try: 348 | return self._uefi_rule["meta"]["namespace"] 349 | except KeyError: 350 | return None 351 | 352 | @property 353 | def license(self) -> Optional[str]: 354 | """Get rule license from the metadata block""" 355 | 356 | try: 357 | return self._uefi_rule["meta"]["license"] 358 | except KeyError: 359 | return None 360 | 361 | @property 362 | def cve_number(self) -> Optional[str]: 363 | """Get CVE number from the metadata block""" 364 | 365 | try: 366 | return self._uefi_rule["meta"]["CVE number"] 367 | except KeyError: 368 | return None 369 | 370 | @property 371 | def vendor_id(self) -> Optional[str]: 372 | """Get vendor id from the metadata block""" 373 | 374 | try: 375 | return self._uefi_rule["meta"]["vendor id"] 376 | except KeyError: 377 | return None 378 | 379 | @property 380 | def cvss_score(self) -> Optional[str]: 381 | """Get CVSS score from the metadata block""" 382 | 383 | try: 384 | return self._uefi_rule["meta"]["CVSS score"] 385 | except KeyError: 386 | return None 387 | 388 | @property 389 | def advisory(self) -> Optional[str]: 390 | """Get advisory link from the metadata block""" 391 | 392 | try: 393 | return self._uefi_rule["meta"]["advisory"] 394 | except KeyError: 395 | return None 396 | 397 | @property 398 | def description(self) -> Optional[str]: 399 | """Get optional rule description from the metadata block""" 400 | 401 | try: 402 | return self._uefi_rule["meta"]["description"] 403 | except KeyError: 404 | return None 405 | 406 | @property 407 | def url(self) -> Optional[str]: 408 | """Get optional rule URL from the metadata block""" 409 | 410 | try: 411 | return self._uefi_rule["meta"]["url"] 412 | except KeyError: 413 | return None 414 | 415 | @property 416 | def target(self) -> Optional[str]: 417 | """Get optional rule target from the metadata block""" 418 | 419 | try: 420 | return self._uefi_rule["meta"]["target"] 421 | except KeyError: 422 | return None 423 | 424 | @property 425 | def volume_guids(self) -> List[str]: 426 | """Get any volume GUIDs from the metadata block""" 427 | 428 | try: 429 | return self._uefi_rule["meta"]["volume guids"] 430 | except KeyError: 431 | return list() 432 | 433 | def _get_variants(self) -> Dict[str, UefiRuleVariant]: 434 | """Get rules variants""" 435 | 436 | variants: Dict[str, UefiRuleVariant] = dict() 437 | if "variants" in self._uefi_rule: 438 | for variant in self._uefi_rule["variants"]: 439 | variants[variant] = UefiRuleVariant( 440 | rule_content=self._uefi_rule["variants"][variant] 441 | ) 442 | 443 | if "variants" not in self._uefi_rule: 444 | rule_content = { 445 | k: self._uefi_rule[k] for k in self._uefi_rule if k != "meta" 446 | } 447 | variants["default"] = UefiRuleVariant(rule_content=rule_content) 448 | 449 | return variants 450 | 451 | @property 452 | def variants(self) -> Dict[str, UefiRuleVariant]: 453 | """Get GUIDs from rule""" 454 | 455 | if self._variants is None: 456 | self._variants = self._get_variants() 457 | return self._variants 458 | 459 | 460 | class UefiScannerError(Exception): 461 | """Generic scanner error exception.""" 462 | 463 | def __init__(self, value: str) -> None: 464 | self.value = value 465 | 466 | def __str__(self): 467 | return repr(self.value) 468 | 469 | 470 | class UefiScannerRes: 471 | """Scanner result for single rule""" 472 | 473 | def __init__(self, rule: UefiRule, variant_label: str, res: bool) -> None: 474 | self.rule = rule 475 | self.variant_label = variant_label 476 | self.res = res 477 | 478 | 479 | class UefiScanner: 480 | """helper object for scanning an EFI image with multiple rules""" 481 | 482 | PROTOCOL: int = 0 483 | PPI: int = 1 484 | 485 | def __init__(self, uefi_analyzer: UefiAnalyzer, uefi_rules: List[UefiRule]): 486 | self._uefi_analyzer: UefiAnalyzer = uefi_analyzer 487 | self._uefi_rules: List[UefiRule] = uefi_rules 488 | self._valid_rules: bool = self._check_rules() 489 | if not self._valid_rules: 490 | raise UefiScannerError( 491 | "Invalid rule format. Visit https://github.com/binarly-io/FwHunt to find the latest version of the rules format." 492 | ) 493 | self._results: Optional[List[UefiScannerRes]] = None 494 | 495 | def _check_rule(self, rule: UefiRule) -> bool: 496 | variants: Optional[Dict[str, UefiRuleVariant]] = None 497 | try: 498 | variants = rule.variants 499 | except KeyError: 500 | return False 501 | 502 | if not isinstance(variants, dict): 503 | return False 504 | 505 | for label in variants: 506 | if not isinstance(label, str): 507 | return False 508 | if not isinstance(variants[label], UefiRuleVariant): 509 | return False 510 | 511 | return True 512 | 513 | def _check_rules(self) -> bool: 514 | for rule in self._uefi_rules: 515 | if not self._check_rule(rule): 516 | return False 517 | return True 518 | 519 | def _and_strings(self, strings: List[str]) -> bool: 520 | res = True 521 | for string in strings: 522 | res &= not not self._uefi_analyzer._rz.cmdj("/j {}".format(string)) 523 | if not res: 524 | break 525 | return res 526 | 527 | def _or_strings(self, strings: List[str]) -> bool: 528 | res = False 529 | for string in strings: 530 | res |= not not self._uefi_analyzer._rz.cmdj("/j {}".format(string)) 531 | if res: 532 | break 533 | return res 534 | 535 | def _and_hex_strings(self, strings: List[str]) -> bool: 536 | res = True 537 | for string in strings: 538 | res &= not not self._uefi_analyzer._rz.cmdj("/xj {}".format(string)) 539 | if not res: 540 | break 541 | return res 542 | 543 | def _or_hex_strings(self, strings: List[str]) -> bool: 544 | res = False 545 | for string in strings: 546 | res |= not not self._uefi_analyzer._rz.cmdj("/xj {}".format(string)) 547 | if res: 548 | break 549 | return res 550 | 551 | def _and_wide_strings(self, strings: List[dict]) -> bool: 552 | res = True 553 | for item in strings: 554 | if "utf16le" in item: 555 | res &= not not self._uefi_analyzer._rz.cmdj( 556 | "/wj {}".format(item["utf16le"]) 557 | ) 558 | elif "utf16be" in item: 559 | string = item["utf16be"] 560 | res &= not not self._uefi_analyzer._rz.cmdj( 561 | "/xj {}".format( 562 | binascii.hexlify(string.encode("utf-16be")).decode() 563 | ) 564 | ) 565 | else: 566 | raise UefiScannerError("Wrong wide string format") 567 | 568 | if not res: 569 | break 570 | 571 | return res 572 | 573 | def _or_wide_strings(self, strings: List[dict]) -> bool: 574 | res = False 575 | for item in strings: 576 | if "utf16le" in item: 577 | res |= not not self._uefi_analyzer._rz.cmdj( 578 | "/wj {}".format(item["utf16le"]) 579 | ) 580 | elif "utf16be" in item: 581 | string = item["utf16be"] 582 | res |= not not self._uefi_analyzer._rz.cmdj( 583 | "/xj {}".format( 584 | binascii.hexlify(string.encode("utf-16be")).decode() 585 | ) 586 | ) 587 | else: 588 | raise UefiScannerError("Wrong wide string format") 589 | 590 | if res: 591 | break 592 | 593 | return res 594 | 595 | def _strings_scanner(self, rule_strings: Dict[str, List[str]]) -> bool: 596 | """Match strings""" 597 | 598 | final_res = True 599 | for op in rule_strings: 600 | # check kind of matches 601 | if op not in ["and", "or", "not-any", "not-all"]: 602 | raise UefiScannerError( 603 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 604 | ) 605 | 606 | res = True 607 | if op == "and": # AND 608 | res = self._and_strings(rule_strings[op]) 609 | 610 | if op == "or": # OR 611 | res = self._or_strings(rule_strings[op]) 612 | 613 | if op == "not-any": # NOT OR 614 | res = not self._or_strings(rule_strings[op]) 615 | 616 | if op == "not-all": # NOT AND 617 | res = not self._and_strings(rule_strings[op]) 618 | 619 | final_res &= res # AND between all sets of strings 620 | if not final_res: 621 | break 622 | 623 | return final_res 624 | 625 | def _wide_strings_scanner( 626 | self, rule_wide_strings: Dict[str, List[Dict[str, str]]] 627 | ) -> bool: 628 | """Match wide strings""" 629 | 630 | final_res = True 631 | for op in rule_wide_strings: 632 | # check kind of matches 633 | if op not in ["and", "or", "not-any", "not-all"]: 634 | raise UefiScannerError( 635 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 636 | ) 637 | 638 | res = True 639 | if op == "and": # AND 640 | res = self._and_wide_strings(rule_wide_strings[op]) 641 | 642 | if op == "or": # OR 643 | res = self._or_wide_strings(rule_wide_strings[op]) 644 | 645 | if op == "not-any": # NOT OR 646 | res = not self._or_wide_strings(rule_wide_strings[op]) 647 | 648 | if op == "not-all": # NOT AND 649 | res = not self._and_wide_strings(rule_wide_strings[op]) 650 | 651 | final_res &= res # AND between all sets of strings 652 | if not final_res: 653 | break 654 | 655 | return final_res 656 | 657 | def _hex_strings_scanner(self, rule_hex_strings: Dict[str, List[str]]) -> bool: 658 | """Match hex strings""" 659 | 660 | final_res = True 661 | for op in rule_hex_strings: 662 | # check kind of matches 663 | if op not in ["and", "or", "not-any", "not-all"]: 664 | raise UefiScannerError( 665 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 666 | ) 667 | 668 | res = True 669 | if op == "and": # AND 670 | res = self._and_hex_strings(rule_hex_strings[op]) 671 | 672 | if op == "or": # OR 673 | res = self._or_hex_strings(rule_hex_strings[op]) 674 | 675 | if op == "not-any": # NOT OR 676 | res = not self._or_hex_strings(rule_hex_strings[op]) 677 | 678 | if op == "not-all": # NOT AND 679 | res = not self._and_hex_strings(rule_hex_strings[op]) 680 | 681 | final_res &= res # AND between all sets of strings 682 | if not final_res: 683 | break 684 | 685 | return final_res 686 | 687 | def _search_nvram(self, nvram_rule: NvramVariable) -> bool: 688 | for nvram_analyzer in self._uefi_analyzer.nvram_vars: 689 | if ( 690 | nvram_rule.name == nvram_analyzer.name 691 | and nvram_rule.guid == nvram_analyzer.guid 692 | and nvram_rule.service.name == nvram_analyzer.service.name 693 | ): 694 | return True 695 | return False 696 | 697 | def _and_nvram(self, nvram_vars: List[NvramVariable]) -> bool: 698 | res = True 699 | for nvram_var in nvram_vars: 700 | res &= self._search_nvram(nvram_var) 701 | if not res: 702 | break 703 | return res 704 | 705 | def _or_nvram(self, nvram_vars: List[NvramVariable]) -> bool: 706 | res = False 707 | for nvram_var in nvram_vars: 708 | res |= self._search_nvram(nvram_var) 709 | if res: 710 | break 711 | return res 712 | 713 | def _compare_nvram_vars(self, rule_nvram: Dict[str, List[NvramVariable]]) -> bool: 714 | """Compare NVRAM variables""" 715 | 716 | final_res = True 717 | for op in rule_nvram: 718 | # check kind of matches 719 | if op not in ["and", "or", "not-any", "not-all"]: 720 | raise UefiScannerError( 721 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 722 | ) 723 | 724 | res = True 725 | if op == "and": # AND 726 | res = self._and_nvram(rule_nvram[op]) 727 | 728 | if op == "or": # OR 729 | res = self._or_nvram(rule_nvram[op]) 730 | 731 | if op == "not-any": # NOT OR 732 | res = not self._or_nvram(rule_nvram[op]) 733 | 734 | if op == "not-all": # NOT AND 735 | res = not self._and_nvram(rule_nvram[op]) 736 | 737 | final_res &= res # AND between all sets of NVRAM variables 738 | if not final_res: 739 | break 740 | 741 | return final_res 742 | 743 | def _search_protocol(self, protocol_rule: UefiProtocol, mode: int) -> bool: 744 | items: List[UefiProtocol] = self._uefi_analyzer.protocols 745 | if mode == UefiScanner.PPI: 746 | items = self._uefi_analyzer.ppi_list 747 | for protocol_analyzer in items: 748 | if ( 749 | protocol_rule.name == protocol_analyzer.name 750 | and protocol_rule.value == protocol_analyzer.value 751 | and protocol_rule.service == protocol_analyzer.service 752 | ): 753 | return True 754 | return False 755 | 756 | def _and_protocols(self, protocols: List[UefiProtocol], mode: int) -> bool: 757 | res = True 758 | for protocol in protocols: 759 | res &= self._search_protocol(protocol, mode) 760 | if not res: 761 | break 762 | return res 763 | 764 | def _or_protocols(self, protocols: List[UefiProtocol], mode: int) -> bool: 765 | res = False 766 | for protocol in protocols: 767 | res |= self._search_protocol(protocol, mode) 768 | if res: 769 | break 770 | return res 771 | 772 | def _compare_protocols( 773 | self, rule_protocol: Dict[str, List[UefiProtocol]], mode: int 774 | ) -> bool: 775 | """Compare protocols""" 776 | 777 | final_res = True 778 | for op in rule_protocol: 779 | # check kind of matches 780 | if op not in ["and", "or", "not-any", "not-all"]: 781 | raise UefiScannerError( 782 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 783 | ) 784 | 785 | res = True 786 | if op == "and": # AND 787 | res = self._and_protocols(rule_protocol[op], mode) 788 | 789 | if op == "or": # OR 790 | res = self._or_protocols(rule_protocol[op], mode) 791 | 792 | if op == "not-any": # NOT OR 793 | res = not self._or_protocols(rule_protocol[op], mode) 794 | 795 | if op == "not-all": # NOT AND 796 | res = not self._and_protocols(rule_protocol[op], mode) 797 | 798 | final_res &= res # AND between all sets of protocols 799 | if not final_res: 800 | break 801 | 802 | return final_res 803 | 804 | def _search_guid(self, guid_rule: UefiGuid) -> bool: 805 | for guid in self._uefi_analyzer.protocol_guids: 806 | if guid_rule.name == guid.name or guid_rule.value == guid.value: 807 | # True if name or value is matches 808 | # so that UNKNOWN_GUIDs can be specified 809 | return True 810 | return False 811 | 812 | def _and_guids(self, guids: List[UefiGuid]) -> bool: 813 | res = True 814 | for guid in guids: 815 | res &= self._search_guid(guid) 816 | if not res: 817 | break 818 | return res 819 | 820 | def _or_guids(self, guids: List[UefiGuid]) -> bool: 821 | res = False 822 | for guid in guids: 823 | res |= self._search_guid(guid) 824 | if res: 825 | break 826 | return res 827 | 828 | def _compare_guids(self, rule_guid: Dict[str, List[UefiGuid]]) -> bool: 829 | """Compare GUIDs""" 830 | 831 | final_res = True 832 | for op in rule_guid: 833 | # check kind of matches 834 | if op not in ["and", "or", "not-any", "not-all"]: 835 | raise UefiScannerError( 836 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 837 | ) 838 | 839 | res = True 840 | if op == "and": # AND 841 | res = self._and_guids(rule_guid[op]) 842 | 843 | if op == "or": # OR 844 | res = self._or_guids(rule_guid[op]) 845 | 846 | if op == "not-any": # NOT OR 847 | res = not self._or_guids(rule_guid[op]) 848 | 849 | if op == "not-all": # NOT AND 850 | res = not self._and_guids(rule_guid[op]) 851 | 852 | final_res &= res # AND between all sets of GUIDs 853 | if not final_res: 854 | break 855 | 856 | return final_res 857 | 858 | def _get_bounds(self, insns: List[Dict[str, Any]]) -> Tuple: 859 | """Get function end address""" 860 | 861 | funcs = list( 862 | filter( 863 | lambda addr: addr, 864 | [addr.get("offset", None) for addr in self._uefi_analyzer.functions], 865 | ) 866 | ) 867 | funcs.sort() 868 | 869 | start = insns[0].get("offset", None) 870 | end_insn = insns[-1].get("offset", None) 871 | 872 | if start is None or end_insn is None: 873 | return tuple((None, None)) 874 | 875 | if start == funcs[-1]: 876 | return tuple((start, end_insn)) 877 | 878 | try: 879 | start_index = funcs.index(start) 880 | except ValueError: 881 | return tuple((start, end_insn)) 882 | 883 | end_func = funcs[start_index + 1] 884 | if end_insn < end_func: 885 | return tuple((start, end_insn)) 886 | 887 | return tuple((start, end_func)) 888 | 889 | @staticmethod 890 | def _tree_debug(start: int, end: int, depth: int) -> None: 891 | if not depth: 892 | logger.debug( 893 | f"\nFunction tree in the handler at {start:#x} (from {start:#x} to {end:#x})" 894 | ) 895 | else: 896 | prefix = depth * "--" 897 | logger.debug(f"{prefix}{start:#x} (from {start:#x} to {end:#x})") 898 | 899 | def _get_bounds_rec(self, start_addr: int, depth: int, debug: bool) -> bool: 900 | """Recursively traverse the function and find the boundaries of all child functions""" 901 | 902 | self._uefi_analyzer._rz.cmd(f"s {start_addr:#x}") 903 | self._uefi_analyzer._rz.cmd("af") 904 | 905 | insns = self._uefi_analyzer._rz.cmd("pdrj") 906 | # prevent error messages to sys.stderr from rizin: 907 | # https://github.com/rizinorg/rz-pipe/blob/0f7ac66e6d679ebb03be26bf61a33f9ccf199f27/python/rzpipe/open_base.py#L261 908 | try: 909 | insns = json.loads(insns) 910 | except (ValueError, KeyError, TypeError): 911 | return False 912 | 913 | # append function bounds 914 | start, end = self._get_bounds(insns) 915 | 916 | if start is not None and end is not None and start_addr == start: 917 | self._funcs_bounds.append((start, end)) 918 | 919 | if debug: 920 | self._tree_debug(start_addr, end, depth) 921 | depth += 1 922 | 923 | # scan child functions 924 | for insn in insns: 925 | if insn.get("type", None) != "call": 926 | continue 927 | if "esil" not in insn: 928 | continue 929 | esil = insn["esil"].split(",") 930 | if esil[-3:] != ["=[]", "rip", "="]: 931 | continue 932 | try: 933 | address = int(esil[0]) 934 | if address not in self._rec_addrs: 935 | self._rec_addrs.append(address) 936 | self._get_bounds_rec(address, depth, debug) 937 | except ValueError: 938 | continue 939 | 940 | return True 941 | 942 | def _hex_strings_scanner_bounds(self, pattern: str, start: int, end: int) -> bool: 943 | """Match hex strings""" 944 | 945 | res = self._uefi_analyzer._rz.cmdj(f"/xj {pattern}") 946 | if not res: 947 | return False 948 | 949 | for sres in res: 950 | offset = sres.get("offset", None) 951 | if offset is None: 952 | continue 953 | 954 | if offset >= start and offset <= end: 955 | return True 956 | 957 | return False 958 | 959 | def _clear_cache(self) -> None: 960 | self._funcs_bounds: List[Any] = list() 961 | self._rec_addrs: List[Any] = list() 962 | 963 | def _code_scan_rec(self, address: int, pattern: str) -> bool: 964 | self._clear_cache() 965 | 966 | self._get_bounds_rec(address, depth=0, debug=False) 967 | if not len(self._funcs_bounds): 968 | return False 969 | 970 | for start, end in self._funcs_bounds: 971 | if self._hex_strings_scanner_bounds(pattern, start, end): 972 | return True 973 | 974 | return False 975 | 976 | def _get_handlers(self, c: CodePattern) -> List[Any]: 977 | if c.place == "child_sw_smi_handlers": 978 | return self._uefi_analyzer.child_swsmi_handlers 979 | if c.place == "smi_handlers": 980 | return self._uefi_analyzer.smi_handlers 981 | return [ 982 | handler 983 | for handler in self._uefi_analyzer.smi_handlers 984 | if handler.place == c.place 985 | ] 986 | 987 | def _single_code_scan(self, c: CodePattern) -> bool: 988 | search_res = False 989 | if c.unspecified: 990 | return not not self._uefi_analyzer._rz.cmdj("/xj {}".format(c.pattern)) 991 | for handler in self._get_handlers(c): 992 | search_res = self._code_scan_rec(handler.address, c.pattern) 993 | if search_res: 994 | break 995 | return search_res 996 | 997 | def _and_code(self, cs: List[CodePattern]) -> bool: 998 | res = True 999 | for c in cs: 1000 | res &= self._single_code_scan(c) 1001 | if not res: 1002 | break 1003 | 1004 | return res 1005 | 1006 | def _or_code(self, cs: List[CodePattern]) -> bool: 1007 | res = False 1008 | for c in cs: 1009 | res |= self._single_code_scan(c) 1010 | if res: 1011 | break 1012 | 1013 | return res 1014 | 1015 | def _code_scanner(self, rule_code: Dict[str, List[CodePattern]]) -> bool: 1016 | """Compare code patterns""" 1017 | 1018 | final_res = True 1019 | for op in rule_code: 1020 | # check kind of matches 1021 | if op not in ["and", "or", "not-any", "not-all"]: 1022 | raise UefiScannerError( 1023 | f"Invalid kind of matches: {op} (possible kinds of matches: and, or, not-any, not-all)" 1024 | ) 1025 | 1026 | res = True 1027 | if op == "and": # AND 1028 | res = self._and_code(rule_code[op]) 1029 | 1030 | if op == "or": # OR 1031 | res = self._or_code(rule_code[op]) 1032 | 1033 | if op == "not-any": # NOT OR 1034 | res = not self._or_code(rule_code[op]) 1035 | 1036 | if op == "not-all": # NOT AND 1037 | res = not self._and_code(rule_code[op]) 1038 | 1039 | final_res &= res # AND between all sets of NVRAM variables 1040 | if not final_res: 1041 | break 1042 | 1043 | return final_res 1044 | 1045 | def _get_results_variants( 1046 | self, rule_variant: UefiRuleVariant, target: Optional[str] 1047 | ) -> bool: 1048 | if target in ["firmware"]: 1049 | # match hex strings 1050 | return self._hex_strings_scanner(rule_variant.hex_strings) 1051 | 1052 | res = True 1053 | 1054 | # compare NVRAM variables 1055 | res &= self._compare_nvram_vars(rule_variant.nvram_vars) 1056 | if not res: 1057 | return res 1058 | 1059 | # compare protocols 1060 | res &= self._compare_protocols(rule_variant.protocols, UefiScanner.PROTOCOL) 1061 | if not res: 1062 | return res 1063 | 1064 | # compare GUIDs 1065 | res &= self._compare_guids(rule_variant.guids) 1066 | if not res: 1067 | return res 1068 | 1069 | # compare PPI 1070 | res &= self._compare_protocols(rule_variant.ppi_list, UefiScanner.PPI) 1071 | if not res: 1072 | return res 1073 | 1074 | # match code patterns 1075 | res &= self._code_scanner(rule_variant.code) 1076 | if not res: 1077 | return res 1078 | 1079 | # match strings 1080 | res &= self._strings_scanner(rule_variant.strings) 1081 | if not res: 1082 | return res 1083 | 1084 | # match wide strings 1085 | res &= self._wide_strings_scanner(rule_variant.wide_strings) 1086 | if not res: 1087 | return res 1088 | 1089 | # match hex strings 1090 | res &= self._hex_strings_scanner(rule_variant.hex_strings) 1091 | if not res: 1092 | return res 1093 | 1094 | return res 1095 | 1096 | def _get_results(self) -> List[UefiScannerRes]: 1097 | results: List[UefiScannerRes] = list() 1098 | 1099 | for uefi_rule in self._uefi_rules: 1100 | for variant in uefi_rule.variants: 1101 | res = self._get_results_variants( 1102 | uefi_rule.variants[variant], uefi_rule.target 1103 | ) 1104 | results.append( 1105 | UefiScannerRes(rule=uefi_rule, variant_label=variant, res=res) 1106 | ) 1107 | 1108 | return results 1109 | 1110 | @property 1111 | def results(self) -> List[UefiScannerRes]: 1112 | """Get scanning results as a list of matched rules""" 1113 | 1114 | if self._results is None: 1115 | self._results = self._get_results() 1116 | 1117 | return self._results 1118 | --------------------------------------------------------------------------------