# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 Eduardo Aguiar
#
# 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, 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, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import pymobius.registry.user_accounts
import pymobius.registry.cached_credentials
import pymobius.registry.main
from pymobius.registry import *
import mobius
import binascii
import re

ANT_ID = 'turing'
ANT_NAME = 'Turing'
ANT_VERSION = '1.0'

REGEX_WORDS = re.compile ("(\w[\w']*\w|\w)")

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Generic dataholder class
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class dataholder (object):
  pass

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Ant: Turing
# @author Eduardo Aguiar
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Ant (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, item):
    self.name = 'Turing Cryptographic Agent'
    self.version = '1.0'
    self.__item = item

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Run ant
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def run (self):
    if not self.__item.datasource:
      return

    self.__retrieve_passwords ()
    self.__retrieve_password_hashes ()
    self.__test_passwords ()
    self.__create_passwords_from_hashes ()
    self.__save_data ()

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Save data into model
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __save_data (self):
    c = self.__item.case
    transaction = c.new_transaction ()

    # remove old data
    self.__item.remove_passwords ()
    self.__item.remove_password_hashes ()

    # save passwords
    for tmp_p in self.__passwords:
      p = self.__item.new_password (tmp_p.type, tmp_p.value, tmp_p.description)
      p.set_attribute ('item', self.__item.name)

      for name, value in tmp_p.metadata:
        p.set_attribute (name, value)

    # save password hashes
    for tmp_h in self.__password_hashes:
      h = self.__item.new_password_hash (tmp_h.type, tmp_h.value, tmp_h.description)

      if tmp_h.password != None:
        h.password = tmp_h.password

      for name, value in tmp_h.metadata:
        h.set_attribute (name, value)

    self.__item.set_ant (ANT_ID, ANT_NAME, ANT_VERSION)
    transaction.commit ()
  
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve passwords
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_passwords (self):
    self.__passwords = []

    try:
      ant = pymobius.registry.main.Ant (self.__item)

      for reg in ant.get_data ():
        self.__retrieve_passwords_from_registry (reg)
    except Exception, e:
      print 'Warning:', e
      #raise

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve passwords from registry
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_passwords_from_registry (self, registry):
    self.__retrieve_passwords_from_registry_lsa (registry)
    self.__retrieve_passwords_from_registry_outlook (registry)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve passwords from LSA secrets
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_passwords_from_registry_lsa (self, registry):

    for key in registry.get_key_by_mask ('\\HKLM\\SECURITY\\Policy\\Secrets\\*'):
          
      # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa378826(v=vs.85).aspx
      if key.name == 'DefaultPassword':
        description = 'Automatic logon'
        ptype = 'os.default'
      
      elif key.name == '0083343a-f925-4ed7-b1d6-d95d17a0b57b-RemoteDesktopHelpAssistantAccount':
        description = 'User HelpAssistant logon'
        ptype = 'os.user'

      elif key.name == 'aspnet_WP_PASSWORD':
        description = 'User ASPNET logon'
        ptype = 'os.user'

      elif key.name == 'SCM:{3D14228D-FBE1-11D0-995D-00C04FD919C1}':
        description = 'IIS IWAM'
        ptype = 'app.iis'

      # @see http://nvidia.custhelp.com/app/answers/detail/a_id/3067/~/what-is-nvidia-%E2%80%99updatususer%E2%80%99%3F
      elif key.name == '_SC_nvUpdatusService':
        description = 'User UpdatusUser logon'
        ptype = 'os.user'

      elif key.name.startswith ('_SC_DB2'):
        description = 'DB2'
        ptype = 'app.db2'

      elif key.name.startswith ('_SC_postgresql-') or key.name.startswith ('_SC_pgsql-'):
        description = 'PostgreSQL'
        ptype = 'app.postgresql'

      else:
        description = None
        ptype = None

      # add current and old passwords
      if description:
        currval = get_data_as_string (key.get_data_by_path ('Currval\\(default)'))
        
        if currval:
          p = dataholder ()
          p.type = ptype
          p.value = currval
          p.description = description + ' password'

          p.metadata = []
          p.metadata.append (('source', 'LSA key %s currval' % key.name))

          self.__passwords.append (p)

        oldval = get_data_as_string (key.get_data_by_path ('Oldval\\(default)'))
        
        if oldval:
          p = dataholder ()
          p.type = ptype
          p.value = oldval
          p.description = description + ' old password'

          p.metadata = []
          p.metadata.append (('source', 'LSA key %s oldval' % key.name))

          self.__passwords.append (p)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve passwords for Outlook Express and Outlook 98-2000
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_passwords_from_registry_outlook (self, registry):

    for user_key in registry.get_key_by_mask ('\\HKU\\*'):
      sid = user_key.name
      pssp_key = user_key.get_key_by_path ('Software\\Microsoft\\Protected Storage System Provider\\' + sid)
 
      for subkey in user_key.get_key_by_mask ('Software\\Microsoft\\Internet Account Manager\\Accounts\*'):
        account_name = get_data_as_string (subkey.get_data_by_name ('SMTP Display Name'))
        email_address = get_data_as_string (subkey.get_data_by_name ('SMTP Email Address'))

        # POP3 password
        pop3_value_name = subkey.get_data_by_name ('POP3 Password2')
        pop3_password = self.__get_pssp_password (pssp_key, pop3_value_name)

        if pop3_password:
          p = dataholder ()
          p.type = 'email.pop3'
          p.value = pop3_password
          p.description = "E-mail " + email_address + " POP3 password"

          p.metadata = []
          p.metadata.append (('source', 'PSSP POP3 Password2 value'))
          p.metadata.append (('email', email_address))
          p.metadata.append (('user_sid', sid))
          self.__passwords.append (p)

        # SMTP password
        smtp_value_name = subkey.get_data_by_name ('SMTP Password2')
        smtp_password = self.__get_pssp_password (pssp_key, smtp_value_name)

        if smtp_password:
          p = dataholder ()
          p.type = 'email.smtp'
          p.value = smtp_password
          p.description = "E-mail " + email_address + " SMTP password"

          p.metadata = []
          p.metadata.append (('source', 'PSSP SMTP Password2 value'))
          p.metadata.append (('email', email_address))
          p.metadata.append (('user_sid', sid))
          self.__passwords.append (p)

        # HTTP password
        http_value_name = subkey.get_data_by_name ('HTTPMail Password2')
        http_password = self.__get_pssp_password (pssp_key, http_value_name)

        if http_password:
          p = dataholder ()
          p.type = 'email.http'
          p.value = http_password
          p.description = "E-mail " + email_address + " HTTP password"

          p.metadata = []
          p.metadata.append (('source', 'PSSP HTTPMail Password2 value'))
          p.metadata.append (('email', email_address))
          p.metadata.append (('user_sid', sid))
          self.__passwords.append (p)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Get Protected Storage password
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 
  def __get_pssp_password (self, pssp_key, idx_data):
    password = None
    
    if pssp_key and idx_data:
      value_name = unicode (idx_data.data[2:], 'utf-16-le').rstrip ('\0').encode ('utf-8')
      data = pssp_key.get_data_by_path ('Data\\220d5cc1-853a-11d0-84bc-00c04fd43f8f\\417e2d75-84bd-11d0-84bb-00c04fd43f8f\\' + value_name)

      if data:
        password = data.data.rstrip ('\0')

    return password

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve password hashes
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_password_hashes (self):
    self.__password_hashes = []

    try:
      ant = pymobius.registry.main.Ant (self.__item)

      for reg in ant.get_data ():
        self.__retrieve_password_hashes_from_registry (reg)
    except Exception, e:
      print 'Warning:', e
      #raise

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve password hashes from registry
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_password_hashes_from_registry (self, reg):
    self.__retrieve_password_hashes_from_registry_user_accounts (reg)
    self.__retrieve_password_hashes_from_registry_cached_credential (reg)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve password hashes from user accounts
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_password_hashes_from_registry_user_accounts (self, registry):

    for account in pymobius.registry.user_accounts.get (registry):
      h_set = set ()

      for h in account.hashes:
        value = binascii.hexlify (h.value)
        k = (h.type, value, h.is_current)

        if k not in h_set:
          h_set.add (k)

          if h.is_current:
            description = 'User ' + account.username + ' password hash'
          else:
            description = 'User ' + account.username + ' old password hash'

          d = dataholder ()
          d.type = h.type
          d.value = value
          d.description = description
          d.password = None

          d.metadata = []
          d.metadata.append (('user_rid', str (account.rid)))
          d.metadata.append (('user_gid', str (account.gid)))
          d.metadata.append (('username', account.username))
          d.metadata.append (('fullname', account.fullname))
          d.metadata.append (('admin_comment', account.admin_comment))
          d.metadata.append (('user_comment', account.user_comment))
          d.metadata.append (('is_current', str (h.is_current)))
          self.__password_hashes.append (d)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve password hashes from cached credential
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_password_hashes_from_registry_cached_credential (self, registry):

    for name, credential in pymobius.registry.cached_credentials.get (registry):

      if credential.encryption_algorithm == 0:
        t = 'msdcc1'

      elif credential.encryption_algorithm == 10:
        t = 'msdcc2'

      else:
        mobius.core.log ('Unknown credential encryption algorithm (%d)' % credential.encryption_algorithm)
        t = None

      if t:
        d = dataholder ()
        d.type = t
        d.value = binascii.hexlify (credential.mscachehash)
        d.description = 'User ' + credential.username + ' cached credential hash'
        d.password = None
        
        d.metadata = []
        d.metadata.append (('username', credential.username))

        if t == 'msdcc2':
          d.metadata.append (('iterations', '10240'))

        self.__password_hashes.append (d)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Test passwords
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __test_passwords (self):

    # Test using mobius.turing
    turing = mobius.turing.turing ()
    hashes = [ h for h in self.__password_hashes if h.password == None ]

    for h in hashes:
      status, password = turing.get_hash_password (h.type, h.value)

      if status == 1:
        h.password = password

    # Generate wordlist composed of the following:
    # a. item passwords
    # b. case passwords
    # c. item's user names
    # d. case's user names
    hashes = [ h for h in self.__password_hashes if h.password == None ]
    if not hashes:
      return

    case = self.__item.case
    keywords = set (p.value for p in self.__passwords)
    keywords.update (set (p.value for p in case.get_passwords ()))
    
    for h in self.__password_hashes:
      metadata = dict (h.metadata)
      keywords.update (REGEX_WORDS.findall (metadata.get ('username', '')))
      keywords.update (REGEX_WORDS.findall (metadata.get ('fullname', '')))
      keywords.update (REGEX_WORDS.findall (metadata.get ('admin_comment', '')))
      keywords.update (REGEX_WORDS.findall (metadata.get ('user_comment', '')))

    for h in case.get_password_hashes ():
      keywords.update (REGEX_WORDS.findall (h.get_attribute ('username')))
      keywords.update (REGEX_WORDS.findall (h.get_attribute ('fullname')))
      keywords.update (REGEX_WORDS.findall (h.get_attribute ('admin_comment')))
      keywords.update (REGEX_WORDS.findall (h.get_attribute ('user_comment')))

    keywords = list (keywords)

    # Test keywords
    i = 0
    l = len (keywords)

    while i < l and hashes:   
      keyword = keywords[i]
      nt_value = binascii.hexlify (mobius.turing.hash_nt (keyword))
      lm_value = binascii.hexlify (mobius.turing.hash_lm (keyword))
      h_tmp = []

      for h in hashes:
        attrs = dict (h.metadata)

        if h.type == 'nt':
          value = nt_value
          
        elif h.type == 'lm':
          value = lm_value

        elif h.type == 'msdcc1':
          value = binascii.hexlify (mobius.turing.hash_msdcc1 (keyword, attrs['username']))

        elif h.type == 'msdcc2':
          value = binascii.hexlify (mobius.turing.hash_msdcc2 (keyword, attrs['username'], int (attrs['iterations'])))

        if value == h.value:
          h.password = keyword
        else:
          h_tmp.append (h)

      if len (h_tmp) < len (hashes):
        hashes = h_tmp

      i = i + 1

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Create passwords from revealed password hashes
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __create_passwords_from_hashes (self):
    hashes = [ h for h in self.__password_hashes if h.password != None ]

    turing = mobius.turing.turing ()
    transaction = turing.new_transaction ()

    for h in hashes:
      p = dataholder ()
      p.value = h.password
      p.type = 'os.user'
      p.metadata = h.metadata[:]
      p.metadata.append (('hash_type', h.type))
      p.metadata.append (('hash_value', h.value))

      username = dict (p.metadata).get ('username')

      if h.type in ("nt", "lm"):
        p.description = "User %s logon password" % username

      elif h.type in ("msdcc1", "msdcc2"):
        p.description = "User %s cached credential password" % username

      self.__passwords.append (p)
      turing.set_hash (h.type, h.value, h.password)
      
    transaction.commit ()
