Init. commit
This commit is contained in:
108
HumanAI-Forensic-Hard/scripts/bigpooldump.py
Normal file
108
HumanAI-Forensic-Hard/scripts/bigpooldump.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import logging
|
||||
from typing import Iterator, List, Optional, Tuple
|
||||
|
||||
from volatility3.framework import exceptions, interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.plugins.windows import bigpools
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BigPoolDump(interfaces.plugins.PluginInterface):
|
||||
|
||||
|
||||
_version = (0, 1, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
name="kernel",
|
||||
description="Windows kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="bigpools", component=bigpools.BigPools, version=(2, 0, 0)
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="tags",
|
||||
description="Comma-separated list of pool tags to dump (e.g. TcDN,Tcpt)",
|
||||
optional=False,
|
||||
),
|
||||
requirements.BooleanRequirement(
|
||||
name="include-free",
|
||||
description="Include freed allocations",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
def _dump_one(self, addr: int, size: int, tag: str) -> Optional[str]:
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
layer = self.context.layers[kernel.layer_name]
|
||||
|
||||
filename = f"bigpool.{tag}.0x{addr:016x}.0x{size:x}.dmp"
|
||||
try:
|
||||
data = layer.read(addr, size, pad=True)
|
||||
except exceptions.InvalidAddressException:
|
||||
return None
|
||||
|
||||
try:
|
||||
with self.open(filename) as fp:
|
||||
fp.write(data)
|
||||
return filename
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def _generator(self) -> Iterator[Tuple[int, Tuple[object, ...]]]:
|
||||
tags = [t.strip() for t in (self.config.get("tags") or "").split(",") if t.strip()]
|
||||
if not tags:
|
||||
vollog.warning("No tags specified")
|
||||
return
|
||||
|
||||
show_free = bool(self.config.get("include-free"))
|
||||
|
||||
for big_pool in bigpools.BigPools.list_big_pools(
|
||||
context=self.context,
|
||||
kernel_module_name=self.config["kernel"],
|
||||
tags=tags,
|
||||
show_free=show_free,
|
||||
):
|
||||
tag = big_pool.get_key()
|
||||
size = big_pool.get_number_of_bytes()
|
||||
if isinstance(size, interfaces.renderers.BaseAbsentValue):
|
||||
continue
|
||||
addr = int(big_pool.Va) & ~1
|
||||
|
||||
dumped_as = self._dump_one(addr, int(size), tag)
|
||||
if dumped_as is None:
|
||||
dumped_as = renderers.UnreadableValue()
|
||||
|
||||
status = "Free" if big_pool.is_free() else "Allocated"
|
||||
|
||||
yield (
|
||||
0,
|
||||
(
|
||||
format_hints.Hex(addr),
|
||||
tag,
|
||||
format_hints.Hex(int(size)),
|
||||
big_pool.get_pool_type(),
|
||||
status,
|
||||
dumped_as,
|
||||
),
|
||||
)
|
||||
|
||||
def run(self) -> renderers.TreeGrid:
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Allocation", format_hints.Hex),
|
||||
("Tag", str),
|
||||
("NumberOfBytes", format_hints.Hex),
|
||||
("PoolType", str),
|
||||
("Status", str),
|
||||
("File output", str),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
188
HumanAI-Forensic-Hard/scripts/extract_password_candidates.py
Normal file
188
HumanAI-Forensic-Hard/scripts/extract_password_candidates.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import argparse
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Tuple
|
||||
|
||||
|
||||
ASCII_RE_TEMPLATE = rb"[ -~]{%d,%d}"
|
||||
UTF16LE_ASCII_RE_TEMPLATE = rb"(?:[ -~]\x00){%d,%d}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hit:
|
||||
s: str
|
||||
score: float
|
||||
file: Path
|
||||
offset: int
|
||||
kind: str
|
||||
count: int = 1
|
||||
|
||||
|
||||
def iter_files(paths: Iterable[str]) -> Iterator[Path]:
|
||||
for p in paths:
|
||||
path = Path(p)
|
||||
if path.is_dir():
|
||||
for child in sorted(path.rglob("*")):
|
||||
if child.is_file():
|
||||
yield child
|
||||
elif path.is_file():
|
||||
yield path
|
||||
|
||||
|
||||
def shannon_entropy(s: str) -> float:
|
||||
if not s:
|
||||
return 0.0
|
||||
freq: Dict[str, int] = {}
|
||||
for ch in s:
|
||||
freq[ch] = freq.get(ch, 0) + 1
|
||||
n = len(s)
|
||||
ent = 0.0
|
||||
for c in freq.values():
|
||||
p = c / n
|
||||
ent -= p * math.log2(p)
|
||||
return ent
|
||||
|
||||
|
||||
BAD_SUBSTRINGS = (
|
||||
"\\\\",
|
||||
"\\Registry\\",
|
||||
"\\Registry",
|
||||
"\\BaseNamedObjects\\",
|
||||
"\\BaseNamedObjects",
|
||||
":\\",
|
||||
"/",
|
||||
"System32",
|
||||
"Windows",
|
||||
"Microsoft",
|
||||
"CLSID",
|
||||
"AppX",
|
||||
"shell:::",
|
||||
"atom(",
|
||||
".dll",
|
||||
".exe",
|
||||
".sys",
|
||||
".ini",
|
||||
".mui",
|
||||
".nls",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".ttf",
|
||||
".otf",
|
||||
".wav",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".sqlite",
|
||||
)
|
||||
|
||||
|
||||
def looks_passwordish(s: str) -> bool:
|
||||
|
||||
if any(ch in s for ch in ('\\', '/', ':', '<', '>', '"', "'", '=', '\t', '\r', '\n')):
|
||||
return False
|
||||
if any(bad in s for bad in BAD_SUBSTRINGS):
|
||||
return False
|
||||
if s.startswith("http://") or s.startswith("https://"):
|
||||
return False
|
||||
|
||||
if s.count(" ") >= 4:
|
||||
return False
|
||||
|
||||
if len(set(s)) <= 3:
|
||||
return False
|
||||
|
||||
if re.fullmatch(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", s):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def score_string(s: str) -> float:
|
||||
has_lower = any("a" <= c <= "z" for c in s)
|
||||
has_upper = any("A" <= c <= "Z" for c in s)
|
||||
has_digit = any("0" <= c <= "9" for c in s)
|
||||
has_special = any(not c.isalnum() for c in s)
|
||||
|
||||
ent = shannon_entropy(s)
|
||||
score = ent * len(s)
|
||||
score += 5.0 * has_lower
|
||||
score += 5.0 * has_upper
|
||||
score += 5.0 * has_digit
|
||||
score += 5.0 * has_special
|
||||
if " " in s:
|
||||
score -= 2.0
|
||||
if s.islower() or s.isupper():
|
||||
score -= 1.0
|
||||
if all(c in "0123456789abcdefABCDEF" for c in s):
|
||||
score -= 3.0
|
||||
return score
|
||||
|
||||
|
||||
def extract_hits(data: bytes, *, min_len: int, max_len: int) -> Iterator[Tuple[str, int, str]]:
|
||||
ascii_re = re.compile(ASCII_RE_TEMPLATE % (min_len, max_len))
|
||||
utf16_re = re.compile(UTF16LE_ASCII_RE_TEMPLATE % (min_len, max_len))
|
||||
|
||||
for m in ascii_re.finditer(data):
|
||||
s = m.group(0).decode("ascii", errors="ignore")
|
||||
yield s, m.start(), "ascii"
|
||||
|
||||
for m in utf16_re.finditer(data):
|
||||
raw = m.group(0)
|
||||
s = raw[::2].decode("ascii", errors="ignore")
|
||||
yield s, m.start(), "utf16le"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("paths", nargs="+", help="Files/dirs to scan")
|
||||
ap.add_argument("--min-len", type=int, default=8)
|
||||
ap.add_argument("--max-len", type=int, default=64)
|
||||
ap.add_argument("--top", type=int, default=80)
|
||||
ap.add_argument("--grep", type=str, default="", help="Only show hits containing this substring")
|
||||
args = ap.parse_args()
|
||||
|
||||
best: Dict[str, Hit] = {}
|
||||
grep = args.grep
|
||||
|
||||
for fp in iter_files(args.paths):
|
||||
|
||||
if fp.suffix.lower() not in (".dmp", ".mem", ".raw", ".bin", ""):
|
||||
continue
|
||||
|
||||
try:
|
||||
data = fp.read_bytes()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for s, off, kind in extract_hits(data, min_len=args.min_len, max_len=args.max_len):
|
||||
if grep and grep not in s:
|
||||
continue
|
||||
if not looks_passwordish(s):
|
||||
continue
|
||||
sc = score_string(s)
|
||||
existing = best.get(s)
|
||||
if existing is None:
|
||||
best[s] = Hit(s=s, score=sc, file=fp, offset=off, kind=kind)
|
||||
else:
|
||||
existing.count += 1
|
||||
if sc > existing.score:
|
||||
existing.score = sc
|
||||
existing.file = fp
|
||||
existing.offset = off
|
||||
existing.kind = kind
|
||||
|
||||
hits: List[Hit] = sorted(best.values(), key=lambda h: h.score, reverse=True)
|
||||
if not hits:
|
||||
print("[!] No candidates found")
|
||||
return 2
|
||||
|
||||
for h in hits[: args.top]:
|
||||
print(f"{h.score:8.2f}\t{h.count:4d}\t{h.kind}\t{h.file}\t0x{h.offset:X}\t{h.s}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
346
HumanAI-Forensic-Hard/scripts/extract_vc_fat32.py
Normal file
346
HumanAI-Forensic-Hard/scripts/extract_vc_fat32.py
Normal file
@@ -0,0 +1,346 @@
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
|
||||
SECTOR_SIZE = 512
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fat32BPB:
|
||||
bytes_per_sector: int
|
||||
sectors_per_cluster: int
|
||||
reserved_sectors: int
|
||||
num_fats: int
|
||||
total_sectors: int
|
||||
fat_size_sectors: int
|
||||
root_cluster: int
|
||||
|
||||
@property
|
||||
def first_fat_sector(self) -> int:
|
||||
return self.reserved_sectors
|
||||
|
||||
@property
|
||||
def first_data_sector(self) -> int:
|
||||
return self.reserved_sectors + self.num_fats * self.fat_size_sectors
|
||||
|
||||
|
||||
class VeraCryptFileVolume:
|
||||
"""Reads plaintext sectors from a VeraCrypt file container that is already decrypted in RAM (keys known)."""
|
||||
|
||||
def __init__(self, container: Path, xts_key: bytes, data_offset: int, data_length: Optional[int] = None):
|
||||
self.container = container
|
||||
self.xts_key = xts_key
|
||||
self.base_file_sector = data_offset // SECTOR_SIZE
|
||||
if data_offset % SECTOR_SIZE != 0:
|
||||
raise ValueError("data_offset must be sector-aligned")
|
||||
|
||||
st = container.stat()
|
||||
if data_length is None:
|
||||
# Assume standard file container layout: header area at start AND backup header at end.
|
||||
data_length = st.st_size - 2 * data_offset
|
||||
self.data_length = data_length
|
||||
if self.data_length < SECTOR_SIZE or self.data_length % SECTOR_SIZE != 0:
|
||||
raise ValueError("data_length must be a positive multiple of sector size")
|
||||
self.total_sectors = self.data_length // SECTOR_SIZE
|
||||
|
||||
self._fh = open(container, "rb")
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self._fh.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _decrypt_sector(self, file_sector: int, ct: bytes) -> bytes:
|
||||
tweak = int(file_sector).to_bytes(16, "little", signed=False)
|
||||
dec = Cipher(algorithms.AES(self.xts_key), modes.XTS(tweak)).decryptor()
|
||||
return dec.update(ct) + dec.finalize()
|
||||
|
||||
def read_sector(self, vol_sector: int) -> bytes:
|
||||
if not (0 <= vol_sector < self.total_sectors):
|
||||
raise ValueError("volume sector out of range")
|
||||
file_sector = self.base_file_sector + vol_sector
|
||||
self._fh.seek(file_sector * SECTOR_SIZE)
|
||||
ct = self._fh.read(SECTOR_SIZE)
|
||||
if len(ct) != SECTOR_SIZE:
|
||||
raise IOError("short read")
|
||||
return self._decrypt_sector(file_sector, ct)
|
||||
|
||||
|
||||
def parse_fat32_bpb(boot_sector: bytes) -> Fat32BPB:
|
||||
oem = boot_sector[3:11]
|
||||
if oem != b"MSDOS5.0":
|
||||
raise ValueError(f"Unexpected OEM {oem!r} (expected MSDOS5.0)")
|
||||
|
||||
bps = struct.unpack_from("<H", boot_sector, 11)[0]
|
||||
spc = boot_sector[13]
|
||||
rsvd = struct.unpack_from("<H", boot_sector, 14)[0]
|
||||
nfats = boot_sector[16]
|
||||
root_ent_cnt = struct.unpack_from("<H", boot_sector, 17)[0]
|
||||
tot16 = struct.unpack_from("<H", boot_sector, 19)[0]
|
||||
fatsz16 = struct.unpack_from("<H", boot_sector, 22)[0]
|
||||
tot32 = struct.unpack_from("<I", boot_sector, 32)[0]
|
||||
fatsz32 = struct.unpack_from("<I", boot_sector, 36)[0]
|
||||
root_clus = struct.unpack_from("<I", boot_sector, 44)[0]
|
||||
|
||||
if bps != SECTOR_SIZE:
|
||||
raise ValueError(f"Unsupported bytes/sector: {bps}")
|
||||
if root_ent_cnt != 0:
|
||||
raise ValueError("Not FAT32 (root entry count != 0)")
|
||||
if fatsz16 != 0:
|
||||
raise ValueError("Not FAT32 (FATSz16 != 0)")
|
||||
|
||||
tot = tot32 or tot16
|
||||
if tot == 0:
|
||||
raise ValueError("Invalid total sectors")
|
||||
if spc == 0:
|
||||
raise ValueError("Invalid sectors/cluster")
|
||||
if nfats < 1:
|
||||
raise ValueError("Invalid number of FATs")
|
||||
if fatsz32 == 0:
|
||||
raise ValueError("Invalid FAT size")
|
||||
if root_clus < 2:
|
||||
raise ValueError("Invalid root cluster")
|
||||
|
||||
return Fat32BPB(
|
||||
bytes_per_sector=bps,
|
||||
sectors_per_cluster=spc,
|
||||
reserved_sectors=rsvd,
|
||||
num_fats=nfats,
|
||||
total_sectors=tot,
|
||||
fat_size_sectors=fatsz32,
|
||||
root_cluster=root_clus,
|
||||
)
|
||||
|
||||
|
||||
def sanitize_name(s: str) -> str:
|
||||
# Minimal cross-platform path sanitization.
|
||||
s = s.replace("\\", "_").replace("/", "_").replace(":", "_")
|
||||
s = s.strip().strip(".")
|
||||
return s or "_"
|
||||
|
||||
|
||||
def parse_short_name(ent: bytes) -> str:
|
||||
name = ent[0:8].decode("ascii", errors="ignore").rstrip(" ")
|
||||
ext = ent[8:11].decode("ascii", errors="ignore").rstrip(" ")
|
||||
if not ext:
|
||||
return name
|
||||
return f"{name}.{ext}"
|
||||
|
||||
|
||||
def parse_lfn_part(ent: bytes) -> str:
|
||||
# LFN is UTF-16LE in three chunks
|
||||
raw = ent[1:11] + ent[14:26] + ent[28:32]
|
||||
out_chars: List[str] = []
|
||||
for i in range(0, len(raw), 2):
|
||||
(ch,) = struct.unpack_from("<H", raw, i)
|
||||
if ch in (0x0000, 0xFFFF):
|
||||
continue
|
||||
out_chars.append(chr(ch))
|
||||
return "".join(out_chars)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirEntry:
|
||||
name: str
|
||||
is_dir: bool
|
||||
cluster: int
|
||||
size: int
|
||||
|
||||
|
||||
class Fat32:
|
||||
def __init__(self, vol: VeraCryptFileVolume, bpb: Fat32BPB):
|
||||
self.vol = vol
|
||||
self.bpb = bpb
|
||||
self._fat_sector_cache: Dict[int, bytes] = {}
|
||||
|
||||
def vol_sector_for_cluster(self, cluster: int) -> int:
|
||||
return self.bpb.first_data_sector + (cluster - 2) * self.bpb.sectors_per_cluster
|
||||
|
||||
def read_fat_sector(self, fat_sector_index: int) -> bytes:
|
||||
if fat_sector_index not in self._fat_sector_cache:
|
||||
self._fat_sector_cache[fat_sector_index] = self.vol.read_sector(
|
||||
self.bpb.first_fat_sector + fat_sector_index
|
||||
)
|
||||
return self._fat_sector_cache[fat_sector_index]
|
||||
|
||||
def fat_entry(self, cluster: int) -> int:
|
||||
# FAT32 entry is 4 bytes, lower 28 bits used
|
||||
off = cluster * 4
|
||||
sector_index = off // SECTOR_SIZE
|
||||
sector_off = off % SECTOR_SIZE
|
||||
sec = self.read_fat_sector(sector_index)
|
||||
(val,) = struct.unpack_from("<I", sec, sector_off)
|
||||
return val & 0x0FFFFFFF
|
||||
|
||||
def iter_cluster_chain(self, start_cluster: int, *, max_steps: int = 200000) -> Iterator[int]:
|
||||
cl = start_cluster
|
||||
steps = 0
|
||||
while 2 <= cl < 0x0FFFFFF8:
|
||||
yield cl
|
||||
nxt = self.fat_entry(cl)
|
||||
if nxt == cl:
|
||||
break
|
||||
cl = nxt
|
||||
steps += 1
|
||||
if steps > max_steps:
|
||||
raise RuntimeError("cluster chain too long (loop?)")
|
||||
|
||||
def read_cluster(self, cluster: int) -> bytes:
|
||||
vsec0 = self.vol_sector_for_cluster(cluster)
|
||||
buf = bytearray()
|
||||
for i in range(self.bpb.sectors_per_cluster):
|
||||
buf += self.vol.read_sector(vsec0 + i)
|
||||
return bytes(buf)
|
||||
|
||||
def read_chain_data(self, start_cluster: int, size: int) -> bytes:
|
||||
buf = bytearray()
|
||||
for cl in self.iter_cluster_chain(start_cluster):
|
||||
buf += self.read_cluster(cl)
|
||||
if len(buf) >= size:
|
||||
break
|
||||
return bytes(buf[:size])
|
||||
|
||||
def read_directory(self, start_cluster: int) -> List[DirEntry]:
|
||||
entries: List[DirEntry] = []
|
||||
lfn_parts: List[str] = []
|
||||
|
||||
for cl in self.iter_cluster_chain(start_cluster):
|
||||
data = self.read_cluster(cl)
|
||||
for off in range(0, len(data), 32):
|
||||
ent = data[off : off + 32]
|
||||
first = ent[0]
|
||||
if first == 0x00:
|
||||
return entries
|
||||
if first == 0xE5:
|
||||
lfn_parts.clear()
|
||||
continue
|
||||
|
||||
attr = ent[11]
|
||||
if attr == 0x0F:
|
||||
lfn_parts.append(parse_lfn_part(ent))
|
||||
continue
|
||||
|
||||
name = ""
|
||||
if lfn_parts:
|
||||
name = "".join(reversed(lfn_parts))
|
||||
lfn_parts.clear()
|
||||
else:
|
||||
name = parse_short_name(ent)
|
||||
|
||||
# Skip volume labels
|
||||
if attr & 0x08:
|
||||
continue
|
||||
# Skip "." and ".."
|
||||
if name in (".", ".."):
|
||||
continue
|
||||
|
||||
is_dir = bool(attr & 0x10)
|
||||
hi = struct.unpack_from("<H", ent, 20)[0]
|
||||
lo = struct.unpack_from("<H", ent, 26)[0]
|
||||
first_cluster = (hi << 16) | lo
|
||||
size = struct.unpack_from("<I", ent, 28)[0]
|
||||
|
||||
entries.append(
|
||||
DirEntry(
|
||||
name=name,
|
||||
is_dir=is_dir,
|
||||
cluster=first_cluster,
|
||||
size=size,
|
||||
)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def walk_and_extract(
|
||||
fs: Fat32,
|
||||
start_cluster: int,
|
||||
out_dir: Path,
|
||||
rel_path: str = "",
|
||||
*,
|
||||
max_files: int = 5000,
|
||||
) -> List[Path]:
|
||||
extracted: List[Path] = []
|
||||
stack: List[Tuple[int, str]] = [(start_cluster, rel_path)]
|
||||
seen_dirs: set[Tuple[int, str]] = set()
|
||||
|
||||
while stack:
|
||||
cl, rpath = stack.pop()
|
||||
key = (cl, rpath)
|
||||
if key in seen_dirs:
|
||||
continue
|
||||
seen_dirs.add(key)
|
||||
|
||||
for ent in fs.read_directory(cl):
|
||||
name = sanitize_name(ent.name)
|
||||
child_rel = os.path.join(rpath, name) if rpath else name
|
||||
out_path = out_dir / child_rel
|
||||
|
||||
if ent.is_dir:
|
||||
if ent.cluster >= 2:
|
||||
stack.append((ent.cluster, child_rel))
|
||||
continue
|
||||
|
||||
if ent.cluster < 2:
|
||||
continue
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = fs.read_chain_data(ent.cluster, ent.size)
|
||||
out_path.write_bytes(data)
|
||||
extracted.append(out_path)
|
||||
if len(extracted) >= max_files:
|
||||
return extracted
|
||||
|
||||
return extracted
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Extract FAT32 files from a VeraCrypt container using recovered XTS keys")
|
||||
ap.add_argument("container", type=Path)
|
||||
ap.add_argument("--key-a", required=True, help="32-byte hex key A (data key)")
|
||||
ap.add_argument("--key-b", required=True, help="32-byte hex key B (tweak key)")
|
||||
ap.add_argument("--data-offset", default="0x20000", help="Start of encrypted volume area (default: 0x20000)")
|
||||
ap.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=Path("out") / "vc_extracted",
|
||||
help="Output directory (default: out/vc_extracted)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
key_a = bytes.fromhex(args.key_a)
|
||||
key_b = bytes.fromhex(args.key_b)
|
||||
if len(key_a) != 32 or len(key_b) != 32:
|
||||
raise SystemExit("Keys must be 32 bytes each")
|
||||
|
||||
data_offset = int(str(args.data_offset), 0)
|
||||
xts_key = key_a + key_b
|
||||
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vol = VeraCryptFileVolume(args.container, xts_key, data_offset)
|
||||
try:
|
||||
boot = vol.read_sector(0) # volume sector 0 maps to file sector base_file_sector
|
||||
bpb = parse_fat32_bpb(boot)
|
||||
print(
|
||||
f"[i] FAT32: total_sectors={bpb.total_sectors} spc={bpb.sectors_per_cluster} "
|
||||
f"reserved={bpb.reserved_sectors} fats={bpb.num_fats} fatsz={bpb.fat_size_sectors} root={bpb.root_cluster}"
|
||||
)
|
||||
|
||||
fs = Fat32(vol, bpb)
|
||||
extracted = walk_and_extract(fs, bpb.root_cluster, args.out_dir)
|
||||
print(f"[i] Extracted {len(extracted)} files into {args.out_dir}")
|
||||
return 0
|
||||
finally:
|
||||
vol.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
459
HumanAI-Forensic-Hard/scripts/find_aes_keys_in_dumps.py
Normal file
459
HumanAI-Forensic-Hard/scripts/find_aes_keys_in_dumps.py
Normal file
@@ -0,0 +1,459 @@
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Iterator, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
|
||||
SBOX = [
|
||||
0x63,
|
||||
0x7C,
|
||||
0x77,
|
||||
0x7B,
|
||||
0xF2,
|
||||
0x6B,
|
||||
0x6F,
|
||||
0xC5,
|
||||
0x30,
|
||||
0x01,
|
||||
0x67,
|
||||
0x2B,
|
||||
0xFE,
|
||||
0xD7,
|
||||
0xAB,
|
||||
0x76,
|
||||
0xCA,
|
||||
0x82,
|
||||
0xC9,
|
||||
0x7D,
|
||||
0xFA,
|
||||
0x59,
|
||||
0x47,
|
||||
0xF0,
|
||||
0xAD,
|
||||
0xD4,
|
||||
0xA2,
|
||||
0xAF,
|
||||
0x9C,
|
||||
0xA4,
|
||||
0x72,
|
||||
0xC0,
|
||||
0xB7,
|
||||
0xFD,
|
||||
0x93,
|
||||
0x26,
|
||||
0x36,
|
||||
0x3F,
|
||||
0xF7,
|
||||
0xCC,
|
||||
0x34,
|
||||
0xA5,
|
||||
0xE5,
|
||||
0xF1,
|
||||
0x71,
|
||||
0xD8,
|
||||
0x31,
|
||||
0x15,
|
||||
0x04,
|
||||
0xC7,
|
||||
0x23,
|
||||
0xC3,
|
||||
0x18,
|
||||
0x96,
|
||||
0x05,
|
||||
0x9A,
|
||||
0x07,
|
||||
0x12,
|
||||
0x80,
|
||||
0xE2,
|
||||
0xEB,
|
||||
0x27,
|
||||
0xB2,
|
||||
0x75,
|
||||
0x09,
|
||||
0x83,
|
||||
0x2C,
|
||||
0x1A,
|
||||
0x1B,
|
||||
0x6E,
|
||||
0x5A,
|
||||
0xA0,
|
||||
0x52,
|
||||
0x3B,
|
||||
0xD6,
|
||||
0xB3,
|
||||
0x29,
|
||||
0xE3,
|
||||
0x2F,
|
||||
0x84,
|
||||
0x53,
|
||||
0xD1,
|
||||
0x00,
|
||||
0xED,
|
||||
0x20,
|
||||
0xFC,
|
||||
0xB1,
|
||||
0x5B,
|
||||
0x6A,
|
||||
0xCB,
|
||||
0xBE,
|
||||
0x39,
|
||||
0x4A,
|
||||
0x4C,
|
||||
0x58,
|
||||
0xCF,
|
||||
0xD0,
|
||||
0xEF,
|
||||
0xAA,
|
||||
0xFB,
|
||||
0x43,
|
||||
0x4D,
|
||||
0x33,
|
||||
0x85,
|
||||
0x45,
|
||||
0xF9,
|
||||
0x02,
|
||||
0x7F,
|
||||
0x50,
|
||||
0x3C,
|
||||
0x9F,
|
||||
0xA8,
|
||||
0x51,
|
||||
0xA3,
|
||||
0x40,
|
||||
0x8F,
|
||||
0x92,
|
||||
0x9D,
|
||||
0x38,
|
||||
0xF5,
|
||||
0xBC,
|
||||
0xB6,
|
||||
0xDA,
|
||||
0x21,
|
||||
0x10,
|
||||
0xFF,
|
||||
0xF3,
|
||||
0xD2,
|
||||
0xCD,
|
||||
0x0C,
|
||||
0x13,
|
||||
0xEC,
|
||||
0x5F,
|
||||
0x97,
|
||||
0x44,
|
||||
0x17,
|
||||
0xC4,
|
||||
0xA7,
|
||||
0x7E,
|
||||
0x3D,
|
||||
0x64,
|
||||
0x5D,
|
||||
0x19,
|
||||
0x73,
|
||||
0x60,
|
||||
0x81,
|
||||
0x4F,
|
||||
0xDC,
|
||||
0x22,
|
||||
0x2A,
|
||||
0x90,
|
||||
0x88,
|
||||
0x46,
|
||||
0xEE,
|
||||
0xB8,
|
||||
0x14,
|
||||
0xDE,
|
||||
0x5E,
|
||||
0x0B,
|
||||
0xDB,
|
||||
0xE0,
|
||||
0x32,
|
||||
0x3A,
|
||||
0x0A,
|
||||
0x49,
|
||||
0x06,
|
||||
0x24,
|
||||
0x5C,
|
||||
0xC2,
|
||||
0xD3,
|
||||
0xAC,
|
||||
0x62,
|
||||
0x91,
|
||||
0x95,
|
||||
0xE4,
|
||||
0x79,
|
||||
0xE7,
|
||||
0xC8,
|
||||
0x37,
|
||||
0x6D,
|
||||
0x8D,
|
||||
0xD5,
|
||||
0x4E,
|
||||
0xA9,
|
||||
0x6C,
|
||||
0x56,
|
||||
0xF4,
|
||||
0xEA,
|
||||
0x65,
|
||||
0x7A,
|
||||
0xAE,
|
||||
0x08,
|
||||
0xBA,
|
||||
0x78,
|
||||
0x25,
|
||||
0x2E,
|
||||
0x1C,
|
||||
0xA6,
|
||||
0xB4,
|
||||
0xC6,
|
||||
0xE8,
|
||||
0xDD,
|
||||
0x74,
|
||||
0x1F,
|
||||
0x4B,
|
||||
0xBD,
|
||||
0x8B,
|
||||
0x8A,
|
||||
0x70,
|
||||
0x3E,
|
||||
0xB5,
|
||||
0x66,
|
||||
0x48,
|
||||
0x03,
|
||||
0xF6,
|
||||
0x0E,
|
||||
0x61,
|
||||
0x35,
|
||||
0x57,
|
||||
0xB9,
|
||||
0x86,
|
||||
0xC1,
|
||||
0x1D,
|
||||
0x9E,
|
||||
0xE1,
|
||||
0xF8,
|
||||
0x98,
|
||||
0x11,
|
||||
0x69,
|
||||
0xD9,
|
||||
0x8E,
|
||||
0x94,
|
||||
0x9B,
|
||||
0x1E,
|
||||
0x87,
|
||||
0xE9,
|
||||
0xCE,
|
||||
0x55,
|
||||
0x28,
|
||||
0xDF,
|
||||
0x8C,
|
||||
0xA1,
|
||||
0x89,
|
||||
0x0D,
|
||||
0xBF,
|
||||
0xE6,
|
||||
0x42,
|
||||
0x68,
|
||||
0x41,
|
||||
0x99,
|
||||
0x2D,
|
||||
0x0F,
|
||||
0xB0,
|
||||
0x54,
|
||||
0xBB,
|
||||
0x16,
|
||||
]
|
||||
|
||||
|
||||
def rot_word_be(w: int) -> int:
|
||||
return ((w << 8) & 0xFFFFFFFF) | ((w >> 24) & 0xFF)
|
||||
|
||||
|
||||
def sub_word_be(w: int) -> int:
|
||||
return (
|
||||
(SBOX[(w >> 24) & 0xFF] << 24)
|
||||
| (SBOX[(w >> 16) & 0xFF] << 16)
|
||||
| (SBOX[(w >> 8) & 0xFF] << 8)
|
||||
| (SBOX[w & 0xFF])
|
||||
)
|
||||
|
||||
|
||||
def rot_word_le(w: int) -> int:
|
||||
b = w.to_bytes(4, "little")
|
||||
b = b[1:] + b[:1]
|
||||
return int.from_bytes(b, "little")
|
||||
|
||||
|
||||
def sub_word_le(w: int) -> int:
|
||||
b = w.to_bytes(4, "little")
|
||||
sb = bytes([SBOX[x] for x in b])
|
||||
return int.from_bytes(sb, "little")
|
||||
|
||||
|
||||
def xtime(x: int) -> int:
|
||||
x <<= 1
|
||||
if x & 0x100:
|
||||
x ^= 0x11B
|
||||
return x & 0xFF
|
||||
|
||||
|
||||
def rcon_word(i: int, *, endian: str) -> int:
|
||||
rc = 1
|
||||
for _ in range(1, i):
|
||||
rc = xtime(rc)
|
||||
if endian == "be":
|
||||
return rc << 24
|
||||
return rc
|
||||
|
||||
|
||||
def total_words_for_nk(nk: int) -> int:
|
||||
if nk == 4:
|
||||
nr = 10
|
||||
elif nk == 6:
|
||||
nr = 12
|
||||
elif nk == 8:
|
||||
nr = 14
|
||||
else:
|
||||
raise ValueError(f"Unsupported Nk={nk}")
|
||||
return 4 * (nr + 1)
|
||||
|
||||
|
||||
def expand_key(words0: Sequence[int], *, nk: int, endian: str) -> List[int]:
|
||||
tw = total_words_for_nk(nk)
|
||||
w = list(words0[:nk]) + [0] * (tw - nk)
|
||||
|
||||
if endian == "be":
|
||||
rot = rot_word_be
|
||||
sub = sub_word_be
|
||||
elif endian == "le":
|
||||
rot = rot_word_le
|
||||
sub = sub_word_le
|
||||
else:
|
||||
raise ValueError("endian must be 'be' or 'le'")
|
||||
|
||||
for i in range(nk, tw):
|
||||
temp = w[i - 1]
|
||||
if i % nk == 0:
|
||||
temp = sub(rot(temp)) ^ rcon_word(i // nk, endian=endian)
|
||||
elif nk > 6 and i % nk == 4:
|
||||
temp = sub(temp)
|
||||
w[i] = w[i - nk] ^ temp
|
||||
return w
|
||||
|
||||
|
||||
def schedule_matches(words: Sequence[int], *, nk: int, endian: str) -> bool:
|
||||
tw = total_words_for_nk(nk)
|
||||
if len(words) < tw:
|
||||
return False
|
||||
exp = expand_key(words, nk=nk, endian=endian)
|
||||
return all((words[i] & 0xFFFFFFFF) == exp[i] for i in range(tw))
|
||||
|
||||
|
||||
def iter_files(paths: Iterable[str]) -> Iterator[Path]:
|
||||
for p in paths:
|
||||
path = Path(p)
|
||||
if path.is_dir():
|
||||
for child in sorted(path.rglob("*")):
|
||||
if child.is_file():
|
||||
yield child
|
||||
elif path.is_file():
|
||||
yield path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Hit:
|
||||
file: Path
|
||||
offset: int
|
||||
nk: int
|
||||
word_endian: str
|
||||
byte_endian: str
|
||||
key_bytes: bytes
|
||||
|
||||
|
||||
def scan_file(fp: Path, *, nk_list: Sequence[int]) -> List[Hit]:
|
||||
data = fp.read_bytes()
|
||||
hits: List[Hit] = []
|
||||
|
||||
for nk in nk_list:
|
||||
tw = total_words_for_nk(nk)
|
||||
nbytes = tw * 4
|
||||
if len(data) < nbytes:
|
||||
continue
|
||||
|
||||
for off in range(0, len(data) - nbytes + 1, 4):
|
||||
for word_endian in ("little", "big"):
|
||||
words = [
|
||||
int.from_bytes(
|
||||
data[off + 4 * i : off + 4 * i + 4], byteorder=word_endian
|
||||
)
|
||||
for i in range(tw)
|
||||
]
|
||||
|
||||
for byte_endian in ("be", "le"):
|
||||
if schedule_matches(words, nk=nk, endian=byte_endian):
|
||||
if byte_endian == "be":
|
||||
key = b"".join(w.to_bytes(4, "big") for w in words[:nk])
|
||||
else:
|
||||
key = b"".join(w.to_bytes(4, "little") for w in words[:nk])
|
||||
hits.append(
|
||||
Hit(
|
||||
file=fp,
|
||||
offset=off,
|
||||
nk=nk,
|
||||
word_endian=word_endian,
|
||||
byte_endian=byte_endian,
|
||||
key_bytes=key,
|
||||
)
|
||||
)
|
||||
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Find AES key schedules in raw dumps")
|
||||
ap.add_argument("paths", nargs="+", help="Files and/or directories to scan")
|
||||
ap.add_argument("--nk", type=int, default=8, help="AES Nk words: 4=128-bit, 6=192-bit, 8=256-bit (default: 8)")
|
||||
ap.add_argument(
|
||||
"--also",
|
||||
type=str,
|
||||
default="",
|
||||
help="Comma-separated extra Nk values to scan (e.g. 4,6)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
nk_list = [args.nk]
|
||||
if args.also:
|
||||
for part in args.also.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
nk_list.append(int(part))
|
||||
nk_list = sorted(set(nk_list))
|
||||
|
||||
seen: Set[Tuple[int, str, bytes]] = set()
|
||||
total = 0
|
||||
|
||||
for fp in iter_files(args.paths):
|
||||
if fp.suffix.lower() not in (".dmp", ".bin", ".raw", ".mem", ""):
|
||||
continue
|
||||
try:
|
||||
hits = scan_file(fp, nk_list=nk_list)
|
||||
except Exception:
|
||||
continue
|
||||
for h in hits:
|
||||
k = (h.nk, h.word_endian, h.byte_endian, h.key_bytes)
|
||||
if k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
total += 1
|
||||
print(
|
||||
f"{h.file}\t0x{h.offset:X}\tNk={h.nk}\tword={h.word_endian}\tbytes={h.byte_endian}\tkey={h.key_bytes.hex()}"
|
||||
)
|
||||
|
||||
if total == 0:
|
||||
print("[!] No AES key schedules found")
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
99
HumanAI-Forensic-Hard/scripts/probe_vc_xts.py
Normal file
99
HumanAI-Forensic-Hard/scripts/probe_vc_xts.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
|
||||
def decrypt_xts_sector(ct: bytes, xts_key: bytes, sector_no: int) -> bytes:
|
||||
tweak = int(sector_no).to_bytes(16, "little", signed=False)
|
||||
cipher = Cipher(algorithms.AES(xts_key), modes.XTS(tweak))
|
||||
dec = cipher.decryptor()
|
||||
return dec.update(ct) + dec.finalize()
|
||||
|
||||
|
||||
def looks_like_boot_sector(pt: bytes) -> List[str]:
|
||||
hits: List[str] = []
|
||||
if len(pt) < 512:
|
||||
return hits
|
||||
if pt[510:512] == b"\x55\xaa":
|
||||
hits.append("55aa")
|
||||
sig = pt[3:11]
|
||||
if sig == b"NTFS ":
|
||||
hits.append("NTFS")
|
||||
if sig == b"EXFAT ":
|
||||
hits.append("EXFAT")
|
||||
if sig.startswith(b"FAT"):
|
||||
hits.append(sig.decode("ascii", errors="ignore"))
|
||||
if pt[0] in (0xEB, 0xE9) and pt[2] == 0x90:
|
||||
hits.append("jmp")
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Probe VeraCrypt container using AES-XTS keys")
|
||||
ap.add_argument("container", type=Path)
|
||||
ap.add_argument("--key-a", required=True, help="32-byte hex key A")
|
||||
ap.add_argument("--key-b", required=True, help="32-byte hex key B")
|
||||
ap.add_argument(
|
||||
"--offsets",
|
||||
default="0,0x10000,0x20000",
|
||||
help="Comma-separated file offsets to try (default: 0,0x10000,0x20000)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--tweak-bases",
|
||||
default="auto",
|
||||
help="Comma-separated sector numbers to try as tweak base, or 'auto' for 0 and offset/512",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
key_a = bytes.fromhex(args.key_a)
|
||||
key_b = bytes.fromhex(args.key_b)
|
||||
if len(key_a) != 32 or len(key_b) != 32:
|
||||
raise SystemExit("Keys must be 32 bytes each (64 hex chars)")
|
||||
|
||||
offsets: List[int] = []
|
||||
for part in args.offsets.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
offsets.append(int(part, 0))
|
||||
|
||||
data = args.container.read_bytes()
|
||||
|
||||
key_orders: List[Tuple[str, bytes]] = [
|
||||
("A||B", key_a + key_b),
|
||||
("B||A", key_b + key_a),
|
||||
]
|
||||
|
||||
for off in offsets:
|
||||
if off + 512 > len(data):
|
||||
continue
|
||||
ct = data[off : off + 512]
|
||||
|
||||
if args.tweak_bases.strip().lower() == "auto":
|
||||
tweak_bases = sorted({0, off // 512})
|
||||
else:
|
||||
tweak_bases = []
|
||||
for part in args.tweak_bases.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
tweak_bases.append(int(part, 0))
|
||||
|
||||
for base in tweak_bases:
|
||||
for label, xts_key in key_orders:
|
||||
pt = decrypt_xts_sector(ct, xts_key, base)
|
||||
hits = looks_like_boot_sector(pt)
|
||||
if hits:
|
||||
print(
|
||||
f"[+] offset=0x{off:X} tweak={base} order={label} hits={','.join(hits)} sig={pt[3:11]!r}"
|
||||
)
|
||||
print(pt[:64].hex())
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
101
HumanAI-Forensic-Hard/scripts/scan_vc_password_struct.py
Normal file
101
HumanAI-Forensic-Hard/scripts/scan_vc_password_struct.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Iterator, Tuple
|
||||
|
||||
|
||||
def iter_input_files(paths: Iterable[str]) -> Iterator[Path]:
|
||||
for p in paths:
|
||||
path = Path(p)
|
||||
if path.is_dir():
|
||||
for child in sorted(path.rglob("*")):
|
||||
if child.is_file():
|
||||
yield child
|
||||
elif path.is_file():
|
||||
yield path
|
||||
|
||||
|
||||
def scan_password_structs(
|
||||
data: bytes, *, min_len: int, max_len: int
|
||||
) -> Iterator[Tuple[int, int, str]]:
|
||||
"""
|
||||
Heuristic scan for the (TrueCrypt/VeraCrypt) Password struct:
|
||||
uint32 Length; char Text[...];
|
||||
|
||||
We look for:
|
||||
<len:uint32le> <len bytes printable ASCII> <any byte> 0x00 0x00 0x00
|
||||
|
||||
This mirrors volatility3's truecrypt passphrase finder which validates the
|
||||
3 bytes *after* the presumed NUL terminator but doesn't explicitly check
|
||||
the terminator byte itself.
|
||||
"""
|
||||
mv = memoryview(data)
|
||||
n = len(data)
|
||||
min_total = 4 + min_len + 4
|
||||
if n < min_total:
|
||||
return
|
||||
|
||||
|
||||
unpack_from = struct.unpack_from
|
||||
for i in range(0, n - min_total + 1):
|
||||
(length,) = unpack_from("<I", mv, i)
|
||||
if length < min_len or length > max_len:
|
||||
continue
|
||||
|
||||
start = i + 4
|
||||
end = start + length
|
||||
tail = end + 4
|
||||
if tail > n:
|
||||
continue
|
||||
|
||||
pw = mv[start:end]
|
||||
|
||||
if any((c < 0x20 or c >= 0x7F) for c in pw):
|
||||
continue
|
||||
|
||||
if data[end + 1 : tail] != b"\x00\x00\x00":
|
||||
continue
|
||||
|
||||
try:
|
||||
pw_str = pw.tobytes().decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
yield i, length, pw_str
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("paths", nargs="+", help="Files and/or directories to scan")
|
||||
ap.add_argument("--min-len", type=int, default=5)
|
||||
ap.add_argument("--max-len", type=int, default=64)
|
||||
args = ap.parse_args()
|
||||
|
||||
seen = set()
|
||||
hits = 0
|
||||
for fp in iter_input_files(args.paths):
|
||||
try:
|
||||
data = fp.read_bytes()
|
||||
except Exception as exc:
|
||||
print(f"[!] Failed to read {fp}: {exc}")
|
||||
continue
|
||||
|
||||
for off, length, pw in scan_password_structs(
|
||||
data, min_len=args.min_len, max_len=args.max_len
|
||||
):
|
||||
key = (pw,)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
hits += 1
|
||||
print(f"{fp}\t0x{off:X}\t{length}\t{pw}")
|
||||
|
||||
if hits == 0:
|
||||
print("[!] No candidates found")
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
185
HumanAI-Forensic-Hard/scripts/veracrypt.py
Normal file
185
HumanAI-Forensic-Hard/scripts/veracrypt.py
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
import logging
|
||||
from typing import Generator, Iterable, List, Tuple
|
||||
|
||||
from volatility3.framework import constants, interfaces, objects, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import configuration
|
||||
from volatility3.framework.objects.utility import array_to_string
|
||||
from volatility3.framework.renderers import format_hints
|
||||
from volatility3.framework.symbols import intermed
|
||||
from volatility3.framework.symbols.windows.extensions import pe
|
||||
from volatility3.plugins.windows import modules
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Passphrase(interfaces.plugins.PluginInterface):
|
||||
"""VeraCrypt/TrueCrypt cached passphrase finder (driver .data scan)."""
|
||||
|
||||
_version = (0, 1, 0)
|
||||
_required_framework_version = (2, 5, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[configuration.RequirementInterface]:
|
||||
return [
|
||||
requirements.ModuleRequirement(
|
||||
"kernel",
|
||||
description="Windows kernel",
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="modules", component=modules.Modules, version=(3, 0, 0)
|
||||
),
|
||||
requirements.IntRequirement(
|
||||
name="min-length",
|
||||
description="Minimum length of passphrases to identify",
|
||||
default=5,
|
||||
optional=True,
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
name="driver",
|
||||
description=(
|
||||
"Driver name substring to scan (case-insensitive), "
|
||||
"e.g. 'veracrypt', 'veracrypt-x64.sys', 'truecrypt.sys'"
|
||||
),
|
||||
default="veracrypt",
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
|
||||
def scan_module(
|
||||
self, module_base: int, layer_name: str
|
||||
) -> Generator[Tuple[int, str], None, None]:
|
||||
pe_table_name = intermed.IntermediateSymbolTable.create(
|
||||
self.context, self.config_path, "windows", "pe", class_types=pe.class_types
|
||||
)
|
||||
dos_header: pe.IMAGE_DOS_HEADER = self.context.object(
|
||||
pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER",
|
||||
layer_name,
|
||||
module_base,
|
||||
)
|
||||
|
||||
data_section: objects.StructType = next(
|
||||
sec
|
||||
for sec in dos_header.get_nt_header().get_sections()
|
||||
if array_to_string(sec.Name) == ".data"
|
||||
)
|
||||
base: int = data_section.VirtualAddress + module_base
|
||||
size: int = data_section.Misc.VirtualSize
|
||||
|
||||
# Looking at `Length` in TrueCrypt/Common/Password.h::Password struct
|
||||
DWORD_SIZE_BYTES: int = 4
|
||||
fmt = objects.DataFormatInfo(
|
||||
length=DWORD_SIZE_BYTES, byteorder="little", signed=True
|
||||
)
|
||||
int32 = objects.templates.ObjectTemplate(
|
||||
objects.Integer, pe_table_name + constants.BANG + "int", data_format=fmt
|
||||
)
|
||||
count, not_aligned = divmod(size, DWORD_SIZE_BYTES)
|
||||
if not_aligned:
|
||||
raise ValueError("PE data section not DWORD-aligned!")
|
||||
|
||||
lengths = self.context.object(
|
||||
pe_table_name + constants.BANG + "array",
|
||||
layer_name,
|
||||
base,
|
||||
count=count,
|
||||
subtype=int32,
|
||||
)
|
||||
|
||||
min_length = self.config.get("min-length")
|
||||
for length in lengths:
|
||||
|
||||
if not min_length <= length <= 64:
|
||||
continue
|
||||
|
||||
offset = length.vol["offset"] + DWORD_SIZE_BYTES
|
||||
passphrase: objects.Bytes = self.context.object(
|
||||
pe_table_name + constants.BANG + "bytes",
|
||||
layer_name,
|
||||
offset,
|
||||
length=length,
|
||||
)
|
||||
|
||||
|
||||
if not all(0x20 <= c < 0x7F for c in passphrase):
|
||||
continue
|
||||
|
||||
buf: objects.Bytes = self.context.object(
|
||||
pe_table_name + constants.BANG + "bytes",
|
||||
layer_name,
|
||||
offset + length + 1,
|
||||
length=3,
|
||||
)
|
||||
if any(buf):
|
||||
continue
|
||||
|
||||
yield offset, passphrase.decode(encoding="ascii")
|
||||
|
||||
def _find_driver_bases(
|
||||
self, mods: Iterable[interfaces.objects.ObjectInterface]
|
||||
) -> List[int]:
|
||||
driver_substr = (self.config.get("driver") or "").lower().strip()
|
||||
|
||||
def matches(mod_name: str, needle: str) -> bool:
|
||||
return needle and needle in mod_name
|
||||
|
||||
def bases_for(needle: str) -> List[int]:
|
||||
out: List[int] = []
|
||||
for mod in mods:
|
||||
try:
|
||||
name = mod.BaseDllName.get_string().lower()
|
||||
except Exception:
|
||||
continue
|
||||
if matches(name, needle):
|
||||
out.append(int(mod.DllBase))
|
||||
return out
|
||||
|
||||
if driver_substr:
|
||||
bases = bases_for(driver_substr)
|
||||
if bases:
|
||||
return bases
|
||||
|
||||
for needle in ("veracrypt", "truecrypt"):
|
||||
bases = bases_for(needle)
|
||||
if bases:
|
||||
return bases
|
||||
|
||||
return []
|
||||
|
||||
def _generator(self):
|
||||
kernel = self.context.modules[self.config["kernel"]]
|
||||
mods: Iterable[interfaces.objects.ObjectInterface] = modules.Modules.list_modules(
|
||||
self.context, self.config["kernel"]
|
||||
)
|
||||
|
||||
driver_bases = self._find_driver_bases(mods)
|
||||
if not driver_bases:
|
||||
vollog.warning(
|
||||
"No VeraCrypt driver module found in the modules list. Unable to proceed."
|
||||
)
|
||||
return
|
||||
|
||||
seen = set()
|
||||
for module_base in driver_bases:
|
||||
try:
|
||||
for offset, password in self.scan_module(module_base, kernel.layer_name):
|
||||
key = (offset, password)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
yield (0, (format_hints.Hex(offset), len(password), password))
|
||||
except Exception as exc:
|
||||
vollog.debug("Failed scanning module at 0x%x: %s", module_base, exc)
|
||||
|
||||
def run(self) -> renderers.TreeGrid:
|
||||
return renderers.TreeGrid(
|
||||
[
|
||||
("Offset", format_hints.Hex),
|
||||
("Length", int),
|
||||
("Password", str),
|
||||
],
|
||||
self._generator(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user