LUKS, hashcat, and hidden volumes

TL;DR: You can encrypt/protect a volume/partition using LUKS, and then nest another encrypted volume inside it (using VeraCrypt or similar), which results in the hash for the LUKS volume not being validated correctly using hashcat.

Why is this an issue?

Brute forcing LUKS2 volumes is computationally intensive, and current validation processes may result in incomplete or inaccurate results. The following are notes related to testing and validation of encrypted volumes which do not have a root filesystem.

Create a LUKS device (default version is 2). Password is set as 'password'

$ cryptsetup luksFormat /dev/sdc

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

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sdc:
Verify passphrase:

Verify /dev/sdc is now a LUKS device

$ cryptsetup luksDump /dev/sdc
LUKS header information
Version:        2
Epoch:          3
Metadata area:  16384 [bytes]
Keyslots area:  16744448 [bytes]
UUID:           f4bd2ee4-969d-4022-b61e-23524e83f592
Label:          (no label)
Subsystem:      (no subsystem)
Flags:          (no flags)

Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 512 [bytes]

Keyslots:
  0: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      argon2id
        Time cost:  5
        Memory:     1048576
        Threads:    4
        Salt:       60 a0 ab 32 26 09 c0 7b 1c bf 08 a0 2a 2f a6 0f
                    3b ab aa 65 d3 33 73 05 ac e1 72 a8 b9 a8 6d 44
        AF stripes: 4000
        AF hash:    sha256
        Area offset:32768 [bytes]
        Area length:258048 [bytes]
        Digest ID:  0
Tokens:
Digests:
  0: pbkdf2
        Hash:       sha256
        Iterations: 118724
        Salt:       50 d4 e4 45 2e 30 1a b9 a7 72 4e b6 eb 94 de cb
                    d7 f6 f1 93 65 c7 76 fb 22 6f d9 d5 9f 3f 1a 3d
        Digest:     93 25 02 6f f6 82 09 72 d5 19 b3 74 44 aa 10 65
                    a0 6a 61 5c bf 4c fc 13 2a bf fc f8 73 9a ab 91

Run luks2hashcat against /dev/sdc

python3 luks2hashcat.py /dev/sdc > sdc.hash

Verify the contents of sdc.hash, noting the first line of values (luks,2,argon2id,sha256, etc etc) aligns with the values we observed above. Note the end of the file (after the final $ placeholder) only contains zeroes - we'll come back to that shortly.

Let's run hashcat (v7.1.2, which supports argon2) against the hash with a simple dictionary attack (-a 0) and using 100 words in passwords.list (which contains 'password' as a candidate)

$ hashcat.exe -a 0 -m 34100 sdc.hash passwords.list

Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 34100 (LUKS v2 argon2 + SHA-256 + AES)
Hash.Target......: $luks$2$argon2id$sha256$aes$xts-plain64$512$m=10485...000000
Time.Started.....: Sun Nov 09 12:41:24 2025 (24 secs)
Time.Estimated...: Sun Nov 09 12:41:48 2025 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (passwords.list)
Guess.Queue......: 1/1 (100.00%)

Strange - should've identified the correct password, right? That's because the LUKS container doesn't contain any data, and appears to be required to prove decryption.

Let's go back to /dev/sdc and create an ext4 filesystem, then compare our hashes and try again.

$ lsblk -lf
NAME FSTYPE      FSVER LABEL UUID                         FSAVAIL FSUSE% MOUNTPOINTS
sdc  crypto_LUKS 2           f4bd2ee4-969d-4022-b61e-23524e83f592

$ cryptsetup luksOpen /dev/sdc sdc
Enter passphrase for /dev/sdc:

$ mkfs.ext4 /dev/mapper/sdc
mke2fs 1.46.5 (30-Dec-2021)
Creating filesystem with 520192 4k blocks and 130048 inodes
Filesystem UUID: 20bb32f6-e8cc-4ea8-a483-136ea7d5fed8
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912

Allocating group tables: done
Writing inode tables: done
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done

$ cryptsetup luksClose sdc
$ lsblk -lf
NAME FSTYPE      FSVER LABEL UUID                         FSAVAIL FSUSE% MOUNTPOINTS
sdc  crypto_LUKS 2           f4bd2ee4-969d-4022-b61e-23524e83f592

$ lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sdc      8:32   0    2G  0 disk

$ python3 luks2hashcat.py /dev/sdc > sdc.ext4.hash

A quick comparison between sdc.hash and sdc.ext4.hash shows that the data portion we identified above, is now populated with data (since there's a filesystem, the container is no longer empty and actually contains data).

If we run hashcat again using the same command, we'll see the password is now cracked.

$3b900f5a933786926fedf8e520089c600444d103fba763f5490822e0b3b1f97bcf4ed4c7eb3580bad2a63a73eef5ce539369995d6898c59f6b9c246f028f082051e958d5b110e8f90df0cf81216b8776fd1c93bf7146b8e493253f25c2d21a7a3be2c8a57a04874c75f75acd1d5406e06a7901756df56e07a536c9bfaec652aecaefd4e4bae3dc38a639d919c3fbc54b98d6a66ef90475248a76868a47fe1c31b064943c2835542ce1cae1a5b5e38455ae8cd615357132c915abcc19159afa2e0a058960b0e9e63447bf53c7168bc0fddf8ee7edadb7e8a59a616004d5b9e008428ed88cd837dc85b76c255306375c60bd362b6bc6966328a1bda6c97b1e4c6194c194d4f347eb420cd93821dcf120060e65b679a8641bf2731b9982adc81da24f0074894566c0c3ed15f49d9340941575a45801bca5c089954459cf90b7b45b64dabe51ded570ca9d1d7cd52b696822aa10bcd798942ad893ce8b5e0b5c0aef03044855b463c7a70c81216967956b44670fa14d73512ba394fbfae743e8b277ffb6303de7e30e2ccc82ad8fe7e304d34b91319ae4e8c494a71273c93dd488ed2c7dadd4dd406b6680e9d2471afe48968d01b2b95d02d0be8bd3254d17b17594afe6f89126cb7d4def020c6e31b8ee71e055afe43b3c35a0238b862c9d3951fa75b90f60d21f1845d0b54b7f5de8b9fade8bbaa5d00ea9a427d5bc9b27c782a4:password

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 34100 (LUKS v2 argon2 + SHA-256 + AES)
Hash.Target......: $luks$2$argon2id$sha256$aes$xts-plain64$512$m=10485...c782a4
Time.Started.....: Sun Nov 09 12:48:26 2025 (9 secs)
Time.Estimated...: Sun Nov 09 12:48:35 2025 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (passwords.list)

This raised the question - does the data portion of the hash have to be verified by opening/mounting the container/filesystem etc before the password candidate will be identified as valid? What if the encrypted block device/partition was intended to be mounted by another program (VeraCrypt, for example) - how will this complicate recovery efforts?

Using a fresh /dev/sdc, we'll do the following;

  1. Create a LUKS2 volume, password is password.

  2. Leave the block device unformatted (no filesystem).

  3. Mount the LUKS2 volume as 'sdc'

  4. Encrypt /dev/mapper/sdc using VeraCrypt, 'veracrypt' as the password

Completing steps 1 to 3, and then running luks2hashcat against /dev/sdc, we can see the data portion is still zeroes. So we'll create a VeraCrypt container (using /dev/mapper/sdc)

bc$0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
$ cryptsetup luksOpen /dev/sdc sdc
Enter passphrase for /dev/sdc:

$ lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINTS

sdc      8:32   0    2G  0 disk
└─sdc  253:0    0    2G  0 crypt

$ veracrypt -t -c
Volume type:
 1) Normal
 2) Hidden
Select [1]:

Enter volume path: /dev/mapper/sdc

Encryption Algorithm:
 1) AES
 2) Serpent
 3) Twofish
 4) Camellia
 5) Kuznyechik
 6) AES(Twofish)
 7) AES(Twofish(Serpent))
 8) Camellia(Kuznyechik)
 9) Camellia(Serpent)
 10) Kuznyechik(AES)
 11) Kuznyechik(Serpent(Camellia))
 12) Kuznyechik(Twofish)
 13) Serpent(AES)
 14) Serpent(Twofish(AES))
 15) Twofish(Serpent)
Select [1]:

Hash algorithm:
 1) SHA-512
 2) SHA-256
 3) BLAKE2s-256
 4) Whirlpool
 5) Streebog
Select [1]:

Filesystem:
 1) None
 2) FAT
 3) Linux Ext2
 4) Linux Ext3
 5) Linux Ext4
 6) NTFS
 7) exFAT
 8) Btrfs
Select [2]: 5

Enter password:
WARNING: Short passwords are easy to crack using brute force techniques!

We recommend choosing a password consisting of 20 or more characters. Are you sure you want to use a short password? (y=Yes/n=No) [No]: y

Re-enter password:
Enter PIM:
Enter keyfile path [none]:

Please type at least 320 randomly chosen characters and then press Enter:
Done: 100.000%  Speed:  25 MiB/s  Left: 0 s

The VeraCrypt volume has been successfully created.

$ cryptsetup luksDump /dev/sdc
LUKS header information
Version:        2
Epoch:          3
Metadata area:  16384 [bytes]
Keyslots area:  16744448 [bytes]
UUID:           089aa779-bc36-41e6-a207-73981b4d3e36
Label:          (no label)
Subsystem:      (no subsystem)
Flags:          (no flags)

Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 512 [bytes]

Keyslots:
  0: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      argon2id
        Time cost:  4
        Memory:     997316
        Threads:    4
        Salt:       d2 50 7c 75 e0 59 6e c7 1a 6b 8c df b2 15 62 81
                    8c 7b 17 7e f2 fd 50 e5 73 04 c8 a5 75 19 fc 11
        AF stripes: 4000
        AF hash:    sha256
        Area offset:32768 [bytes]
        Area length:258048 [bytes]
        Digest ID:  0
Tokens:
Digests:
  0: pbkdf2
        Hash:       sha256
        Iterations: 89043
        Salt:       65 9c e9 12 ba 39 f9 6a 36 1a 41 c7 e0 31 6e 1a
                    59 e0 89 26 69 ee 6d 40 b6 59 62 c6 91 03 1d 07
        Digest:     5e 29 39 16 6f 23 e9 e4 57 09 b6 4b 14 7b fa 9b
                    03 ed 18 15 70 6a cb 9f 6d da 59 8e e7 3c c2 06

Verify there is no filesystem on /dev/sdc, or sdc (/dev/mapper/sdc)

$ lsblk -lf
NAME FSTYPE      FSVER LABEL UUID                         FSAVAIL FSUSE% MOUNTPOINTS
sdc  crypto_LUKS 2           089aa779-bc36-41e6-a207-73981b4d3e36
sdc

Mount VeraCrypt container and verify

$ mkdir /mnt/vc
$ veracrypt /dev/mapper/sdc /mnt/vc
Enter password for /dev/mapper/sdc:
Enter PIM for /dev/mapper/sdc:
Enter keyfile [none]:
Protect hidden volume (if any)? (y=Yes/n=No) [No]:

$ ls /mnt/vc
lost+found

$ lsblk -lf
NAME       FSTYPE      FSVER LABEL UUID                            FSAVAIL FSUSE% MOUNTPOINTS
sdc        crypto_LUKS 2           089aa779-bc36-41e6-a207-73981b4d3e36
sdc
veracrypt1 ext4        1.0         00adf9dc-6299-4a42-86ac-dfeca64c21d6    1.8G     0% /mnt/vc

Unmount both the VeraCrypt and LUKS volumes.

$ veracrypt -u /dev/mapper/sdc; cryptsetup luksClose sdc

Calculate the hash for /dev/sdc (expecting a non-zero footer)

$ python3 luks2hashcat.py /dev/sdc > sdc.vc.hash

$ tail -c 1024 -f sdc.vc.hash
c26e500a620208c74f17b177524a2d2b2c91dfcc3ab683ce7a075848290321c9b4b810b147c3e4f640e751de74e35b7c48b36ff08f5f13d98524eec50730fbd28722d80867849230535dfb932d9347188fe413f6e4106a0d731dbe8380f4853b1b482354879cb06f91a0c76cea8a5d6a4bf13178edb196769429ab7854635e92cf1ba3dfe48e548038e779b93d0942e7860dab90e39ecffa243c8c7146600ef7de2a5f86d214b7e99f64218ad052e9010d3d105b5546ef20e1be4cfe062691b51efdde4c874ee059d2ef860b685404b805b73b518593df96439517df9bfd9ef6a1248a7f42dabd6fd06329357bea2f7c315d2846131f03fad7705e9545f23e5b643575c29fa9d7f24c4d7a7cea93a9e7772a8be6e7e58a21a009a7497b8f4a0dc7bfb4ce37792a79d4a8c2d55ae4720c0bcd6666065c0f1d1a9fece9db41cabd94fdded4adc8d0cd69d63acb9b5493b334f2030df05f327115fcf3654b53baa3b7769aa63f1c1985a09cc004911ac694871e46efee372613ac8ce1f6020ea81abbb31c55be8704f266309fc2d654d1763e061fe759943e5c61084beeccb7555ca56d6b8e3c91e91cb2e102ff26bec883cc54edfc491930c09981d45bf463828d954f03f194399c5f8c6875d1a2705453dd89ac0c097948ff2b8cc8d207e058377789a5b2d5ae2ae4e2c78e7c436e4baf7a62e1b81b2abfe5f4d64164381a2b

Run hashcat again.. no results.

$ hashcat.exe -a 0 -m 34100 sdc.vc.hash passwords.list

Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 34100 (LUKS v2 argon2 + SHA-256 + AES)
Hash.Target......: $luks$2$argon2id$sha256$aes$xts-plain64$512$m=99731...81a2bf
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (passwords.list)
Guess.Queue......: 1/1 (100.00%)

Last updated