Ir al contenido principal

Análisis Forense - Obteniendo cuentas de LastPass encriptadas con análisis de memoria.

Esta publicación se me ocurrió debido a que esta semana leí la noticia PSA: LastPass Does Not Encrypt Everything In Your Vault básicamente LastPass no está cifrando todos los datos del usuario.

LastPass es una de las plataformas más utilizadas para almacenar nuestras cuentas de usuario y contraseñas y poder acceder a ellas siempre que lo necesitemos. 

LastPass guarda con un cifrado AES de 256 bits toda la información más sensible de sus usuarios, como sus nombres, el usuario y la contraseña de una página web, la dirección URL de la página web asociada a dichos datos no se guarda de forma cifrada.


En la imagen anterior se puede visualizar que el parámetro URL está cifrado de forma diferente que los parámetros anteriores a él, esto es debido a que esta pasado por hexadecimal si se utiliza un conversor se podrá ver en texto plano la URL.


Básicamente de esto se trata la investigación … 

Para lograr obtener algo “hacer hack” no necesariamente debes ir en contra y romper puedes hacer bypass y en este caso le haremos honor al título de esta publicación utilizaremos el análisis forense para obtener todas las cuentas y contraseñas del usuario. 

¿Pensamos? En algún momento toda esta data debe ser descifrada, debe mostrarse tal cual es para poder ser tratada, pues aquí viene una de las cosas que más me gusta jugar y es investigar la memoria y descubrir que pasa en ella como en la publicación Hackeando la memoria de Windows en tiempo real (POC)

Si estas en una investigación y en el equipo te encuentras con el siguiente directorio:

C:\Users\El_Usuario\AppData\Local\Google\Chrome\User Data\Default\Extensions\hdokiejnpimakedhajhdlcegeplioahd

Debes saber que está presente LastPass en Chrome y si el equipo aún se encuentra encendido debemos hacer un dump de memoria *.raw/*.mem como ya se ha realizado en otras publicaciones.
Para este análisis utilizaremos Volatility y el siguiente plugin de Kevin Breen que utiliza Yara rules para poder llevar a cabo la tarea.

# Donated under Volatility Foundation, Inc. Individual Contributor Licensing Agreement
# LastPass - Recover memory resident account information.
# Author: Kevin Breen
# Thanks to the guide on http://www.ghettoforensics.com/2013/10/dumping-malware-configuration-data-from.html

import volatility.plugins.taskmods as taskmods
import volatility.win32.tasks as tasks
import volatility.utils as utils
import volatility.debug as debug
import volatility.plugins.malware.malfind as malfind
import volatility.conf as conf
import re
import json
import string

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


signatures = {
    'lastpass_struct_a': 'rule lastpass_strcuta {strings: $a = /{"reqinfo":.*"lplanguage":""}/ condition: $a}\n'
                         'rule lastpass_strcutb {strings: $a = /"tld":".*?","unencryptedUsername":".*?","realmmatch"/ condition: $a}\n'
                         'rule lastpass_strcutc {strings: $a = /{"cmd":"save"(.*?)"tld":"(.*?)"}/ condition: $a}\n'
                         'rule lastpass_strcutd {strings: $a = /"realurl":"(.*?)"domains":"(.*?)"/ condition: $a}\n'
                         'rule lastpass_strcute {strings: $a = /{"cmd":"save"(.*?)"formdata"(.*?)}/ condition: $a}\n'
                         'rule lastpass_priv1 {strings: $a = /LastPassPrivateKey<(.*?)>LastPassPrivateKey/ condition: $a}'
}

config = conf.ConfObject()
config.add_option('CONFSIZE', short_option='C', default=4000,
                           help ='Config data size',
                           action ='store', type='int')
config.add_option('YARAOFFSET', short_option='Y', default=0,
                           help ='YARA start offset',
                           action ='store', type='int')

class LastPass(taskmods.PSList):
    """ Extract lastpass data from process. """

    def calculate(self):
        """ Required: Runs YARA search to find hits """
        if not has_yara:
            debug.error('Yara must be installed for this plugin')

        addr_space = utils.load_as(self._config)
        rules = yara.compile(sources = signatures)
        for task in self.filter_tasks(tasks.pslist(addr_space)):
            if not task.ImageFileName.lower() in ['chrome.exe', 'firefox.exe', 'iexplore.exe']:
                continue
            scanner = malfind.VadYaraScanner(task=task, rules=rules)
            for hit, address in scanner.scan():
                yield task, address

    def string_clean_hex(self, line):
        line = str(line)
        new_line = ''
        for c in line:
            if c in string.printable:
                new_line += c
            else:
                new_line += '\\x' + c.encode('hex')
        return new_line


    def clean_json(self, raw_data):
        # We deliberately pull in too much data to make sure we get it all.
        # Now parse it out again

        if raw_data.startswith('LastPassPrivate'):
            pattern = 'LastPassPrivateKey<(.*?)>LastPassPrivateKey'
            key_data = re.search(pattern, raw_data).group(0)
            if not any(substring in key_data for substring in ['"+a+"', 'indexOf']):
                return self.string_clean_hex(key_data)

        else:

            if raw_data.startswith("{\"cmd"):
                if 'formdata' in raw_data:
                    pattern = '{"cmd":"save"(.*?)"formdata"(.*?)}'
                    val_type = 'mixedform'
                else:
                    pattern = '{"cmd":"save"(.*?)"tld":"(.*?)"}'
                    val_type = 'mixed'

            elif raw_data.startswith("\"realurl"):
                pattern = '"realurl":"(.*?)"domains":"(.*?)"'
                val_type = 'mixed'

            elif raw_data.startswith("\"tld"):
                pattern = '"tld":".*?","unencryptedUsername":".*?","realmmatch"'
                val_type = 'username'

            elif raw_data.startswith('{"reqinfo"'):
                pattern = '{"reqinfo":.*?"lplanguage":""}'
                val_type = 'password'
            else:
                pass

            match = re.search(pattern, raw_data)
            real_data = self.string_clean_hex(match.group(0))

            if val_type == 'username':
                vars = real_data.split(',')
                tld = vars[0].split(':')[1].strip('"')
                username = vars[1].split(':')[1].strip('"')
                return {'type': 'username', 'username': username, 'tld': tld}

            elif val_type == 'mixedform':
                if '"tld":"' in real_data:
                    # Try to parse as json
                    try:
                        json_data = json.loads(real_data)
                        tld = json_data['tld']
                        password = json_data['password']
                        username = json_data['username']
                        clean_data = {'type': 'mixed', 'username': username, 'tld': tld, 'password': password}
                    except ValueError:
                        # Json fails manual parse
                        tld = re.search('"tld":"(.*?)"', real_data)
                        if not tld:
                            tld = re.search('"domains":"(.*?)"', real_data)
                        if tld:
                            tld = tld.group(0).split(':')[-1].strip('"')
                        else:
                            tld = 'unknown'
                        try:
                            password = re.search('"password":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                        except:
                            password = 'Unknown'
                        try:
                            username = re.search('"username":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                        except:
                            username = 'Unknown'
                        clean_data = {'type': 'mixed', 'username': username, 'tld': tld, 'password': password}

                else:
                    tld = re.search('"url":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                    username = re.search('login(.*?)text', real_data).group(0).split('\\t')[1]
                    password = re.search('password(.*?)password', real_data).group(0).split('\\t')[1]
                    clean_data = {'type': 'mixed', 'username': username, 'tld': tld, 'password': password}

                return clean_data

            elif val_type == 'mixed':
                # Try to parse as json
                try:
                    json_data = json.loads(real_data)
                    tld = json_data['tld']
                    password = json_data['password']
                    username = json_data['username']
                    clean_data = {'type': 'mixed', 'username': username, 'tld': tld, 'password': password}
                except ValueError:
                    # Json fails manual parse
                    tld = re.search('"tld":"(.*?)"', real_data)
                    if not tld:
                        tld = re.search('"domains":"(.*?)"', real_data)

                    if tld:
                        tld = tld.group(0).split(':')[-1].strip('"')
                    else:
                        tld = 'unknown'

                    try:
                        password = re.search('"password":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                    except:
                        password = 'Unknown'
                    try:
                        username = re.search('"username":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                    except:
                        username = 'Unknown'

                    clean_data = {'type': 'mixed', 'username': username, 'tld': tld, 'password': password}
            else:
                # Try to parse as json
                try:
                    json_data = json.loads(real_data)
                    tld = json_data['domains']
                    password = json_data['value']
                    clean_data = {'type': 'password', 'password': password, 'tld': tld}
                except ValueError:
                    # Json fails manual parse
                    password = re.search('"value":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                    tld = re.search('"domains":"(.*?)"', real_data).group(0).split(':')[-1].strip('"')
                    clean_data = {'type': 'password', 'password': password, 'tld': tld}

            return clean_data

    def render_text(self, outfd, data):
        """ Required: Parse data and display """
        outfd.write("Searching for LastPass Signatures\n")

        rules = yara.compile(sources=signatures)

        results = {}
        priv_keys = []

        for task, address in data:  # iterate the yield values from calculate()
            outfd.write('Found pattern in Process: {0} ({1})\n'.format(task.ImageFileName, task.UniqueProcessId))
            proc_addr_space = task.get_process_address_space()
            raw_data = proc_addr_space.read(address + self._config.YARAOFFSET, self._config.CONFSIZE)
            if raw_data:
                clean_data = self.clean_json(raw_data)
                if not clean_data:
                    continue
                if 'PrivateKey' in clean_data:
                    priv_keys.append(clean_data)
                else:
                    # If we already created the dict
                    if clean_data['tld'] in results:
                        if 'username' in clean_data:
                            if results[clean_data['tld']]['username'] == 'Unknown':
                                results[clean_data['tld']]['username'] = clean_data['username']
                        if 'password' in clean_data:
                            if results[clean_data['tld']]['password'] == 'Unknown':
                                results[clean_data['tld']]['password'] = clean_data['password']
                    # Else create the dict
                    else:
                        if 'username' in clean_data:
                            username = clean_data['username']
                        else:
                            username = 'Unknown'
                        if 'password' in clean_data:
                            password = clean_data['password']
                        else:
                            password = 'Unknown'
                        results[clean_data['tld']] = {'username': username, 'password': password}

        for k, v in results.iteritems():
            outfd.write("\nFound LastPass Entry for {0}\n".format(k))
            outfd.write('UserName: {0}\n'.format(v['username']))
            outfd.write('Pasword: {0}\n'.format(v['password']))
        outfd.write('\n')

        for key in priv_keys:
            outfd.write('\nFound Private Key\n')
            outfd.write('{0}\n'.format(key))

El comando que utilizaremos es el siguiente:

volatility --plugins='/root/VolPlugins' --profile=Win7SP1x64 -f '/root/Escritorio/POC_CASE01 /TESTING-PC-20170125-023936.raw' lastpass

Donde invocamos a volatility, le indicamos la ruta del plugin, indicamos el perfil del OS, la ruta en donde se encuentra el volcado y finalmente invocamos el plugin lastpass.


Después de algunos minutos obtendremos las cuentas del usuario almacenadas con el Plugin LastPass en Chrome Browser.





Comentarios

Te sugiero las siguientes publicaciones 🙂🤙

Obteniendo strings de procesos en memoria con ReadProcessMemory().

Análisis Forense - Extracción de datos desde un dispositivo móvil Android para análisis lógico.

Análisis Forense - Extrayendo y reconstruyendo imágenes y sesión de usuario desde un volcado de memoria.