From f40dc401bcbbeff4013a942485d5ed75c084ad15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20S=C3=A6taberget?= <2572988+Lars-Saetaberget@users.noreply.github.com> Date: Wed, 19 Oct 2022 13:03:34 +0200 Subject: [PATCH 1/3] Add IV support to lastpass (-m 6800) If you are from the past and need the old functionality, just use zeroes for the IV. For instance, the old example hash would then become: 82dbb8ccc9c7ead8c38a92a6b5740f94:500:pmix@trash-mail.com:00000000000000000000000000000000 --- OpenCL/m06800-pure.cl | 16 ++++++++++++--- src/modules/module_06800.c | 40 +++++++++++++++++++++++++++++++----- tools/test_modules/m06800.pm | 8 +++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/OpenCL/m06800-pure.cl b/OpenCL/m06800-pure.cl index d386938fa..48efc5306 100644 --- a/OpenCL/m06800-pure.cl +++ b/OpenCL/m06800-pure.cl @@ -18,6 +18,11 @@ #define COMPARE_S M2S(INCLUDE_PATH/inc_comp_single.cl) #define COMPARE_M M2S(INCLUDE_PATH/inc_comp_multi.cl) +typedef struct lastpass +{ + u32 iv[4]; +} lastpass_t; + typedef struct lastpass_tmp { u32 ipad[8]; @@ -70,7 +75,7 @@ DECLSPEC void hmac_sha256_run_V (PRIVATE_AS u32x *w0, PRIVATE_AS u32x *w1, PRIVA sha256_transform_vector (w0, w1, w2, w3, digest); } -KERNEL_FQ void m06800_init (KERN_ATTR_TMPS (lastpass_tmp_t)) +KERNEL_FQ void m06800_init (KERN_ATTR_TMPS_ESALT (lastpass_tmp_t, lastpass_t)) { /** * base @@ -154,7 +159,7 @@ KERNEL_FQ void m06800_init (KERN_ATTR_TMPS (lastpass_tmp_t)) } } -KERNEL_FQ void m06800_loop (KERN_ATTR_TMPS (lastpass_tmp_t)) +KERNEL_FQ void m06800_loop (KERN_ATTR_TMPS_ESALT (lastpass_tmp_t, lastpass_t)) { const u64 gid = get_global_id (0); @@ -260,7 +265,7 @@ KERNEL_FQ void m06800_loop (KERN_ATTR_TMPS (lastpass_tmp_t)) } } -KERNEL_FQ void m06800_comp (KERN_ATTR_TMPS (lastpass_tmp_t)) +KERNEL_FQ void m06800_comp (KERN_ATTR_TMPS_ESALT (lastpass_tmp_t, lastpass_t)) { const u64 gid = get_global_id (0); const u64 lid = get_local_id (0); @@ -367,6 +372,11 @@ KERNEL_FQ void m06800_comp (KERN_ATTR_TMPS (lastpass_tmp_t)) out[2] = hc_swap32_S (out[2]); out[3] = hc_swap32_S (out[3]); + out[0] ^= esalt_bufs[DIGESTS_OFFSET_HOST].iv[0]; + out[1] ^= esalt_bufs[DIGESTS_OFFSET_HOST].iv[1]; + out[2] ^= esalt_bufs[DIGESTS_OFFSET_HOST].iv[2]; + out[3] ^= esalt_bufs[DIGESTS_OFFSET_HOST].iv[3]; + truncate_block_4x4_le_S (out, salt_len); if ((out[0] == salt_buf[0]) diff --git a/src/modules/module_06800.c b/src/modules/module_06800.c index 1b30c2459..593efa936 100644 --- a/src/modules/module_06800.c +++ b/src/modules/module_06800.c @@ -25,7 +25,7 @@ static const u64 OPTS_TYPE = OPTS_TYPE_STOCK_MODULE | OPTS_TYPE_PT_GENERATE_LE; static const u32 SALT_TYPE = SALT_TYPE_EMBEDDED; static const char *ST_PASS = "hashcat"; -static const char *ST_HASH = "82dbb8ccc9c7ead8c38a92a6b5740f94:500:pmix@trash-mail.com"; +static const char *ST_HASH = "02eb97e869e0ddc7dc760fc633b4b54d:100100:pmix@trash-mail.com:9b071db7b8e265d4cadd3eb65ac0864a"; u32 module_attack_exec (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ATTACK_EXEC; } u32 module_dgst_pos0 (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS0; } @@ -42,6 +42,11 @@ u32 module_salt_type (MAYBE_UNUSED const hashconfig_t *hashconfig, const char *module_st_hash (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_HASH; } const char *module_st_pass (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_PASS; } +typedef struct lastpass +{ + u32 iv[4]; +} lastpass_t; + typedef struct lastpass_tmp { u32 ipad[8]; @@ -52,6 +57,13 @@ typedef struct lastpass_tmp } lastpass_tmp_t; +u64 module_esalt_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) +{ + const u64 esalt_size = (const u64) sizeof (lastpass_t); + + return esalt_size; +} + bool module_unstable_warning (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra, MAYBE_UNUSED const hc_device_param_t *device_param) { // AMD Radeon Pro W5700X Compute Engine; 1.2 (Apr 22 2021 21:54:44); 11.3.1; 20E241 @@ -120,7 +132,7 @@ int module_hash_decode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSE hc_token_t token; - token.token_cnt = 3; + token.token_cnt = 4; token.len_min[0] = 32; token.len_max[0] = 64; @@ -138,6 +150,12 @@ int module_hash_decode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSE token.sep[2] = ':'; token.attr[2] = TOKEN_ATTR_VERIFY_LENGTH; + token.len_min[3] = 32; + token.len_max[3] = 32; + token.sep[3] = ':'; + token.attr[3] = TOKEN_ATTR_VERIFY_LENGTH + | TOKEN_ATTR_VERIFY_HEX; + const int rc_tokenizer = input_tokenizer ((const u8 *) line_buf, line_len, &token); if (rc_tokenizer != PARSER_OK) return (rc_tokenizer); @@ -165,6 +183,12 @@ int module_hash_decode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSE if (parse_rc == false) return (PARSER_SALT_LENGTH); + lastpass_t *lastpass = (lastpass_t *) esalt_buf; + + const int iv_size = hex_decode ((const u8 *) token.buf[3], token.len[3], (u8 *) lastpass->iv); + + if (iv_size != sizeof (lastpass->iv)) return (PARSER_IV_LENGTH); + return (PARSER_OK); } @@ -172,19 +196,25 @@ int module_hash_encode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSE { const u32 *digest = (const u32 *) digest_buf; + const lastpass_t *lastpass = (const lastpass_t *) esalt_buf; + char tmp_salt[SALT_MAX * 2]; const int salt_len = generic_salt_encode (hashconfig, (const u8 *) salt->salt_buf, (const int) salt->salt_len, (u8 *) tmp_salt); tmp_salt[salt_len] = 0; - return snprintf (line_buf, line_size, "%08x%08x%08x%08x:%u:%s", + return snprintf (line_buf, line_size, "%08x%08x%08x%08x:%u:%s:%08x%08x%08x%08x", digest[0], digest[1], digest[2], digest[3], salt->salt_iter + 1, - tmp_salt); + tmp_salt, + byte_swap_32(lastpass->iv[0]), + byte_swap_32(lastpass->iv[1]), + byte_swap_32(lastpass->iv[2]), + byte_swap_32(lastpass->iv[3])); } void module_init (module_ctx_t *module_ctx) @@ -207,7 +237,7 @@ void module_init (module_ctx_t *module_ctx) module_ctx->module_dgst_pos3 = module_dgst_pos3; module_ctx->module_dgst_size = module_dgst_size; module_ctx->module_dictstat_disable = MODULE_DEFAULT; - module_ctx->module_esalt_size = MODULE_DEFAULT; + module_ctx->module_esalt_size = module_esalt_size; module_ctx->module_extra_buffer_size = MODULE_DEFAULT; module_ctx->module_extra_tmp_size = MODULE_DEFAULT; module_ctx->module_extra_tuningdb_block = MODULE_DEFAULT; diff --git a/tools/test_modules/m06800.pm b/tools/test_modules/m06800.pm index 9c64422b9..51d9d0430 100644 --- a/tools/test_modules/m06800.pm +++ b/tools/test_modules/m06800.pm @@ -18,9 +18,9 @@ sub module_generate_hash { my $word = shift; my $salt = shift; - my $iter = shift // 500; + my $iter = shift // 100100; - my $iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + my $iv = random_bytes(16); my $hasher = Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 256); @@ -45,7 +45,9 @@ sub module_generate_hash my $hash_buf = substr (unpack ("H*", $encrypt), 0, 32); - my $hash = sprintf ("%s:%i:%s", $hash_buf, $iter, $salt); + my $iv_buf = unpack("H*", $iv); + + my $hash = sprintf ("%s:%i:%s:%s", $hash_buf, $iter, $salt, $iv_buf); return $hash; } From b2cd1d1afb3fecedb2ff55c98a67f1e08f6f9ea9 Mon Sep 17 00:00:00 2001 From: hansvh <6390369+hans-vh@users.noreply.github.com> Date: Fri, 21 Oct 2022 14:49:59 +0200 Subject: [PATCH 2/3] Add lastpass2hashcat.py --- tools/lastpass2hashcat.py | 166 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100755 tools/lastpass2hashcat.py diff --git a/tools/lastpass2hashcat.py b/tools/lastpass2hashcat.py new file mode 100755 index 000000000..d4fc095a9 --- /dev/null +++ b/tools/lastpass2hashcat.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Author: hansvh <6390369+hans-vh@users.noreply.github.com> +# Version: 0.0.6 +# License: MIT + +""" +Files can be found here: +Android: /data/data/com.lastpass.lpandroid/files +Others: See https://support.lastpass.com/help/where-is-my-lastpass-data-stored-on-my-computer-lp070008 + +Tested OK with: +- LastPass for Android (com.lastpass.lpandroid) v5.12.0.10004 +- LastPass for Chrome v4.101.1 +- LastPass for Opera v4.101.1 +- LastPass for Firefox v4.101.0 +""" + +import sys +import os +import sqlite3 +from base64 import b64decode +from re import search + + +def parse_encu(data): + """Parse ENCU and return IV and AES-256-CBC encrypted email to compare against""" + data = data.decode("utf-8") + + initialization_vector = None + encrypted_email = None + + try: + # Format: ![B64]|[B64] + result = search(r"^!(.*)\|(.*)$", data) + initialization_vector = result.group(1) + encrypted_email = result.group(2) + + initialization_vector = b64decode(initialization_vector).hex() + encrypted_email = b64decode(encrypted_email).hex() + except: + # B64 Only. This implies EBC, not CBC, mode and IV is found elsewhere, e.g., in database + encrypted_email = b64decode(data).hex() + + return initialization_vector, encrypted_email + + +def open_file(file_name): + """Open file and return contents""" + with open(file_name, "rb") as file_handle: + return file_handle.read() + + +def parse_vault(xml): + """Parse Vault according to format: 4 bytes ASCII identifier, 4 bytes size, size bytes data""" + magic_bytes = xml[:4].decode("utf-8") + if magic_bytes != "LPAV": + sys.exit(f"Expected LPAV in base 64 decoded XML, but found {magic_bytes}") + + offset = 0 + while offset < len(xml): + identifier = xml[offset:offset + 4].decode("utf-8") + offset = offset + 4 + size = int.from_bytes(xml[offset:offset + 4], byteorder='big') + offset = offset + 4 + data = xml[offset:offset + size] + + if identifier == 'ENCU': + initialization_vector, encrypted_email = parse_encu(data) + return initialization_vector, encrypted_email + + offset = offset + size + + return None, None + + +def sqlite_parse_chromium(cur): + """Chrome and Opera""" + iterations = -1 + xml = "" + try: + res = cur.execute("SELECT data FROM LastPassData WHERE type='accts'") + (xml,) = res.fetchone() + result = search(r"^iterations=(\d+);(.*)$", xml) + iterations = result.group(1) + xml = result.group(2) + xml = b64decode(xml) + except: + return None, None + + return iterations, xml + + +def sqlite_parse_firefox(cur): + """Firefox""" + iterations = -1 + encu = "" + try: + res = cur.execute("SELECT value FROM data WHERE key LIKE '%sch'") + encu, = res.fetchone() + encu = encu.decode("utf-8") + encu = encu[encu.find("!"):] + encu = encu[:encu.find("\n")] + encu = bytes(encu, "utf-8") + res = cur.execute("SELECT value FROM data WHERE key LIKE '%key_iter'") + iterations, = res.fetchone() + iterations = int(iterations) + except: + return None, None + + return iterations, encu + + +def main(): + """Entry point""" + if len(sys.argv) < 3: + sys.exit(f"Usage: {sys.argv[0]} ") + + file_name = sys.argv[1] + if not os.path.exists(file_name): + sys.exit(f"File {file_name} does not exist") + + file_content = open_file(file_name) + magic_bytes = file_content[:5].decode("utf-8") + + # Output will contain the following fields (in order), colon separated + encrypted_email = "" + iterations = -1 + email = sys.argv[2].lower() + initialization_vector = "" + + if magic_bytes == "LPB64": + # Android App + iterations = 100100 + xml = b64decode(file_content[5:]) + initialization_vector, encrypted_email = parse_vault(xml) + + elif magic_bytes == "SQLit": + # Browser Extension + con = sqlite3.connect(file_name) + cur = con.cursor() + + # First try Chromium based browsers + iterations, xml = sqlite_parse_chromium(cur) + if iterations and xml: + initialization_vector, encrypted_email = parse_vault(xml) + + # Then try Firefox + if not encrypted_email or not iterations or not initialization_vector: + iterations, encu = sqlite_parse_firefox(cur) + initialization_vector, encrypted_email = parse_encu(encu) + + # Finally give up + if not encrypted_email or not iterations or not initialization_vector: + sys.exit("Unexpected behaviour in SQLite database parsing") + + con.close() + else: + sys.exit(f"Expected LPB64 or SQLit in file, but found {magic_bytes}") + + print(f"{encrypted_email}:{iterations}:{email}:{initialization_vector}") + + +if __name__ == "__main__": + main() From 3c3e0bf1a24ab792dcb270ae9f8e8da2aa887eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20S=C3=A6taberget?= Date: Fri, 21 Oct 2022 14:50:45 +0200 Subject: [PATCH 3/3] Update changes.txt --- docs/changes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changes.txt b/docs/changes.txt index 01aef0566..e89cdc9ce 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -10,6 +10,7 @@ ## Technical ## +- Modules: Added support for non-zero IVs for -m 6800 (Lastpass). Also added `tools/lastpass2hashcat.py` - Status Code: Add specific return code for self-test fail (-11) * changes v6.2.5 -> v6.2.6