-
-
Save alexlanghart/771aa24cb307335f8ed5380a45dffc6b to your computer and use it in GitHub Desktop.
# Author: Alex Langhart | |
# This script "un-decrypts" a veracrypt drive that was accidentally decrypted twice. | |
# The veracrypt recovery disk allows the user to decrypt twice, but offers no way | |
# to un-decrypt it, hence the need for this script. Re-encrypting the drive using | |
# the standard process wouldn't work because it generates new random salts. | |
from cryptography.hazmat.primitives import hashes | |
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
import os | |
from cryptography.hazmat.primitives.ciphers import ( | |
Cipher, algorithms, modes | |
) | |
import logging | |
SALT_LEN_BYTES = 64 | |
KEYSIZE = 32 | |
HEADER_LBA_OFFSET = 62 | |
MASTER_KEY_HEADER_POS = 256 | |
MASTER_KEY_LEN = 64 | |
logging.basicConfig(filename="recoverylog.txt", | |
filemode='a', | |
format='[%(asctime)s,%(msecs)d]\t%(message)s', | |
datefmt='%H:%M:%S', | |
level=logging.DEBUG) | |
logging.getLogger().addHandler(logging.StreamHandler()) | |
# This is the device file that contains the partition table and the veracrypt header | |
device_file = "/dev/nvme0n1" | |
with open(device_file, "rb") as f: | |
all_lbas_bin = bytearray(f.read(512 * 100)) | |
# Add potential passwords as byte strings here | |
pws = [b""] | |
for pw_idx in range(len(pws)): | |
for operation in ["decryptor"]: #["decryptor", "encryptor"]: | |
for byteorder in ["big"]: #["big", "little"]: | |
for algname in ["SHA512"]: #["SHA512", "SHA256"]: | |
for iterations in [500000]: #[500000, 200000, 1000, 0]: | |
header_bin = all_lbas_bin[HEADER_LBA_OFFSET*512:(HEADER_LBA_OFFSET+1)*512] | |
assert(len(header_bin) == 512) | |
salt_bin = header_bin[:SALT_LEN_BYTES] | |
encrypted_bin = header_bin[SALT_LEN_BYTES:] | |
kdf = PBKDF2HMAC( | |
algorithm=getattr(hashes, algname)(), | |
length=KEYSIZE * 2, # output size of kdf.derive(). XTS uses 2 keys | |
salt=bytes(salt_bin), | |
iterations=iterations, | |
) | |
key = kdf.derive(pws[pw_idx]) | |
assert(len(key) == KEYSIZE * 2) | |
if key.startswith(b'\x77\xee\xf1\x22') or key.startswith(b'\x78\x78\xda\x66'): | |
logging.info(f"Key found: {key}") | |
else: | |
continue | |
cryptor = getattr(Cipher( | |
algorithms.AES(key), | |
modes.XTS((0).to_bytes(16, byteorder=byteorder, signed=False).rjust(16, b'\0')), | |
), operation)() | |
plaintext = cryptor.update(bytes(encrypted_bin)) + cryptor.finalize() | |
logging.info(f"pw:{pw_idx}\t{operation}\t{byteorder}\t{algname}\t{iterations}:\t" | |
f"{plaintext[:20]}...{plaintext[-20:]}") | |
if plaintext[:4] in [b"VERA", b'TRUE']: | |
master_key = plaintext[MASTER_KEY_HEADER_POS - SALT_LEN_BYTES:][:MASTER_KEY_LEN] | |
logging.info(f"Found master key: {master_key}") | |
start_pos = int.from_bytes(plaintext[108 - SALT_LEN_BYTES:][:8], byteorder, signed=False) | |
end_pos = int.from_bytes(plaintext[116 - SALT_LEN_BYTES:][:8], byteorder, signed=False) | |
logging.info(f"start={plaintext[108 - SALT_LEN_BYTES:][:8]}=>{start_pos} end={plaintext[116 - SALT_LEN_BYTES:][:8]}=>{end_pos}") | |
break | |
logging.info("") | |
# This is the file that will be un-decrypted | |
target_file = "/dev/nvme0n1p3" | |
src = open(target_file, "rb") | |
dst = os.fdopen(os.open(target_file, os.O_RDWR | os.O_CREAT), 'rb+') | |
dst.seek(0, 0) | |
operation = "encryptor" | |
def read_in_chunks(file_object, chunk_size=512): | |
"""Lazy function (generator) to read a file piece by piece. | |
Default chunk size: 1k.""" | |
while True: | |
data = file_object.read(chunk_size) | |
if not data: | |
break | |
yield data | |
try: | |
for lba, chunk in enumerate(read_in_chunks(src)): | |
cryptor = getattr(Cipher( | |
algorithms.AES(master_key), | |
modes.XTS((int(start_pos / 512) + lba).to_bytes(16, byteorder="little", signed=False).rjust(16, b'\0')), | |
), operation)() | |
plaintext = cryptor.update(chunk) + cryptor.finalize() | |
if lba % 10000 == 0: | |
logging.info(f"lba:{lba} ({lba * 100 / 3855822848:.2f}%):\t{plaintext[:10]}...") | |
dst.write(plaintext) | |
finally: | |
logging.info(f"Final lba: {lba}") | |
logging.info(f"DONE!") |
@eyeoncomputing that means it never reached L72, meaning it didn’t successfully decrypt the master key. You may need to expand the values in L37-L41 to find the right combination of parameters. The individual values currently selected are the ones that worked for me but they can vary depending on how it was originally encrypted.
Running from any Linux/mac is fine as long as it has byte level read access to the /dev/*
file
@alexlanghart thanks for the prompt reply. I think i got the right combination in L37-L41 now, but got OpenSSL errors now:
mint@mint:~/Downloads$ sudo python3 undecrypt.py
Traceback (most recent call last):
File "/home/mint/Downloads/undecrypt.py", line 54, in
key = kdf.derive(pws[pw_idx])
File "/usr/lib/python3/dist-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py", line 56, in derive
return self._backend.derive_pbkdf2_hmac(
File "/usr/lib/python3/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 509, in derive_pbkdf2_hmac
self.openssl_assert(res == 1)
File "/usr/lib/python3/dist-packages/cryptography/hazmat/backends/openssl/backend.py", line 242, in openssl_assert
return binding._openssl_assert(self._lib, ok, errors=errors)
File "/usr/lib/python3/dist-packages/cryptography/hazmat/bindings/openssl/binding.py", line 77, in _openssl_assert
raise InternalError(
cryptography.exceptions.InternalError: Unknown OpenSSL error. This error is commonly encountered when another library is not cleaning up the OpenSSL error stack. If you are using cryptography with another library that uses OpenSSL try disabling it before reporting a bug. Otherwise please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this. ([_OpenSSLErrorWithText(code=478150779, lib=57, reason=123, reason_text=b'error:1C80007B:Provider routines::invalid iteration count')])
I am using Linux Mint 21.2 MATE distro, stock. I believe its using Python 3.10 or 3.11 that is preinstalled. And it can see and access /dev/nvme0n1 fine
@eyeoncomputing It's saying invalid iteration count
, so the iteration count you're using on L41 may be a problem
You're right, if the iteration is set to 0 in any of the combinations I tried, it will give that message. I will have to try all the combinations and give an update on how it goes later. Thanks again
@eyeoncomputing I don't know if you're manually updating the values for each attempt, but keep in mind that it's looping through those lists of parameter values to search through the combinations until it finds one that works. So for example you can use:
for iterations in [500000, 200000, 1000, 1]:
to have it automatically try each value.
(see the comments on those lines for some values that I tried searching through in my own case)
@eyeoncomputing You might also want to comment out L57-L60:
if key.startswith(b'\x77\xee\xf1\x22') or key.startswith(b'\x78\x78\xda\x66'):
logging.info(f"Key found: {key}")
else:
continue
I don't remember exactly why I had this here. I think I was trying to find the parameters that corresponded to a key I had found earlier.
Hi! I'm trying to recover a HD which I decrypted using the wrong rescue disk, and I think your script is my only option. I used "/dev/sda" as device_file and "/dev/sda2" as target_file.
It found the master key and spent almost 24h undecrypting, finally stopping at lba 1950181649, however, the partition is not recognized as a VeraCrypt Volume now, and Disks shows contents "Unknown" and there is not even an option to mount it.
Did I do something wrong? Should I had undecrypted the entire sda? Thanks for your help.
Edit: To provide more info and things that maybe went wrong:
- I didn't change lines L14-18, maybe the values in my case are not the same? It found the master key so I guess they are correct, but I'm not sure.
- Your script uses AES but my HD was originally encrypted using a cascade (AES-Twofish-Serpent).
- With the wrong rescue disk, I restored headers and then decrypted the disk. Then I restored the headers with the "correct" rescue disk. Should I have kept the "wrong" headers before launching your script?
In case I need to redo the process, is your script easily reverted or should I restore an image I created using Linux Disks utility? Will this image include everything (headers, partitions,etc.)? I'm afraid to do even more damage.
@Ludenife I would guess that L14-18 can be left the same since it sounds like it found the master key. Finding the master key is definitely the hardest part, and you might even want to make a copy of that key somewhere.
I don't think I'd trust trying to to revert the script's changes. It's probably better to start with a fresh copy of the disc. /dev/sda
is the whole disc, while /dev/sda2
is a partition (i.e. subset) of the disc. You can perform a byte-level copy from one disc to another with a CLI tool like dd
. Be careful though, it's joked that dd
stands for disc destroyer
because it won't ask for confirmations or anything. Just be sure you provide the right input/output arguments and you know which /dev/*
filename is associated with each disc, using tools like fdisk
.
You said that after trying to undecrypt it, it wasn't recognized as a VeraCrypt volume, but that should be expected. This script is for when you've accidentally decrypted twice rather than just once. If the script works, you should end up with regular, decrypted data (not VeraCrypt/encrypted). You might then be able to mount the partition as a filesystem, depending on the type of filesystem it had before.
Ah I see, you decrypted it with the wrong rescue disk. That might require some significant changes to the script. The master key that the script is finding is the original master key that your disc was encrypted with, and it's the one that should have been used to decrypt it. Instead of using that, you'll probably need to update the script to find the key in the recovery disc that you used, and then undecrypt the disc using that.
If you know the HD was encrypted with a different cascade of algorithms, you'll probably need to update the script to account for that. I would try experimenting with variations by having it print the decrypted data as a hexdump/ascii without writing anything to the disc, until you start seeing expected output, like visible text.
For undoing the wrong recovery disc, I don't think it'll matter what cascade of algorithms you originally used when encrypting the disc. I believe the recovery disc you used will contain details about the algorithm(s) it used. I could be wrong though - in my case it was much easier because I knew it was only AES.
@alexlanghart thanks for your fast reply.
I already created an image of the disk with the GNOME Disks utility, which I guess is the same as creating it with dd, right? I will restore this instead of trying to revert the script's changes.
Yes, I decrypted with the wrong rescue disk, there is an official tool for this (VeraRescue) but it didn't work. It was not a completely unrelated rescue disk tho, just that I updated the VeraCrypt version and created a new one, so the algorithm and parameters should be the same. I forgot to mention it is a Windows System drive, it has 4 partitions, 3 smaller ones (which I guess are the boot and recovery ones which I think system encryption doesn't encrypt), and the main one with all the data, which is the one I can't access.
If I understand you correctly, the master key that the script finds doesn't change even if I restore the boot loader or the volume headers with the "wrong" rescue disk? Any clue about how to find the master key of the rescue disk?
Thanks again for your help.
@Ludenife Most likely the disk utility cloning will work the same as dd.
In my case it was also a Windows system drive with 4+ partitions, and there was only one partition that was encrypted.
Right, I don't believe using the rescue disk would modify the header LBAs of the encrypted disk that contain the master key.
As for how to find the master key from the rescue disk, you'll probably need to dig through the veracrypt code and figure out how it builds the rescue disk. That's how I was able to write this script originally. It looks like you'll want to start by reading through this function: https://github.com/veracrypt/VeraCrypt/blob/master/src/Common/BootEncryption.cpp#L3686
That link is to the master
tree though, so you'll probably want to change it to whatever veracrypt version the rescue disk was made with.
After a very brief, cursory look over that function, I'd say it's probably just copying the whole header block (containing the master key) over to the rescue disk, around here. If that's the case, that's probably good news, because I think you could just point the script to look at that file instead and search for the master key the same way, though you might need to adjust some LBA or byte offset.
Solved! I was viewing a hexdump of the disk after restoring the image when I realized that it was actually not encrypted. Even if the rescue disk was old, it DID work, but for some reason the header was corrupted. Neither Windows, nor Linux recognized it as NTFS, GNOME Disk utility showed it as "Encrypted?" like the other disk, and VeraCrypt also treated it as an encrypted volume, I had even viewed the hexdump before, but until I scrolled enough it looked like random data.
In the end, a simple ntfsfix /dev/sda2
fixed everything and I could directly boot from Windows again (after backing up everything first).
Thanks for your help. Even if in the end the script was not needed, I'm very grateful, it pointed me in the right direction, along with your advice, and while it was a bad experience I learned a lot.
Hi, I am trying to recover a college professor's accidental double decrypted VeraCrypt drive (2TB SSD). What is the best procedure to use this python script? OS (Windows, Linux)? Preinstallation (PE) environment? Thanks for your efforts.
UPDATE: Ran the script in latest versions of Linux Mint and Ubuntu with python 3, edited script with possible passwords (just one really), it detected the NVME drive and partition (that was double decrypted) correctly and got error for line 104 "master key" not found/defined on both distros. Is the script supposed to derive the Master Key or am I supposed to input that? Not sure where to go from here. Thanks for the help.