mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-28 14:47:22 -07:00
feat: add LLM Markov Attack (menu option 15)
Add a new attack mode that uses a local LLM via Ollama to generate password candidates, converts them into hashcat .hcstat2 Markov statistics via hcstat2gen, and runs a Markov-enhanced mask attack. Two generation sub-modes: - Wordlist-based: feeds sample from an existing wordlist to the LLM as pattern context (config-selectable default with Y/N override) - Target-based: prompts for company name, industry, and location for contextual password generation Pipeline: Ollama API -> candidate file -> hcstat2gen -> LZMA compress -> hashcat -a 3 --markov-hcstat2 Config additions: ollamaUrl, ollamaModel, markovCandidateCount, markovWordlist. No new pip dependencies (uses stdlib urllib/lzma). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
91906e3313
commit
e205e6e886
+5
-1
@@ -22,5 +22,9 @@
|
||||
"bandrel_common_basedwords": "welcome,password,p@ssword,p@$$word,changeme,letmein,summer,winter,spring,springtime,fall,autumn,monday,tuesday,wednesday,thursday,friday,saturday,sunday,january,february,march,april,may,june,july,august,september,october,november,december,christmas,easter,covid19",
|
||||
"hashview_url": "http://localhost:8443",
|
||||
"hashview_api_key": "",
|
||||
"hashmob_api_key": ""
|
||||
"hashmob_api_key": "",
|
||||
"ollamaUrl": "http://localhost:11434",
|
||||
"ollamaModel": "llama3.2",
|
||||
"markovCandidateCount": 5000,
|
||||
"markovWordlist": "rockyou.txt"
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ def get_main_menu_options():
|
||||
"12": _attacks.thorough_combinator,
|
||||
"13": _attacks.bandrel_method,
|
||||
"14": _attacks.loopback_attack,
|
||||
"15": _attacks.markov_attack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": weakpass_wordlist_menu,
|
||||
"92": download_hashmob_wordlists,
|
||||
|
||||
@@ -486,3 +486,48 @@ def middle_combinator(ctx: Any) -> None:
|
||||
|
||||
def bandrel_method(ctx: Any) -> None:
|
||||
ctx.hcatBandrel(ctx.hcatHashType, ctx.hcatHashFile)
|
||||
|
||||
|
||||
def markov_attack(ctx: Any) -> None:
|
||||
print("\n\tLLM Markov Attack")
|
||||
print("\t(1) Wordlist-based generation")
|
||||
print("\t(2) Target-based generation")
|
||||
choice = input("\nSelect generation mode: ").strip()
|
||||
|
||||
if choice == "1":
|
||||
default_wl = ctx.markovWordlist
|
||||
if isinstance(default_wl, list):
|
||||
default_wl = default_wl[0] if default_wl else ""
|
||||
print(f"\nDefault wordlist: {default_wl}")
|
||||
override = input("Use a different wordlist? [y/N]: ").strip().lower()
|
||||
if override == "y":
|
||||
wordlist = ctx.select_file_with_autocomplete("Enter wordlist path")
|
||||
wordlist = ctx._resolve_wordlist_path(wordlist, ctx.hcatWordlists)
|
||||
else:
|
||||
wordlist = default_wl
|
||||
count_input = input(
|
||||
f"Number of candidates to generate [{ctx.markovCandidateCount}]: "
|
||||
).strip()
|
||||
count = int(count_input) if count_input else ctx.markovCandidateCount
|
||||
ctx.hcatMarkov(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, "wordlist", wordlist, count
|
||||
)
|
||||
|
||||
elif choice == "2":
|
||||
company = input("Company name: ").strip()
|
||||
industry = input("Industry: ").strip()
|
||||
location = input("Location: ").strip()
|
||||
count_input = input(
|
||||
f"Number of candidates to generate [{ctx.markovCandidateCount}]: "
|
||||
).strip()
|
||||
count = int(count_input) if count_input else ctx.markovCandidateCount
|
||||
target_info = {
|
||||
"company": company,
|
||||
"industry": industry,
|
||||
"location": location,
|
||||
}
|
||||
ctx.hcatMarkov(
|
||||
ctx.hcatHashType, ctx.hcatHashFile, "target", target_info, count
|
||||
)
|
||||
else:
|
||||
print("Invalid selection.")
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import lzma
|
||||
import shutil
|
||||
import logging
|
||||
import binascii
|
||||
@@ -19,6 +20,8 @@ import readline
|
||||
import subprocess
|
||||
import shlex
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from types import SimpleNamespace
|
||||
|
||||
#!/usr/bin/env python3
|
||||
@@ -451,9 +454,47 @@ except KeyError as e:
|
||||
default_config.get("hcatDebugLogPath", "./hashcat_debug")
|
||||
)
|
||||
|
||||
try:
|
||||
ollamaUrl = config_parser["ollamaUrl"]
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
ollamaUrl = default_config.get("ollamaUrl", "http://localhost:11434")
|
||||
try:
|
||||
ollamaModel = config_parser["ollamaModel"]
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
ollamaModel = default_config.get("ollamaModel", "llama3.2")
|
||||
try:
|
||||
markovCandidateCount = int(config_parser["markovCandidateCount"])
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
markovCandidateCount = int(default_config.get("markovCandidateCount", 5000))
|
||||
try:
|
||||
markovWordlist = config_parser["markovWordlist"]
|
||||
except KeyError as e:
|
||||
print(
|
||||
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
markovWordlist = default_config.get("markovWordlist", "rockyou.txt")
|
||||
|
||||
hcatExpanderBin = "expander.bin"
|
||||
hcatCombinatorBin = "combinator.bin"
|
||||
hcatPrinceBin = "pp64.bin"
|
||||
hcatHcstat2genBin = "hcstat2gen.bin"
|
||||
|
||||
|
||||
def _resolve_wordlist_path(wordlist, base_dir):
|
||||
@@ -588,6 +629,7 @@ hcatGoodMeasureBaseList = _normalize_wordlist_setting(
|
||||
hcatGoodMeasureBaseList, wordlists_dir
|
||||
)
|
||||
hcatPrinceBaseList = _normalize_wordlist_setting(hcatPrinceBaseList, wordlists_dir)
|
||||
markovWordlist = _normalize_wordlist_setting(markovWordlist, wordlists_dir)
|
||||
|
||||
if not SKIP_INIT:
|
||||
# Verify hashcat binary is available
|
||||
@@ -653,6 +695,20 @@ if not SKIP_INIT:
|
||||
except SystemExit:
|
||||
print("PRINCE attacks will not be available.")
|
||||
|
||||
# Verify hcstat2gen binary (optional, for Markov attacks)
|
||||
# Note: hcstat2gen is part of hashcat-utils, already in hate_crack repo
|
||||
hcstat2gen_path = (
|
||||
hate_path + "/hashcat-utils/bin/" + hcatHcstat2genBin
|
||||
)
|
||||
try:
|
||||
ensure_binary(
|
||||
hcstat2gen_path,
|
||||
build_dir=os.path.join(hate_path, "hashcat-utils"),
|
||||
name="hcstat2gen",
|
||||
)
|
||||
except SystemExit:
|
||||
print("LLM Markov attacks will not be available.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Module initialization error: {e}")
|
||||
if not shutil.which("hashcat") and not os.path.exists("/usr/bin/hashcat"):
|
||||
@@ -1446,6 +1502,200 @@ def hcatBandrel(hcatHashType, hcatHashFile):
|
||||
hcatProcess.kill()
|
||||
|
||||
|
||||
# LLM Markov Attack
|
||||
def hcatMarkov(hcatHashType, hcatHashFile, mode, context_data, candidate_count):
|
||||
global hcatProcess
|
||||
candidates_path = f"{hcatHashFile}.markov_candidates"
|
||||
hcstat2_raw_path = f"{hcatHashFile}.hcstat2_raw"
|
||||
hcstat2_path = f"{hcatHashFile}.hcstat2"
|
||||
hcstat2gen_path = os.path.join(
|
||||
hate_path, "hashcat-utils", "bin", hcatHcstat2genBin
|
||||
)
|
||||
|
||||
if not os.path.isfile(hcstat2gen_path):
|
||||
print(
|
||||
f"Error: hcstat2gen not found at {hcstat2gen_path}. "
|
||||
"LLM Markov attacks are not available."
|
||||
)
|
||||
return
|
||||
|
||||
# Step A: Build LLM prompt based on mode
|
||||
if mode == "wordlist":
|
||||
wordlist_path = context_data
|
||||
if not os.path.isfile(wordlist_path):
|
||||
print(f"Error: Wordlist not found: {wordlist_path}")
|
||||
return
|
||||
sample_lines = []
|
||||
try:
|
||||
with open(wordlist_path, "r", errors="ignore") as f:
|
||||
for i, line in enumerate(f):
|
||||
if i >= 500:
|
||||
break
|
||||
stripped = line.strip()
|
||||
if stripped:
|
||||
sample_lines.append(stripped)
|
||||
except Exception as e:
|
||||
print(f"Error reading wordlist: {e}")
|
||||
return
|
||||
wordlist_sample = "\n".join(sample_lines)
|
||||
prompt = (
|
||||
"You are a password generation expert. Below is a sample of real passwords. "
|
||||
"Study the patterns, character choices, and structures. Generate exactly "
|
||||
f"{candidate_count} new unique passwords that follow similar patterns but "
|
||||
"are NOT copies of the input. Output ONLY passwords, one per line, no "
|
||||
"numbering or explanation.\n\n"
|
||||
f"Sample passwords:\n{wordlist_sample}"
|
||||
)
|
||||
elif mode == "target":
|
||||
company = context_data.get("company", "")
|
||||
industry = context_data.get("industry", "")
|
||||
location = context_data.get("location", "")
|
||||
prompt = (
|
||||
f"Generate exactly {candidate_count} realistic passwords that employees at "
|
||||
f"{company} in the {industry} industry located in {location} might use. "
|
||||
"Include variations with:\n"
|
||||
"- Company name variations and abbreviations\n"
|
||||
"- Common password patterns (Season+Year, Name+Numbers)\n"
|
||||
"- Keyboard walks and common substitutions (@ for a, 3 for e, etc.)\n"
|
||||
"- Location-based words and local references\n"
|
||||
"- Industry-specific terminology\n"
|
||||
"Output ONLY the passwords, one per line, no numbering or explanation."
|
||||
)
|
||||
else:
|
||||
print(f"Error: Unknown Markov generation mode: {mode}")
|
||||
return
|
||||
|
||||
# Step B: Call Ollama API to generate candidates
|
||||
print(f"Generating {candidate_count} password candidates via Ollama ({ollamaModel})...")
|
||||
api_url = f"{ollamaUrl}/api/generate"
|
||||
payload = json.dumps({
|
||||
"model": ollamaModel,
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
}).encode("utf-8")
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
api_url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=600) as resp:
|
||||
result = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Error: Could not connect to Ollama at {ollamaUrl}: {e}")
|
||||
print("Ensure Ollama is running (ollama serve) and try again.")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Error calling Ollama API: {e}")
|
||||
return
|
||||
|
||||
response_text = result.get("response", "")
|
||||
raw_lines = response_text.strip().split("\n")
|
||||
# Filter out blank lines and lines that look like numbering/explanation
|
||||
candidates = []
|
||||
for line in raw_lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
# Strip leading numbering like "1. " or "1) " or "- "
|
||||
cleaned = re.sub(r"^\d+[.)]\s*", "", stripped)
|
||||
cleaned = re.sub(r"^[-*]\s*", "", cleaned)
|
||||
cleaned = cleaned.strip()
|
||||
if cleaned and len(cleaned) <= 128:
|
||||
candidates.append(cleaned)
|
||||
|
||||
if not candidates:
|
||||
print("Error: Ollama returned no usable password candidates.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(candidates_path, "w") as f:
|
||||
for candidate in candidates:
|
||||
f.write(candidate + "\n")
|
||||
except Exception as e:
|
||||
print(f"Error writing candidates file: {e}")
|
||||
return
|
||||
|
||||
print(f"Generated {len(candidates)} password candidates -> {candidates_path}")
|
||||
|
||||
# Step C: Run hcstat2gen to build Markov stats
|
||||
print("Building Markov statistics with hcstat2gen...")
|
||||
try:
|
||||
with open(candidates_path, "rb") as candidates_file:
|
||||
hcatProcess = subprocess.Popen(
|
||||
[hcstat2gen_path, hcstat2_raw_path],
|
||||
stdin=candidates_file,
|
||||
)
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Killing PID {0}...".format(str(hcatProcess.pid)))
|
||||
hcatProcess.kill()
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Error running hcstat2gen: {e}")
|
||||
return
|
||||
|
||||
if not os.path.isfile(hcstat2_raw_path):
|
||||
print("Error: hcstat2gen did not produce output file.")
|
||||
return
|
||||
|
||||
# Step D: LZMA compress the raw hcstat2 file
|
||||
print("Compressing hcstat2 file with LZMA...")
|
||||
try:
|
||||
with open(hcstat2_raw_path, "rb") as raw_file:
|
||||
raw_data = raw_file.read()
|
||||
compressed = lzma.compress(
|
||||
raw_data,
|
||||
format=lzma.FORMAT_RAW,
|
||||
filters=[{"id": lzma.FILTER_LZMA1}],
|
||||
)
|
||||
with open(hcstat2_path, "wb") as out_file:
|
||||
out_file.write(compressed)
|
||||
except Exception as e:
|
||||
print(f"Error compressing hcstat2 file: {e}")
|
||||
return
|
||||
|
||||
print(f"Markov statistics file created -> {hcstat2_path}")
|
||||
|
||||
# Step E: Run hashcat mask attack with custom Markov stats
|
||||
print("Running Markov-enhanced mask attack...")
|
||||
cmd = [
|
||||
hcatBin,
|
||||
"-m",
|
||||
hcatHashType,
|
||||
hcatHashFile,
|
||||
"--session",
|
||||
generate_session_id(),
|
||||
"-o",
|
||||
f"{hcatHashFile}.out",
|
||||
"-a",
|
||||
"3",
|
||||
f"--markov-hcstat2={hcstat2_path}",
|
||||
"--increment",
|
||||
"--increment-min=1",
|
||||
"--increment-max=14",
|
||||
"?a?a?a?a?a?a?a?a?a?a?a?a?a?a",
|
||||
]
|
||||
cmd.extend(shlex.split(hcatTuning))
|
||||
_append_potfile_arg(cmd)
|
||||
hcatProcess = subprocess.Popen(cmd)
|
||||
try:
|
||||
hcatProcess.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("Killing PID {0}...".format(str(hcatProcess.pid)))
|
||||
hcatProcess.kill()
|
||||
|
||||
# Step F: Cleanup temp files
|
||||
for temp_file in [candidates_path, hcstat2_raw_path]:
|
||||
try:
|
||||
if os.path.isfile(temp_file):
|
||||
os.remove(temp_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Middle fast Combinator Attack
|
||||
def hcatMiddleCombinator(hcatHashType, hcatHashFile):
|
||||
global hcatProcess
|
||||
@@ -2756,6 +3006,10 @@ def loopback_attack():
|
||||
return _attacks.loopback_attack(_attack_ctx())
|
||||
|
||||
|
||||
def markov_attack():
|
||||
return _attacks.markov_attack(_attack_ctx())
|
||||
|
||||
|
||||
# convert hex words for recycling
|
||||
def convert_hex(working_file):
|
||||
processed_words = []
|
||||
@@ -2983,6 +3237,7 @@ def get_main_menu_options():
|
||||
"12": thorough_combinator,
|
||||
"13": bandrel_method,
|
||||
"14": loopback_attack,
|
||||
"15": markov_attack,
|
||||
"90": download_hashmob_rules,
|
||||
"91": analyze_rules,
|
||||
"92": download_hashmob_wordlists,
|
||||
@@ -3563,6 +3818,7 @@ def main():
|
||||
print("\t(12) Thorough Combinator Attack")
|
||||
print("\t(13) Bandrel Methodology")
|
||||
print("\t(14) Loopback Attack")
|
||||
print("\t(15) LLM Markov Attack")
|
||||
print("\n\t(90) Download rules from Hashmob.net")
|
||||
print("\n\t(91) Analyze Hashcat Rules")
|
||||
print("\t(92) Download wordlists from Hashmob.net")
|
||||
|
||||
@@ -25,6 +25,7 @@ MENU_OPTION_TEST_CASES = [
|
||||
("12", CLI_MODULE._attacks, "thorough_combinator", "thorough"),
|
||||
("13", CLI_MODULE._attacks, "bandrel_method", "bandrel"),
|
||||
("14", CLI_MODULE._attacks, "loopback_attack", "loopback"),
|
||||
("15", CLI_MODULE._attacks, "markov_attack", "markov"),
|
||||
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
|
||||
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
|
||||
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
|
||||
|
||||
Reference in New Issue
Block a user