1
mirror of https://github.com/hashcat/hashcat synced 2024-12-01 20:18:12 +01:00
hashcat/tools/luks2hashcat.py
2022-06-25 13:54:20 +02:00

329 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
import sys
from argparse import ArgumentParser
from collections import namedtuple
from dataclasses import dataclass
from os import SEEK_SET
from struct import Struct
from sys import stderr
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)
assert iterations >= 0, "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):
assert magic == self.MAGIC, "Invalid magic bytes"
self.magic = magic
assert version == self.VERSION, "Invalid version"
self.version = version
if isinstance(cipher, bytes):
try:
cipher = bytes_to_str(cipher)
except UnicodeDecodeError as e:
raise ValueError("Cannot decode cipher") from e
self.cipher = self.Cipher(cipher)
if isinstance(mode, bytes):
try:
mode = bytes_to_str(mode)
except UnicodeDecodeError as e:
raise ValueError("Cannot decode mode") from e
self.mode = self.Mode(mode)
if isinstance(hash, bytes):
try:
hash = bytes_to_str(hash)
except UnicodeDecodeError as e:
raise ValueError("Cannot decode hash") from e
self.hash = self.Hash(hash)
self.payload = payload
self.key_size = self.KeySize(key_size)
self.digest = digest
self.salt = salt
assert iterations > 0, "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]
assert all(isinstance(key, KeyVersion1) for key in keys), "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)
assert len(header) == header_struct.size, "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)
assert len(af) == (header.key_bytes * key.stripes), "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)
assert len(payload) == PAYLOAD_SIZE, "File contains less data than needed"
# 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
for key in header.keys:
if key.active not in [KeyVersion1.Active.ENABLED, KeyVersion1.Active.ENABLED_OLD]:
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)
break
else:
# all keys are disabled
raise ValueError("All keys are disabled")
# main
def main(args):
# 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(args)
# prepare struct
header_struct = Struct(">6sH")
with open(args.path, "rb") as file:
# read pre header
header = file.read(header_struct.size)
assert len(header) == header_struct.size, "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,
}
assert header.magic in magic_bytes, "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
if __name__ == "__main__":
try:
main(sys.argv[1:])
except IOError as e:
print('Error:', e.strerror, file=stderr)
except (AssertionError, ValueError) as e:
print('Error:', e, file=stderr)