Skip to content
Advertisement

How to programatically find the start and end bytes of LUKS header?

How can I programmatically determine the exact start byte and exact end byte of a LUKS header on a block storage device?

I use Linux Unified Key Setup (LUKS) for Full Disk Encryption (FDE), so all of the data on my drive is encrypted using a strong master key that’s not derived from my passpharse, and I’m working on a script that will securely wipe the drive in an panic/emergency shutdown situation (ie: someone is physically stealing your laptop).

Let’s say I have a 1000T drive and time is too short (~30 seconds) in my emergency scenario to actually fill the drive with random bytes. Instead, I’d just like to overwrite the header because all of the data on the drive is worthless if the header (containing the keyslots with the master key and salts) is lost–even if the passphrase were recovered via rubber-hose cryptanalysis.

How can I programmatically safely determine the start byte and end byte of the LUKS header so I know what to overwrite?

Note: The solution provided must be valid for both LUKS1 (released in 2014) and LUKS2 (released in 2018).

In LUKS1, I’ve found that the start is 0 and the end is determined by multiplying the payload-offset field from the container’s binary header by 512. For example

root@disp4117:~# hexdump -Cs 6 -n 2 luksVol1
00000006  00 01                                             |..|
00000008
root@disp4117:~# hexdump -Cs 104 -n 4 luksVol1
00000068  00 00 10 00                                       |....|
0000006c
root@disp4117:~# 

In this case, the LUKS1 header ends at byte 4096 * 512 = 2097152.

In LUKS2, it’s much trickier as it requires parsing the JSON metadata object in the LUKS header.

Advertisement

Answer

Here’s a quick python script that will output the start and end bytes for a given LUKS container:

#!/usr/bin/python
################################################################################
# File:    luksHeaderBounds.py
# Purpose: Determine the start and end bytes of a LUKS header
# Authors: Michael Altfield <michael@michaelaltfield.net>
# Created: 2020-03-18
# Updated: 2020-03-18
# Version: 0.1
################################################################################

# DEPENDS
import sys, struct, json;

# MAIN BODY

if len(sys.argv) != 2:
    print "Usage: luksHeaderBounds.py <device>"
    exit(1)

device = sys.argv[1]

# first we get the LUKS version
with open(device, "rb") as f:

    # the LUKS version field is a big endian unsigned short (uint16_t = '>H')
    #  * https://docs.python.org/2/library/struct.html

    # the LUKS version field starts at offset 6 bytes and is 2 bytes long
    # so we get the first 8 bytes and the last 2 bytes of that
    #  * https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf

    luksVersion = struct.unpack( '>H', f.read(8)[-2:] )[0];

if luksVersion == 1:
    # LUKS1 - https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf

    with open(device, "rb") as f:

        # the payload-offset field is a big endian unsigned long (uint32_t = '>L')
        #  * https://docs.python.org/2/library/struct.html

        # in LUKS1, the payload-offset field starts at offset 104 bytes and is 4 bytes long
        # so we get the first 108 bytes and the last 4 bytes of that
        #  * https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf

        payloadOffset = struct.unpack( '>L', f.read(108)[-4:] )[0];

    # the payload-offset is just the number of (512-byte sized) sectors
    # to get bytes we must multiply it by 512
    luksHeaderEnd = 512 * payloadOffset

elif luksVersion == 2:
    # LUKS2 - https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf

    # first we get the header size from the primary binary header
    with open(device, "rb") as f:

        # the hdr_size field is a big endian unsigned long long (uint64_t = '>Q')
        #  * https://docs.python.org/2/library/struct.html

        # in LUKS2, the hdr_size field starts at offset 8 bytes and is 8 bytes long
        # so we get the first 16 bytes and the last 8 bytes of that
        #  * https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf

        hdr_size=struct.unpack( '>Q', f.read(16)[-8:] )[0];

    # the JSON plaintext metadata object starts after the header (4096 bytes)
    # and ends at (hdr_size - 4096)
    jsonSize = hdr_size - 4096;

    with open(device, "rb") as f:
        luksMetadata = f.read(hdr_size)[-jsonSize:];

    decoder = json.JSONDecoder()
    jsonMetadata = decoder.raw_decode(luksMetadata)

    # the LUKS2 header ends at the start of the first data segment
    luksHeaderEnd=jsonMetadata[0]['segments']['0']['offset']

else:

    print "ERROR: Unable to determine LUKS version"
    exit(1)

print "Device: " +str(device)
print "  LUKS Header Start Byte: " +str(0)
print "  LUKS Header End Byte:   " +str(luksHeaderEnd)

exit(0)

And an example execution for a LUKS1 volume:

root@disp4117:~# ./luksHeaderBounds.py luksVol1
Device: luksVol1
  LUKS Header Start Byte: 0
  LUKS Header End Byte:   2097152
root@disp4117:~# 

And an example execution for a LUKS2 volume:

root@disp4117:~# ./luksHeaderBounds.py luksVol2
Device: luksVol2
  LUKS Header Start Byte: 0
  LUKS Header End Byte:   16777216
root@disp4117:~# 
Advertisement