# Volatility
#
# Copyright (c) 2010 - 2012 Michael Ligh <michael.ligh@mnin.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details. 
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
#

import volatility.utils as utils
import volatility.obj as obj
import volatility.plugins.common as common
import volatility.debug as debug
import volatility.win32.tasks as tasks
import volatility.win32.modules as modules

try:
    import distorm3
    has_distorm = True
except ImportError:
    has_distorm = False

class ImpScan(common.AbstractWindowsCommand):
    """Scan for calls to imported functions"""

    def __init__(self, config, *args, **kwargs):

        common.AbstractWindowsCommand.__init__(self, config, *args, **kwargs)

        # Define a new PID option instead of inheriting from 
        # taskmods.DllList because this one cannot be a comma
        # separated list of PIDs.
        config.add_option('PID', short_option = 'p', default = None,
                          help = 'Process ID (leave off to scan kernel memory)',
                          action = 'store', type = 'int')
        # The base address in kernel or process memory where 
        # we begin scanning. This is an executable region with
        # assembly instructions like a .text or .code PE section.
        config.add_option('BASE', short_option = 'b', default = None,
                          help = 'Base address in process memory if --pid ' +
                          'is supplied, otherwise an address in kernel space',
                          action = 'store', type = 'int')
        # The size in bytes of data to scan from the base address. 
        config.add_option('SIZE', short_option = 's', default = None,
                          help = 'Size of memory to scan',
                          action = 'store', type = 'int')
        ## FIXME. ImpScan currently does not work on wow64 processes. 
        ## Add an option to override the profile's memory_model and 
        ## allow 32bit disasm on x64 operating systems. 

        self.forwarded_imports = {
            "RtlGetLastWin32Error" : "kernel32.dll!GetLastError",
            "RtlSetLastWin32Error" : "kernel32.dll!SetLastError",
            "RtlRestoreLastWin32Error" : "kernel32.dll!SetLastError",
            "RtlAllocateHeap" : "kernel32.dll!HeapAlloc",
            "RtlReAllocateHeap" : "kernel32.dll!HeapReAlloc",
            "RtlFreeHeap" : "kernel32.dll!HeapFree",
            "RtlEnterCriticalSection" : "kernel32.dll!EnterCriticalSection",
            "RtlLeaveCriticalSection" : "kernel32.dll!LeaveCriticalSection",
            "RtlDeleteCriticalSection" : "kernel32.dll!DeleteCriticalSection",
            "RtlZeroMemory" : "kernel32.dll!ZeroMemory",
            "RtlSizeHeap" : "kernel32.dll!HeapSize",
            "RtlUnwind" : "kernel32.dll!RtlUnwind",
            }

    @staticmethod
    def enum_apis(all_mods):
        """Enumerate all exported functions from kernel 
        or process space. 

        @param all_mods: list of _LDR_DATA_TABLE_ENTRY 

        To enum kernel APIs, all_mods is a list of drivers. 
        To enum process APIs, all_mods is a list of DLLs. 

        The function name is used if available, otherwise 
        we take the ordinal value. 
        """
        exports = {}

        for mod in all_mods:
            for ordinal, func_addr, func_name in mod.exports():
                name = func_name or ordinal or ''
                exports[int(mod.DllBase + func_addr)] = (mod, str(name))

        return exports

    def _call_or_unc_jmp(self, op):
        """Determine if an instruction is a call or an
        unconditional jump

        @param op: a distorm3 Op object
        """
        return ((op.flowControl == 'FC_CALL' and
                op.mnemonic == "CALL") or
                (op.flowControl == 'FC_UNC_BRANCH' and
                op.mnemonic == "JMP"))

    def _vicinity_scan(self, addr_space, calls_imported,
                apis, base_address, data_len, forward):
        """Scan forward from the lowest IAT entry found or
        backward from the highest IAT entry found. We do this 
        because not every imported function will be called 
        from the code section and sometimes page(s) with the 
        calls are unavailable. 

        @param addr_space: an AS
        @param calls_imported: dictionary of confirmed imports
        @param apis: dictionary of exported functions in the AS 
        @param base_address: memory base address 
        @param data_len: size in bytes to check from base_address
        @param forwared: the direction for the vicinity scan
        """

        sortedlist = calls_imported.keys()
        sortedlist.sort()

        if not sortedlist:
            return

        size_of_address = addr_space.profile.get_obj_size("address")

        if forward:
            start_addr = sortedlist[0]
        else:
            start_addr = sortedlist[len(sortedlist) - 1]

        # We stop scanning when the threshold reaches zero. This
        # value is decremented each invalid or duplicate API call
        # seen. It resets when a valid API call is seen. 
        threshold = 5
        i = 0

        while threshold and i < 0x2000:
            if forward:
                next_addr = start_addr + (i * size_of_address)
            else:
                next_addr = start_addr - (i * size_of_address)

            call_dest = obj.Object("address", offset = next_addr,
                            vm = addr_space).v()

            if (not call_dest or
                    call_dest < base_address or
                    call_dest > base_address + data_len):
                threshold -= 1
                i += 1
                continue

            # Reset the threshold if we found a valid API call, 
            # otherwise decrement the threshold by one 
            if call_dest in apis and call_dest not in calls_imported:
                calls_imported[next_addr] = call_dest
                threshold = 5
            else:
                threshold -= 1

            i += 1

    def _original_import(self, mod_name, func_name):
        """Revert a forwarded import to the original module 
        and function name. 

        @param mod_name: current module name 
        @param func_name: current function name 
        """

        if func_name in self.forwarded_imports:
            return self.forwarded_imports[func_name].split("!")
        else:
            return mod_name, func_name

    def call_scan(self, addr_space, base_address, data):
        """Disassemble a block of data and yield possible 
        calls to imported functions. We're looking for 
        instructions such as these:

        x86:
        CALL DWORD [0x1000400]
        JMP  DWORD [0x1000400]
        
        x64:
        CALL QWORD [RIP+0x989d]

        On x86, the 0x1000400 address is an entry in the 
        IAT or call table. It stores a DWORD which is the 
        location of the API function being called. 

        On x64, the 0x989d is a relative offset from the
        current instruction (RIP). 

        @param addr_space: an AS to scan with
        @param base_address: memory base address
        @param data: buffer of data found at base_address 
        """

        end_address = base_address + len(data)

        memory_model = addr_space.profile.metadata.get('memory_model', '32bit')

        if memory_model == '32bit':
            mode = distorm3.Decode32Bits
        else:
            mode = distorm3.Decode64Bits

        for op in distorm3.Decompose(base_address, data, mode):

            if not op.valid:
                continue

            iat_loc = None

            if memory_model == '32bit':
                if (self._call_or_unc_jmp(op) and
                        op.operands[0].type == 'AbsoluteMemoryAddress'):
                    iat_loc = (op.operands[0].disp) & 0xffffffff
            else:
                if (self._call_or_unc_jmp(op) and
                        'FLAG_RIP_RELATIVE' in op.flags and
                        op.operands[0].type == 'AbsoluteMemory'):
                    iat_loc = op.address + op.size + op.operands[0].disp

            if (not iat_loc or
                    (iat_loc < base_address) or
                    (iat_loc > end_address)):
                continue

            # This is the address being called 
            call_dest = obj.Object("address", offset = iat_loc,
                            vm = addr_space)

            if call_dest == None:
                continue

            yield op.address, iat_loc, int(call_dest)

    def calculate(self):

        if not has_distorm:
            debug.error("You must install distorm3")

        addr_space = utils.load_as(self._config)

        all_tasks = list(tasks.pslist(addr_space))
        all_mods = list(modules.lsmod(addr_space))

        # Operate in kernel mode if pid is not supplied
        if not self._config.PID:
            if not self._config.BASE:
                debug.error("You must specify --BASE")

            base_address = self._config.BASE
            size_to_read = self._config.SIZE

            # Get the size from the module list if its not supplied
            if not size_to_read:
                for module in all_mods:
                    if module.DllBase == base_address:
                        size_to_read = module.SizeOfImage
                        break
                if not size_to_read:
                    debug.error("You must specify --SIZE")

            kernel_space = tasks.find_space(addr_space,
                                    all_tasks, base_address)

            if not kernel_space:
                debug.error("Cannot read supplied address")

            data = kernel_space.zread(base_address, size_to_read)
            apis = self.enum_apis(all_mods)
            addr_space = kernel_space
        else:
            # In process mode, we find the process by PID
            task = None

            for atask in all_tasks:
                if atask.UniqueProcessId == self._config.PID:
                    task = atask
                    break

            if not task:
                debug.error("You must supply an active PID")

            task_space = task.get_process_address_space()

            if not task_space:
                debug.error("Cannot acquire process AS")

            all_mods = list(task.get_load_modules())

            # PEB is paged or no DLLs loaded 
            if not all_mods:
                debug.error("Cannot load DLLs in process AS")

            # If an address is supplied with a size, try to get
            # the size from the vad node. If neither are supplied, 
            # assume we should carve the main process executable. 
            if self._config.BASE:
                base_address = self._config.BASE
                size_to_read = self._config.SIZE

                if not size_to_read:
                    for vad in task.VadRoot.traverse():
                        if base_address >= vad.Start and base_address <= vad.End:
                            size_to_read = vad.Length
                    if not size_to_read:
                        debug.error("You must specify --SIZE")
            else:
                # Its OK to blindly take the 0th element because the 
                # executable is always the first module to load. 
                base_address = all_mods[0].DllBase
                size_to_read = all_mods[0].SizeOfImage

            if not task_space.is_valid_address(base_address):
                debug.error("Address is not valid in process AS")

            data = task_space.zread(base_address, size_to_read)
            apis = self.enum_apis(all_mods)
            addr_space = task_space

        # This is a dictionary of confirmed API calls.
        calls_imported = dict(
                (iat, call)
                for (_, iat, call) in self.call_scan(addr_space, base_address, data)
                if call in apis
                )

        # Scan forward 
        self._vicinity_scan(addr_space,
                calls_imported, apis, base_address, len(data),
                forward = True)

        # Scan reverse 
        self._vicinity_scan(addr_space,
                calls_imported, apis, base_address, len(data),
                forward = False)

        for iat, call in sorted(calls_imported.items()):
            yield iat, call, apis[call][0], apis[call][1]

    def render_text(self, outfd, data):
        """Render as text"""

        self.table_header(outfd,
                         [("IAT", "[addrpad]"),
                          ("Call", "[addrpad]"),
                          ("Module", "20"),
                          ("Function", ""),
                         ])

        for iat, call, mod, func in data:

            mod_name, func_name = self._original_import(
                                        str(mod.BaseDllName or ''),
                                        func)
            self.table_row(outfd,
                           iat, call,
                           mod_name,
                           func_name)

    def render_idc(self, outfd, data):
        """Render as IDC"""

        outfd.write("#include <idc.idc>\nstatic main(void) {\n")

        for iat, _, mod, func in data:

            _, func_name = self._original_import(
                                        str(mod.BaseDllName or ''),
                                        func)

            outfd.write("   MakeDword(0x{0:08X});\n".format(iat))
            outfd.write("   MakeName(0x{0:08X}, \"{1}\");\n".format(iat, func_name))

        outfd.write("Exit(0);}")
