# Volatility
# Copyright (C) 2007-2013 Volatility Foundation
#
# Authors:
# Michael Hale Ligh <michael.ligh@mnin.org>
#
# Contributors/References:
#   ## Based on sslkeyfinder: http://www.trapkit.de/research/sslkeyfinder/
#
# This file is part of Volatility.
#
# Volatility is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License Version 2 as
# published by the Free Software Foundation.  You may not use, modify or
# distribute this program under any other version of the GNU General
# Public License.
#
# Volatility 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 Volatility.  If not, see <http://www.gnu.org/licenses/>.
#

import os, sys, subprocess
import volatility.obj as obj
import volatility.debug as debug
import volatility.plugins.procdump as procdump
import volatility.utils as utils
import volatility.win32.tasks as tasks
import volatility.plugins.malware.malfind as malfind

try:
    import yara
    has_yara = True
except ImportError:
    has_yara = False

#--------------------------------------------------------------------------------
# object classes
#--------------------------------------------------------------------------------

class _X509_PUBLIC_CERT(obj.CType):
    """Class for x509 public key certificates"""
    
    @property
    def Size(self):
        """
        The certificate size (in bytes) is a product of this
        object's Size1 and Size2 members. 
        """
        return (self.Size1 << 8 & 0xFFFF) + self.Size2

    def object_as_string(self):
        """
        Get the object's data as a string. in this case its
        the certificate header and body. 
        """
        return self.obj_vm.zread(self.obj_offset, self.Size + 4)

    def is_valid(self):
        """
        This implements the check described in sslfinder:
        http://www.trapkit.de/research/sslkeyfinder/
        """

        if not obj.CType.is_valid(self):
            return False

        return self.Size < 0xFFF

    def as_openssl(self, file_name):
        """
        Represent this object as openssl-parsed certificate.

        Since OpenSSL does not accept DERs from STDIN, we 
        have to redirect it to a file first. 

        @param file_name: a file on disk where this object
        has been dumped. the caller should ensure that the
        file exists before calling this function. 
        """
        return subprocess.Popen(
                ['openssl', 'x509', '-in', file_name, '-inform', 'DER', '-text'], 
                stdout = subprocess.PIPE, 
                stderr = subprocess.PIPE
                ).communicate()[0]
        
class _PKCS_PRIVATE_CERT(_X509_PUBLIC_CERT):
    """Class for PKCS private key certificates"""
    
    def as_openssl(self, file_name):
        return subprocess.Popen(
                ['openssl', 'rsa', '-check', '-in', file_name, '-inform', 'DER', '-text'], 
                stdout = subprocess.PIPE, 
                stderr = subprocess.PIPE
                ).communicate()[0]

class SSLKeyModification(obj.ProfileModification):
    """Applies to all windows profiles (maybe linux?)"""

    conditions = {'os': lambda x: x == 'windows'}

    def modification(self, profile):

        profile.vtypes.update({
            '_X509_PUBLIC_CERT': [ None, {
                'Size1': [ 0x2, ['unsigned char']], 
                'Size2': [ 0x3, ['unsigned char']], 
                }],
            '_PKCS_PRIVATE_CERT': [ None, {
                'Size1': [ 0x2, ['unsigned char']], 
                'Size2': [ 0x3, ['unsigned char']], 
                }],
            })

        profile.object_classes.update({
            '_X509_PUBLIC_CERT': _X509_PUBLIC_CERT, 
            '_PKCS_PRIVATE_CERT': _PKCS_PRIVATE_CERT, 
            })

# Inherit from ProcExeDump for access to the --dump-dir option
class DumpCerts(procdump.ProcExeDump):
    """Dump RSA private and public SSL keys"""

    def __init__(self, config, *args, **kwargs):
        procdump.ProcExeDump.__init__(self, config, *args, **kwargs)

        config.remove_option("UNSAFE")
        config.add_option("SSL", short_option = 's', 
                          default = False,
                          help = "Use OpenSSL for certificate parsing", action = "store_true")
        config.add_option("PHYSICAL", short_option = 'P',
                          default = False, 
                          help = "Scan across physical space (in deallocated/freed storage)",
                          action = "store_true")

    def calculate(self):
        addr_space = utils.load_as(self._config)

        if not has_yara:
            debug.error("You must install yara to use this plugin")

        if not self._config.DUMP_DIR:
            debug.error("You must supply a --dump-dir parameter")

        # Wildcard signatures to scan for 
        rules = yara.compile(sources = {
            'x509' : 'rule x509 {strings: $a = {30 82 ?? ?? 30 82 ?? ??} condition: $a}',
            'pkcs' : 'rule pkcs {strings: $a = {30 82 ?? ?? 02 01 00} condition: $a}',
            })

        # These signature names map to these data structures
        type_map = {
            'x509' : '_X509_PUBLIC_CERT', 
            'pkcs' : '_PKCS_PRIVATE_CERT',
        }
        
        if self._config.PHYSICAL:
            # Find the FileAddressSpace
            while addr_space.__class__.__name__ != "FileAddressSpace":
                addr_space = addr_space.base 
            scanner = malfind.DiscontigYaraScanner(address_space = addr_space, 
                                                   rules = rules)
            for hit, address in scanner.scan():
                cert = obj.Object(type_map.get(hit.rule), 
                                            vm = scanner.address_space,
                                            offset = address, 
                                            )
                if cert.is_valid():
                    yield None, cert
        else:
            for process in self.filter_tasks(tasks.pslist(addr_space)):
                scanner = malfind.VadYaraScanner(task = process, rules = rules)
                for hit, address in scanner.scan():
                    cert = obj.Object(type_map.get(hit.rule), 
                                            vm = scanner.address_space,
                                            offset = address, 
                                            )
                    if cert.is_valid():
                        yield process, cert

    def get_parsed_fields(self, openssl, fields = ["O", "OU"]):
        """
        Get fields from the parsed openssl output. 

        @param openssl: the output of an openssl command

        @param fields: fields of the SSL public or private
        key certificate that you want to get.

        @returns: a tuple of the field found and the field value.
        
        """
        for line in openssl.split("\n"):
            if "Subject:" in line:
                line = line[line.find("Subject:") + 10:]
                pairs = line.split(",")
                for pair in pairs:
                    try:
                        val, var = pair.split("=")
                    except ValueError:
                        continue
                    val = val.strip()
                    var = var.strip()
                    if val in fields:
                        yield (val, var)

    def render_text(self, outfd, data):

        self.table_header(outfd, [("Pid", "8"), 
                                  ("Process", "16"), 
                                  ("Address", "[addrpad]"), 
                                  ("Type", "20"), 
                                  ("Length", "8"), 
                                  ("File", "24"), 
                                  ("Subject", "")])

        for process, cert in data:

            if cert.obj_name == "_X509_PUBLIC_CERT":
                ext = ".crt"
            else:
                ext = ".key"

            if process:
                file_name = "{0}-{1:x}{2}".format(process.UniqueProcessId, 
                                                  cert.obj_offset, ext)
            else:
                file_name = "phys.{0:x}{1}".format(cert.obj_offset, ext)

            full_path = os.path.join(self._config.DUMP_DIR, file_name)

            with open(full_path, "wb") as cert_file:
                cert_file.write(cert.object_as_string())

            if self._config.SSL:
                openssl_string = cert.as_openssl(full_path)
                parsed_subject = '/'.join([v[1] for v in self.get_parsed_fields(openssl_string)])
            else:
                parsed_subject = ""

            self.table_row(outfd, 
                    process.UniqueProcessId if process else "-", 
                    process.ImageFileName if process else "-", 
                    cert.obj_offset, cert.obj_name, 
                    cert.Size, file_name, parsed_subject)
