diff --git a/.gitignore b/.gitignore index 9869655..9efc18a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ hate_crack/princeprocessor/ *.ollama_candidates *.filtered research/ +--help +4_char_all +all_hashes.enabled +some_histories diff --git a/README.md b/README.md index 5321ec0..048a541 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/hashcat-utils b/hashcat-utils index 64074c5..8bbf2ba 160000 --- a/hashcat-utils +++ b/hashcat-utils @@ -1 +1 @@ -Subproject commit 64074c5d5428c9591cf9a4ab9a2b20a26510efbe +Subproject commit 8bbf2baf7b341c8ec23ca91e44e0ac7d7fcc0355 diff --git a/hate_crack.py b/hate_crack.py index 70aa0a6..8ac3762 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -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, diff --git a/hate_crack/attacks.py b/hate_crack/attacks.py index 41df1c2..d30f1d0 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -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") diff --git a/hate_crack/main.py b/hate_crack/main.py index a0d2c09..739b0f5 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -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, diff --git a/tests/test_random_rules_attack.py b/tests/test_random_rules_attack.py new file mode 100644 index 0000000..d9d7fff --- /dev/null +++ b/tests/test_random_rules_attack.py @@ -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)) diff --git a/tests/test_random_rules_wrapper.py b/tests/test_random_rules_wrapper.py new file mode 100644 index 0000000..be6894f --- /dev/null +++ b/tests/test_random_rules_wrapper.py @@ -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 diff --git a/tests/test_ui_menu_options.py b/tests/test_ui_menu_options.py index 012ff3b..f99f69c 100644 --- a/tests/test_ui_menu_options.py +++ b/tests/test_ui_menu_options.py @@ -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"),