# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#

import struct
import logging
from typing import List, Optional

from volatility3.framework import interfaces, exceptions
from volatility3.framework.configuration import requirements
from volatility3.plugins import yarascan
from volatility3.plugins.windows.malware import direct_system_calls

vollog = logging.getLogger(__name__)


class IndirectSystemCalls(direct_system_calls.DirectSystemCalls):
    """Detects the Indirect System Call technique used to bypass EDRs."""

    _required_framework_version = (2, 4, 0)
    _version = (1, 0, 0)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.syscall_finder = direct_system_calls.syscall_finder_type(
            # gets the target address of a indirect jmp
            self._indirect_syscall_block_target,
            # we are looking for indirect system calls, so we don't want 'syscall' instructions in our code block
            False,
            # jmp [address]; ret
            "/\\xff\\x25[^\\xc3]{,24}\\xc3/",
            # any of these mean we aren't in a malicious indirect call
            ["call", "leave", "int3", "ret"],
            # stop at jmp, this should reference the system call instruction
            ["jmp"],
        )

    @classmethod
    def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
        # create a list of requirements for vadyarascan
        vadyarascan_requirements = [
            requirements.ModuleRequirement(
                name="kernel",
                description="Windows kernel",
                architectures=["Intel32", "Intel64"],
            ),
            requirements.VersionRequirement(
                name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0)
            ),
            requirements.VersionRequirement(
                name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0)
            ),
            requirements.VersionRequirement(
                name="direct_system_calls",
                component=direct_system_calls.DirectSystemCalls,
                version=(2, 0, 0),
            ),
        ]

        # get base yarascan requirements for command line options
        yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements()

        # return the combined requirements
        return yarascan_requirements + vadyarascan_requirements

    @staticmethod
    def _indirect_syscall_block_target(
        proc_layer: interfaces.layers.DataLayerInterface, inst
    ) -> Optional[int]:
        """
        This function determines the address of a jmp in the following form:

        jmp [address]

        To determine this, we must:
        1) Pull the 4 byte relative offset of 'address' inside the instruction
        2) Compute the full address of this relative offset
        3) Read from the address as it is being dereferenced
        4) Ensure the target address points to a 'syscall' instruction

        Args:
            proc_layer: the layer of the potential syscall block
            inst: the terminating instruction of the syscall block check
        Returns:
            The target address of the jump if it can be computed
        """

        try:
            jmp_address_str = proc_layer.read(inst.address, 6)
        except exceptions.InvalidAddressException:
            return None

        # Should be an jmp...
        if jmp_address_str[0:2] != b"\xff\x25":
            return None

        # get the address of the 'jmp [address]' instruction
        relative_offset = struct.unpack("<I", jmp_address_str[2:])[0]
        if not relative_offset or relative_offset == -1:
            return None

        # compute the target address of the jmp (dereference)
        jmp_address = inst.address + relative_offset + 6
        try:
            jmp_target_str = proc_layer.read(jmp_address, 8)
        except exceptions.InvalidAddressException:
            return None

        # compute from the target address then read from it
        jmp_target_address = struct.unpack("<Q", jmp_target_str)[0]
        try:
            jmp_target = proc_layer.read(jmp_target_address, 2)
        except exceptions.InvalidAddressException:
            return None

        # check that the address points to a 'syscall' instruction
        if jmp_target == b"\x0f\x05":
            return jmp_target_address

        return None
