Skip to content

Commit

Permalink
Merge pull request #1488 from elicn/dev-uefi
Browse files Browse the repository at this point in the history
Periodic maintenance PR
  • Loading branch information
xwings authored Oct 11, 2024
2 parents 3d71cf9 + 6cd5f26 commit a9ae692
Show file tree
Hide file tree
Showing 25 changed files with 1,498 additions and 1,415 deletions.
90 changes: 70 additions & 20 deletions qiling/loader/pe_uefi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from pefile import PE
import os
from typing import Any, Mapping, Optional, Sequence
from pefile import PE

from unicorn.unicorn_const import UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC

from qiling import Qiling
from qiling.const import QL_ARCH
Expand Down Expand Up @@ -96,17 +99,64 @@ def map_and_load(self, path: str, context: UefiContext, exec_now: bool=False):
# use image base only if it does not point to NULL
image_base = pe.OPTIONAL_HEADER.ImageBase or context.next_image_base
image_size = ql.mem.align_up(pe.OPTIONAL_HEADER.SizeOfImage)
image_name = os.path.basename(path)

assert (image_base % ql.mem.pagesize) == 0, 'image base is expected to be page-aligned'

if image_base != pe.OPTIONAL_HEADER.ImageBase:
pe.relocate_image(image_base)

pe.parse_data_directories()
data = bytes(pe.get_memory_mapped_image())
# pe.parse_data_directories()

sec_alignment = pe.OPTIONAL_HEADER.SectionAlignment

def __map_sections():
"""Load file sections to memory, each in its own memory region protected by
its defined permissions. That allows separation of code and data, which makes
it easier to detect abnomal behavior or memory corruptions.
"""

# load the header
hdr_data = bytes(pe.header)
hdr_base = image_base
hdr_size = ql.mem.align_up(len(hdr_data), sec_alignment)
hdr_perm = UC_PROT_READ

ql.mem.map(hdr_base, hdr_size, hdr_perm, image_name)
ql.mem.write(hdr_base, hdr_data)

# load sections
for section in pe.sections:
if not section.IMAGE_SCN_MEM_DISCARDABLE:
sec_name = section.Name.rstrip(b'\x00').decode()
sec_data = bytes(section.get_data(ignore_padding=True))
sec_base = image_base + section.get_VirtualAddress_adj()
sec_size = ql.mem.align_up(len(sec_data), sec_alignment)

sec_perm = sum((
section.IMAGE_SCN_MEM_READ * UC_PROT_READ,
section.IMAGE_SCN_MEM_WRITE * UC_PROT_WRITE,
section.IMAGE_SCN_MEM_EXECUTE * UC_PROT_EXEC
))

ql.mem.map(sec_base, sec_size, sec_perm, f'{image_name} ({sec_name})')
ql.mem.write(sec_base, sec_data)

def __map_all():
"""Load the entire file to memory as a single memory region.
"""

data = bytes(pe.get_memory_mapped_image())

ql.mem.map(image_base, image_size, info=image_name)
ql.mem.write(image_base, data)

# if sections are aligned to page, we can map them separately
if (sec_alignment % ql.mem.pagesize) == 0:
__map_sections()
else:
__map_all()

ql.mem.map(image_base, image_size, info="[module]")
ql.mem.write(image_base, data)
ql.log.info(f'Module {path} loaded to {image_base:#x}')

entry_point = image_base + pe.OPTIONAL_HEADER.AddressOfEntryPoint
Expand All @@ -120,7 +170,7 @@ def map_and_load(self, path: str, context: UefiContext, exec_now: bool=False):
self.install_loaded_image_protocol(image_base, image_size)

# this would be used later be loader.find_containing_image
self.images.append(Image(image_base, image_base + image_size, path))
self.images.append(Image(image_base, image_base + image_size, os.path.abspath(path)))

# update next memory slot to allow sequencial loading. its availability
# is unknown though
Expand Down Expand Up @@ -160,7 +210,7 @@ def unload_modules(self, context: UefiContext) -> bool:

for handle in context.loaded_image_protocol_modules:
struct_addr = context.protocols[handle][self.loaded_image_protocol_guid]
loaded_image_protocol = EfiLoadedImageProtocol.EFI_LOADED_IMAGE_PROTOCOL.loadFrom(self.ql, struct_addr)
loaded_image_protocol = EfiLoadedImageProtocol.EFI_LOADED_IMAGE_PROTOCOL.load_from(self.ql.mem, struct_addr)

unload_ptr = loaded_image_protocol.Unload.value

Expand Down Expand Up @@ -223,19 +273,19 @@ def __init_dxe_environment(self, ql: Qiling) -> DxeContext:
context = DxeContext(ql)

# initialize and locate heap
heap_base = int(profile['heap_address'], 0)
heap_size = int(profile['heap_size'], 0)
heap_base = profile.getint('heap_address')
heap_size = profile.getint('heap_size')
context.init_heap(heap_base, heap_size)
ql.log.info(f'DXE heap at {heap_base:#010x}')

# initialize and locate stack
stack_base = int(profile['stack_address'], 0)
stack_size = int(profile['stack_size'], 0)
stack_base = profile.getint('stack_address')
stack_size = profile.getint('stack_size')
context.init_stack(stack_base, stack_size)
ql.log.info(f'DXE stack at {context.top_of_stack:#010x}')

# base address for next image
context.next_image_base = int(profile['image_address'], 0)
context.next_image_base = profile.getint('image_address')

# statically allocating 4 KiB for ST, RT, BS, DS and about 100 configuration table entries.
# the actual size needed was rounded up to the nearest page boundary.
Expand Down Expand Up @@ -272,23 +322,23 @@ def __init_smm_environment(self, ql: Qiling) -> SmmContext:
context = SmmContext(ql)

# set smram boundaries
context.smram_base = int(profile["smram_base"], 0)
context.smram_size = int(profile["smram_size"], 0)
context.smram_base = profile.getint('smram_base')
context.smram_size = profile.getint('smram_size')

# initialize and locate heap
heap_base = int(profile["heap_address"], 0)
heap_size = int(profile["heap_size"], 0)
heap_base = profile.getint('heap_address')
heap_size = profile.getint('heap_size')
context.init_heap(heap_base, heap_size)
ql.log.info(f"SMM heap at {heap_base:#010x}")

# initialize and locate stack
stack_base = int(profile['stack_address'], 0)
stack_size = int(profile['stack_size'], 0)
stack_base = profile.getint('stack_address')
stack_size = profile.getint('stack_size')
context.init_stack(stack_base, stack_size)
ql.log.info(f'SMM stack at {context.top_of_stack:#010x}')

# base address for next image
context.next_image_base = int(profile['image_address'], 0)
context.next_image_base = profile.getint('image_address')

# statically allocating 4 KiB for SMM ST and about 100 configuration table entries
# the actual size needed was rounded up to the nearest page boundary.
Expand Down Expand Up @@ -325,7 +375,7 @@ def run(self):
raise QlErrorArch("Unsupported architecture")

# x86-64 arch only
if ql.arch.type != QL_ARCH.X8664:
if ql.arch.type is not QL_ARCH.X8664:
raise QlErrorArch("Only 64-bit modules are supported at the moment")

self.loaded_image_protocol_guid = ql.os.profile["LOADED_IMAGE_PROTOCOL"]["Guid"]
Expand Down
45 changes: 31 additions & 14 deletions qiling/os/uefi/PiMultiPhase.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
#!/usr/bin/env python3
#
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from .UefiBaseType import *
from .ProcessorBind import *
from .UefiBaseType import EFI_PHYSICAL_ADDRESS
from .ProcessorBind import UINT64, STRUCT, ENUM_UC


EFI_SMRAM_OPEN = 0x00000001
EFI_SMRAM_CLOSED = 0x00000002
EFI_SMRAM_LOCKED = 0x00000004
EFI_CACHEABLE = 0x00000008
EFI_ALLOCATED = 0x00000010
EFI_NEEDS_TESTING = 0x00000020
EFI_NEEDS_ECC_INITIALIZATION = 0x00000040

EFI_SMRAM_OPEN = 0x00000001
EFI_SMRAM_CLOSED = 0x00000002
EFI_SMRAM_LOCKED = 0x00000004
EFI_CACHEABLE = 0x00000008
EFI_ALLOCATED = 0x00000010
EFI_NEEDS_TESTING = 0x00000020
EFI_NEEDS_ECC_INITIALIZATION = 0x00000040

class EFI_SMRAM_DESCRIPTOR(STRUCT):
_fields_ = [
('PhysicalStart', EFI_PHYSICAL_ADDRESS),
('CpuStart', EFI_PHYSICAL_ADDRESS),
('PhysicalSize', UINT64),
('RegionState', UINT64)
('PhysicalStart', EFI_PHYSICAL_ADDRESS),
('CpuStart', EFI_PHYSICAL_ADDRESS),
('PhysicalSize', UINT64),
('RegionState', UINT64)
]


class EFI_VARIABLE(ENUM_UC):
_members_ = {
'NON_VOLATILE': 0x00000001,
'BOOTSERVICE_ACCESS': 0x00000002,
'RUNTIME_ACCESS': 0x00000004,
'HARDWARE_ERROR_RECORD': 0x00000008,
'AUTHENTICATED_WRITE_ACCESS': 0x00000010,
'TIME_BASED_AUTHENTICATED_WRITE_ACCESS': 0x00000020,
'APPEND_WRITE': 0x00000040,
'ENHANCED_AUTHENTICATED_ACCESS': 0x00000080
}


__all__ = [
'EFI_SMRAM_DESCRIPTOR',
'EFI_VARIABLE',
'EFI_SMRAM_OPEN',
'EFI_SMRAM_CLOSED',
'EFI_SMRAM_LOCKED',
Expand Down
100 changes: 26 additions & 74 deletions qiling/os/uefi/ProcessorBind.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
#!/usr/bin/env python3
#
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from __future__ import annotations

import ctypes
from contextlib import contextmanager
from typing import Mapping, MutableMapping, Sequence, Optional

from qiling import Qiling
from functools import lru_cache
from typing import Mapping, Sequence, Union

from qiling.os.struct import BaseStructEL


bits = 64
psize = bits // 8

dummy_ptr_type = {
32 : ctypes.c_uint32,
64 : ctypes.c_uint64
}[bits]

_pointer_type_cache: MutableMapping[str, type] = {}
@lru_cache(maxsize=None)
def PTR(ptype: Union[type, None]) -> type:
"""Generate a pseudo pointer type.
"""

def PTR(ptype: Optional[type]) -> type:
pname = 'c_void' if ptype is None else ptype.__name__

if pname not in _pointer_type_cache:
_pointer_type_cache[pname] = type(f'LP_{psize}_{pname}', (dummy_ptr_type,), {})
return type(f'LP_{psize}_{pname}', (UINTN,), {})


def FUNCPTR(rettype: Union[type, None], *argtypes: type) -> type:
"""Generate a pseudo function pointer type.
"""

return PTR(ctypes.CFUNCTYPE(rettype, *argtypes))

return _pointer_type_cache[pname]

VOID = None
INT8 = ctypes.c_int8
Expand All @@ -44,75 +50,18 @@ def PTR(ptype: Optional[type]) -> type:
CHAR8 = UINT8
CHAR16 = UINT16

FUNCPTR = lambda *args: PTR(ctypes.CFUNCTYPE(*args))
STRUCT = BaseStructEL
UNION = ctypes.Union

CPU_STACK_ALIGNMENT = 16
PAGE_SIZE = 0x1000

class STRUCT(ctypes.LittleEndianStructure):
"""An abstract class for C structures.
"""

# Structures are packed by default; when needed, padding should be added
# manually through placeholder fields
_pack_ = 1

def __init__(self):
pass

def saveTo(self, ql: Qiling, address: int) -> None:
"""Store self contents to a specified memory address.
"""

data = bytes(self)

ql.mem.write(address, data)

@classmethod
def loadFrom(cls, ql: Qiling, address: int) -> 'STRUCT':
"""Construct an instance of the structure from saved contents.
"""

data = bytes(ql.mem.read(address, cls.sizeof()))

return cls.from_buffer_copy(data)

@classmethod
@contextmanager
def bindTo(cls, ql: Qiling, address: int):
instance = cls.loadFrom(ql, address)

try:
yield instance
finally:
instance.saveTo(ql, address)

@classmethod
def sizeof(cls) -> int:
"""Get the C structure size in bytes.
"""

return ctypes.sizeof(cls)

@classmethod
def offsetof(cls, fname: str) -> int:
"""Get the offset of a field in the C structure.
"""

return getattr(cls, fname).offset

@classmethod
def memberat(cls, offset: int) -> Optional[str]:
"""Get the member name at a given offset.
"""

return next((fname for fname, *_ in cls._fields_ if cls.offsetof(fname) == offset), None)

class EnumMeta(type(ctypes.c_int)):
def __getattr__(self, key):
return self._members_.index(key)


class ENUM(ctypes.c_int, metaclass=EnumMeta):
"""An abstract class for continuous C enums.
"""
Expand All @@ -121,10 +70,12 @@ class ENUM(ctypes.c_int, metaclass=EnumMeta):
# names will be enumerate by their corresponding index in the list
_members_: Sequence[str] = []


class EnumUCMeta(type(ctypes.c_int)):
def __getattr__(self, key):
return self._members_[key]


class ENUM_UC(ctypes.c_int, metaclass=EnumUCMeta):
"""An abstract class for uncontinuous C enums.
"""
Expand All @@ -133,6 +84,7 @@ class ENUM_UC(ctypes.c_int, metaclass=EnumUCMeta):
# names will be enumerate by their paired value
_members_: Mapping[str, int] = {}


__all__ = [
'VOID',
'INT8',
Expand All @@ -158,4 +110,4 @@ class ENUM_UC(ctypes.c_int, metaclass=EnumUCMeta):

'CPU_STACK_ALIGNMENT',
'PAGE_SIZE'
]
]
Loading

0 comments on commit a9ae692

Please sign in to comment.