diff --git a/bindings/python/README.md b/bindings/python/README.md new file mode 100644 index 0000000..f18521b --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,2 @@ +# libhat-python + diff --git a/bindings/python/src/libhat/__init__.py b/bindings/python/src/libhat/__init__.py new file mode 100644 index 0000000..1d4ca2d --- /dev/null +++ b/bindings/python/src/libhat/__init__.py @@ -0,0 +1,136 @@ +import ctypes +from contextlib import nullcontext + +from ._ffi import _library +from .address import Address +from .enums import Protection, ScanAlignment, ScanHint +from .error import LibhatError +from .module import Module, Section, Segment +from .signature import Signature + +__all__ = [ + # functions + 'get_version', + 'get_version_num', + 'parse_signature', + 'create_signature', + 'find_pattern', + 'find_pattern_mod', + 'is_readable', + 'is_writable', + 'is_executable', + 'get_process_module', + 'get_module', + 'module_at', + + # types + 'Signature', + 'Module', + 'Section', + 'Segment', + 'Protection', + 'ScanAlignment', + 'ScanHint', + 'Address', + 'LibhatError', + 'MemoryBuffer', +] + +MemoryBuffer = bytes | bytearray | memoryview | ctypes.Array + + +def get_version() -> str: + return _library.libhat_get_version().decode('utf-8') + + +def get_version_num() -> int: + return _library.libhat_get_version_num() + + +def parse_signature(signature_str: str) -> Signature: + handle = ctypes.c_void_p() + status = _library.libhat_parse_signature(signature_str.encode('utf-8'), ctypes.byref(handle)) + if status != 0: + raise LibhatError(status) + return Signature(handle) + + +def create_signature(sig_bytes: bytes, sig_mask: bytes) -> Signature: + if len(sig_bytes) != len(sig_mask): + raise ValueError('Mismatch between len(sig_bytes) and len(sig_mask)') + handle = ctypes.c_void_p() + status = _library.libhat_create_signature(sig_bytes, sig_mask, len(sig_mask), ctypes.byref(handle)) + if status != 0: + raise LibhatError(status) + return Signature(handle) + + +def find_pattern(sig: str | Signature, buf: MemoryBuffer, align: ScanAlignment = ScanAlignment.X1, + hints: ScanHint = ScanHint(0)) -> int | None: + data, size = _underlying_buffer(buf) + + context = parse_signature(sig) if isinstance(sig, str) else nullcontext(sig) + with context as s: + result = ctypes.c_void_p() + status = _library.libhat_find_pattern(s.handle, data, size, ctypes.byref(result), align.value, hints.value) + if status != 0: + raise LibhatError(status) + return result.value - data.value if result else None + + +def find_pattern_mod(sig: str | Signature, mod: Module, section: str, align: ScanAlignment = ScanAlignment.X1, + hints: ScanHint = ScanHint(0)) -> Address | None: + context = parse_signature(sig) if isinstance(sig, str) else nullcontext(sig) + with context as s: + result = ctypes.c_void_p() + status = _library.libhat_find_pattern_mod(s.handle, mod.handle, section.encode('utf-8'), ctypes.byref(result), + align.value, hints.value) + if status != 0: + raise LibhatError(status) + return Address(result.value) + + +def is_readable(buf: MemoryBuffer) -> bool: + data, size = _underlying_buffer(buf) + return _library.libhat_is_readable(data, size) + + +def is_writable(buf: MemoryBuffer) -> bool: + data, size = _underlying_buffer(buf) + return _library.libhat_is_writable(data, size) + + +def is_executable(buf: MemoryBuffer) -> bool: + data, size = _underlying_buffer(buf) + return _library.libhat_is_executable(data, size) + + +def get_process_module() -> Module: + return Module(ctypes.cast(_library.libhat_get_process_module(), ctypes.c_void_p)) + + +def get_module(name: str) -> Module | None: + handle = ctypes.cast(_library.libhat_get_module(name.encode('utf-8')), ctypes.c_void_p) + return Module(handle) if handle else None + + +def module_at(addr: Address) -> Module | None: + handle = ctypes.cast(_library.libhat_module_at(addr), ctypes.c_void_p) + return Module(handle) if handle else None + + +def _underlying_buffer(buf: MemoryBuffer) -> tuple[ctypes.c_void_p, int]: + if isinstance(buf, bytes): + return ctypes.cast(buf, ctypes.c_void_p), len(buf) + elif isinstance(buf, bytearray): + return ctypes.cast((ctypes.c_ubyte * len(buf)).from_buffer(buf), ctypes.c_void_p), len(buf) + elif isinstance(buf, memoryview): + if buf.readonly: + raise ValueError('memoryview must be writable') + buf = buf.cast('B') + return ctypes.cast((ctypes.c_ubyte * len(buf)).from_buffer(buf), ctypes.c_void_p), len(buf) + elif isinstance(buf, ctypes.Array): + size = ctypes.sizeof(buf) + return ctypes.cast((ctypes.c_ubyte * size).from_buffer(buf), ctypes.c_void_p), size + else: + raise ValueError(f'unexpected type for buf: {type(buf)}') diff --git a/bindings/python/src/libhat/_ffi.py b/bindings/python/src/libhat/_ffi.py new file mode 100644 index 0000000..047d79a --- /dev/null +++ b/bindings/python/src/libhat/_ffi.py @@ -0,0 +1,218 @@ +import ctypes +import ctypes.util +import sys +from pathlib import Path + + +def _load_library() -> ctypes.CDLL: + ext = '.so' + if sys.platform.startswith('darwin'): + ext = '.dylib' + elif sys.platform.startswith('win'): + ext = '.dll' + + load_dir = Path(__file__).parent + local = load_dir / ('libhat_c' + ext) + if local.exists(): + return ctypes.cdll.LoadLibrary(str(local.absolute())) + + if found := ctypes.util.find_library('libhat'): + return ctypes.cdll.LoadLibrary(found) + + raise RuntimeError('could not locate libhat_c shared library') + + +_library = _load_library() + +# ------------------------------------------------------------------------ +# C types +# ------------------------------------------------------------------------ +c_uintptr = ctypes.c_uint64 if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_uint32 + +# ------------------------------------------------------------------------ +# Structures +# ------------------------------------------------------------------------ +class Span(ctypes.Structure): + _fields_ = [ + ('data', ctypes.c_void_p), + ('size', ctypes.c_size_t), + ] + + def view(self) -> memoryview: + return memoryview((ctypes.c_ubyte * self.size).from_address(self.data)) + + +# ------------------------------------------------------------------------ +# Callback types +# ------------------------------------------------------------------------ +ForEachSectionCallback = ctypes.CFUNCTYPE( + ctypes.c_bool, + ctypes.c_char_p, + Span, + ctypes.c_int, + ctypes.c_void_p, +) + +ForEachSegmentCallback = ctypes.CFUNCTYPE( + ctypes.c_bool, + Span, + ctypes.c_int, + ctypes.c_void_p, +) + +# ------------------------------------------------------------------------ +# API +# ------------------------------------------------------------------------ + +# +# const char* libhat_get_version(); +# +_library.libhat_get_version.argtypes = [] +_library.libhat_get_version.restype = ctypes.c_char_p + +# +# int libhat_get_version_num(); +# +_library.libhat_get_version_num.argtypes = [] +_library.libhat_get_version_num.restype = ctypes.c_int + +# +# const char* libhat_status_to_string(libhat_status status); +# +_library.libhat_status_to_string.argtypes = [ctypes.c_int] +_library.libhat_status_to_string.restype = ctypes.c_char_p + +# +# libhat_status libhat_parse_signature( +# const char* signatureStr, +# const libhat_signature** signatureOut +# ); +# +_library.libhat_parse_signature.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)] +_library.libhat_parse_signature.restype = ctypes.c_int + +# +# libhat_status libhat_create_signature( +# const char* bytes, +# const char* mask, +# size_t size, +# const libhat_signature** signatureOut +# ); +# +_library.libhat_create_signature.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, + ctypes.POINTER(ctypes.c_void_p)] +_library.libhat_create_signature.restype = ctypes.c_int + +# +# libhat_status libhat_find_pattern( +# const libhat_signature* signature, +# const void* buffer, +# size_t size, +# const void** resultOut, +# libhat_alignment align, +# libhat_hint hints +# ); +# +_library.libhat_find_pattern.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, + ctypes.POINTER(ctypes.c_void_p), ctypes.c_int, ctypes.c_int] +_library.libhat_find_pattern.restype = ctypes.c_int + +# +# libhat_status libhat_find_pattern_mod( +# const libhat_signature* signature, +# const libhat_module* module, +# const char* section, +# const void** resultOut, +# libhat_alignment align, +# libhat_hint hints +# ); +# +_library.libhat_find_pattern_mod.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, + ctypes.POINTER(ctypes.c_void_p), ctypes.c_int, ctypes.c_int] +_library.libhat_find_pattern_mod.restype = ctypes.c_int + +# +# libhat_status libhat_module_address(const libhat_module* module, uintptr_t* out); +# +_library.libhat_module_address.argtypes = [ctypes.c_void_p, ctypes.POINTER(c_uintptr)] +_library.libhat_module_address.restype = ctypes.c_int + +# +# libhat_status libhat_module_get_data(const libhat_module* module, libhat_span* out); +# +_library.libhat_module_get_data.argtypes = [ctypes.c_void_p, ctypes.POINTER(Span)] +_library.libhat_module_get_data.restype = ctypes.c_int + +# +# libhat_status libhat_module_get_executable_data(const libhat_module* module, libhat_span* out); +# +_library.libhat_module_get_executable_data.argtypes = [ctypes.c_void_p, ctypes.POINTER(Span)] +_library.libhat_module_get_executable_data.restype = ctypes.c_int + +# +# libhat_status libhat_module_get_section_data(const libhat_module* module, const char* name, libhat_span* out); +# +_library.libhat_module_get_section_data.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(Span)] +_library.libhat_module_get_section_data.restype = ctypes.c_int + +# +# libhat_status libhat_module_for_each_section( +# const libhat_module* module, +# libhat_for_each_section_cb callback, +# void* user_data +# ); +# +_library.libhat_module_for_each_section.argtypes = [ctypes.c_void_p, ForEachSectionCallback, ctypes.c_void_p] +_library.libhat_module_for_each_section.restype = ctypes.c_int + +# +# libhat_status libhat_module_for_each_segment( +# const libhat_module* module, +# libhat_for_each_segment_cb callback, +# void* user_data +# ); +# +_library.libhat_module_for_each_segment.argtypes = [ctypes.c_void_p, ForEachSegmentCallback, ctypes.c_void_p] +_library.libhat_module_for_each_segment.restype = ctypes.c_int + +# +# bool libhat_is_readable(const void* data, size_t size); +# +_library.libhat_is_readable.argtypes = [ctypes.c_void_p, ctypes.c_size_t] +_library.libhat_is_readable.restype = ctypes.c_bool + +# +# bool libhat_is_writable(const void* data, size_t size); +# +_library.libhat_is_writable.argtypes = [ctypes.c_void_p, ctypes.c_size_t] +_library.libhat_is_writable.restype = ctypes.c_bool + +# +# bool libhat_is_executable(const void* data, size_t size); +# +_library.libhat_is_executable.argtypes = [ctypes.c_void_p, ctypes.c_size_t] +_library.libhat_is_executable.restype = ctypes.c_bool + +# +# const libhat_module* libhat_get_process_module(); +# +_library.libhat_get_process_module.argtypes = [] +_library.libhat_get_process_module.restype = ctypes.c_void_p + +# +# const libhat_module* libhat_get_module(const char* name); +# +_library.libhat_get_module.argtypes = [ctypes.c_char_p] +_library.libhat_get_module.restype = ctypes.c_void_p + +# +# const libhat_module* libhat_module_at(const void* address); +# +_library.libhat_module_at.argtypes = [ctypes.c_void_p] +_library.libhat_module_at.restype = ctypes.c_void_p + +# +# void libhat_free(const void* object); +# +_library.libhat_free.argtypes = [ctypes.c_void_p] +_library.libhat_free.restype = None diff --git a/bindings/python/src/libhat/address.py b/bindings/python/src/libhat/address.py new file mode 100644 index 0000000..1a9a4a7 --- /dev/null +++ b/bindings/python/src/libhat/address.py @@ -0,0 +1,2 @@ +class Address(int): + """A virtual memory address""" diff --git a/bindings/python/src/libhat/enums.py b/bindings/python/src/libhat/enums.py new file mode 100644 index 0000000..9967c1c --- /dev/null +++ b/bindings/python/src/libhat/enums.py @@ -0,0 +1,19 @@ +from enum import Enum, Flag, auto + + +class Protection(Flag): + Read = auto() + Write = auto() + Execute = auto() + + +class ScanAlignment(Enum): + X1 = 1 + X4 = 4 + X16 = 16 + + +class ScanHint(Flag): + X86_64 = auto() + PAIR0 = auto() + AARCH64 = auto() diff --git a/bindings/python/src/libhat/error.py b/bindings/python/src/libhat/error.py new file mode 100644 index 0000000..dc3b3b9 --- /dev/null +++ b/bindings/python/src/libhat/error.py @@ -0,0 +1,6 @@ +from ._ffi import _library + +class LibhatError(Exception): + def __init__(self, status: int): + self.status = status + super().__init__(_library.libhat_status_to_string(status).decode('utf-8')) diff --git a/bindings/python/src/libhat/module.py b/bindings/python/src/libhat/module.py new file mode 100644 index 0000000..e5e739b --- /dev/null +++ b/bindings/python/src/libhat/module.py @@ -0,0 +1,101 @@ +import ctypes +from collections.abc import Callable +from dataclasses import dataclass + +from ._ffi import _library, c_uintptr, ForEachSectionCallback, ForEachSegmentCallback, Span +from .address import Address +from .enums import Protection +from .error import LibhatError + + +@dataclass(frozen=True) +class Section: + name: str + data: memoryview + protection: Protection + + +@dataclass(frozen=True) +class Segment: + data: memoryview + protection: Protection + + +class Module: + def __init__(self, handle: ctypes.c_void_p): + if not handle: + raise ValueError('handle must not be nullptr') + self._handle = handle + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + + def close(self): + if self._handle: + _library.libhat_free(self._handle) + self._handle = None + + @property + def handle(self): + return self._handle + + def _check_handle(self) -> ctypes.c_void_p: + if not self._handle: + raise RuntimeError('Attempted operation on a Module that has already been freed') + return self._handle + + def address(self) -> Address: + result = c_uintptr() + status = _library.libhat_module_address(self._check_handle(), ctypes.byref(result)) + if status != 0: + raise LibhatError(status) + return Address(result.value) + + def get_module_data(self) -> memoryview: + result = Span() + status = _library.libhat_module_get_data(self._check_handle(), ctypes.byref(result)) + if status != 0: + raise LibhatError(status) + return result.view() + + def get_executable_data(self) -> memoryview: + result = Span() + status = _library.libhat_module_get_executable_data(self._check_handle(), ctypes.byref(result)) + if status != 0: + raise LibhatError(status) + return result.view() + + def get_section_data(self, name: str) -> memoryview: + result = Span() + status = _library.libhat_module_get_section_data(self._check_handle(), name.encode('utf-8'), + ctypes.byref(result)) + if status != 0: + raise LibhatError(status) + return result.view() + + def for_each_section(self, callback: Callable[[Section], bool]): + user_data = ctypes.cast(ctypes.pointer(ctypes.py_object(callback)), ctypes.c_void_p) + status = _library.libhat_module_for_each_section(self._check_handle(), _section_trampoline, user_data) + if status != 0: + raise LibhatError(status) + + def for_each_segment(self, callback: Callable[[Segment], bool]): + user_data = ctypes.cast(ctypes.pointer(ctypes.py_object(callback)), ctypes.c_void_p) + status = _library.libhat_module_for_each_segment(self._check_handle(), _segment_trampoline, user_data) + if status != 0: + raise LibhatError(status) + + +@ForEachSectionCallback +def _section_trampoline(name: bytes, data: Span, prot: int, user_data: int) -> bool: + callback = ctypes.cast(user_data, ctypes.POINTER(ctypes.py_object)).contents.value + return callback(Section(name.decode('utf-8'), data.view(), Protection(prot))) + + +@ForEachSegmentCallback +def _segment_trampoline(data: Span, prot: int, user_data: int) -> bool: + callback = ctypes.cast(user_data, ctypes.POINTER(ctypes.py_object)).contents.value + return callback(Segment(data.view(), Protection(prot))) diff --git a/bindings/python/src/libhat/py.typed b/bindings/python/src/libhat/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/bindings/python/src/libhat/signature.py b/bindings/python/src/libhat/signature.py new file mode 100644 index 0000000..e6c6fcf --- /dev/null +++ b/bindings/python/src/libhat/signature.py @@ -0,0 +1,25 @@ +import ctypes + +from ._ffi import _library + + +class Signature: + def __init__(self, handle: ctypes.c_void_p): + if not handle: + raise ValueError('handle must not be nullptr') + self._handle = handle + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + + def close(self): + if self._handle: + _library.libhat_free(self._handle) + self._handle = None + + @property + def handle(self): + return self._handle