You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hashcat/tools/luks2hashcat.py

350 lines
9.3 KiB

#!/usr/bin/env python3
#
# Author......: See docs/credits.txt
# License.....: MIT
#
from argparse import ArgumentParser
from collections import namedtuple
from dataclasses import dataclass
from os import SEEK_SET
from struct import Struct
from typing import List
try:
from enum import auto, IntEnum, StrEnum
except ImportError:
from enum import auto, Enum, IntEnum
class StrEnum(str, Enum):
def _generate_next_value_(name, start, count, last_values):
return name.lower()
__str__ = str.__str__
__format__ = str.__format__
# consts
SIGNATURE = "$luks$"
SECTOR_SIZE = 512
# utils
def bytes_to_str(value):
"""
Convert encoded padded bytes string into str.
"""
return value.rstrip(b"\0").decode()
# pre-header
TmpHeaderPre = namedtuple(
"TmpHeaderPre",
(
"magic",
"version",
),
)
# version 1
TmpKeyVersion1 = namedtuple(
"TmpKeyVersion1",
(
"active",
"iterations",
"salt",
"material_offset",
"stripes",
),
)
@dataclass(init=False)
class KeyVersion1:
class Active(IntEnum):
ENABLED = 0x00AC71F3
DISABLED = 0x0000DEAD
ENABLED_OLD = 0xCAFE
DISABLED_OLD = 0x0000
active: Active
iterations: int
salt: bytes
af: bytes
def __init__(self, active, iterations, salt, af):
self.active = self.Active(active)
if (self.active in [self.Active.ENABLED, self.Active.ENABLED_OLD]) and (iterations <= 0):
raise ValueError("key iterations cannot be less than zero")
self.iterations = iterations
self.salt = salt
self.af = af
TmpHeaderVersion1 = namedtuple(
"TmpHeaderVersion1",
(
"magic",
"version",
"cipher",
"mode",
"hash",
"payload_offset",
"key_bytes",
"digest",
"salt",
"iterations",
"uuid",
"keys",
),
)
@dataclass(init=False)
class HeaderVersion1:
MAGIC = b"LUKS\xba\xbe"
VERSION = 0x0001
class Cipher(StrEnum):
AES = auto()
TWOFISH = auto()
SERPENT = auto()
class Mode(StrEnum):
CBC_ESSIV_SHA256 = "cbc-essiv:sha256"
CBC_PLAIN = "cbc-plain"
CBC_PLAIN64 = "cbc-plain64"
XTS_PLAIN = "xts-plain"
XTS_PLAIN64 = "xts-plain64"
class Hash(StrEnum):
RIPEMD160 = auto()
SHA1 = auto()
SHA256 = auto()
SHA512 = auto()
WHIRLPOOL = auto()
class KeySize(IntEnum):
SIZE_128 = 128
SIZE_256 = 256
SIZE_512 = 512
magic: bytes
version: int
cipher: Cipher
mode: Mode
hash: Hash
payload: bytes
key_size: KeySize
digest: bytes
salt: bytes
iterations: int
uuid: str
keys: List[KeyVersion1]
def __init__(self, magic, version, cipher, mode, hash, payload, key_size, digest, salt, iterations, uuid, keys):
if magic != self.MAGIC:
raise ValueError("invalid magic bytes")
self.magic = magic
if version != self.VERSION:
raise ValueError("invalid version")
self.version = version
if isinstance(cipher, bytes):
try:
cipher = bytes_to_str(cipher)
self.cipher = self.Cipher(cipher)
except UnicodeDecodeError as e:
raise ValueError("cannot decode cipher") from e
except ValueError as e:
raise ValueError("invalid cipher value") from e
if isinstance(mode, bytes):
try:
mode = bytes_to_str(mode)
self.mode = self.Mode(mode)
except UnicodeDecodeError as e:
raise ValueError("cannot decode mode") from e
except ValueError as e:
raise ValueError("invalid mode value") from e
if isinstance(hash, bytes):
try:
hash = bytes_to_str(hash)
self.hash = self.Hash(hash)
except UnicodeDecodeError as e:
raise ValueError("cannot decode hash") from e
except ValueError as e:
raise ValueError("invalid hash value") from e
self.payload = payload
try:
self.key_size = self.KeySize(key_size)
except ValueError as e:
raise ValueError("invalid key size provided") from e
self.digest = digest
self.salt = salt
try:
iterations = int(iterations)
except ValueError as e:
raise ValueError("iterations is not a number") from e
if iterations <= 0:
raise ValueError("iterations cannot be less or equal to zero")
self.iterations = iterations
if isinstance(uuid, bytes):
try:
uuid = bytes_to_str(uuid)
except UnicodeDecodeError as e:
raise ValueError("cannot decode UUID") from e
self.uuid = uuid
if all(isinstance(key, tuple) for key in keys):
keys = [KeyVersion1(*key) for key in keys]
elif all(isinstance(key, dict) for key in keys):
keys = [KeyVersion1(**key) for key in keys]
if any(not isinstance(key, KeyVersion1) for key in keys):
raise ValueError("not a key object provided")
self.keys = keys
def extract_version1(file):
# consts
KEYS_COUNT = 8
PADDING_LENGTH = 432
PAYLOAD_SIZE = 512 # sizeof (u32) * 128
# prepare structs
key_struct = Struct(">LL32sLL")
header_struct = Struct(
">6sH32s32s32sLL20s32sL40s" + str(key_struct.size * KEYS_COUNT) + "s" + str(PADDING_LENGTH) + "x"
)
# read header
header = file.read(header_struct.size)
if len(header) < header_struct.size:
raise ValueError("file contains less data than needed")
# convert bytes into temporary header
header = header_struct.unpack(header)
header = TmpHeaderVersion1(*header)
# convert bytes into temporary keys
tmp_keys = [TmpKeyVersion1(*key) for key in key_struct.iter_unpack(header.keys)]
# read keys' af
keys = []
for key in tmp_keys:
file.seek(key.material_offset * SECTOR_SIZE, SEEK_SET)
af = file.read(header.key_bytes * key.stripes)
if len(af) < (header.key_bytes * key.stripes):
raise ValueError("file contains less data than needed")
key = KeyVersion1(key.active, key.iterations, key.salt, af)
keys.append(key)
# read payload
file.seek(header.payload_offset * SECTOR_SIZE, SEEK_SET)
payload = file.read(PAYLOAD_SIZE)
if len(payload) < PAYLOAD_SIZE:
raise ValueError("file contains less data than needed")
if sum(payload) == 0:
raise ValueError("file not initialized - payload contains zeros only")
# convert into header
header = HeaderVersion1(
header.magic,
header.version,
header.cipher,
header.mode,
header.hash,
payload,
header.key_bytes * 8,
header.digest,
header.salt,
header.iterations,
header.uuid,
keys,
)
# check for any active key
if all(key.active not in [KeyVersion1.Active.ENABLED, KeyVersion1.Active.ENABLED_OLD] for key in header.keys):
# all keys are disabled
raise ValueError("all keys are disabled")
for key in header.keys:
if key.active not in [KeyVersion1.Active.ENABLED, KeyVersion1.Active.ENABLED_OLD]:
# skip inactive keys
continue
hash = SIGNATURE + "$".join(
map(
str,
[
header.version,
header.hash,
header.cipher,
header.mode,
int(header.key_size),
key.iterations,
key.salt.hex(),
key.af.hex(),
header.payload.hex(),
],
)
)
print(hash)
# main
if __name__ == "__main__":
# prepare parser and parse args
parser = ArgumentParser(description="luks2hashcat extraction tool")
parser.add_argument("path", type=str, help="path to LUKS container")
args = parser.parse_args()
# prepare struct
header_struct = Struct(">6sH")
try:
with open(args.path, "rb") as file:
# read pre header
header = file.read(header_struct.size)
if len(header) < header_struct.size:
parser.error("file contains less data than needed")
# convert bytes into temporary pre header
header = header_struct.unpack(header)
header = TmpHeaderPre(*header)
# check magic bytes
magic_bytes = {
HeaderVersion1.MAGIC,
}
if header.magic not in magic_bytes:
parser.error("improper magic bytes")
# back to start of the file
file.seek(0, SEEK_SET)
# extract with proper function
try:
mapping = {
HeaderVersion1.VERSION: extract_version1,
}
extract = mapping[header.version]
extract(file)
except KeyError as e:
raise ValueError("unsupported version") from e
except IOError as e:
parser.error(e.strerror.lower())
except ValueError as e:
parser.error(str(e))