Skip to content

Instantly share code, notes, and snippets.

@alexlanghart
Created November 4, 2022 15:07
Show Gist options
  • Save alexlanghart/771aa24cb307335f8ed5380a45dffc6b to your computer and use it in GitHub Desktop.
Save alexlanghart/771aa24cb307335f8ed5380a45dffc6b to your computer and use it in GitHub Desktop.
Un-decrypt a veracrypt drive that was accidentally decrypted twice
# 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!")
@Ludenife
Copy link

Ludenife commented Jan 22, 2024

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.

@alexlanghart
Copy link
Author

alexlanghart commented Jan 23, 2024

@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.

@Ludenife
Copy link

@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.

@alexlanghart
Copy link
Author

alexlanghart commented Jan 23, 2024

@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.

@Ludenife
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment