diff --git a/.gitmodules b/.gitmodules index 965b1ca..e084200 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "HashcatRosetta"] path = HashcatRosetta url = https://github.com/bandrel/HashcatRosetta.git +[submodule "omen"] + path = omen + url = https://github.com/RUB-SysSec/OMEN.git diff --git a/Makefile b/Makefile index 9278900..87fc76f 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ submodules-pre: @# Ensure required directories exist (whether as submodules or vendored copies). @test -d hashcat-utils || { echo "Error: missing required directory: hashcat-utils"; exit 1; } @test -d princeprocessor || { echo "Error: missing required directory: princeprocessor"; exit 1; } + @test -d omen || { echo "Warning: missing directory: omen (OMEN attacks will not be available)"; } @# Keep per-length expander sources in sync (expander8.c..expander24.c). @# Patch hashcat-utils/src/Makefile so these new expanders are compiled by default. @bases="hashcat-utils hate_crack/hashcat-utils"; for base in $$bases; do src="$$base/src/expander.c"; test -f "$$src" || continue; for i in $$(seq 8 36); do dst="$$base/src/expander$$i.c"; if [ ! -f "$$dst" ]; then cp "$$src" "$$dst"; perl -pi -e "s/#define LEN_MAX 7/#define LEN_MAX $$i/g" "$$dst"; fi; done; mk="$$base/src/Makefile"; test -f "$$mk" || continue; exp_bins=""; exp_exes=""; for i in $$(seq 8 36); do exp_bins="$$exp_bins expander$$i.bin"; exp_exes="$$exp_exes expander$$i.exe"; done; EXP_BINS="$$exp_bins" perl -pi -e 'if(/^native:/ && index($$_, "expander8.bin") < 0){chomp; $$_ .= "$$ENV{EXP_BINS}"; $$_ .= "\n";}' "$$mk"; EXP_EXES="$$exp_exes" perl -pi -e 'if(/^windows:/ && index($$_, "expander8.exe") < 0){chomp; $$_ .= "$$ENV{EXP_EXES}"; $$_ .= "\n";}' "$$mk"; perl -0777 -pi -e 's/\n# Auto-added by hate_crack \\(submodules-pre\\)\n.*\z/\n/s' "$$mk"; printf '%s\n' '' '# Auto-added by hate_crack (submodules-pre)' 'expander%.bin: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_NATIVE} $${CFLAGS_NATIVE} $${LDFLAGS_NATIVE} -o bin/$$@ $$<' >> "$$mk"; printf '%s\n' '' 'expander%.exe: src/expander%.c' >> "$$mk"; printf '\t%s\n' '$${CC_WINDOWS} $${CFLAGS_WINDOWS} -o bin/$$@ $$<' >> "$$mk"; done @@ -34,14 +35,18 @@ vendor-assets: exit 1; \ fi @echo "Syncing assets into package for uv tool install..." - @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor + @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor hate_crack/omen @cp -R hashcat-utils hate_crack/ @cp -R princeprocessor hate_crack/ + @if [ -d omen ]; then \ + cp -R omen hate_crack/; \ + rm -rf hate_crack/omen/.git; \ + fi @rm -rf hate_crack/hashcat-utils/.git hate_crack/princeprocessor/.git clean-vendor: @echo "Cleaning up vendored assets from working tree..." - @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor + @rm -rf hate_crack/hashcat-utils hate_crack/princeprocessor hate_crack/omen install: submodules vendor-assets @echo "Detecting OS and installing dependencies..." diff --git a/config.json.example b/config.json.example index fa5b07e..424e762 100644 --- a/config.json.example +++ b/config.json.example @@ -24,5 +24,7 @@ "hashview_api_key": "", "hashmob_api_key": "", "ollamaModel": "mistral", - "ollamaNumCtx": 2048 + "ollamaNumCtx": 2048, + "omenTrainingList": "rockyou.txt", + "omenMaxCandidates": 1000000 } diff --git a/hate_crack.py b/hate_crack.py index f018b00..a73b440 100755 --- a/hate_crack.py +++ b/hate_crack.py @@ -84,6 +84,7 @@ def get_main_menu_options(): "13": _attacks.bandrel_method, "14": _attacks.loopback_attack, "15": _attacks.ollama_attack, + "16": _attacks.omen_attack, "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 420e23d..29c989a 100644 --- a/hate_crack/attacks.py +++ b/hate_crack/attacks.py @@ -499,3 +499,23 @@ def ollama_attack(ctx: Any) -> None: "location": location, } ctx.hcatOllama(ctx.hcatHashType, ctx.hcatHashFile, "target", target_info) + + +def omen_attack(ctx: Any) -> None: + print("\n\tOMEN Attack (Ordered Markov ENumerator)") + omen_dir = os.path.join(ctx.hate_path, "omen") + model_exists = os.path.isfile(os.path.join(omen_dir, "IP.level")) + if not model_exists: + print("\n\tNo OMEN model found. Training is required before generation.") + training_source = input( + "\n\tTraining source (path to password list, or press Enter for default): " + ).strip() + if not training_source: + training_source = ctx.omenTrainingList + ctx.hcatOmenTrain(training_source) + max_candidates = input( + f"\n\tMax candidates to generate ({ctx.omenMaxCandidates}): " + ).strip() + if not max_candidates: + max_candidates = str(ctx.omenMaxCandidates) + ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates)) diff --git a/hate_crack/main.py b/hate_crack/main.py index 24ef4ad..cf23d04 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -212,19 +212,19 @@ def ensure_binary(binary_path, build_dir=None, name=None): "\nRun 'make install' from the repository directory to install with assets:" ) print(" cd /path/to/hate_crack && make install") - quit(1) + sys.exit(1) # Binary missing - need to build print(f"Error: {name or 'binary'} not found at {binary_path}.") print("\nPlease build the utilities by running:") print(f" cd {build_dir} && make") print("\nEnsure build tools (gcc, make) are installed on your system.") - quit(1) + sys.exit(1) else: print( f"Error: {name or binary_path} not found or not executable at {binary_path}." ) - quit(1) + sys.exit(1) return binary_path @@ -468,10 +468,31 @@ except KeyError as e: ) ollamaNumCtx = int(default_config.get("ollamaNumCtx", 2048)) +try: + omenTrainingList = config_parser["omenTrainingList"] +except KeyError as e: + print( + "{0} is not defined in config.json using defaults from config.json.example".format( + e + ) + ) + omenTrainingList = default_config.get("omenTrainingList", "rockyou.txt") +try: + omenMaxCandidates = int(config_parser["omenMaxCandidates"]) +except KeyError as e: + print( + "{0} is not defined in config.json using defaults from config.json.example".format( + e + ) + ) + omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000)) + hcatExpanderBin = "expander.bin" hcatCombinatorBin = "combinator.bin" hcatPrinceBin = "pp64.bin" hcatHcstat2genBin = "hcstat2gen.bin" +hcatOmenCreateBin = "createNG" +hcatOmenEnumBin = "enumNG" def _resolve_wordlist_path(wordlist, base_dir): @@ -606,6 +627,7 @@ hcatGoodMeasureBaseList = _normalize_wordlist_setting( hcatGoodMeasureBaseList, wordlists_dir ) hcatPrinceBaseList = _normalize_wordlist_setting(hcatPrinceBaseList, wordlists_dir) +omenTrainingList = _normalize_wordlist_setting(omenTrainingList, wordlists_dir) if not SKIP_INIT: # Verify hashcat binary is available # hcatBin should be in PATH or be an absolute path (resolved from hcatPath + hcatBin if configured) @@ -615,14 +637,14 @@ if not SKIP_INIT: print( f"Hashcat binary not found at {hcatBin}. Please check configuration and try again." ) - quit(1) + sys.exit(1) else: # hcatBin should be in PATH if shutil.which(hcatBin) is None: print( f'Hashcat binary "{hcatBin}" not found in PATH. Please check configuration and try again.' ) - quit(1) + sys.exit(1) # Verify hashcat-utils binaries exist and work # Note: hashcat-utils is part of hate_crack repo, not hashcat installation @@ -656,7 +678,7 @@ if not SKIP_INIT: print(f"Error: {name} binary at {binary_path} failed to execute: {e}") print("The binary may be compiled for the wrong architecture.") print("Try recompiling hashcat-utils for your system.") - quit(1) + sys.exit(1) # Verify princeprocessor binary # Note: princeprocessor is part of hate_crack repo, not hashcat installation @@ -682,6 +704,23 @@ if not SKIP_INIT: except SystemExit: print("LLM attacks will not be available.") + # Verify OMEN binaries (optional, for OMEN attack) + omen_create_path = os.path.join(hate_path, "omen", hcatOmenCreateBin) + omen_enum_path = os.path.join(hate_path, "omen", hcatOmenEnumBin) + try: + ensure_binary( + omen_create_path, + build_dir=os.path.join(hate_path, "omen"), + name="OMEN createNG", + ) + ensure_binary( + omen_enum_path, + build_dir=os.path.join(hate_path, "omen"), + name="OMEN enumNG", + ) + except SystemExit: + print("OMEN 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"): @@ -2072,6 +2111,67 @@ def hcatPrince(hcatHashType, hcatHashFile): prince_proc.kill() +# OMEN Attack - Train model +def hcatOmenTrain(training_file): + omen_dir = os.path.join(hate_path, "omen") + 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 + if not os.path.isfile(training_file): + print(f"Error: Training file not found: {training_file}") + return + print(f"Training OMEN model with: {training_file}") + cmd = [create_bin, "--iPwdList", training_file] + print(f"[*] Running: {_format_cmd(cmd)}") + proc = subprocess.Popen(cmd, cwd=omen_dir) + try: + proc.wait() + except KeyboardInterrupt: + print("Killing PID {0}...".format(str(proc.pid))) + proc.kill() + return + if proc.returncode == 0: + print("OMEN model training complete.") + else: + print(f"OMEN training failed with exit code {proc.returncode}") + + +# OMEN Attack - Generate candidates and pipe to hashcat +def hcatOmen(hcatHashType, hcatHashFile, max_candidates): + global hcatProcess + omen_dir = os.path.join(hate_path, "omen") + enum_bin = os.path.join(omen_dir, hcatOmenEnumBin) + if not os.path.isfile(enum_bin): + print(f"Error: OMEN enumNG binary not found: {enum_bin}") + return + enum_cmd = [enum_bin, "-p", "-m", str(max_candidates)] + hashcat_cmd = [ + hcatBin, + "-m", + hcatHashType, + hcatHashFile, + "--session", + generate_session_id(), + "-o", + f"{hcatHashFile}.out", + ] + hashcat_cmd.extend(shlex.split(hcatTuning)) + _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=omen_dir, stdout=subprocess.PIPE) + hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout) + enum_proc.stdout.close() + try: + hcatProcess.wait() + enum_proc.wait() + except KeyboardInterrupt: + print("Killing PID {0}...".format(str(hcatProcess.pid))) + hcatProcess.kill() + enum_proc.kill() + + # Extra - Good Measure def hcatGoodMeasure(hcatHashType, hcatHashFile): global hcatExtraCount @@ -2987,6 +3087,10 @@ def ollama_attack(): return _attacks.ollama_attack(_attack_ctx()) +def omen_attack(): + return _attacks.omen_attack(_attack_ctx()) + + # convert hex words for recycling def convert_hex(working_file): processed_words = [] @@ -3215,6 +3319,7 @@ def get_main_menu_options(): "13": bandrel_method, "14": loopback_attack, "15": ollama_attack, + "16": omen_attack, "90": download_hashmob_rules, "91": analyze_rules, "92": download_hashmob_wordlists, @@ -3867,6 +3972,7 @@ def main(): print("\t(13) Bandrel Methodology") print("\t(14) Loopback Attack") print("\t(15) LLM Attack") + print("\t(16) OMEN 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") diff --git a/omen b/omen new file mode 160000 index 0000000..10aa99e --- /dev/null +++ b/omen @@ -0,0 +1 @@ +Subproject commit 10aa99e30bb88a10052d389feb53f739254eb1d1 diff --git a/tests/test_omen_attack.py b/tests/test_omen_attack.py new file mode 100644 index 0000000..cd8ae12 --- /dev/null +++ b/tests/test_omen_attack.py @@ -0,0 +1,151 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + + +@pytest.fixture +def main_module(hc_module): + """Return the underlying hate_crack.main module for direct patching.""" + return hc_module._main + + +class TestHcatOmenTrain: + def test_builds_correct_command(self, main_module, tmp_path): + training_file = tmp_path / "passwords.txt" + training_file.write_text("password123\nletmein\n") + omen_dir = tmp_path / "omen" + omen_dir.mkdir() + create_bin = omen_dir / "createNG" + create_bin.touch() + create_bin.chmod(0o755) + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ), patch("hate_crack.main.subprocess.Popen") as mock_popen: + mock_proc = MagicMock() + mock_proc.wait.return_value = None + mock_proc.returncode = 0 + mock_popen.return_value = mock_proc + + main_module.hcatOmenTrain(str(training_file)) + + mock_popen.assert_called_once() + cmd = mock_popen.call_args[0][0] + assert cmd[0] == str(create_bin) + assert "--iPwdList" in cmd + assert str(training_file) in cmd + + def test_missing_binary(self, main_module, tmp_path, capsys): + training_file = tmp_path / "passwords.txt" + training_file.write_text("test\n") + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ): + main_module.hcatOmenTrain(str(training_file)) + captured = capsys.readouterr() + assert "createNG binary not found" in captured.out + + def test_missing_training_file(self, main_module, tmp_path, capsys): + omen_dir = tmp_path / "omen" + omen_dir.mkdir() + create_bin = omen_dir / "createNG" + create_bin.touch() + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenCreateBin", "createNG" + ): + main_module.hcatOmenTrain("/nonexistent/file.txt") + captured = capsys.readouterr() + assert "Training file not found" in captured.out + + +class TestHcatOmen: + def test_builds_correct_pipe_commands(self, main_module, tmp_path): + omen_dir = tmp_path / "omen" + omen_dir.mkdir() + enum_bin = omen_dir / "enumNG" + enum_bin.touch() + enum_bin.chmod(0o755) + + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenEnumBin", "enumNG" + ), patch.object(main_module, "hcatBin", "hashcat"), patch.object( + main_module, "hcatTuning", "--force" + ), patch.object( + main_module, "hcatPotfilePath", "" + ), patch.object( + main_module, "hcatHashFile", "/tmp/hashes.txt", create=True + ), patch( + "hate_crack.main.subprocess.Popen" + ) as mock_popen: + mock_enum_proc = MagicMock() + mock_enum_proc.stdout = MagicMock() + mock_hashcat_proc = MagicMock() + mock_hashcat_proc.wait.return_value = None + mock_enum_proc.wait.return_value = None + mock_popen.side_effect = [mock_enum_proc, mock_hashcat_proc] + + main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000) + + assert mock_popen.call_count == 2 + # First call: enumNG + enum_cmd = mock_popen.call_args_list[0][0][0] + assert enum_cmd[0] == str(enum_bin) + assert "-p" in enum_cmd + assert "-m" in enum_cmd + assert "500000" in enum_cmd + # Second call: hashcat + hashcat_cmd = mock_popen.call_args_list[1][0][0] + assert hashcat_cmd[0] == "hashcat" + assert "1000" in hashcat_cmd + assert "/tmp/hashes.txt" in hashcat_cmd + + def test_missing_binary(self, main_module, tmp_path, capsys): + with patch.object(main_module, "hate_path", str(tmp_path)), patch.object( + main_module, "hcatOmenEnumBin", "enumNG" + ): + main_module.hcatOmen("1000", "/tmp/hashes.txt", 500000) + captured = capsys.readouterr() + assert "enumNG binary not found" in captured.out + + +class TestOmenAttackHandler: + def test_prompts_and_calls_hcatOmen(self): + ctx = MagicMock() + ctx.hate_path = "/fake/path" + ctx.omenTrainingList = "/fake/rockyou.txt" + ctx.omenMaxCandidates = 1000000 + ctx.hcatHashType = "1000" + ctx.hcatHashFile = "/tmp/hashes.txt" + + with patch("os.path.isfile", return_value=True), patch( + "builtins.input", return_value="" + ): + from hate_crack.attacks import omen_attack + + omen_attack(ctx) + + ctx.hcatOmen.assert_called_once_with("1000", "/tmp/hashes.txt", 1000000) + + def test_trains_when_no_model(self): + ctx = MagicMock() + ctx.hate_path = "/fake/path" + ctx.omenTrainingList = "/fake/rockyou.txt" + ctx.omenMaxCandidates = 1000000 + ctx.hcatHashType = "1000" + ctx.hcatHashFile = "/tmp/hashes.txt" + + def fake_isfile(path): + return "IP.level" not in path + + with patch("os.path.isfile", side_effect=fake_isfile), patch( + "builtins.input", return_value="" + ): + from hate_crack.attacks import omen_attack + + omen_attack(ctx) + + ctx.hcatOmenTrain.assert_called_once_with("/fake/rockyou.txt") + ctx.hcatOmen.assert_called_once() diff --git a/tests/test_ui_menu_options.py b/tests/test_ui_menu_options.py index 9ecf38f..2df6ebd 100644 --- a/tests/test_ui_menu_options.py +++ b/tests/test_ui_menu_options.py @@ -26,6 +26,7 @@ MENU_OPTION_TEST_CASES = [ ("13", CLI_MODULE._attacks, "bandrel_method", "bandrel"), ("14", CLI_MODULE._attacks, "loopback_attack", "loopback"), ("15", CLI_MODULE._attacks, "ollama_attack", "ollama"), + ("16", CLI_MODULE._attacks, "omen_attack", "omen"), ("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"), ("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"), ("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),