Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions qiling/os/windows/dlls/kernel32/libloaderapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from qiling.os.windows.api import *
from qiling.os.windows.const import *
from qiling.os.windows.fncc import *
from qiling.os.windows.utils import has_lib_ext
from qiling.os.windows.utils import has_lib_ext, resolve_export

def _GetModuleHandle(ql: Qiling, address: int, params):
lpModuleName = params["lpModuleName"]
Expand Down Expand Up @@ -169,10 +169,12 @@ def hook_GetProcAddress(ql: Qiling, address: int, params):

return next((addr for addr, entry in ql.loader.export_symbols.items() if search_func(entry)), 0)

# in any other case, look through the import address table for that dll
iat = ql.loader.import_address_table[dll_name]
# in any other case, resolve against the target dll's own export table
# (falling back to its import table first for backwards compatibility --
# see resolve_export()'s docstring)
image = next(image for image in ql.loader.images if image.base == hModule)

return iat.get(procname or ordinal, 0)
return resolve_export(ql, image, name=procname, ordinal=ordinal) or 0

def _LoadLibrary(ql: Qiling, address: int, params):
lpLibFileName = params["lpLibFileName"]
Expand Down
16 changes: 9 additions & 7 deletions qiling/os/windows/dlls/ntdll.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,22 +419,24 @@ def hook_LdrGetProcedureAddress(ql: Qiling, address: int, params):
FunctionAddress = params['FunctionAddress']

# Check if dll is loaded
dll_name = next((os.path.basename(path).casefold() for base, _, path in ql.loader.images if base == ModuleHandle), None)
image = next((image for image in ql.loader.images if image.base == ModuleHandle), None)

if dll_name is None:
if image is None:
ql.log.debug(f'Could not find specified handle {ModuleHandle} in loaded DLL')
return STATUS_DLL_NOT_FOUND

identifier = utils.read_pansi_string(ql, FunctionName) if FunctionName else Ordinal
iat = ql.loader.import_address_table[dll_name]
name = utils.read_pansi_string(ql, FunctionName) if FunctionName else None
ordinal = None if FunctionName else Ordinal

if not identifier:
if not name and not ordinal:
return STATUS_INVALID_PARAMETER

if identifier not in iat:
address = utils.resolve_export(ql, image, name=name, ordinal=ordinal)

if not address:
return STATUS_PROCEDURE_NOT_FOUND

ql.mem.write_ptr(FunctionAddress, iat[identifier])
ql.mem.write_ptr(FunctionAddress, address)

return STATUS_SUCCESS

Expand Down
101 changes: 100 additions & 1 deletion qiling/os/windows/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

from typing import Iterable, Optional, Tuple, TypeVar
import os

from typing import Iterable, Optional, Tuple, TypeVar, Union

import pefile

from unicorn import UcError

from qiling import Qiling
from qiling.exception import QlErrorSyscallError
from qiling.loader.loader import Image
from qiling.os.const import POINTER
from qiling.os.windows.fncc import STDCALL
from qiling.os.windows.wdk_const import *
Expand All @@ -29,6 +34,100 @@ def has_lib_ext(name: str) -> bool:
return ext in ("dll", "exe", "sys", "drv")


def __export_from_dll_file(ql: Qiling, image: Image, name: Optional[bytes], ordinal: Optional[int], seen: set) -> Optional[int]:
"""Resolve an export by parsing the target DLL's export directory from disk.
Forwarder exports (e.g. kernel32.HeapAlloc -> ntdll.RtlAllocateHeap) are
followed to the DLL that actually implements the function.
"""

try:
pe = pefile.PE(image.path, fast_load=True)
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']])
except (pefile.PEFormatError, OSError):
# a malformed pe or a file that vanished after being mapped -> treat as not found
return None

export_dir = getattr(pe, 'DIRECTORY_ENTRY_EXPORT', None)

if export_dir is None:
return None

for sym in export_dir.symbols:
if (name is not None and sym.name == name) or (ordinal is not None and sym.ordinal == ordinal):
if sym.forwarder:
return __follow_forwarder(ql, sym.forwarder, seen)

return image.base + sym.address

return None


def __follow_forwarder(ql: Qiling, forwarder: bytes, seen: set) -> Optional[int]:
# a forwarder is a 'dll.symbol' or 'dll.#ordinal' string, e.g. b'NTDLL.RtlAllocateHeap'
if forwarder in seen or b'.' not in forwarder:
return None

seen.add(forwarder)

# split on the last dot: the module name may itself contain dots (api-ms-win-*).
# latin1 decodes any byte, so a corrupt module name degrades to a miss rather than raising.
target_dll, _, target_sym = forwarder.rpartition(b'.')
target_name = f'{target_dll.decode("latin1")}.dll'.casefold()

if target_sym.startswith(b'#'):
if not target_sym[1:].isdigit():
return None

fwd_name, fwd_ordinal = None, int(target_sym[1:])
else:
fwd_name, fwd_ordinal = target_sym, None

image = ql.loader.get_image_by_name(target_name, casefold=True)

if image is None:
return None

return resolve_export(ql, image, name=fwd_name, ordinal=fwd_ordinal, seen=seen)


def resolve_export(ql: Qiling, image: Image, *, name: Union[str, bytes, None] = None, ordinal: Optional[int] = None, seen: Optional[set] = None) -> Optional[int]:
"""Resolve the address of a function exported by the DLL mapped at ``image``.

Resolution proceeds in two steps:

1. Look the function up in ``ql.loader.import_address_table[dll_name]``
first. This is the table qiling populates while loading dependencies;
consulting it first keeps every already-working resolution unchanged.
2. Fall back to parsing the DLL's real export directory from disk with
pefile, following forwarder exports to the DLL that implements them.

Args:
name : function name (str or bytes); mutually exclusive with ``ordinal``
ordinal : export ordinal; used when ``name`` is not given

Returns: the resolved address, or ``None`` if the function was not found.
"""

# export names in the tables and in pefile are bytes; normalize early so a
# name-based lookup can match (str keys never would).
if isinstance(name, str):
name = name.encode('latin1')

key = name if name is not None else ordinal

if key is None:
return None

# 1. the table qiling already built (backwards compatible)
iat = ql.loader.import_address_table.get(os.path.basename(image.path).casefold())

if iat and iat.get(key):
return iat[key]

# 2. the target dll's own export table
return __export_from_dll_file(ql, image, name, ordinal, seen if seen is not None else set())


def io_Write(ql: Qiling, in_buffer: bytes) -> int:
major_func = ql.loader.driver_object.MajorFunction[IRP_MJ_WRITE]

Expand Down