Skip to content

pe_utils

flavor.psp.format_2025.pe_utils

Windows PE Executable Utilities.

Provides utilities for manipulating Windows PE (Portable Executable) files to ensure compatibility with PSPF format when data is appended after the executable.

Functions

expand_dos_stub

expand_dos_stub(data: bytes) -> bytes

Expand the DOS stub of a PE executable to match Rust/MSVC binary size.

This fixes Windows PE loader rejection of Go binaries when PSPF data is appended. The DOS stub is expanded from 128 bytes (0x80) to 240 bytes (0xF0) to match Rust binaries.

Process: 1. Extract MZ header (first 64 bytes) 2. Extract DOS stub code (bytes 64 to current PE offset) 3. Extract PE header and remainder 4. Insert padding to expand stub to target size 5. Update e_lfanew pointer to new PE offset

Parameters:

Name Type Description Default
data bytes

Original PE executable data

required

Returns:

Type Description
bytes

Modified PE executable with expanded DOS stub

Raises:

Type Description
ValueError

If data is not a valid PE executable

Source code in flavor/psp/format_2025/pe_utils.py
def expand_dos_stub(data: bytes) -> bytes:
    """
    Expand the DOS stub of a PE executable to match Rust/MSVC binary size.

    This fixes Windows PE loader rejection of Go binaries when PSPF data
    is appended. The DOS stub is expanded from 128 bytes (0x80) to 240 bytes
    (0xF0) to match Rust binaries.

    Process:
    1. Extract MZ header (first 64 bytes)
    2. Extract DOS stub code (bytes 64 to current PE offset)
    3. Extract PE header and remainder
    4. Insert padding to expand stub to target size
    5. Update e_lfanew pointer to new PE offset

    Args:
        data: Original PE executable data

    Returns:
        Modified PE executable with expanded DOS stub

    Raises:
        ValueError: If data is not a valid PE executable
    """
    if not is_pe_executable(data):
        raise ValueError("Data is not a Windows PE executable")

    current_pe_offset = get_pe_header_offset(data)
    if current_pe_offset is None:
        raise ValueError("Invalid PE header offset")

    if current_pe_offset >= TARGET_DOS_STUB_SIZE:
        logger.debug(
            "DOS stub already adequate size",
            current=f"0x{current_pe_offset:x}",
            target=f"0x{TARGET_DOS_STUB_SIZE:x}",
        )
        return data

    # Calculate padding needed
    padding_size = TARGET_DOS_STUB_SIZE - current_pe_offset

    logger.info(
        "Expanding DOS stub for Windows compatibility",
        current_pe_offset=f"0x{current_pe_offset:x}",
        target_pe_offset=f"0x{TARGET_DOS_STUB_SIZE:x}",
        padding_bytes=padding_size,
    )

    # Build new executable:
    # 1. MZ header + DOS stub (up to current PE offset)
    # 2. Padding (zeros to expand stub)
    # 3. PE header and remainder
    mz_and_dos_stub = data[0:current_pe_offset]
    pe_header_and_remainder = data[current_pe_offset:]
    padding = b"\x00" * padding_size

    new_data = bytearray(mz_and_dos_stub + padding + pe_header_and_remainder)

    # Update e_lfanew pointer at offset 0x3C to point to new PE header location
    struct.pack_into("<I", new_data, 0x3C, TARGET_DOS_STUB_SIZE)

    # CRITICAL: Update all section PointerToRawData values
    # When we shift the file content forward, section data moves but the section
    # table entries still point to old offsets. We must update them.
    _update_section_offsets(new_data, padding_size)

    # Update SizeOfHeaders to reflect expanded DOS stub size
    _update_size_of_headers(new_data, padding_size)

    # Update data directories (Certificate Table uses absolute file offsets)
    _update_data_directories(new_data, padding_size)

    # Update debug directory entries (PointerToRawData fields use absolute file offsets)
    _update_debug_directory(new_data, padding_size)

    # Verify the modification
    new_pe_offset = get_pe_header_offset(bytes(new_data))
    if new_pe_offset != TARGET_DOS_STUB_SIZE:
        raise ValueError(
            f"Failed to update PE offset: expected 0x{TARGET_DOS_STUB_SIZE:x}, got 0x{new_pe_offset:x}"
        )

    logger.debug(
        "DOS stub expansion complete",
        original_size=len(data),
        new_size=len(new_data),
        bytes_added=padding_size,
        new_pe_offset=f"0x{new_pe_offset:x}",
    )

    return bytes(new_data)

get_launcher_type

get_launcher_type(launcher_data: bytes) -> str

Detect launcher type from PE characteristics.

Go and Rust compilers produce PE files with different characteristics: - Go: Minimal DOS stub (PE offset 0x80 / 128 bytes) - Rust: Larger DOS stub (PE offset 0xE8 / 232 bytes or more)

Parameters:

Name Type Description Default
launcher_data bytes

Launcher binary data

required

Returns:

Type Description
str

"go", "rust", or "unknown"

Source code in flavor/psp/format_2025/pe_utils.py
def get_launcher_type(launcher_data: bytes) -> str:
    """
    Detect launcher type from PE characteristics.

    Go and Rust compilers produce PE files with different characteristics:
    - Go: Minimal DOS stub (PE offset 0x80 / 128 bytes)
    - Rust: Larger DOS stub (PE offset 0xE8 / 232 bytes or more)

    Args:
        launcher_data: Launcher binary data

    Returns:
        "go", "rust", or "unknown"
    """
    if not is_pe_executable(launcher_data):
        return "unknown"

    pe_offset = get_pe_header_offset(launcher_data)
    if pe_offset is None:
        return "unknown"

    # Go binaries have PE offset 0x80, Rust has 0xE8 or larger
    if pe_offset == 0x80:
        logger.debug("Detected Go launcher", pe_offset=f"0x{pe_offset:x}")
        return "go"
    elif pe_offset >= 0xE8:
        logger.debug("Detected Rust launcher", pe_offset=f"0x{pe_offset:x}")
        return "rust"
    else:
        logger.debug("Unknown launcher type", pe_offset=f"0x{pe_offset:x}")
        return "unknown"

get_pe_header_offset

get_pe_header_offset(data: bytes) -> int | None

Read the PE header offset from the DOS header.

The offset is stored at position 0x3C (e_lfanew field) as a 4-byte little-endian integer.

Parameters:

Name Type Description Default
data bytes

PE executable data

required

Returns:

Type Description
int | None

PE header offset, or None if invalid

Source code in flavor/psp/format_2025/pe_utils.py
def get_pe_header_offset(data: bytes) -> int | None:
    """
    Read the PE header offset from the DOS header.

    The offset is stored at position 0x3C (e_lfanew field) as a 4-byte
    little-endian integer.

    Args:
        data: PE executable data

    Returns:
        PE header offset, or None if invalid
    """
    if len(data) < 0x40:
        return None

    # Read e_lfanew field at offset 0x3C
    pe_offset: int = struct.unpack("<I", data[0x3C:0x40])[0]

    # Validate PE signature at that offset
    if len(data) < pe_offset + 4:
        return None

    pe_signature = data[pe_offset : pe_offset + 4]
    if pe_signature != b"PE\x00\x00":
        logger.warning(
            "Invalid PE signature",
            expected="PE\\x00\\x00",
            actual=pe_signature.hex(),
            offset=f"0x{pe_offset:x}",
        )
        return None

    return pe_offset

is_pe_executable

is_pe_executable(data: bytes) -> bool

Check if data starts with a valid Windows PE executable header.

Parameters:

Name Type Description Default
data bytes

Binary data to check

required

Returns:

Type Description
bool

True if data starts with "MZ" signature (PE executable)

Source code in flavor/psp/format_2025/pe_utils.py
def is_pe_executable(data: bytes) -> bool:
    """
    Check if data starts with a valid Windows PE executable header.

    Args:
        data: Binary data to check

    Returns:
        True if data starts with "MZ" signature (PE executable)
    """
    return len(data) >= 2 and data[0:2] == b"MZ"

needs_dos_stub_expansion

needs_dos_stub_expansion(data: bytes) -> bool

Check if a PE executable needs DOS stub expansion.

Go binaries use minimal DOS stub (128 bytes / 0x80) which is incompatible with Windows PE loader when PSPF data is appended. This function detects such binaries.

Parameters:

Name Type Description Default
data bytes

PE executable data

required

Returns:

Type Description
bool

True if DOS stub needs expansion (Go binary with 0x80 stub)

Source code in flavor/psp/format_2025/pe_utils.py
def needs_dos_stub_expansion(data: bytes) -> bool:
    """
    Check if a PE executable needs DOS stub expansion.

    Go binaries use minimal DOS stub (128 bytes / 0x80) which is incompatible
    with Windows PE loader when PSPF data is appended. This function detects
    such binaries.

    Args:
        data: PE executable data

    Returns:
        True if DOS stub needs expansion (Go binary with 0x80 stub)
    """
    if not is_pe_executable(data):
        return False

    pe_offset = get_pe_header_offset(data)
    if pe_offset is None:
        return False

    # Check if this is a Go binary with minimal DOS stub (0x80 = 128 bytes)
    # Rust/MSVC binaries typically use 0xE8-0xF0 (232-240 bytes)
    if pe_offset == 0x80:
        logger.debug(
            "Detected Go binary with minimal DOS stub",
            pe_offset=f"0x{pe_offset:x}",
            dos_stub_size=pe_offset,
        )
        return True

    logger.trace(
        "PE binary has adequate DOS stub size",
        pe_offset=f"0x{pe_offset:x}",
        dos_stub_size=pe_offset,
    )
    return False

process_launcher_for_pspf

process_launcher_for_pspf(launcher_data: bytes) -> bytes

Process launcher binary for PSPF embedding compatibility.

This is the main entry point for PE manipulation. It uses a hybrid approach: - Go launchers: Use PE overlay (no modifications, PSPF appended after sections) - Rust launchers: Use DOS stub expansion (PSPF at fixed 0xF0 offset)

Phase 29: Go binaries are fundamentally incompatible with DOS stub expansion due to their PE structure (15 sections, unusual section names, missing data directories). The PE overlay approach is the industry standard and preserves 100% PE structure integrity.

Parameters:

Name Type Description Default
launcher_data bytes

Original launcher binary

required

Returns:

Type Description
bytes

Processed launcher binary (expanded if Rust, unchanged if Go/Unix)

Source code in flavor/psp/format_2025/pe_utils.py
def process_launcher_for_pspf(launcher_data: bytes) -> bytes:
    """
    Process launcher binary for PSPF embedding compatibility.

    This is the main entry point for PE manipulation. It uses a hybrid approach:
    - Go launchers: Use PE overlay (no modifications, PSPF appended after sections)
    - Rust launchers: Use DOS stub expansion (PSPF at fixed 0xF0 offset)

    Phase 29: Go binaries are fundamentally incompatible with DOS stub expansion
    due to their PE structure (15 sections, unusual section names, missing data
    directories). The PE overlay approach is the industry standard and preserves
    100% PE structure integrity.

    Args:
        launcher_data: Original launcher binary

    Returns:
        Processed launcher binary (expanded if Rust, unchanged if Go/Unix)
    """
    if not is_pe_executable(launcher_data):
        # Not a Windows PE executable, return unchanged (Unix binary)
        logger.trace("Launcher is not a PE executable, no processing needed")
        return launcher_data

    launcher_type = get_launcher_type(launcher_data)

    if launcher_type == "go":
        # Go launcher: Use PE overlay approach (zero modifications)
        # PSPF data will be appended after all PE sections
        logger.info("Using PE overlay approach for Go launcher (no PE modifications)")
        return launcher_data
    elif launcher_type == "rust":
        # Rust launcher: Use DOS stub expansion (PSPF at fixed 0xF0 offset)
        if needs_dos_stub_expansion(launcher_data):
            logger.info("Expanding DOS stub for Rust launcher (PSPF at 0xF0)")
            return expand_dos_stub(launcher_data)
        else:
            logger.trace("Rust launcher already has adequate DOS stub")
            return launcher_data
    else:
        # Unknown launcher type: Safe default is no modification (PE overlay)
        logger.info("Unknown launcher type, using PE overlay approach")
        return launcher_data