-
-
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!") |
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.
@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.