Files
wifi-gps-mapper-public/cracklab/client/hashcat.py
T
2026-10-06 21:08:12 +03:00

674 lines
14 KiB
Python

from __future__ import annotations
import hashlib
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
class HashcatError(RuntimeError):
def __init__(
self,
message: str,
*,
exit_code: int | None = None,
) -> None:
super().__init__(message)
self.exit_code = exit_code
@dataclass(frozen=True)
class HashcatRunResult:
exit_code: int
restore_seen: bool
def restore_path(
job_dir: Path,
step: dict,
) -> Path:
return (
job_dir
/ "state"
/ f"step-{step['step_no']:03d}.restore"
)
def completed_marker(
job_dir: Path,
step: dict,
) -> Path:
return (
job_dir
/ "state"
/ f"step-{step['step_no']:03d}.completed"
)
def _session_name(step: dict) -> str:
session = step.get("session_name")
if not isinstance(session, str) or not session:
raise HashcatError(
f"Step {step.get('step_no')} has no session_name."
)
return session
def _resolve_wordlist(
config,
resource: str,
step_no,
) -> Path:
if not isinstance(resource, str) or not resource:
raise HashcatError(
f"Step {step_no} has invalid dictionary resource."
)
prefix = "wordlist:"
if not resource.startswith(prefix):
raise HashcatError(
f"Step {step_no} has unsupported dictionary resource: "
f"{resource}"
)
name = resource[len(prefix):]
if not name or "/" in name or "\\" in name or name in (".", ".."):
raise HashcatError(
f"Step {step_no} has invalid dictionary resource: "
f"{resource}"
)
dictionary = config.wordlists.get(name)
if dictionary is None:
raise HashcatError(
f"Wordlist resource {resource!r} is not configured."
)
dictionary = dictionary.resolve()
if not dictionary.is_file():
raise HashcatError(
f"Configured wordlist does not exist: {dictionary}"
)
return dictionary
def build_start_command(
config,
job_dir: Path,
job: dict,
step: dict,
) -> list[str]:
exe = config.hashcat_exe.resolve()
if not exe.is_file():
raise HashcatError(
f"Hashcat executable does not exist: {exe}"
)
hash_file = (
job_dir
/ "hashes"
/ job["hash_file_name"]
).resolve()
if not hash_file.is_file():
raise HashcatError(
f"Hash file does not exist: {hash_file}"
)
restore = restore_path(
job_dir,
step,
).resolve()
session = _session_name(step)
attack_mode = step.get("definition", {}).get(
"attack_mode"
)
if not isinstance(attack_mode, int):
raise HashcatError(
f"Step {step.get('step_no')} has invalid "
"attack_mode."
)
definition = step["definition"]
cmd = [
str(exe),
"-m",
str(job["hash_mode"]),
"-a",
str(attack_mode),
"--session",
session,
"--restore-file-path",
str(restore),
"--potfile-path",
str(config.potfile_path.resolve()),
"--logfile-disable",
"-w",
str(
definition.get(
"workload_profile",
1,
)
),
]
if attack_mode in (0, 1, 6, 7):
dictionary_definition = (
definition.get(
"dictionaries"
)
if attack_mode == 1
else definition.get(
"dictionary"
)
)
if attack_mode == 0:
if not isinstance(
dictionary_definition,
dict,
):
raise HashcatError(
f"Step {step.get('step_no')} "
"attack mode 0 has no dictionary."
)
resources = [
dictionary_definition.get(
"resource"
)
]
elif attack_mode == 1:
if (
not isinstance(
dictionary_definition,
list,
)
or len(dictionary_definition) != 2
):
raise HashcatError(
f"Step {step.get('step_no')} "
"attack mode 1 requires exactly "
"two dictionaries."
)
resources = [
item.get("resource")
if isinstance(item, dict)
else None
for item in dictionary_definition
]
else:
if not isinstance(
dictionary_definition,
dict,
):
raise HashcatError(
f"Step {step.get('step_no')} "
f"attack mode {attack_mode} "
"has no dictionary."
)
resources = [
dictionary_definition.get(
"resource"
)
]
dictionaries = [
_resolve_wordlist(
config,
resource,
step.get("step_no"),
)
for resource in resources
]
induction = (
job_dir
/ "state"
/ "induct"
).resolve()
outfile_check = (
job_dir
/ "state"
/ "outfiles"
).resolve()
induction.mkdir(
parents=True,
exist_ok=True,
)
outfile_check.mkdir(
parents=True,
exist_ok=True,
)
cmd.extend(
[
"--induction-dir",
str(induction),
"--outfile-check-dir",
str(outfile_check),
str(hash_file),
]
)
cmd.extend(
str(dictionary)
for dictionary in dictionaries
)
if attack_mode in (6, 7):
mask = definition.get("mask")
if not isinstance(mask, str) or not mask:
raise HashcatError(
f"Step {step.get('step_no')} "
f"attack mode {attack_mode} has no mask."
)
if attack_mode == 6:
cmd.append(mask)
else:
cmd.insert(
len(cmd) - len(dictionaries),
mask,
)
elif attack_mode == 3:
mask = definition.get("mask")
if not isinstance(mask, str) or not mask:
raise HashcatError(
f"Step {step.get('step_no')} "
"attack mode 3 has no mask."
)
cmd.extend(
[
str(hash_file),
mask,
]
)
else:
raise HashcatError(
f"Unsupported attack mode {attack_mode}. "
"Use a structured adapter for new modes."
)
return cmd
def build_restore_command(
config,
job_dir: Path,
step: dict,
) -> list[str]:
exe = config.hashcat_exe.resolve()
if not exe.is_file():
raise HashcatError(
f"Hashcat executable does not exist: {exe}"
)
restore = restore_path(
job_dir,
step,
).resolve()
return [
str(exe),
"--session",
_session_name(step),
"--restore-file-path",
str(restore),
"--restore",
]
def build_show_command(
config,
job_dir: Path,
job: dict,
) -> list[str]:
exe = config.hashcat_exe.resolve()
if not exe.is_file():
raise HashcatError(
f"Hashcat executable does not exist: {exe}"
)
hash_file = (
job_dir
/ "hashes"
/ job["hash_file_name"]
).resolve()
if not hash_file.is_file():
raise HashcatError(
f"Hash file does not exist: {hash_file}"
)
return [
str(exe),
"-m",
str(job["hash_mode"]),
"--potfile-path",
str(config.potfile_path.resolve()),
"--show",
str(hash_file),
]
def command_text(cmd: list[str]) -> str:
return " ".join(
f'"{value}"' if " " in value else value
for value in cmd
)
def run_hashcat(
cmd: list[str],
log_path: Path,
*,
on_output: Callable[[bytes], None] | None = None,
) -> int:
log_path.parent.mkdir(
parents=True,
exist_ok=True,
)
if not cmd:
raise HashcatError(
"Hashcat command is empty."
)
hashcat_dir = (
Path(cmd[0])
.resolve()
.parent
)
text = command_text(cmd)
with log_path.open(
"ab",
) as log:
log.write(
b"\n=== START ===\n"
)
log.write(
text.encode(
"utf-8",
errors="replace",
)
)
log.write(b"\n")
log.flush()
try:
process = subprocess.Popen(
cmd,
cwd=str(hashcat_dir),
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
creationflags=getattr(
subprocess,
"CREATE_NEW_PROCESS_GROUP",
0,
),
)
except OSError as exc:
log.write(
(
"\n=== PROCESS START ERROR ===\n"
f"{exc}\n"
).encode(
"utf-8",
errors="replace",
)
)
raise HashcatError(
f"Failed to start Hashcat: {exc}"
) from exc
try:
if process.stdout is None:
raise HashcatError(
"Hashcat stdout pipe was not created."
)
while True:
chunk = process.stdout.read(4096)
if not chunk:
break
log.write(chunk)
log.flush()
if on_output is not None:
on_output(chunk)
except BaseException:
try:
process.kill()
except OSError:
pass
raise
finally:
if process.stdout is not None:
process.stdout.close()
exit_code = process.wait()
log.write(
(
"\n=== END ===\n"
f"exit_code={exit_code}\n"
).encode(
"utf-8",
errors="replace",
)
)
log.flush()
return exit_code
def run_step(
config,
job_dir: Path,
job: dict,
step: dict,
log_path: Path,
*,
restore: bool = False,
) -> HashcatRunResult:
restore_file = restore_path(
job_dir,
step,
)
if restore:
if not restore_file.is_file():
raise HashcatError(
f"Restore requested but restore file "
f"does not exist: {restore_file}"
)
cmd = build_restore_command(
config,
job_dir,
step,
)
else:
cmd = build_start_command(
config,
job_dir,
job,
step,
)
exit_code = run_hashcat(
cmd,
log_path,
on_output=lambda chunk: print(
chunk.decode(
"utf-8",
errors="replace",
),
end="",
flush=True,
),
)
restore_seen = restore_file.is_file()
if exit_code not in (0, 1):
raise HashcatError(
f"Hashcat exited with unsupported "
f"code {exit_code}.",
exit_code=exit_code,
)
return HashcatRunResult(
exit_code=exit_code,
restore_seen=restore_seen,
)
def run_show(
config,
job_dir: Path,
job: dict,
output_path: Path,
log_path: Path,
) -> str:
cmd = build_show_command(
config,
job_dir,
job,
)
output_path.parent.mkdir(
parents=True,
exist_ok=True,
)
log_path.parent.mkdir(
parents=True,
exist_ok=True,
)
hashcat_dir = (
Path(cmd[0])
.resolve()
.parent
)
text = command_text(cmd)
with log_path.open(
"ab",
) as log:
log.write(
b"\n=== SHOW START ===\n"
)
log.write(
text.encode(
"utf-8",
errors="replace",
)
)
log.write(b"\n")
log.flush()
try:
process = subprocess.run(
cmd,
cwd=str(hashcat_dir),
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
except OSError as exc:
log.write(
(
"\n=== SHOW START ERROR ===\n"
f"{exc}\n"
).encode(
"utf-8",
errors="replace",
)
)
raise HashcatError(
f"Failed to start Hashcat --show: {exc}"
) from exc
output = process.stdout or b""
output_path.write_bytes(
output
)
log.write(output)
log.write(
(
"\n=== SHOW END ===\n"
f"exit_code={process.returncode}\n"
).encode(
"utf-8",
errors="replace",
)
)
log.flush()
if process.returncode not in (0, 1):
raise HashcatError(
f"Hashcat --show exited with "
f"code {process.returncode}."
)
return output.decode(
"utf-8",
errors="replace",
)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
while True:
chunk = handle.read(1024 * 1024)
if not chunk:
break
digest.update(chunk)
return digest.hexdigest()