Something went wrong. Try again.
Native control, inspection, UI automation and evidence capture for a running Witcher 3 process.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255"""Small, read-only PE32+ AMD64 inspector, not a Windows loader.
``PEImage(data: bytes)`` validates DOS/NT/section headers and exposes ``machine``,``timestamp``, ``image_base``, ``size_of_image`` and a tuple of immutable``Section`` records. Invalid or unsupported images/ranges raise ``ValueError``.No file access, subprocesses, optional dependencies or runtime state are used.
``read_rva(rva, size)`` returns bytes only when the entire nonempty range liesin one file-backed region (headers or one section). Zero-fill, gaps, ambiguousoverlaps and raw padding past a nonzero VirtualSize are not readable. A zeroVirtualSize uses SizeOfRawData. Section ``backed_size`` exposes this same rule.
``imports()`` validates the ordinary import directory on demand and returns``dict[str, list[str | int]]``: lowercase DLL names, case-preserved symbol names,and integer ordinals in 0..65535 (distinct from a symbol named e.g. "#123").Repeated DLL descriptors are combined, preserving import order and duplicates.An absent directory returns {}. Delay imports, exports and other directoriesare intentionally not inspected. Bound IAT addresses are not symbol names;OriginalFirstThunk is preferred, with FirstThunk used only when it is absent.
``find("48 8B ?? 90", executable_only=True)`` returns all matching RVAs, sorted,including overlapping matches. Tokens must be two hex digits or ``??``.Matches never cross sections or include virtual zero-fill/raw alignment padding.Only sections with IMAGE_SCN_MEM_EXECUTE are searched by default, never headers.Use the section records for optional named-region checks such as .text.
Resource limits intentionally reject oversized/unusual images: 1 GiB file,4 GiB virtual image, 96 sections, 16 data directories, 4096 import descriptors,65536 total import slots (including terminators), 4096 bytes per import name,and 4096 signature tokens. This is for exact-build compatibility inspection,not permissive parsing of every image accepted by Windows."""
from dataclasses import dataclassimport reimport struct
_MAX_FILE_SIZE = 1 << 30_MAX_IMAGE_SIZE = 1 << 32_MAX_SECTIONS = 96_MAX_DESCRIPTORS = 4096_MAX_THUNKS = 65536_MAX_NAME = 4096_MAX_PATTERN = 4096_EXECUTE = 0x20000000_ORDINAL_FLAG = 1 << 63
@dataclass(frozen=True)class Section: name: str rva: int virtual_size: int raw_offset: int raw_size: int characteristics: int
@property def backed_size(self) -> int: """Bytes present both in the file and the section's virtual extent.""" return min(self.raw_size, self.virtual_size or self.raw_size)
class PEImage: """Inspect immutable PE file bytes; import validation is deferred to imports."""
def __init__(self, data: bytes): if not isinstance(data, bytes): raise TypeError("PEImage requires immutable bytes") if not 64 <= len(data) <= _MAX_FILE_SIZE: raise ValueError("invalid PE file size") self._data = data if data[:2] != b"MZ": raise ValueError("invalid DOS signature") nt = self._unpack("<I", 0x3C)[0] if nt < 64 or self._bytes(nt, 4) != b"PE\0\0": raise ValueError("invalid NT signature or offset") (self.machine, count, self.timestamp, _, _, optional_size, _) = self._unpack("<HHIIIHH", nt + 4) if self.machine != 0x8664: raise ValueError("only AMD64 PE images are supported") if not 1 <= count <= _MAX_SECTIONS: raise ValueError("invalid section count") if not 112 <= optional_size <= 4096: raise ValueError("invalid optional header size") optional = nt + 24 self._bytes(optional, optional_size) if self._unpack("<H", optional)[0] != 0x20B: raise ValueError("only PE32+ images are supported") self.image_base = self._unpack("<Q", optional + 24)[0] self.size_of_image, self._headers_size = self._unpack("<II", optional + 56) directories = self._unpack("<I", optional + 108)[0] if directories > 16 or 112 + directories * 8 > optional_size: raise ValueError("invalid data directory count") section_table = optional + optional_size table_end = section_table + count * 40 if not table_end <= self._headers_size <= len(data): raise ValueError("invalid or truncated header range") if not self._headers_size <= self.size_of_image < _MAX_IMAGE_SIZE: raise ValueError("invalid image size") if self.image_base + self.size_of_image > 1 << 64: raise ValueError("image address range overflows") self._imports = ( self._unpack("<II", optional + 120) if directories >= 2 else (0, 0) ) sections = [] virtual_ranges = [(0, self._headers_size)] raw_ranges = [(0, self._headers_size)] for index in range(count): offset = section_table + index * 40 name = self._bytes(offset, 8).split(b"\0", 1)[0] try: name = name.decode("ascii") except UnicodeDecodeError as exc: raise ValueError("non-ASCII section name") from exc virtual_size, rva, raw_size, raw_offset = self._unpack("<IIII", offset + 8) characteristics = self._unpack("<I", offset + 36)[0] virtual_extent = virtual_size or raw_size if rva + virtual_extent > self.size_of_image: raise ValueError("section exceeds image range") if virtual_extent: virtual_ranges.append((rva, rva + virtual_extent)) if raw_size: self._check_range(raw_offset, raw_size) raw_ranges.append((raw_offset, raw_offset + raw_size)) sections.append(Section( name, rva, virtual_size, raw_offset, raw_size, characteristics, )) for ranges in (virtual_ranges, raw_ranges): ordered = sorted(ranges) if any(left[1] > right[0] for left, right in zip(ordered, ordered[1:])): raise ValueError("overlapping PE regions are ambiguous") self.sections = tuple(sections)
def _check_range(self, offset: int, size: int) -> None: if offset < 0 or size < 0 or offset + size > len(self._data): raise ValueError("truncated PE file range")
def _bytes(self, offset: int, size: int) -> bytes: self._check_range(offset, size) return self._data[offset:offset + size]
def _unpack(self, fmt: str, offset: int) -> tuple: return struct.unpack(fmt, self._bytes(offset, struct.calcsize(fmt)))
def _region(self, rva: int) -> tuple[int, int]: """Return file offset and remaining backed bytes for an RVA.""" if type(rva) is not int or not 0 <= rva < self.size_of_image: raise ValueError("invalid RVA") if rva < self._headers_size: return rva, self._headers_size - rva for section in self.sections: delta = rva - section.rva if 0 <= delta < section.backed_size: return section.raw_offset + delta, section.backed_size - delta raise ValueError("RVA is not file-backed")
def _file_range(self, rva: int, size: int) -> int: if type(size) is not int or size <= 0: raise ValueError("RVA size must be a positive integer") offset, available = self._region(rva) if size > available: raise ValueError("RVA range is not completely file-backed") return offset
def read_rva(self, rva: int, size: int) -> bytes: """Read a complete positive-sized range from one file-backed region.""" offset = self._file_range(rva, size) return self._data[offset:offset + size]
def _cstring(self, rva: int) -> str: offset, available = self._region(rva) end = self._data.find(b"\0", offset, offset + min(available, _MAX_NAME + 1)) if end <= offset: raise ValueError("empty, unterminated or oversized import name") try: return self._data[offset:end].decode("ascii") except UnicodeDecodeError as exc: raise ValueError("non-ASCII import name") from exc
def imports(self) -> dict[str, list[str | int]]: """Read ordinary imports; int entries are checked 16-bit ordinals.""" rva, size = self._imports if not rva and not size: return {} if not rva or size < 20: raise ValueError("invalid import directory range") directory = self._file_range(rva, size) result: dict[str, list[str | int]] = {} remaining = _MAX_THUNKS for index in range(_MAX_DESCRIPTORS + 1): if (index + 1) * 20 > size: raise ValueError("unterminated import descriptor table") descriptor = self._unpack("<IIIII", directory + index * 20) if not any(descriptor): return result if index == _MAX_DESCRIPTORS: raise ValueError("too many import descriptors") original, _, _, name_rva, first = descriptor if not name_rva or not first: raise ValueError("missing import name or FirstThunk") dll = self._cstring(name_rva).lower() names = result.setdefault(dll, []) thunk_rva = original or first offset, available = self._region(thunk_rva) for slot in range(min(available // 8, remaining)): thunk = self._unpack("<Q", offset + slot * 8)[0] remaining -= 1 if not thunk: # Even when OriginalFirstThunk is present, the IAT must # have a complete, terminated file-backed table. iat = self._file_range(first, (slot + 1) * 8) if self._unpack("<Q", iat + slot * 8)[0]: raise ValueError("unterminated import address table") break if thunk & _ORDINAL_FLAG: if thunk & ~(_ORDINAL_FLAG | 0xFFFF): raise ValueError("reserved bits in ordinal import") names.append(thunk & 0xFFFF) else: if thunk > 0xFFFFFFFF: raise ValueError("invalid import name RVA") name = self._cstring(thunk + 2) self._file_range(thunk, 2 + len(name) + 1) names.append(name) else: raise ValueError("unterminated or oversized import thunk table") raise ValueError("unterminated import descriptor table")
def find(self, pattern: str, executable_only: bool = True) -> list[int]: """Return every matching section RVA for fixed hex bytes and ?? tokens.""" if not isinstance(pattern, str) or len(pattern) > _MAX_PATTERN * 4: raise ValueError("invalid or oversized signature") tokens = pattern.split() if not 1 <= len(tokens) <= _MAX_PATTERN: raise ValueError("invalid signature length") parts = [] for token in tokens: if token == "??": parts.append(b".") elif re.fullmatch(r"[0-9a-fA-F]{2}", token): parts.append(re.escape(bytes.fromhex(token))) else: raise ValueError("signature tokens must be hex bytes or ??") matcher = re.compile(b"(?=" + b"".join(parts) + b")", re.DOTALL) matches = [] for section in self.sections: if executable_only and not section.characteristics & _EXECUTE: continue # Slice to enforce both lower and upper bounds for lookahead. data = self._data[section.raw_offset:section.raw_offset + section.backed_size] matches.extend(section.rva + match.start() for match in matcher.finditer(data)) return sorted(matches)