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

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