Created
November 4, 2022 15:07
-
-
Save alexlanghart/771aa24cb307335f8ed5380a45dffc6b to your computer and use it in GitHub Desktop.
Un-decrypt a veracrypt drive that was accidentally decrypted twice
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.