#CHIPSEC: Platform Security Assessment Framework
#Copyright (c) 2010-2016, Intel Corporation
# 
#This program is free software; you can redistribute it and/or
#modify it under the terms of the GNU General Public License
#as published by the Free Software Foundation; Version 2.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
#Contact information:
#chipsec@intel.com
#



"""
Checks protections of the S3 resume boot-script implemented by the UEFI based firmware

References:

`VU#976132 UEFI implementations do not properly secure the EFI S3 Resume Boot Path boot script <https://www.kb.cert.org/vuls/id/976132>`_

`Technical Details of the S3 Resume Boot Script Vulnerability <http://www.intelsecurity.com/advanced-threat-research/content/WP_Intel_ATR_S3_ResBS_Vuln.pdf>`_ by Intel Security's Advanced Threat Research team. 

`Attacks on UEFI Security <https://events.ccc.de/congress/2014/Fahrplan/system/attachments/2557/original/AttacksOnUEFI_Slides.pdf>`_ by Rafal Wojtczuk and Corey Kallenberg.

`Attacking UEFI Boot Script <https://bromiumlabs.files.wordpress.com/2015/01/venamis_whitepaper.pdf>`_ by Rafal Wojtczuk and Corey Kallenberg.

`Exploiting UEFI boot script table vulnerability <http://blog.cr4.sh/2015/02/exploiting-uefi-boot-script-table.html>`_ by Dmytro Oleksiuk.

Usage:

    >>> chipsec_main.py -m common.uefi.s3bootscript [-a <script_address>]

Examples:

    >>> chipsec_main.py -m common.uefi.s3bootscript
    >>> chipsec_main.py -m common.uefi.s3bootscript -a 0x00000000BDE10000

"""

from chipsec.module_common import *

import chipsec.hal.uefi
import chipsec.hal.uefi_common

TAGS = [MTAG_BIOS,MTAG_SMM,MTAG_SECUREBOOT]

########################################################################################################
#
# Main module functionality
#
########################################################################################################
BOOTSCRIPT_OK                  = 0x0
BOOTSCRIPT_INSIDE_SMRAM        = 0x1
BOOTSCRIPT_OUTSIDE_SMRAM       = 0x2
DISPATCH_OPCODES_UNPROTECTED   = 0x4
DISPATCH_OPCODES_PROTECTED     = 0x8

HIGH_BIOS_RANGE_SIZE = 2*chipsec.defines.BOUNDARY_1MB 

class s3bootscript(BaseModule):

    def __init__(self):
        BaseModule.__init__(self)
        self._uefi = chipsec.hal.uefi.UEFI( self.cs )

    def is_supported(self):
        supported = self.cs.helper.EFI_supported()
        if not supported: self.logger.log_skipped_check( "OS does not support UEFI Runtime API" )
        return supported

    def is_inside_SMRAM( self, pa ):
        #return ( (self.smrrbase & self.smrrmask) == ( pa & self.smrrmask ) )
        return ( pa >= self.smrambase and pa < self.smramlimit)

    def is_inside_SPI( self, pa ):
        return ( pa >= (chipsec.defines.BOUNDARY_4GB - HIGH_BIOS_RANGE_SIZE) and pa < chipsec.defines.BOUNDARY_4GB )

    def check_dispatch_opcodes( self, bootscript_entries ):
        self.logger.log( '[*] Checking entry-points of Dispatch opcodes..' )
        dispatch_ep_ok = True
        n_dispatch = 0
        for e in bootscript_entries:
            if e.decoded_opcode is None: continue
            if chipsec.hal.uefi_common.S3BootScriptOpcode.EFI_BOOT_SCRIPT_DISPATCH_OPCODE   == e.decoded_opcode.opcode:
               #chipsec.hal.uefi_common.S3BootScriptOpcode.EFI_BOOT_SCRIPT_DISPATCH_2_OPCODE == e.decoded_opcode.opcode:
                n_dispatch += 1
                dispatchstr = "Dispatch opcode (off 0x%04X) with entry-point 0x%016X" % (e.offset_in_script, e.decoded_opcode.entrypoint)
                #self.logger.log( e )
                if not self.is_inside_SMRAM( e.decoded_opcode.entrypoint ) and not self.is_inside_SPI( e.decoded_opcode.entrypoint ):
                    dispatch_ep_ok = False
                    self.logger.log_bad( dispatchstr + " > UNPROTECTED"  )
                else:
                    self.logger.log_good( dispatchstr + " > PROTECTED" )
        self.logger.log( "[*] Found %d Dispatch opcodes" % n_dispatch )
        return dispatch_ep_ok

    def check_s3_bootscript( self, bootscript_pa ):
        res = BOOTSCRIPT_OK
        self.logger.log( "[*] Checking S3 boot-script at 0x%016X" % bootscript_pa )
        # Checking if it's in SMRAM
        scriptInsideSMRAM = self.is_inside_SMRAM( bootscript_pa )
        if scriptInsideSMRAM:
            res |= BOOTSCRIPT_INSIDE_SMRAM
            self.logger.log_good( 'S3 boot-script is in SMRAM' )
            self.logger.log_important( "Note: the test could not verify Dispatch opcodes because the script is in SMRAM. Entry-points of Dispatch opcodes also need to be protected." )
        else:
            res |= BOOTSCRIPT_OUTSIDE_SMRAM
            self.logger.log_bad( 'S3 boot-script is not in SMRAM' )
            self.logger.log( '[*] Reading S3 boot-script from memory..' )
            script_all = self.cs.mem.read_physical_mem( bootscript_pa, 0x100000 )
            self.logger.log( '[*] Decoding S3 boot-script opcodes..' )
            script_entries = chipsec.hal.uefi.parse_script( script_all, False )               
            dispatch_opcodes_ok = self.check_dispatch_opcodes( script_entries )
            if dispatch_opcodes_ok:
                res |= DISPATCH_OPCODES_PROTECTED
                self.logger.log_important( "S3 boot-script is not in protected memory but didn't find unprotected Dispatch entry-points" )
            else:
                res |= DISPATCH_OPCODES_UNPROTECTED
                self.logger.log_bad( 'Entry-points of Dispatch opcodes in S3 boot-script are not in protected memory' )
        return res

    def check_s3_bootscripts( self, bsaddress = None ):
        res    = 0
        status = ModuleResult.ERROR
        scriptInsideSMRAM = False

        if bsaddress:
            bootscript_PAs = [ bsaddress ]
        else:
            found,bootscript_PAs = self._uefi.find_s3_bootscript()
            if not found:
                self.logger.log_good( "Didn't find any S3 boot-scripts in EFI variables" )
                self.logger.log_warn_check( "S3 Boot-Script was not found. Firmware may be using other ways to store/locate it" )
                return ModuleResult.WARNING

            self.logger.log_important( 'Found %d S3 boot-script(s) in EFI variables' % len(bootscript_PAs) )

        for bootscript_pa in bootscript_PAs:
            if 0 == bootscript_pa: continue
            res |= self.check_s3_bootscript( bootscript_pa )

        self.logger.log('')
        #if BOOTSCRIPT_OK == res:
        #    status = ModuleResult.PASSED
        #    self.logger.log_passed_check( 'S3 Boot-Scripts including Dispatch entry-points seem to be stored in protected locations' )

        if (res & BOOTSCRIPT_OUTSIDE_SMRAM) != 0:
            # BOOTSCRIPT_OUTSIDE_SMRAM
            if (res & DISPATCH_OPCODES_UNPROTECTED) != 0:
                # DISPATCH_OPCODES_UNPROTECTED
                status = ModuleResult.FAILED
                self.logger.log_failed_check( 'S3 Boot-Script and Dispatch entry-points do not appear to be protected' )
            else:
                # DISPATCH_OPCODES_PROTECTED
                status = ModuleResult.WARNING
                self.logger.log_warn_check( 'S3 Boot-Script is not in SMRAM but Dispatch entry-points appear to be protected. Recommend further testing' )
        else:
            # BOOTSCRIPT_INSIDE_SMRAM
            status = ModuleResult.WARNING
            self.logger.log_warn_check( "S3 Boot-Script is inside SMRAM. The script is protected but Dispatch opcodes cannot be inspected" )

        self.logger.log_important( "Additional testing of the S3 boot-script can be done using tools.uefi.s3script_modify" )

        return status

    def run( self, module_argv ):
        if len(module_argv) > 2:
            self.logger.error( 'Expected module options: -a <bootscript_address>' )
            return ModuleResult.ERROR

        self.logger.start_test( "S3 Resume Boot-Script Protections" )
        script_pa = None

        if len(module_argv) > 0:
            script_pa = int(module_argv[0],16)
            self.logger.log( '[*] Using manually assigned S3 Boot-Script table base: 0x%016X' % script_pa )
        (self.smrambase, self.smramlimit, self.smramsize) = self.cs.cpu.get_SMRAM()
        if self.smrambase is not None and self.smramlimit is not None:
            self.logger.log( '[*] SMRAM: Base = 0x%016X, Limit = 0x%016X, Size = 0x%08X' % (self.smrambase, self.smramlimit, self.smramsize) )

        try:
            if script_pa is not None: return self.check_s3_bootscripts( script_pa )
            else:                     return self.check_s3_bootscripts( )
        except:
            self.logger.error("The module was not able to recognize the S3 resume boot script on this platform.")
            if self.logger.VERBOSE: raise
            return ModuleResult.ERROR
            

