From e9a1b21c9399b711ba4f7eb1402ab49cc857ed99 Mon Sep 17 00:00:00 2001 From: Aaron <105021049+apop5@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:22:08 -0700 Subject: [PATCH] Adding Uefi Variable Support. (#577) * Adding Uefi Variable Support. Adding cspell exceptions * Adding pywin32 to dependencies to support uefivariablesupport --- .cspell.json | 7 +- edk2toollib/os/__init__.py | 6 + edk2toollib/os/uefivariablesupport.py | 331 ++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 edk2toollib/os/__init__.py create mode 100644 edk2toollib/os/uefivariablesupport.py diff --git a/.cspell.json b/.cspell.json index 3b353840..ca3541ac 100644 --- a/.cspell.json +++ b/.cspell.json @@ -107,6 +107,11 @@ "urlunsplit", "vcvarsall", "vsvars", - "vswhere" + "vswhere", + "uefivariablesupport", + "ntdll", + "NTSTATUS", + "efivarfs", + "chattr" ] } diff --git a/edk2toollib/os/__init__.py b/edk2toollib/os/__init__.py new file mode 100644 index 00000000..87eb3dce --- /dev/null +++ b/edk2toollib/os/__init__.py @@ -0,0 +1,6 @@ +## +# Copyright (c) Microsoft Corporation +# +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +"""A python class to allow interaction with Uefi Variables from an OS.""" diff --git a/edk2toollib/os/uefivariablesupport.py b/edk2toollib/os/uefivariablesupport.py new file mode 100644 index 00000000..3b5719f9 --- /dev/null +++ b/edk2toollib/os/uefivariablesupport.py @@ -0,0 +1,331 @@ +# @file uefivariablesupport.py +# +# Exports Class to allow OS level interaction +# with UEFI variables. Includes Windows and +# Linux support. +# +# +# Copyright (c), Microsoft Corporation +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# GetUefiAllVarNames linux implementation is based on information from +# https://github.com/awslabs/python-uefivars/blob/main/pyuefivars/efivarfs.py +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT +"""Exports UefiVariables class to allow OS level interaction with Uefi Variables. Includes Windows and Linux support. + +This module provides: +- UefiVariable: Class to interfact with Uefi Variables from OS. +""" + +import logging +import os +import struct +import uuid +from ctypes import create_string_buffer + +if os.name == 'nt': + from ctypes import WinError, c_int, c_void_p, c_wchar_p, pointer, windll + from ctypes.wintypes import DWORD + + from win32 import win32api, win32process, win32security + + +EFI_VAR_MAX_BUFFER_SIZE = 1024 * 1024 + + +class UefiVariable(object): + """Class to interact with Uefi Variables under Windows or Linux. + + Methods: + ------- + GetUefiVar - Get a single Uefi Variable's data + GetUefiAllVarNames - Get all Uefi variables + SetUefiVar - Set (or delete) a single Uefi variable. + """ + ERROR_ENVVAR_NOT_FOUND = 0xcb + + def __init__(self) -> None: + """Initialize Class.""" + if os.name == 'nt': + # enable required SeSystemEnvironmentPrivilege privilege + privilege = win32security.LookupPrivilegeValue( + None, "SeSystemEnvironmentPrivilege" + ) + token = win32security.OpenProcessToken( + win32process.GetCurrentProcess(), + win32security.TOKEN_READ | win32security.TOKEN_ADJUST_PRIVILEGES, + ) + win32security.AdjustTokenPrivileges( + token, False, [(privilege, win32security.SE_PRIVILEGE_ENABLED)] + ) + win32api.CloseHandle(token) + + # import firmware variable API + try: + self._GetFirmwareEnvironmentVariable = ( + windll.kernel32.GetFirmwareEnvironmentVariableW + ) + self._GetFirmwareEnvironmentVariable.restype = c_int + self._GetFirmwareEnvironmentVariable.argtypes = [ + c_wchar_p, + c_wchar_p, + c_void_p, + c_int, + ] + self._EnumerateFirmwareEnvironmentVariable = ( + windll.ntdll.NtEnumerateSystemEnvironmentValuesEx + ) + self._EnumerateFirmwareEnvironmentVariable.restype = c_int + self._EnumerateFirmwareEnvironmentVariable.argtypes = [ + c_int, + c_void_p, + c_void_p + ] + self._SetFirmwareEnvironmentVariable = ( + windll.kernel32.SetFirmwareEnvironmentVariableW + ) + self._SetFirmwareEnvironmentVariable.restype = c_int + self._SetFirmwareEnvironmentVariable.argtypes = [ + c_wchar_p, + c_wchar_p, + c_void_p, + c_int, + ] + self._SetFirmwareEnvironmentVariableEx = ( + windll.kernel32.SetFirmwareEnvironmentVariableExW + ) + self._SetFirmwareEnvironmentVariableEx.restype = c_int + self._SetFirmwareEnvironmentVariableEx.argtypes = [ + c_wchar_p, + c_wchar_p, + c_void_p, + c_int, + c_int, + ] + except Exception: + logging.warn( + "Collecting windll Variable functions encountered an error." + ) + else: + pass + + def GetUefiVar(self, name: str, guid: str) -> tuple[int, str]: + """Get a Uefi Variable from the system. + + Args: + name (str): String corresponding to the Unicode Name of the variable. + guid (str): String corresponding to the Uefi Guid of the variable. + + Returns: + Tuple: (error code, string of variable data) + """ + err = 0 + if os.name == 'nt': + efi_var = create_string_buffer(EFI_VAR_MAX_BUFFER_SIZE) + if self._GetFirmwareEnvironmentVariable is not None: + logging.info( + "calling GetFirmwareEnvironmentVariable( name='%s', GUID='%s' ).." + % (name, "{%s}" % guid) + ) + length = self._GetFirmwareEnvironmentVariable( + name, "{%s}" % guid, efi_var, EFI_VAR_MAX_BUFFER_SIZE + ) + if (0 == length) or (efi_var is None): + err = windll.kernel32.GetLastError() + if err != 0 and err != UefiVariable.ERROR_ENVVAR_NOT_FOUND: + logging.error( + "GetFirmwareEnvironmentVariable[Ex] failed (GetLastError = 0x%x)" % err + ) + logging.error(WinError()) + if efi_var is None: + return (err, None) + return (err, efi_var[:length]) + + else: + # the variable name is VariableName-Guid + path = '/sys/firmware/efi/efivars/' + name + '-%s' % guid + + if not os.path.exists(path): + err = UefiVariable.ERROR_ENVVAR_NOT_FOUND + return (err, None) + + efi_var = create_string_buffer(EFI_VAR_MAX_BUFFER_SIZE) + with open(path, 'rb') as fd: + efi_var = fd.read() + + return (err, efi_var[:length]) + + def GetUefiAllVarNames(self) -> tuple[int, bytes]: + """Get all Uefi Variables in the system, and return a byte packed byte string. + + Raises: + Exception: Returned variable could not be parsed. + + Returns: + tuple[int, bytes]: + Integer is return status, 0 for error, non-zero for success. + Bytes are the packed structure for each variable. + struct _VARIABLE_NAME { + ULONG NextEntryOffset; + GUID VendorGuid; + WCHAR Name[ANYSIZE_ARRAY]; + } + """ + status = 0 + if os.name == 'nt': + # From NTSTATUS definition: + # (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) + STATUS_BUFFER_TOO_SMALL = 0xC0000023 + VARIABLE_INFORMATION_NAMES = 1 + + length = DWORD(0) + efi_var_names = create_string_buffer(length.value) + if self._EnumerateFirmwareEnvironmentVariable is not None: + logging.info( + "calling _EnumerateFirmwareEnvironmentVariable to get size.." + ) + status = self._EnumerateFirmwareEnvironmentVariable( + VARIABLE_INFORMATION_NAMES, efi_var_names, pointer(length)) + # Only inspect the lower 32 bits. + status = (0xFFFFFFFF & status) + if status == STATUS_BUFFER_TOO_SMALL: + logging.info( + "calling _EnumerateFirmwareEnvironmentVariable again to get data..") + efi_var_names = create_string_buffer(length.value) + status = self._EnumerateFirmwareEnvironmentVariable( + VARIABLE_INFORMATION_NAMES, efi_var_names, pointer( + length)) + if (0 != status): + logging.error( + "EnumerateFirmwareEnvironmentVariable failed (GetLastError = 0x%x)" % status + ) + return (status, None) + return (status, efi_var_names) + else: + # implementation borrowed from https://github.com/awslabs/python-uefivars/blob/main/pyuefivars/efivarfs.py + path = '/sys/firmware/efi/efivars' + if not os.path.exists(path): + status = UefiVariable.ERROR_ENVVAR_NOT_FOUND + return (status, None) + + vars = os.listdir(path) + + # get the total buffer length, converting to unicode + length = 0 + offset = 0 + for var in vars: + split_string = var.split('-') + name = '-'.join(split_string[:-5]) + name = name.encode('utf-16-le') + name_len = len(name) + length += (4 + 16 + name_len) + + efi_var_names = create_string_buffer(length) + + for var in vars: + # efivarfs stores vars as NAME-GUID + split_string = var.split('-') + try: + # GUID is last 5 elements of split_string + guid = uuid.UUID('-'.join(split_string[-5:])).bytes_le + except ValueError: + raise Exception(f'Could not parse "{var}"') + + # the other part is the name + name = '-'.join(split_string[:-5]) + name = name.encode('utf-16-le') + name_len = len(name) + + # NextEntryOffset + struct.pack_into(' int: + """Set a Uefi Variable into the system. + + Args: + name (str): Unicode name of variable to set + guid (str): Guid to use when setting the variable + var (Optional[bytes]): Bytes to set to the variable. Defaults to None. + attrs (Optional[int]): Attributes to use when setting the variable. Defaults to None. + + Returns: + int: 0 for a failure, non-zero for success + """ + success = 0 # Fail + + if os.name == 'nt': + var_len = 0 + err = 0 + if var is None: + var = bytes(0) + else: + var_len = len(var) + success = 0 # Fail + if attrs is None: + if self._SetFirmwareEnvironmentVariable is not None: + logging.info( + "Calling SetFirmwareEnvironmentVariable (name='%s', Guid='%s')..." + % ( + name, + "{%s}" % guid, + ) + ) + success = self._SetFirmwareEnvironmentVariable( + name, "{%s}" % guid, var, var_len + ) + else: + attrs = int(attrs) + if self._SetFirmwareEnvironmentVariableEx is not None: + logging.info( + f"SetFirmwareEnvironmentVariableEx( name={name}," + + f"GUID={guid}, length={var_len}, attributes={attrs} )" + ) + success = self._SetFirmwareEnvironmentVariableEx( + name, "{%s}" % guid, var, var_len, attrs + ) + + if 0 == success: + err = windll.kernel32.GetLastError() + logging.error( + "SetFirmwareEnvironmentVariable failed (GetLastError = 0x%x)" % err + ) + logging.error(WinError()) + return success + else: + # There is a null terminator at the end of the name + path = '/sys/firmware/efi/efivars/' + name[:-1] + '-' + str(guid) + if var is None: + # we are deleting the variable + if (os.path.exists(path)): + os.remove(path) + success = 1 # expect non-zero success + return success + + if attrs is None: + attrs = 0x7 + + # if the file exists, remove the immutable flag + if (os.path.exists(path)): + os.system('sudo chattr -i ' + path) + + with open(path, 'wb') as fd: + # var data is attribute (UINT32) followed by data + packed = struct.pack('=I', attrs) + packed += var + fd.write(packed) + + return 1 diff --git a/pyproject.toml b/pyproject.toml index b1d3badd..c840aaa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "GitPython >= 3.1.30", "sqlalchemy >= 2.0.0", "pygount >= 1.6.1", + "pywin32==306 ; sys_platform == 'win32'", ] classifiers=[ "Programming Language :: Python :: 3",