Update public source
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user