openssl AES256 with a common standardized Password-Based Key Derivation Function
#1
Hello everyone,

i want to use hashcat to brute-force a password created by multibit classic bitcoin wallet. The wallet key backup uses the following openssl method to generate the backup:

openssl enc -p -aes-256-cbc -a -in \<plaintext file\> -out \<ciphertext file\> -pass pass:\<password\>

From my little knowledge, this is base64 decoded, salted with MD5 hash, so it could run really fast on GPUs.

In the multibit wiki, the followin is stated:

The whole private key file is encrypted. This uses AES256 with a common standardized Password-Based Key Derivation Function. For maximum compatibility I have used the same encryption methodology as in OpenSSL so you can also encrypt and decrypt the files using the command line utility 'openssl'.

Is it possible to use hashcat with it?
Reply
#2
are you talking about this https://github.com/hashcat/hashcat/issues/1538 ?

this has nothing todo with the "openssl" tool at all. very different algorithm.
Reply
#3
(01-28-2020, 02:02 PM)philsmd Wrote: are you talking about this https://github.com/hashcat/hashcat/issues/1538 ?

this has nothing todo with the "openssl" tool at all. very different algorithm.

Not really. There seems to be some multibit version which uses scrypt, but my key backup is made from the openssl command mentioned above.

See here:

https://github.com/Multibit-Legacy/multi...ate%20keys

I can use the multibit2john python script on that, and john also is saying, that this is a MD5 based KDF.

Thank you for your help.
Reply
#4
Examples:

encrypted key backup file from multibit:

Code:
U2FsdGVkX1/Gt5+4m/DQUaahjZ1bZvpbehbiJ8RlZgScHycsuhU6vxfLMpWR1LSHoTJma6igo6eG
CnMqbPYXw9drUjK3BZ2Qo1ZVvWD8pLcaIPM3rcTLAouZjurxZE32


hash generated by multibit2john.py:

Code:
hashcat-20200128133332.key:$multibit$1*c6b79fb89bf0d051*a6a18d9d5b66fa5b7a16e227c46566049c1f272cba153abf17cb329591d4b487

Decrypting with openssl: (i've had to add -md md5, otherwise my openssl would use sha1):

Code:
openssl enc -d -p -aes-256-cbc -a -in hashcat.key -md md5 -out hashcat-decrypt.key -pass pass:test

Code:
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
salt=C6B79FB89BF0D051
key=1D7E307669F6D7224294C3A69BAFD9B94771F74805E97EB8D61CE78878780444
iv =480A54B1CA593A0BDBA73B3072492B1F


out file from openssl:

Code:
KzWpNf4JJC8StHZE9nYtQZpXbhDyxWypxKBUaZMcBEJSZ2oYTiZd 2020-01-21T21:58:35Z

unencrypted key backup from multibit:

Code:
# KEEP YOUR PRIVATE KEYS SAFE !
# Anyone who can read this file can spend your bitcoin.
#
# Format:
#  <Base58 encoded private key>[<whitespace>[<key createdAt>]]
#
#  The Base58 encoded private keys are the same format as
#  produced by the Satoshi client/ sipa dumpprivkey utility.
#
#  Key createdAt is in UTC format as specified by ISO 8601
#  e.g: 2011-12-31T16:42:00Z . The century, 'T' and 'Z' are mandatory
#
KzWpNf4JJC8StHZE9nYtQZpXbhDyxWypxKBUaZMcBEJSZ2oYTiZd 2020-01-28T12:36:13Z
# End of private keys


So it is for sure MD5 used. I simply don't know how to use it with hashcat. Any help would be really cool.
Reply
#5
Ok, from my understanding it works that way:

The salt is:

Code:
c6b79fb89bf0d051

hashcat should generate MD5 hashes with the given attack mode, salt it with the above salt and compare it with the first two AES blocks:

Code:
a6a18d9d5b66fa5b7a16e227c46566049c1f272cba153abf17cb329591d4b487

Am i right?

EDIT: i've also found out, that iteration count for this is 3, if this helps.
Reply
#6
Maybe this can help:

Code:
############### MultiBit ###############
# - MultiBit .key backup files
# - MultiDoge .key backup files
# - Bitcoin Wallet for Android/BlackBerry v3.47+ wallet backup files
# - Bitcoin Wallet for Android/BlackBerry v2.24 and older key backup files
# - Bitcoin Wallet for Android/BlackBerry v2.3 - v3.46 key backup files
# - KnC for Android key backup files (same as the above)

@register_wallet_class
class WalletMultiBit(object):

    class __metaclass__(type):
        @property
        def data_extract_id(cls): return b"mb"

    # MultiBit private key backup file (not the wallet file)
    @staticmethod
    def is_wallet_file(wallet_file):
        wallet_file.seek(0)
        try:  data = base64.b64decode(wallet_file.read(20).lstrip()[:12])
        except TypeError: return False
        return data.startswith(b"Salted__")

    def __init__(self, loading = False):
        assert loading, 'use load_from_* to create a ' + self.__class__.__name__
        aes_library_name = load_aes256_library().__name__
        self._passwords_per_second = 100000 if aes_library_name == "Crypto" else 5000

    def __setstate__(self, state):
        # (re-)load the required libraries after being unpickled
        load_aes256_library(warnings=False)
        self.__dict__ = state

    def passwords_per_seconds(self, seconds):
        return max(int(round(self._passwords_per_second * seconds)), 1)

    # Load a Multibit private key backup file (the part of it we need)
    @classmethod
    def load_from_filename(cls, privkey_filename):
        with open(privkey_filename) as privkey_file:
            # Multibit privkey files contain base64 text split into multiple lines;
            # we need the first 48 bytes after decoding, which translates to 64 before.
            data = b"".join(privkey_file.read(70).split())  # join multiple lines into one
        if len(data) < 64: raise EOFError("Expected at least 64 bytes of text in the MultiBit private key file")
        data = base64.b64decode(data[:64])
        assert data.startswith(b"Salted__"), "WalletBitcoinCore.load_from_filename: file starts with base64 'Salted__'"
        if len(data) < 48:  raise EOFError("Expected at least 48 bytes of decoded data in the MultiBit private key file")
        self = cls(loading=True)
        self._encrypted_block = data[16:48]  # the first two 16-byte AES blocks
        self._salt            = data[8:16]
        return self

    # Import a MultiBit private key that was extracted by extract-multibit-privkey.py
    @classmethod
    def load_from_data_extract(cls, privkey_data):
        assert len(privkey_data) == 24
        print(prog + ": WARNING: read the Usage for MultiBit Classic section of Extract_Scripts.md before proceeding", file=sys.stderr)
        self = cls(loading=True)
        self._encrypted_block = privkey_data[8:]  # a single 16-byte AES block
        self._salt            = privkey_data[:8]
        return self

    def difficulty_info(self):
        return "3 MD5 iterations"

    # This is the time-consuming function executed by worker thread(s). It returns a tuple: if a password
    # is correct return it, else return False for item 0; return a count of passwords checked for item 1
    assert b"1" < b"9" < b"A" < b"Z" < b"a" < b"z"  # the b58 check below assumes ASCII ordering in the interest of speed
    def return_verified_password_or_false(self, orig_passwords):
        # Copy a few globals into local for a small speed boost
        l_md5                = hashlib.md5
        l_aes256_cbc_decrypt  = aes256_cbc_decrypt
        encrypted_block      = self._encrypted_block
        salt                  = self._salt

        # Convert Unicode strings (lazily) to UTF-16 bytestrings, truncating each code unit to 8 bits
        if tstr == unicode:
            passwords = itertools.imap(lambda p: p.encode("utf_16_le", "ignore")[::2], orig_passwords)
        else:
            passwords = orig_passwords

        for count, password in enumerate(passwords, 1):
            salted = password + salt
            key1  = l_md5(salted).digest()
            key2  = l_md5(key1 + salted).digest()
            iv    = l_md5(key2 + salted).digest()
            b58_privkey = l_aes256_cbc_decrypt(key1 + key2, iv, encrypted_block[:16])

            # (all this may be fragile, e.g. what if comments or whitespace precede what's expected in future versions?)
            if b58_privkey[0] in b"LK5Q\x0a#":
                #
                # Does it look like a base58 private key (MultiBit, MultiDoge, or oldest-format Android key backup)?
                if b58_privkey[0] in b"LK5Q":  # private keys always start with L, K, or 5, or for MultiDoge Q
                    for c in b58_privkey[1:]:
                        # If it's outside of the base58 set [1-9A-HJ-NP-Za-km-z], break
                        if c > b"z" or c < b"1" or b"9" < c < b"A" or b"Z" < c < b"a" or c in b"IOl":
                            break
                    # If the loop above doesn't break, it's base58-looking so far
                    else:
                        # If another AES block is available, decrypt and check it as well to avoid false positives
                        if len(encrypted_block) >= 32:
                            b58_privkey = l_aes256_cbc_decrypt(key1 + key2, encrypted_block[:16], encrypted_block[16:32])
                            for c in b58_privkey:
                                if c > b"z" or c < b"1" or b"9" < c < b"A" or b"Z" < c < b"a" or c in b"IOl":
                                    break  # not base58
                            # If the loop above doesn't break, it's base58; we've found it
                            else:
                                return orig_passwords[count-1], count
                        else:
                            # (when no second block is available, there's a 1 in 300 billion false positive rate here)
                            return orig_passwords[count - 1], count
                #
                # Does it look like a bitcoinj protobuf (newest Bitcoin for Android backup)
                elif b58_privkey[2:6] == b"org." and b58_privkey[0] == b"\x0a" and ord(b58_privkey[1]) < 128:
                    for c in b58_privkey[6:14]:
                        # If it doesn't look like a lower alpha domain name of len >= 8 (e.g. 'bitcoin.'), break
                        if c > b"z" or (c < b"a" and c != b"."):
                            break
                    # If the loop above doesn't break, it looks like a domain name; we've found it
                    else:
                        return orig_passwords[count - 1], count
                #
                #  Does it look like a KnC for Android key backup?
                elif b58_privkey == b"# KEEP YOUR PRIV":
                    return orig_passwords[count-1], count

        return False, count


From: https://github.com/gurnec/btcrecover/blo...tcrpass.py
Reply
#7
I don't know about the details, but also the source code you posted says "Multibit Classic" (as the title of the github issue), so maybe there is a lot of confusion because users try to hijack github issues and nobody explains the differences between the formats.

I'm also NO python expert, but your code has 2 very strange "problems" and therefore contributes to even further confusion:
1. there is NO good verification that matches your decrypted text (the code only checks for "LK5Q", but your "plaintext" is "KzWp" as far as I understand)
2. the following code also makes NO logical sense to me (maybe I'm a python noob, but this seems like a bug in the code posted):
Code:
if b58_privkey[0] in b"LK5Q\x0a#":
    if b58_privkey[0] in b"LK5Q"

what is the sense to check for a substring "LK5Q", if already a longer string "LK5Q\x0a#" matches ? This is very weird code to me... maybe other guys in here (like @undeath) can make more sense of this code... but it seems to be flawed and not really working with your example "KzWp"... Did you try to use this btcrpass.py tool to crack your file ? Does it even work ?



update: I guess it's not a substring test, but it matches every character... i.e.
the char b58_privkey[0] could be "L", "K" "5", "Q" "\n" and "#"

very weird verification code indeed and there might be room for a lot of false positives if there are no stricter checks
Reply
#8
so it seems the only check is that the 32 decrypted bytes are base58 chars and the first needs to start with either L, K, 5 or Q.
I think that is approximately a chance of (58^32) / (256^32) .... it's not too bad, but could still result in rare false positives (see https://github.com/hashcat/hashcat/commi...9d8e38b69c , here we have "only" MD5, so it should be very fast... which is bad if you try very hard to reduce false positives)

Isn't there also a checksum involved with those keys or are they just random bytes converted to base58 ? Maybe not the full data needed for a checksum is within the output of multibit2john or btcrecover.
Reply
#9
(01-29-2020, 03:12 PM)philsmd Wrote: so it seems the only check is that the 32 decrypted bytes are base58 chars and the first needs to start with either L, K, 5 or Q.
I think that is approximately a chance of (58^32) / (256^32) .... it's not too bad, but could still result in rare false positives (see https://github.com/hashcat/hashcat/commi...9d8e38b69c , here we have "only" MD5, so it should be very fast... which is bad if you try very hard to reduce false positives)

Isn't there also a checksum involved with those keys or are they just random bytes converted to base58 ? Maybe not the full data needed for a checksum is within the output of multibit2john or btcrecover.

I really appreciate your answers, philsmd, thank you.

Well, i'm not too deep into this, but as far as i know, there is no checksum involved, it is random bytes.

btcreover reports the false positive also in the README:

Code:
Warning: Using the extract-multibit-privkey.py script on a MultiBit Classic key file, as described below, can lead to false positives. A false positive occurs when btcrecover reports that it has found the password, but is mistaken—the password which it displays may not be correct.

Would it be possible to integrate it in hashcat? Running it with GPU power would be very faster, i guess.
Reply
#10
btcrecover can operate in 2 modes:

1. Mode:

It operates on the full key backup file. This way, btcrecover can compare a second AES block:

Code:
  # If another AES block is available, decrypt and check it as well to avoid false positives
                        if len(encrypted_block) >= 32:
                            b58_privkey = l_aes256_cbc_decrypt(key1 + key2, encrypted_block[:16], encrypted_block[16:32])
                            for c in b58_privkey:
                                if c > b"z" or c < b"1" or b"9" < c < b"A" or b"Z" < c < b"a" or c in b"IOl":
                                    break  # not base58
                            # If the loop above doesn't break, it's base58; we've found it


2. Mode:

You use the extract script from btcrecover and btcreover operates on the output from the extract script. the script only extracts the first AES block. This mode is for the purpose, if you want to brute-force from a third-party, which you don't want to send the whole backup. Because in case, he is able to brute-force it, he has the wallet and is the owner of the BTC.

So in my case, i have the key backup file, so we could check the second AES block, which should avoide false positivs.

Maybe i'm wrong? With my limit coding skills i assume it works this way.
Reply