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.
Comentarios
Publicar un comentario
Gracias por comentar aquí ;)