11

In what I now know feel was a stupid decision, I attempted to dual boot Windows and linux by using the windows installer, After booting into the windows installer I chose one of a 2 cloned hard drives about 500GB in size to erase, as they were cloned it wouldn't matter if I picked one over the other.

After doing so, the installer said it changed the partition table for one of the 500GB hard drives and then failed to install windows. Crashing with an error without copying files, or so it says, I am unsure if I can trust it when it says it failed to even begin installing.

So I booted into my linux install to check which drive it had overwritten and manually install it. What greeted me instead was one of my other drives, a 6TB dm-luks and btrfs drive, missing. Not only were both 500GB drives untouched, but the 6TB drive had seemingly a mess of partitions added to it. 6 partitions in the order of 499M, 99M, 499M, 100M, 499M, 100M.

As my drive is quite large and additionally slow, running hexdump -C /dev/sda |grep LUKS has so far produced this much, I will update when it finishes:

8d411ce0  e1 ad 4c 55 4b 53 c0 85  22 3d de 49 dd 44 fd 08  |..LUKS.."=.I.D..|
e6449610  d5 cf 4a 86 9f cc 4c 55  4b 53 a9 a9 16 cc ba 1d  |..J...LUKS......|
446ea9a70  b3 db a9 bf 8b 2e 41 4c  55 4b 53 ef f0 75 b0 18  |......ALUKS..u..|
4732c6040  e0 b3 bb ff 4c 55 4b 53  4c c2 5b 12 c6 41 fc d6  |....LUKSL.[..A..|

So far the only thing that has even touched the disk since this has happened is hexdump, I am hesitant to run testdisk as I have heard it overwrites data on the drive and it does not list luks as something it can search for.

I can see others have used hexdump to check for intact headers, however, I do not know what exactly I am looking for.

What can I do at this point to see if I can recover any bit of the header. Is there a way to run testdisk or another tool to look for luks headers in order to tell if they've been overwritten? Any way that allows me to know whether everything is FUBAR or not is equally as welcome as a way to recover my data.

EDIT

Running hexdump on the first bit of the drive without grep shows that there's at least some intact JSON, from 00005000 to 00005310 shows as much, I am even less sure what I am specifically looking for now to know if its still intact. It seems it overwrote data up until this exact string.

00005000  7b 22 6b 65 79 73 6c 6f  74 73 22 3a 7b 22 30 22  |{"keyslots":{"0"|
00005010  3a 7b 22 74 79 70 65 22  3a 22 6c 75 6b 73 32 22  |:{"type":"luks2"|

Cutting out the data in-between because it includes the salt, but the block ends in:

000052f0  22 2c 22 6b 65 79 73 6c  6f 74 73 5f 73 69 7a 65  |","keyslots_size|
00005300  22 3a 22 31 36 37 34 34  34 34 38 22 7d 7d 00 00  |":"16744448"}}..|
00005310  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*

It it intact enough?

8
  • 2
    | grep LUKS does not help for overwritten headers. This might be your only chance: cryptsetup repair (only for LUKS 2) but you have to know the correct offset and find one of LUKS JSON metadata strings first. Damaged key material cannot be recovered. Unfortunately in a lot of these cases even a slightly damaged LUKS header simply means "game over". Commented Mar 30, 2023 at 10:47
  • 2
    Was the drive partitioned before? If not then the LUKS header would simply sit at offset 0 (and die as soon as anything creates a partition table) and you could check hexdump -C in full (no grep). If it was partitioned, was it a single partition? That would usually be offset 1 MiB these days. Scanning the entire drive for the offset is only useful if you had multiple partitions and LUKS was only one of them at an unknown offset. You can also use strings -t d -n 4 /dev/disk | less and see if any LUKS related strings pop up (aes-xts etc.) for LUKS 2 this would show JSON metadata strings Commented Mar 30, 2023 at 10:57
  • @frostschutz It was treated as a single device, I'm not sure which of the two it was, but you gave me the perfect command to run, because it gave me some hope. I can see an entire block of luks JSON! Im not sure if its fully intact, it looks like just before the block it might have gotten overwritten, How do I tell if its fully intact? It starts with this 00005000 7b 22 6b 65 79 73 6c 6f 74 73 22 3a 7b 22 30 22 |{"keyslots":{"0"| Commented Mar 30, 2023 at 20:22
  • 1
    then try and see if the repair method I linked works for you Commented Mar 30, 2023 at 20:33
  • 1
    Slight confusion here. The JSON you posted above should be the backup header. The key material on the other hand exists only once. Can't say more since I don't know what your header looks like. You need either primary or backup header to be intact. And then you also need intact key material (there is no backup of that). Commented Mar 30, 2023 at 22:33

1 Answer 1

16
+250

cryptsetup repair, Part Two — Full Header Recovery

In order to recover a partially overwritten LUKS2 header, you need at minimum two things: One, the key material of at least one of your keyslots. Two, the metadata describing how that key material is to be used (algorithm, iteration counts, salts, etc.).

The key material is roughly 256KB of random data, usually found at offset 32768 (0x8000) and beyond, however the exact offset and size has to be determined from metadata.

The metadata is a JSON string, usually found at offsets 4096 (0x1000) and 20480 (0x5000). LUKS2 maintains two identical copies of it (primary and secondary header). The key material itself exists only once.

If the partition table itself is also lost, you'll also have to determine the correct partition offset.


Setup:

# truncate -s 128M disk.img
# losetup --find --show --partscan disk.img
/dev/loop0
# parted /dev/loop0 -- mklabel gpt
# parted /dev/loop0 -- mkpart luks $((RANDOM%100))MiB 100%
# cryptsetup luksFormat --type luks2 /dev/loop0p1

WARNING!
========
This will overwrite data on /dev/loop0p1 irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/loop0p1:
Verify passphrase:
# cryptsetup open /dev/loop0p1 luks
Enter passphrase for /dev/loop0p1:
# mkfs.ext2 -L encrypted /dev/mapper/luks
# blkid /dev/mapper/luks
/dev/mapper/luks: LABEL="encrypted" […] TYPE="ext2"

A shiny new encrypted filesystem!

Damage:

# cryptsetup close luks
# wipefs -a /dev/loop0p1
/dev/loop0p1: 6 bytes were erased at offset 0x00000000 (crypto_LUKS): 4c 55 4b 53 ba be
/dev/loop0p1: 6 bytes were erased at offset 0x00004000 (crypto_LUKS): 53 4b 55 4c ba be
# dd count=32 if=/dev/urandom of=/dev/loop0p1
32+0 records in
32+0 records out
16384 bytes (16 kB, 16 KiB) copied, 0.000334077 s, 49.0 MB/s
# wipefs -a /dev/loop0
/dev/loop0: 8 bytes were erased at offset 0x00000200 (gpt): 45 46 49 20 50 41 52 54
/dev/loop0: 8 bytes were erased at offset 0x063ffe00 (gpt): 45 46 49 20 50 41 52 54
/dev/loop0: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
/dev/loop0: calling ioctl to re-read partition table: Success
# losetup -d /dev/loop0

So this is a disk.img with a LUKS2 partition at an unknown offset, with a damaged header on it (magic bytes erased, partially overwritten, partition table wiped).


Metadata recovery:

Since the LUKS2 JSON string is plain ASCII, it can be found with strings, which will also show the offset:

# stdbuf -oL strings -n 64 -t d disk.img | grep '"keyslots":'
60837888 {"keyslots":{"0":{"type":"luks2","key_size":64,"af":{"type":"luks1","stripes":4000,"hash":"sha256"},"area":{"type":"raw","offset":"32768","size":"258048","encryption":"aes-xts-plain64","key_size":64},"kdf":{"type":"argon2id","time":13,"memory":1048576,"cpus":4,"salt":"R1z3arzSCjRb3STaCAnstIygkHCXf0CHf6kXl5yQj/E="}}},"tokens":{},"segments":{"0":{"type":"crypt","offset":"16777216","size":"dynamic","iv_tweak":"0","encryption":"aes-xts-plain64","sector_size":512}},"digests":{"0":{"type":"pbkdf2","keyslots":["0"],"segments":["0"],"hash":"sha256","iterations":324435,"salt":"0nSkpvmDJlvfkDaQteVVo6JdD/Oqt3vnndkZt1Qnd84=","digest":"lefQ21EaiuSdHFhSIFW3wDfMcRqG0HLCAO1bGI3SfvM="}},"config":{"json_size":"12288","keyslots_size":"16744448"}}

So here we have an intact JSON string at offset 60837888. Copy&Paste it into a header.json file. The file should start with { and end with }. You can use jq to make sure it's indeed a valid JSON string, and also to show it in a more human-readable form:

# jq < header.json
{
  "keyslots": {
    "0": {
      "type": "luks2",
[…]
  }
}

Partition recovery:

The offset of the JSON metadata within the LUKS2 header is usually either 4096 or 20480, depending on whether it's the primary or secondary header. You have to substract these values from the offset that strings found earlier.

Thus, the correct partition offset in this case could be either 60837888 - 4096 = 60833792 = 58.02MiB or 60837888 - 20480 = 60817408 = 58 MiB. Since the latter is MiB aligned, it's the more likely candidate for the correct partition offset.

If in doubt, try both.


Key material recovery:

According to the JSON metadata, this LUKS2 header has a single keyslot and its key material is to be found at "offset":"32768","size":"258048". Let's grab it with dd:

# partition=60817408
# offset=32768
# size=258048
# dd bs=1 skip=$((partition+offset)) count=$((size)) if=disk.img of=header.$((offset))

If there are multiple keyslots, repeat this process for each of them.

The key material is supposed to look like random data. To verify this, you could look at the whole thing with hexdump -C.

# hexdump -C header.32768
00000000  f1 3b 23 73 98 d7 8f e3  22 24 9a 9d 5a 2c a9 ae  |.;#s...."$..Z,..|
00000010  95 82 3e c6 df e7 0e a0  f4 ba 54 6c 7f e9 fa f6  |..>.......Tl....|
00000020  b7 12 64 8d 7d a5 ca 4b  c8 89 89 08 3e de 59 0d  |..d.}..K....>.Y.|
[…]
0003efe0  b2 b3 bc cd de 60 17 a7  57 bb 1a 84 5a 15 68 95  |.....`..W...Z.h.|
0003eff0  7f 1f 07 ee ee d1 e8 a2  6c cf 5f 40 0b 73 00 0b  |[email protected]..|
0003f000

Or you could try to compress it and see if the compressed result is any smaller:

# gzip < header.32768 > header.32768.gz
# stat -c %s header.*
258048
258106

Random data usually can't be compressed at all, so if the gzipped version isn't any smaller (or even a few bytes larger), there's a good chance that the whole thing is random data.

True verification is only possible at the very end — when it accepts your passphrase, or not.


Full Header Recovery:

After you've collected the necessary ingredients above, you can attempt rebuilding a full header from them:

# truncate -s 16M luks.recovery
# cryptsetup luksFormat --type luks2 luks.recovery
# cryptsetup luksErase luks.recovery

Use cryptsetup produce a valid, albeit unusable header without keyslots. The purpose here is to obtain a file that is set up with all the correct magic bytes, UUIDs, etc. — unrelated to encryption, but it's what makes a LUKS header a LUKS header.

Now transplant your metadata onto it:

# printf "%s\0" "$(jq -c < header.json)" |
    dd conv=notrunc bs=1 seek=4096 of=luks.recovery
# printf "%s\0" "$(jq -c < header.json)" |
    dd conv=notrunc bs=1 seek=20480 of=luks.recovery

As well as the key material:

# dd conv=notrunc bs=1 seek=32768 if=header.32768 of=luks.recovery

At this point, we'd finally be done, except:

# cryptsetup luksDump luks.recovery
Device luks.recovery is not a valid LUKS device.
# cryptsetup repair luks.recovery
Device luks.recovery is not a valid LUKS device.

Checksum recovery:

Ehhh, what's wrong now? Add --debug to find out:

# cryptsetup luksDump --debug luks.recovery
[…]
# LUKS2 header version 2 of size 16384 bytes, checksum sha256.
# Checksum:5babf58f0f788911897989ff3d9a580de1c22db8869b3b08cd0d6d56906005cb (on-disk)
# Checksum:b2ff5dd7b53978723402103ba914ed87ef2c5b5d9a9062d68363e4df38aebf6f (in-memory)
[…]

LUKS2 has a checksum for its primary and secondary headers. Since we modified the JSON metadata without updating the checksum, it's a mismatch. Thankfully, cryptsetup shows the expected value so we don't have to calculate it manually.

This checksum is part of the binary header, so you have to use xxd -r -p to convert it to binary:

# echo 5babf58f0f788911897989ff3d9a580de1c22db8869b3b08cd0d6d56906005cb | xxd -r -p | hexdump -C
00000000  5b ab f5 8f 0f 78 89 11  89 79 89 ff 3d 9a 58 0d  |[....x...y..=.X.|
00000010  e1 c2 2d b8 86 9b 3b 08  cd 0d 6d 56 90 60 05 cb  |..-...;...mV.`..|
00000020
# echo b2ff5dd7b53978723402103ba914ed87ef2c5b5d9a9062d68363e4df38aebf6f | xxd -r -p | hexdump -C
00000000  b2 ff 5d d7 b5 39 78 72  34 02 10 3b a9 14 ed 87  |..]..9xr4..;....|
00000010  ef 2c 5b 5d 9a 90 62 d6  83 63 e4 df 38 ae bf 6f  |.,[]..b..c..8..o|
00000020

Replace the wrong on-disk checksum with the correct in-memory checksum:

# hexdump -C luks.recovery | grep '5b ab f5 8f 0f 78 89 11'
000001c0  5b ab f5 8f 0f 78 89 11  89 79 89 ff 3d 9a 58 0d  |[....x...y..=.X.|
# echo b2ff5dd7b53978723402103ba914ed87ef2c5b5d9a9062d68363e4df38aebf6f |
    xxd -r -p |
    dd conv=notrunc bs=1 seek=$((0x000001c0)) of=luks.recovery

And that should allow things to proceed.

# cryptsetup repair luks.recovery
# cryptsetup luksDump luks.recovery
LUKS header information
Version:        2
[…]

Wrapping things up:

# losetup --find --show --read-only --offset 60817408 disk.img
/dev/loop0

# cryptsetup open --read-only --header luks.recovery /dev/loop0 luksrecovery
Enter passphrase for /dev/loop0:

# blkid /dev/mapper/luksrecovery
/dev/mapper/luksrecovery: LABEL="encrypted" […] TYPE="ext2"

Done. Finally.

1
  • 8
    An Incredibly intricate response to a seemingly hopeless situation, and it works flawlessly. This is much more than I could gleam from the dm-luks documentation myself, absolutely outstanding dive into it. My data is back, and headers finally backed up. Commented Apr 4, 2023 at 1:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .