Merge pull request #96 from trustedsec/feat/random-rules-attack

feat: add random rules attack (#87)
This commit is contained in:
Justin Bollinger
2026-03-19 15:42:47 -04:00
committed by GitHub
9 changed files with 357 additions and 2 deletions

4
.gitignore vendored
View File

@@ -15,3 +15,7 @@ hate_crack/princeprocessor/
*.ollama_candidates
*.filtered
research/
--help
4_char_all
all_hashes.enabled
some_histories

View File

@@ -618,7 +618,9 @@ All tests use mocked API calls, so they can run without connectivity to a Hashvi
(16) OMEN Attack
(17) Ad-hoc Mask Attack
(18) Markov Brute Force Attack
(19) Permutation Attack
(19) N-gram Attack
(20) Permutation Attack
(21) Random Rules Attack
(90) Download rules from Hashmob.net
(91) Analyze Hashcat Rules
@@ -800,6 +802,14 @@ Generates all character permutations of each word in a targeted wordlist and pip
* WARNING: Scales as N! per word - an 8-character word produces 40,320 permutations. Only practical for words up to ~8 characters.
* Uses `permute.bin < wordlist | hashcat` pipeline pattern
#### Random Rules Attack
Generates a set of random hashcat mutation rules using `generate-rules.bin`, writes them to a temporary file, then runs hashcat against a chosen wordlist with those rules.
* Prompts for rule count (default 65536)
* Prompts for wordlist path with tab-completion and numbered selection
* Temporary rules file is cleaned up after the run regardless of outcome
* Useful when known rule sets are exhausted - explores random rule-space for additional cracks
#### Download Rules from Hashmob.net
Downloads the latest rule files from Hashmob.net's rule repository. These rules are curated and optimized for password cracking and can be used with the Quick Crack and Loopback Attack modes.
@@ -835,6 +845,7 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi
### Version History
Version 2.0+
- Added Random Rules Attack (option 20) using `generate-rules.bin` to generate random mutation rules (#87)
- Added Ad-hoc Mask Attack (option 17) for user-typed hashcat masks with optional custom character sets
- Added Markov Brute Force Attack (option 18) using `hcstat2` statistical tables for password generation
- Consolidated Combinator Attacks (formerly options 10/11/12) into interactive submenu under option 6

View File

@@ -90,6 +90,7 @@ def get_main_menu_options():
"18": _attacks.markov_brute_force,
"19": _attacks.ngram_attack,
"20": _attacks.permute_crack,
"21": _attacks.generate_rules_crack,
"90": download_hashmob_rules,
"91": weakpass_wordlist_menu,
"92": download_hashmob_wordlists,

View File

@@ -637,6 +637,84 @@ def markov_brute_force(ctx: Any) -> None:
ctx.hcatMarkovBruteForce(ctx.hcatHashType, ctx.hcatHashFile, hcatMinLen, hcatMaxLen)
def generate_rules_crack(ctx: Any) -> None:
print("\n" + "=" * 60)
print("RANDOM RULES ATTACK")
print("=" * 60)
print("Generates random hashcat mutation rules and applies them to a wordlist.")
print("Use when known rulesets are exhausted - a chaos mode for rule-space exploration.")
print("=" * 60)
raw_count = input("\nNumber of random rules to generate (65536): ").strip()
try:
rule_count = int(raw_count) if raw_count else 65536
if rule_count < 1:
print("[!] Rule count must be at least 1.")
return
except ValueError:
print("[!] Invalid rule count.")
return
wordlist_files = ctx.list_wordlist_files(ctx.hcatWordlists)
wordlist_entries = [
f"{i}. {file}" for i, file in enumerate(wordlist_files, start=1)
]
max_entry_len = max((len(e) for e in wordlist_entries), default=24)
print_multicolumn_list(
"Wordlists",
wordlist_entries,
min_col_width=max_entry_len,
max_col_width=max_entry_len,
)
def path_completer(text, state):
base = ctx.hcatWordlists
if not text:
pattern = os.path.join(base, "*")
matches = glob.glob(pattern)
else:
text = os.path.expanduser(text)
if text.startswith(("/", "./", "../", "~")):
matches = glob.glob(text + "*")
else:
pattern = os.path.join(base, text + "*")
matches = glob.glob(pattern)
matches = [m + "/" if os.path.isdir(m) else m for m in matches]
try:
return matches[state]
except IndexError:
return None
_configure_readline(path_completer)
wordlist_choice = None
while wordlist_choice is None:
try:
raw_choice = input(
"\nEnter path of wordlist (tab to autocomplete).\n"
f"Press Enter for default wordlist directory [{ctx.hcatWordlists}]: "
)
raw_choice = raw_choice.strip()
if raw_choice == "":
wordlist_choice = ctx.hcatWordlists
elif raw_choice.isdigit() and 1 <= int(raw_choice) <= len(wordlist_files):
chosen = os.path.join(
ctx.hcatWordlists, wordlist_files[int(raw_choice) - 1]
)
if os.path.exists(chosen):
wordlist_choice = chosen
print(wordlist_choice)
elif os.path.exists(raw_choice):
wordlist_choice = raw_choice
else:
print("[!] Wordlist not found. Please enter a valid path.")
return
except ValueError:
print("Please enter a valid number.")
ctx.hcatGenerateRules(ctx.hcatHashType, ctx.hcatHashFile, rule_count, wordlist_choice)
def ngram_attack(ctx: Any) -> None:
print("\n" + "=" * 60)
print("NGRAM ATTACK")

View File

@@ -687,6 +687,7 @@ hcatNgramXCount = 0
hcatHybridCount = 0
hcatExtraCount = 0
hcatRecycleCount = 0
hcatGenerateRulesCount = 0
hcatPermuteCount = 0
hcatProcess: subprocess.Popen[Any] | None = None
debug_mode = False
@@ -2734,6 +2735,51 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords):
hcatProcess.kill()
def hcatGenerateRules(hcatHashType, hcatHashFile, rule_count, wordlist):
global hcatProcess, hcatGenerateRulesCount
generate_rules_path = os.path.join(
hate_path, "hashcat-utils", "bin", "generate-rules.bin"
)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".rule", prefix="hate_crack_random_", delete=False
) as rules_file:
rules_path = rules_file.name
try:
result = subprocess.run(
[generate_rules_path, str(rule_count)],
capture_output=True,
text=True,
check=True,
)
with open(rules_path, "w") as f:
f.write(result.stdout)
cmd = [
hcatBin,
"-m",
hcatHashType,
hcatHashFile,
"--session",
generate_session_id(),
"-o",
f"{hcatHashFile}.out",
"-r",
rules_path,
wordlist,
]
cmd.extend(shlex.split(hcatTuning))
_append_potfile_arg(cmd)
hcatProcess = subprocess.Popen(cmd)
try:
hcatProcess.wait()
except KeyboardInterrupt:
print(f"Killing PID {hcatProcess.pid}...")
hcatProcess.kill()
finally:
if os.path.exists(rules_path):
os.unlink(rules_path)
hcatGenerateRulesCount = lineCount(hcatHashFile + ".out") - hcatHashCracked
def check_potfile():
print("Checking POT file for already cracked hashes...")
_run_hashcat_show(hcatHashType, hcatHashFile, f"{hcatHashFile}.out")
@@ -3552,6 +3598,10 @@ def omen_attack():
return _attacks.omen_attack(_attack_ctx())
def generate_rules_crack():
return _attacks.generate_rules_crack(_attack_ctx())
def permute_crack():
return _attacks.permute_crack(_attack_ctx())
@@ -3786,6 +3836,7 @@ def get_main_menu_items():
("18", "Markov Brute Force Attack"),
("19", "N-gram Attack"),
("20", "Permutation Attack"),
("21", "Random Rules Attack"),
("90", "Download rules from Hashmob.net"),
("91", "Analyze Hashcat Rules"),
("92", "Download wordlists from Hashmob.net"),
@@ -3825,6 +3876,7 @@ def get_main_menu_options():
"18": markov_brute_force,
"19": ngram_attack,
"20": permute_crack,
"21": generate_rules_crack,
"90": lambda: download_hashmob_rules(rules_dir=rulesDirectory),
"91": analyze_rules,
"92": download_hashmob_wordlists,

View File

@@ -0,0 +1,45 @@
import importlib.util
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_cli_module():
os.environ["HATE_CRACK_SKIP_INIT"] = "1"
for key in list(sys.modules.keys()):
if "hate_crack" in key:
del sys.modules[key]
spec = importlib.util.spec_from_file_location(
"hate_crack_cli", PROJECT_ROOT / "hate_crack.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@pytest.fixture
def cli():
return load_cli_module()
def test_generate_rules_crack_in_main_menu(cli):
options = cli.get_main_menu_options()
assert "20" in options
def test_generate_rules_crack_handler_calls_main(cli, tmp_path):
ctx = MagicMock()
ctx.hcatHashType = "1000"
ctx.hcatHashFile = "/tmp/h.txt"
ctx.hcatWordlists = str(tmp_path)
ctx.list_wordlist_files.return_value = []
wl = tmp_path / "words.txt"
wl.write_text("password\n")
with patch("builtins.input", side_effect=["100", str(wl)]):
cli._attacks.generate_rules_crack(ctx)
ctx.hcatGenerateRules.assert_called_once_with("1000", "/tmp/h.txt", 100, str(wl))

View File

@@ -0,0 +1,163 @@
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def main_module(hc_module):
return hc_module._main
def _make_mock_proc(wait_side_effect=None):
proc = MagicMock()
if wait_side_effect is not None:
proc.wait.side_effect = wait_side_effect
else:
proc.wait.return_value = None
proc.pid = 12345
return proc
class TestHcatGenerateRules:
def test_calls_generate_rules_bin(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\nc\n"
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
run_calls = mock_run.call_args_list
assert any("generate-rules.bin" in str(c) for c in run_calls)
def test_calls_hashcat_with_rule_flag(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc) as mock_popen:
main_module.hcatGenerateRules("1000", hash_file, 100, str(wl))
popen_calls = mock_popen.call_args_list
assert any("-r" in str(c) for c in popen_calls)
def test_passes_rule_count_to_generate_rules_bin(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\n"
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result) as mock_run, \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 999, str(wl))
run_calls = mock_run.call_args_list
generate_call = next(
(c for c in run_calls if "generate-rules.bin" in str(c)), None
)
assert generate_call is not None
cmd_args = generate_call[0][0]
assert "999" in cmd_args
def test_cleans_up_temp_file(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
captured_paths = []
import os as _os
original_unlink = _os.unlink
def capturing_unlink(path):
captured_paths.append(path)
original_unlink(path)
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc), \
patch("hate_crack.main.os.unlink", side_effect=capturing_unlink):
main_module.hcatGenerateRules("1000", hash_file, 50, str(wl))
assert any("hate_crack_random_" in p for p in captured_paths), \
f"Expected temp file cleanup, got: {captured_paths}"
def test_keyboard_interrupt_kills_process(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc(wait_side_effect=KeyboardInterrupt())
mock_result = MagicMock()
mock_result.stdout = "l\n"
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=0), \
patch.object(main_module, "hcatHashCracked", 0), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
mock_proc.kill.assert_called_once()
def test_sets_hcatGenerateRulesCount(self, main_module, tmp_path):
wl = tmp_path / "words.txt"
wl.write_text("test\n")
hash_file = str(tmp_path / "hashes.txt")
mock_proc = _make_mock_proc()
mock_result = MagicMock()
mock_result.stdout = "l\nu\n"
# patch.object won't patch reads of module-level globals; set directly
original_cracked = main_module.hcatHashCracked
main_module.hcatHashCracked = 2
try:
with patch.object(main_module, "hcatBin", "hashcat"), \
patch.object(main_module, "hcatTuning", ""), \
patch.object(main_module, "hcatPotfilePath", ""), \
patch.object(main_module, "generate_session_id", return_value="test_session"), \
patch.object(main_module, "lineCount", return_value=5), \
patch("hate_crack.main.subprocess.run", return_value=mock_result), \
patch("hate_crack.main.subprocess.Popen", return_value=mock_proc):
main_module.hcatGenerateRules("1000", hash_file, 10, str(wl))
finally:
main_module.hcatHashCracked = original_cracked
assert main_module.hcatGenerateRulesCount == 3 # 5 - 2

View File

@@ -28,6 +28,7 @@ MENU_OPTION_TEST_CASES = [
("18", CLI_MODULE._attacks, "markov_brute_force", "markov-brute"),
("19", CLI_MODULE._attacks, "ngram_attack", "ngram"),
("20", CLI_MODULE._attacks, "permute_crack", "permute"),
("21", CLI_MODULE._attacks, "generate_rules_crack", "random-rules"),
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),