diff --git a/config.json.example b/config.json.example index 392cdea..df7896e 100644 --- a/config.json.example +++ b/config.json.example @@ -26,5 +26,6 @@ "ollamaNumCtx": 2048, "omenTrainingList": "rockyou.txt", "omenMaxCandidates": 1000000, - "check_for_updates": true + "check_for_updates": true, + "optimizedKernelAttacks": ["hcatDictionary", "hcatQuickDictionary", "hcatBandrel", "hcatGoodMeasure", "hcatRecycle", "hcatBruteForce", "hcatTopMask", "hcatPathwellBruteForce"] } diff --git a/hate_crack/main.py b/hate_crack/main.py index 1ff998d..d5f91ba 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -79,6 +79,46 @@ except ImportError: display_rule_opcodes_summary = None +EXCLUDED_WORDLIST_EXTENSIONS = frozenset({".7z", ".torrent", ".out"}) + + +def list_wordlist_files(directory): + """Return sorted list of filenames in *directory*, excluding non-wordlist artifacts.""" + return sorted( + f + for f in os.listdir(directory) + if f != ".DS_Store" + and not any(f.endswith(ext) for ext in EXCLUDED_WORDLIST_EXTENSIONS) + ) + + +DEFAULT_OPTIMIZED_ATTACKS = frozenset( + { + "hcatDictionary", + "hcatQuickDictionary", + "hcatBandrel", + "hcatGoodMeasure", + "hcatRecycle", + "hcatBruteForce", + "hcatTopMask", + "hcatPathwellBruteForce", + } +) + +_optimized_kernel_attacks = DEFAULT_OPTIMIZED_ATTACKS + + +def _should_use_optimized_kernel(attack_name): + """Return True if *attack_name* should use hashcat's -O (optimized kernels).""" + return attack_name in _optimized_kernel_attacks + + +def _insert_optimized_flag(cmd): + """Insert -O into *cmd* if not already present (from hcatTuning or elsewhere).""" + if "-O" not in cmd and "--optimized-kernel-enable" not in cmd: + cmd.append("-O") + + _DOUBLE_INTERRUPT_WINDOW = 2.0 _last_interrupt_time: float = 0.0 @@ -373,6 +413,13 @@ ollamaNumCtx = int(config_parser.get("ollamaNumCtx", 2048)) omenTrainingList = config_parser.get("omenTrainingList", "rockyou.txt") omenMaxCandidates = int(config_parser.get("omenMaxCandidates", 1000000)) + +try: + _cfg_optimized = config_parser["optimizedKernelAttacks"] + if isinstance(_cfg_optimized, list): + _optimized_kernel_attacks = frozenset(_cfg_optimized) +except KeyError: + pass check_for_updates_enabled = config_parser.get("check_for_updates", True) hcatExpanderBin = "expander.bin" @@ -1009,6 +1056,8 @@ def hcatBruteForce(hcatHashType, hcatHashFile, hcatMinLen, hcatMaxLen): "3", "?a?a?a?a?a?a?a?a?a?a?a?a?a?a", ] + if _should_use_optimized_kernel("hcatBruteForce"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) hcatProcess = subprocess.Popen(cmd) @@ -1026,7 +1075,9 @@ def hcatDictionary(hcatHashType, hcatHashFile): global hcatDictionaryCount global hcatProcess rule_best66 = get_rule_path("best66.rule") - optimized_lists = sorted(glob.glob(os.path.join(hcatWordlists, "*"))) + optimized_lists = [ + os.path.join(hcatWordlists, f) for f in list_wordlist_files(hcatWordlists) + ] if not optimized_lists: optimized_lists = [os.path.join(hcatWordlists, "*")] cmd = [ @@ -1041,6 +1092,8 @@ def hcatDictionary(hcatHashType, hcatHashFile): ] cmd.extend(optimized_lists) cmd.extend(["-r", rule_best66]) + if _should_use_optimized_kernel("hcatDictionary"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) @@ -1132,6 +1185,8 @@ def hcatQuickDictionary( cmd.append("--loopback") if hcatChains: cmd.extend(shlex.split(hcatChains)) + if _should_use_optimized_kernel("hcatQuickDictionary"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg( cmd, use_potfile_path=use_potfile_path, potfile_path=potfile_path @@ -1201,6 +1256,8 @@ def hcatTopMask(hcatHashType, hcatHashFile, hcatTargetTime): "3", f"{hcatHashFile}.hcmask", ] + if _should_use_optimized_kernel("hcatTopMask"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) hcatProcess = subprocess.Popen(cmd) @@ -1417,8 +1474,9 @@ def hcatYoloCombination(hcatHashType, hcatHashFile): global hcatProcess try: while 1: - hcatLeft = random.choice(os.listdir(hcatWordlists)) - hcatRight = random.choice(os.listdir(hcatWordlists)) + _yolo_wordlists = list_wordlist_files(hcatWordlists) + hcatLeft = random.choice(_yolo_wordlists) + hcatRight = random.choice(_yolo_wordlists) left_path = os.path.join(hcatWordlists, hcatLeft) right_path = os.path.join(hcatWordlists, hcatRight) cmd = [ @@ -1484,6 +1542,8 @@ def hcatBandrel(hcatHashType, hcatHashFile): hcatHashFile, mask2.strip(), ] + if _should_use_optimized_kernel("hcatBandrel"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) hcatProcess = subprocess.Popen(cmd) @@ -1527,6 +1587,8 @@ def hcatBandrel(hcatHashType, hcatHashFile): hcatHashFile, mask2.strip(), ] + if _should_use_optimized_kernel("hcatBandrel"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) hcatProcess = subprocess.Popen(cmd) @@ -1968,6 +2030,8 @@ def hcatPathwellBruteForce(hcatHashType, hcatHashFile): "3", os.path.join(hate_path, "masks", "pathwell.hcmask"), ] + if _should_use_optimized_kernel("hcatPathwellBruteForce"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) hcatProcess = subprocess.Popen(cmd) @@ -2035,17 +2099,45 @@ def _omen_model_dir(): return model_dir +_OMEN_REQUIRED_FILES = ["createConfig", "CP.level", "IP.level", "EP.level", "LN.level"] + + +def _omen_model_is_valid(model_dir): + """Return True if all required OMEN model files exist and are non-empty.""" + if not os.path.isdir(model_dir): + return False + for name in _OMEN_REQUIRED_FILES: + path = os.path.join(model_dir, name) + if not os.path.isfile(path) or os.path.getsize(path) == 0: + return False + return True + + +def _omen_model_info(model_dir): + """Read model_info.json from model_dir. Returns dict or None.""" + info_path = os.path.join(model_dir, "model_info.json") + if not os.path.isfile(info_path): + return None + try: + with open(info_path) as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + return None + + # OMEN Attack - Train model def hcatOmenTrain(training_file): + import datetime + omen_dir = _omen_dir create_bin = os.path.join(omen_dir, hcatOmenCreateBin) if not os.path.isfile(create_bin): print(f"Error: OMEN createNG binary not found: {create_bin}") - return + return False training_file = os.path.abspath(training_file) if not os.path.isfile(training_file): print(f"Error: Training file not found: {training_file}") - return + return False model_dir = _omen_model_dir() print(f"Training OMEN model with: {training_file}") print(f"Model output directory: {model_dir}") @@ -2071,11 +2163,21 @@ def hcatOmenTrain(training_file): except KeyboardInterrupt: print("Killing PID {0}...".format(str(proc.pid))) proc.kill() - return - if proc.returncode == 0: - print("OMEN model training complete.") - else: + return False + if proc.returncode != 0: print(f"OMEN training failed with exit code {proc.returncode}") + return False + print("OMEN model training complete.") + info = { + "training_file": training_file, + "trained_at": datetime.datetime.now().isoformat(), + } + try: + with open(os.path.join(model_dir, "model_info.json"), "w") as f: + json.dump(info, f) + except OSError: + pass + return True # OMEN Attack - Generate candidates and pipe to hashcat @@ -2107,7 +2209,9 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates): _append_potfile_arg(hashcat_cmd) print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}") _debug_cmd(hashcat_cmd) - enum_proc = subprocess.Popen(enum_cmd, cwd=model_dir, stdout=subprocess.PIPE) + enum_proc = subprocess.Popen( + enum_cmd, cwd=model_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout) enum_proc.stdout.close() try: @@ -2117,6 +2221,16 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates): print("Killing PID {0}...".format(str(hcatProcess.pid))) hcatProcess.kill() enum_proc.kill() + return + if enum_proc.returncode != 0: + stderr_output = ( + enum_proc.stderr.read().decode("utf-8", errors="replace").strip() + ) + print(f"[!] enumNG failed with exit code {enum_proc.returncode}") + if stderr_output: + print(f"[!] enumNG error: {stderr_output}") + if enum_proc.stderr: + enum_proc.stderr.close() # Extra - Good Measure @@ -2140,6 +2254,8 @@ def hcatGoodMeasure(hcatHashType, hcatHashFile): rule_insidepro, hcatGoodMeasureBaseList, ] + if _should_use_optimized_kernel("hcatGoodMeasure"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) @@ -2264,6 +2380,8 @@ def hcatRecycle(hcatHashType, hcatHashFile, hcatNewPasswords): "-r", rule_path, ] + if _should_use_optimized_kernel("hcatRecycle"): + _insert_optimized_flag(cmd) cmd.extend(shlex.split(hcatTuning)) _append_potfile_arg(cmd) cmd = _add_debug_mode_for_rules(cmd) diff --git a/tests/test_main_utils.py b/tests/test_main_utils.py index 24ca5f0..db8e5e0 100644 --- a/tests/test_main_utils.py +++ b/tests/test_main_utils.py @@ -478,3 +478,91 @@ class TestCleanupWordlistArtifacts: ): # Should not raise even when directories don't exist main_module.cleanup_wordlist_artifacts() + + +class TestListWordlistFiles: + @pytest.mark.parametrize( + "filename, included", + [ + ("rockyou.txt", True), + ("wordlist.lst", True), + ("custom.dict", True), + ("compressed.gz", True), + ("archive.7z", False), + ("file.torrent", False), + ("hashes.out", False), + (".DS_Store", False), + ], + ) + def test_filters_excluded_extensions(self, tmp_path, main_module, filename, included): + (tmp_path / filename).touch() + result = main_module.list_wordlist_files(str(tmp_path)) + if included: + assert filename in result + else: + assert filename not in result + + def test_returns_sorted(self, tmp_path, main_module): + for name in ["zebra.txt", "alpha.txt", "middle.txt"]: + (tmp_path / name).touch() + result = main_module.list_wordlist_files(str(tmp_path)) + assert result == ["alpha.txt", "middle.txt", "zebra.txt"] + + +class TestOptimizedKernel: + @pytest.mark.parametrize( + "attack_name", + [ + "hcatDictionary", + "hcatQuickDictionary", + "hcatBandrel", + "hcatGoodMeasure", + "hcatRecycle", + "hcatBruteForce", + "hcatTopMask", + "hcatPathwellBruteForce", + ], + ) + def test_optimized_attacks_return_true(self, main_module, attack_name): + assert main_module._should_use_optimized_kernel(attack_name) is True + + @pytest.mark.parametrize( + "attack_name", + [ + "hcatCombination", + "hcatYoloCombination", + "hcatMiddleCombinator", + "hcatThoroughCombinator", + "hcatHybrid", + "hcatPrince", + "hcatOmen", + "hcatFingerprint", + "hcatLMtoNT", + ], + ) + def test_non_optimized_attacks_return_false(self, main_module, attack_name): + assert main_module._should_use_optimized_kernel(attack_name) is False + + def test_insert_optimized_flag_adds_O(self, main_module): + cmd = ["hashcat", "-m", "1000"] + main_module._insert_optimized_flag(cmd) + assert "-O" in cmd + + def test_insert_optimized_flag_no_duplicate(self, main_module): + cmd = ["hashcat", "-m", "1000", "-O"] + main_module._insert_optimized_flag(cmd) + assert cmd.count("-O") == 1 + + def test_insert_optimized_flag_respects_long_form(self, main_module): + cmd = ["hashcat", "-m", "1000", "--optimized-kernel-enable"] + main_module._insert_optimized_flag(cmd) + assert "-O" not in cmd + + def test_config_override(self, main_module): + original = main_module._optimized_kernel_attacks + try: + main_module._optimized_kernel_attacks = frozenset({"hcatCustom"}) + assert main_module._should_use_optimized_kernel("hcatCustom") is True + assert main_module._should_use_optimized_kernel("hcatBruteForce") is False + finally: + main_module._optimized_kernel_attacks = original