Init. commit

This commit is contained in:
Caplag
2026-03-02 21:44:22 +03:00
committed by Ivan Z
commit 9511b38280
38 changed files with 4397 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# HumanAI
Мир захлестнул ИИ‑контент: генерации, копии, шум. Но где‑то в памяти системы есть то, что создано человеком. Найди это https://git.caplag.ru/kernel/HumanAI
## Решение
Всего есть два варианта решения:
1. Плановое решение: восстановление контейнера VeraCrypt + извлечение криптоключей из RAM.
2. Незапланированное решение: извлечение флага через кэш миниатюр Windows.
Сразу же загружаем дамп памяти в **Volatility**, выполняем базовые команды (`pslist`, `filescan` и т. д.).
Что интересного узнаём:
- VeraCrypt был запущен;
- контейнер `human.vc` был смонтирован.
### Потихоньку-помаленьку
Проверяем следы присутствия **VeraCrypt**:
```powershell
vol -q -f .\memdump.mem windows.modules | Select-String -Pattern "veracrypt|truecrypt" -CaseSensitive:$false
vol -q -f .\memdump.mem windows.pslist | Select-String -Pattern "veracrypt" -CaseSensitive:$false
vol -q -f .\memdump.mem windows.symlinkscan | Select-String -Pattern "VeraCrypt|Volume" -CaseSensitive:$false
```
Из этого мы сможем вытянуть достаточно важную информацию. Как минимум мы получим *модуль драйвера* VeraCrypt, его *процесс* и *букву смонтированного тома* вида `K:` -> `\Device\VeraCryptVolume...`.
### Плановый вариант решения
Начнем с того, что восстановим `human.vc` из памяти. Ищем файловый объект:
```powershell
vol -q -f .\memdump.mem -r csv windows.filescan | Select-String -Pattern "human\\.vc|\\.vc$" -CaseSensitive:$false
```
Дальше выгружаем файл через `dumpfiles`:
```powershell
mkdir out\dumpfiles -Force | Out-Null
vol -q -f .\memdump.mem -o out\dumpfiles windows.dumpfiles --filter "human\\.vc$|human\\.vc" --ignore-case
```
Обычно нужный файл находится среди объектов `SharedCacheMap` (часто с расширением `.vacb`). П
Пароль может не сохраниться в памяти, поэтому надёжнее идти через мастер-ключи. Одна из рабочих точек входа - *Big Pool*:
```powershell
vol -q -f .\memdump.mem -r csv windows.bigpools | Select-String -Pattern "VCMM|TC|VC" -CaseSensitive:$false
```
Дальше нужно производим выгрузку памяти по подходящим `pool tag` и осуществляема поиск пар ключей AES-256 для XTS (`key_a`, `key_b`). Основными признаками, что мы вышли на верные ключи:
- находятся две разные 32-байтовые последовательности;
- они стабильно повторяются в связанных дампах.
Проверяем, что ключи действительно дешифруют начало файловой системы. Для VeraCrypt данные начнутся после оффсета на `0x20000`. После AES-XTS-дешифровки сектора структура должна быть похожа на boot sector файловой системы и сигнатуры и поля должны выглядеть осмысленно, а не как случайный шум.
После подтверждения ключей и оффсета:
- расшифровываем контейнер;
- парсим файловую систему (в этом таске была FAT32);
- извлекаем файлы и находим флаг.
### Неожиданный вариант решения
Эта ветка сработала без полного дешифрования контейнера. В дампе можно искать строки, связанные с путём к файлу флага:
```powershell
strings -n 6 .\memdump.mem | Select-String -Pattern "flag|thumbcache|\\\.png|\\\.jpg|K:\\" -CaseSensitive:$false
```
Таким образом в памяти встретится путь к `flag.png` на смонтированном томе VeraCrypt. Теперь проверим, есть ли исходный файл в файловых объектах:
```powershell
vol -q -f .\memdump.mem windows.filescan | Select-String -Pattern "flag\\.png|flag" -CaseSensitive:$false
```
Напрямую файл `flag.png` найти не получится, но это не тупик. Приступаем к изучению следующего потенциального кандидата - кэш миниатюр Windows:
```powershell
vol -q -f .\memdump.mem windows.filescan | Select-String -Pattern "thumbcache_.*\\.db|thumbcache" -CaseSensitive:$false
```
Выгрузим найденные объекты:
```powershell
mkdir out\thumbs -Force | Out-Null
vol -q -f .\memdump.mem -o out\thumbs windows.dumpfiles --filter "thumbcache_.*\\.db|thumbcache" --ignore-case
```
И теперь, наконец, извлечем изображения из дампа кэша. Как вариант, можно воспользоваться `carving`:
```powershell
cd .\out\thumbs
Get-ChildItem -File | ForEach-Object { binwalk -e -M $_.FullName }
```
После этого просматриваем извлечённые изображения. Флаг окажется в одной из миниатюр.

View 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(),
)

View 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())

View 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())

View 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())

View 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())

View 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())

View 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(),
)