mirror of
https://github.com/trustedsec/hate_crack.git
synced 2026-07-30 23:50:31 -07:00
feat: add PassGPT attack (#17) - GPT-2 based ML password generator
Add PassGPT as attack mode 17, using a GPT-2 model trained on leaked password datasets to generate candidate passwords. The generator pipes candidates to hashcat via stdin, matching the existing OMEN pipe pattern. - Add standalone generator module (python -m hate_crack.passgpt_generate) - Add [ml] optional dependency group (torch, transformers) - Add config keys: passgptModel, passgptMaxCandidates, passgptBatchSize - Wire up menu entries in main.py, attacks.py, and hate_crack.py - Auto-detect GPU (CUDA/MPS) with CPU fallback - Add unit tests for pipe construction, handler, and ML deps check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2446ef3ed1
commit
39970b41c4
@@ -550,6 +550,7 @@ Tests automatically run on GitHub Actions for every push and pull request (Ubunt
|
|||||||
(14) Loopback Attack
|
(14) Loopback Attack
|
||||||
(15) LLM Attack
|
(15) LLM Attack
|
||||||
(16) OMEN Attack
|
(16) OMEN Attack
|
||||||
|
(17) PassGPT Attack
|
||||||
|
|
||||||
(90) Download rules from Hashmob.net
|
(90) Download rules from Hashmob.net
|
||||||
(91) Analyze Hashcat Rules
|
(91) Analyze Hashcat Rules
|
||||||
@@ -694,6 +695,31 @@ Uses the Ordered Markov ENumerator (OMEN) to train a statistical password model
|
|||||||
* Pipes generated candidates directly into hashcat for cracking
|
* Pipes generated candidates directly into hashcat for cracking
|
||||||
* Model files are stored in `~/.hate_crack/omen/` for persistence across sessions
|
* Model files are stored in `~/.hate_crack/omen/` for persistence across sessions
|
||||||
|
|
||||||
|
#### PassGPT Attack
|
||||||
|
Uses PassGPT, a GPT-2 based password generator trained on leaked password datasets, to generate candidate passwords. PassGPT produces higher-quality candidates than traditional Markov models by leveraging transformer-based language modeling.
|
||||||
|
|
||||||
|
**Requirements:** ML dependencies must be installed separately:
|
||||||
|
```bash
|
||||||
|
uv pip install -e ".[ml]"
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs PyTorch and HuggingFace Transformers. GPU acceleration (CUDA/MPS) is auto-detected but not required.
|
||||||
|
|
||||||
|
**Configuration keys:**
|
||||||
|
* `passgptModel` - HuggingFace model name (default: `javirandor/passgpt-10characters`)
|
||||||
|
* `passgptMaxCandidates` - Maximum candidates to generate (default: 1000000)
|
||||||
|
* `passgptBatchSize` - Generation batch size (default: 1024)
|
||||||
|
|
||||||
|
**Supported models:**
|
||||||
|
* `javirandor/passgpt-10characters` - Trained on passwords up to 10 characters (default)
|
||||||
|
* `javirandor/passgpt-16characters` - Trained on passwords up to 16 characters
|
||||||
|
* Any compatible GPT-2 model on HuggingFace
|
||||||
|
|
||||||
|
**Standalone usage:**
|
||||||
|
```bash
|
||||||
|
python -m hate_crack.passgpt_generate --num 1000 --model javirandor/passgpt-10characters
|
||||||
|
```
|
||||||
|
|
||||||
#### Download Rules from Hashmob.net
|
#### 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.
|
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.
|
||||||
|
|
||||||
@@ -727,6 +753,9 @@ Interactive menu for downloading and managing wordlists from Weakpass.com via Bi
|
|||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
### Version History
|
### Version History
|
||||||
Version 2.0+
|
Version 2.0+
|
||||||
|
Added PassGPT Attack (option 17) using GPT-2 based ML password generation
|
||||||
|
Added PassGPT configuration keys (passgptModel, passgptMaxCandidates, passgptBatchSize)
|
||||||
|
Added [ml] optional dependency group for PyTorch and Transformers
|
||||||
Added OMEN Attack (option 16) using statistical model-based password generation
|
Added OMEN Attack (option 16) using statistical model-based password generation
|
||||||
Added OMEN configuration keys (omenTrainingList, omenMaxCandidates)
|
Added OMEN configuration keys (omenTrainingList, omenMaxCandidates)
|
||||||
Added LLM Attack (option 15) using Ollama for AI-generated password candidates
|
Added LLM Attack (option 15) using Ollama for AI-generated password candidates
|
||||||
|
|||||||
+5
-1
@@ -26,5 +26,9 @@
|
|||||||
"ollamaModel": "mistral",
|
"ollamaModel": "mistral",
|
||||||
"ollamaNumCtx": 2048,
|
"ollamaNumCtx": 2048,
|
||||||
"omenTrainingList": "rockyou.txt",
|
"omenTrainingList": "rockyou.txt",
|
||||||
"omenMaxCandidates": 1000000
|
"omenMaxCandidates": 1000000,
|
||||||
|
"passgptModel": "javirandor/passgpt-10characters",
|
||||||
|
"passgptMaxCandidates": 1000000,
|
||||||
|
"passgptBatchSize": 1024,
|
||||||
|
"check_for_updates": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ def get_main_menu_options():
|
|||||||
"14": _attacks.loopback_attack,
|
"14": _attacks.loopback_attack,
|
||||||
"15": _attacks.ollama_attack,
|
"15": _attacks.ollama_attack,
|
||||||
"16": _attacks.omen_attack,
|
"16": _attacks.omen_attack,
|
||||||
|
"17": _attacks.passgpt_attack,
|
||||||
"90": download_hashmob_rules,
|
"90": download_hashmob_rules,
|
||||||
"91": weakpass_wordlist_menu,
|
"91": weakpass_wordlist_menu,
|
||||||
"92": download_hashmob_wordlists,
|
"92": download_hashmob_wordlists,
|
||||||
|
|||||||
+25
-1
@@ -510,7 +510,8 @@ def omen_attack(ctx: Any) -> None:
|
|||||||
print("\n\tOMEN binaries not found. Build them with:")
|
print("\n\tOMEN binaries not found. Build them with:")
|
||||||
print(f"\t cd {omen_dir} && make")
|
print(f"\t cd {omen_dir} && make")
|
||||||
return
|
return
|
||||||
model_exists = os.path.isfile(os.path.join(omen_dir, "IP.level"))
|
model_dir = os.path.join(os.path.expanduser("~"), ".hate_crack", "omen")
|
||||||
|
model_exists = os.path.isfile(os.path.join(model_dir, "createConfig"))
|
||||||
if not model_exists:
|
if not model_exists:
|
||||||
print("\n\tNo OMEN model found. Training is required before generation.")
|
print("\n\tNo OMEN model found. Training is required before generation.")
|
||||||
training_source = input(
|
training_source = input(
|
||||||
@@ -525,3 +526,26 @@ def omen_attack(ctx: Any) -> None:
|
|||||||
if not max_candidates:
|
if not max_candidates:
|
||||||
max_candidates = str(ctx.omenMaxCandidates)
|
max_candidates = str(ctx.omenMaxCandidates)
|
||||||
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates))
|
ctx.hcatOmen(ctx.hcatHashType, ctx.hcatHashFile, int(max_candidates))
|
||||||
|
|
||||||
|
|
||||||
|
def passgpt_attack(ctx: Any) -> None:
|
||||||
|
print("\n\tPassGPT Attack (ML Password Generator)")
|
||||||
|
if not ctx.HAS_ML_DEPS:
|
||||||
|
print("\n\tPassGPT requires ML dependencies. Install them with:")
|
||||||
|
print('\t uv pip install -e ".[ml]"')
|
||||||
|
return
|
||||||
|
max_candidates = input(
|
||||||
|
f"\n\tMax candidates to generate ({ctx.passgptMaxCandidates}): "
|
||||||
|
).strip()
|
||||||
|
if not max_candidates:
|
||||||
|
max_candidates = str(ctx.passgptMaxCandidates)
|
||||||
|
model_name = input(f"\n\tModel name ({ctx.passgptModel}): ").strip()
|
||||||
|
if not model_name:
|
||||||
|
model_name = ctx.passgptModel
|
||||||
|
ctx.hcatPassGPT(
|
||||||
|
ctx.hcatHashType,
|
||||||
|
ctx.hcatHashFile,
|
||||||
|
int(max_candidates),
|
||||||
|
model_name=model_name,
|
||||||
|
batch_size=ctx.passgptBatchSize,
|
||||||
|
)
|
||||||
|
|||||||
+170
-4
@@ -37,6 +37,15 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
HAS_ML_DEPS = False
|
||||||
|
try:
|
||||||
|
import torch # noqa: F401
|
||||||
|
import transformers # noqa: F401
|
||||||
|
|
||||||
|
HAS_ML_DEPS = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Ensure project root is on sys.path so package imports work when loaded via spec.
|
# Ensure project root is on sys.path so package imports work when loaded via spec.
|
||||||
_root_dir = os.path.dirname(os.path.realpath(__file__))
|
_root_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
if _root_dir not in sys.path:
|
if _root_dir not in sys.path:
|
||||||
@@ -486,6 +495,42 @@ except KeyError as e:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000))
|
omenMaxCandidates = int(default_config.get("omenMaxCandidates", 1000000))
|
||||||
|
try:
|
||||||
|
passgptModel = config_parser["passgptModel"]
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
passgptModel = default_config.get("passgptModel", "javirandor/passgpt-10characters")
|
||||||
|
try:
|
||||||
|
passgptMaxCandidates = int(config_parser["passgptMaxCandidates"])
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
passgptMaxCandidates = int(default_config.get("passgptMaxCandidates", 1000000))
|
||||||
|
try:
|
||||||
|
passgptBatchSize = int(config_parser["passgptBatchSize"])
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
passgptBatchSize = int(default_config.get("passgptBatchSize", 1024))
|
||||||
|
try:
|
||||||
|
check_for_updates_enabled = config_parser["check_for_updates"]
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
"{0} is not defined in config.json using defaults from config.json.example".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
check_for_updates_enabled = default_config.get("check_for_updates", True)
|
||||||
|
|
||||||
hcatExpanderBin = "expander.bin"
|
hcatExpanderBin = "expander.bin"
|
||||||
hcatCombinatorBin = "combinator.bin"
|
hcatCombinatorBin = "combinator.bin"
|
||||||
@@ -823,6 +868,35 @@ def ascii_art():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_updates():
|
||||||
|
"""Check GitHub for a newer release and print a notice if one exists."""
|
||||||
|
try:
|
||||||
|
from hate_crack import __version__
|
||||||
|
|
||||||
|
if not REQUESTS_AVAILABLE:
|
||||||
|
return
|
||||||
|
resp = requests.get(
|
||||||
|
"https://api.github.com/repos/trustedsec/hate_crack/releases/latest",
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
tag = resp.json().get("tag_name", "")
|
||||||
|
latest = tag.lstrip("v")
|
||||||
|
# Compare base version (before any +g... suffix) against remote tag
|
||||||
|
local_base = __version__.split("+")[0]
|
||||||
|
if not latest or not local_base:
|
||||||
|
return
|
||||||
|
from packaging.version import parse
|
||||||
|
|
||||||
|
if parse(latest) > parse(local_base):
|
||||||
|
print(
|
||||||
|
f"\n Update available: {latest} (current: {local_base})."
|
||||||
|
f"\n See https://github.com/trustedsec/hate_crack/releases\n"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# File selector with tab autocomplete
|
# File selector with tab autocomplete
|
||||||
def select_file_with_autocomplete(
|
def select_file_with_autocomplete(
|
||||||
prompt, default=None, allow_multiple=False, base_dir=None
|
prompt, default=None, allow_multiple=False, base_dir=None
|
||||||
@@ -2111,6 +2185,15 @@ def hcatPrince(hcatHashType, hcatHashFile):
|
|||||||
prince_proc.kill()
|
prince_proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
# OMEN model directory - writable location for trained model files.
|
||||||
|
# The binaries live in {hate_path}/omen/ (possibly read-only after install),
|
||||||
|
# but model output (createConfig, *.level) goes to ~/.hate_crack/omen/.
|
||||||
|
def _omen_model_dir():
|
||||||
|
model_dir = os.path.join(os.path.expanduser("~"), ".hate_crack", "omen")
|
||||||
|
os.makedirs(model_dir, exist_ok=True)
|
||||||
|
return model_dir
|
||||||
|
|
||||||
|
|
||||||
# OMEN Attack - Train model
|
# OMEN Attack - Train model
|
||||||
def hcatOmenTrain(training_file):
|
def hcatOmenTrain(training_file):
|
||||||
omen_dir = os.path.join(hate_path, "omen")
|
omen_dir = os.path.join(hate_path, "omen")
|
||||||
@@ -2118,13 +2201,30 @@ def hcatOmenTrain(training_file):
|
|||||||
if not os.path.isfile(create_bin):
|
if not os.path.isfile(create_bin):
|
||||||
print(f"Error: OMEN createNG binary not found: {create_bin}")
|
print(f"Error: OMEN createNG binary not found: {create_bin}")
|
||||||
return
|
return
|
||||||
|
training_file = os.path.abspath(training_file)
|
||||||
if not os.path.isfile(training_file):
|
if not os.path.isfile(training_file):
|
||||||
print(f"Error: Training file not found: {training_file}")
|
print(f"Error: Training file not found: {training_file}")
|
||||||
return
|
return
|
||||||
|
model_dir = _omen_model_dir()
|
||||||
print(f"Training OMEN model with: {training_file}")
|
print(f"Training OMEN model with: {training_file}")
|
||||||
cmd = [create_bin, "--iPwdList", training_file]
|
print(f"Model output directory: {model_dir}")
|
||||||
|
cmd = [
|
||||||
|
create_bin,
|
||||||
|
"--iPwdList",
|
||||||
|
training_file,
|
||||||
|
"-C",
|
||||||
|
os.path.join(model_dir, "createConfig"),
|
||||||
|
"-c",
|
||||||
|
os.path.join(model_dir, "CP"),
|
||||||
|
"-i",
|
||||||
|
os.path.join(model_dir, "IP"),
|
||||||
|
"-e",
|
||||||
|
os.path.join(model_dir, "EP"),
|
||||||
|
"-l",
|
||||||
|
os.path.join(model_dir, "LN"),
|
||||||
|
]
|
||||||
print(f"[*] Running: {_format_cmd(cmd)}")
|
print(f"[*] Running: {_format_cmd(cmd)}")
|
||||||
proc = subprocess.Popen(cmd, cwd=omen_dir)
|
proc = subprocess.Popen(cmd)
|
||||||
try:
|
try:
|
||||||
proc.wait()
|
proc.wait()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@@ -2145,7 +2245,13 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
|||||||
if not os.path.isfile(enum_bin):
|
if not os.path.isfile(enum_bin):
|
||||||
print(f"Error: OMEN enumNG binary not found: {enum_bin}")
|
print(f"Error: OMEN enumNG binary not found: {enum_bin}")
|
||||||
return
|
return
|
||||||
enum_cmd = [enum_bin, "-p", "-m", str(max_candidates)]
|
model_dir = _omen_model_dir()
|
||||||
|
config_path = os.path.join(model_dir, "createConfig")
|
||||||
|
if not os.path.isfile(config_path):
|
||||||
|
print(f"Error: OMEN model not found at {config_path}")
|
||||||
|
print("Run training first (option 16).")
|
||||||
|
return
|
||||||
|
enum_cmd = [enum_bin, "-p", "-m", str(max_candidates), "-C", config_path]
|
||||||
hashcat_cmd = [
|
hashcat_cmd = [
|
||||||
hcatBin,
|
hcatBin,
|
||||||
"-m",
|
"-m",
|
||||||
@@ -2160,7 +2266,7 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
|||||||
_append_potfile_arg(hashcat_cmd)
|
_append_potfile_arg(hashcat_cmd)
|
||||||
print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}")
|
print(f"[*] Running: {_format_cmd(enum_cmd)} | {_format_cmd(hashcat_cmd)}")
|
||||||
_debug_cmd(hashcat_cmd)
|
_debug_cmd(hashcat_cmd)
|
||||||
enum_proc = subprocess.Popen(enum_cmd, cwd=omen_dir, stdout=subprocess.PIPE)
|
enum_proc = subprocess.Popen(enum_cmd, cwd=model_dir, stdout=subprocess.PIPE)
|
||||||
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout)
|
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=enum_proc.stdout)
|
||||||
enum_proc.stdout.close()
|
enum_proc.stdout.close()
|
||||||
try:
|
try:
|
||||||
@@ -2172,6 +2278,56 @@ def hcatOmen(hcatHashType, hcatHashFile, max_candidates):
|
|||||||
enum_proc.kill()
|
enum_proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
# PassGPT Attack - Generate candidates with ML model and pipe to hashcat
|
||||||
|
def hcatPassGPT(
|
||||||
|
hcatHashType,
|
||||||
|
hcatHashFile,
|
||||||
|
max_candidates,
|
||||||
|
model_name=None,
|
||||||
|
batch_size=None,
|
||||||
|
):
|
||||||
|
global hcatProcess
|
||||||
|
if model_name is None:
|
||||||
|
model_name = passgptModel
|
||||||
|
if batch_size is None:
|
||||||
|
batch_size = passgptBatchSize
|
||||||
|
gen_cmd = [
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"hate_crack.passgpt_generate",
|
||||||
|
"--num",
|
||||||
|
str(max_candidates),
|
||||||
|
"--model",
|
||||||
|
model_name,
|
||||||
|
"--batch-size",
|
||||||
|
str(batch_size),
|
||||||
|
]
|
||||||
|
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(gen_cmd)} | {_format_cmd(hashcat_cmd)}")
|
||||||
|
_debug_cmd(hashcat_cmd)
|
||||||
|
gen_proc = subprocess.Popen(gen_cmd, stdout=subprocess.PIPE)
|
||||||
|
hcatProcess = subprocess.Popen(hashcat_cmd, stdin=gen_proc.stdout)
|
||||||
|
gen_proc.stdout.close()
|
||||||
|
try:
|
||||||
|
hcatProcess.wait()
|
||||||
|
gen_proc.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Killing PID {0}...".format(str(hcatProcess.pid)))
|
||||||
|
hcatProcess.kill()
|
||||||
|
gen_proc.kill()
|
||||||
|
|
||||||
|
|
||||||
# Extra - Good Measure
|
# Extra - Good Measure
|
||||||
def hcatGoodMeasure(hcatHashType, hcatHashFile):
|
def hcatGoodMeasure(hcatHashType, hcatHashFile):
|
||||||
global hcatExtraCount
|
global hcatExtraCount
|
||||||
@@ -3091,6 +3247,10 @@ def omen_attack():
|
|||||||
return _attacks.omen_attack(_attack_ctx())
|
return _attacks.omen_attack(_attack_ctx())
|
||||||
|
|
||||||
|
|
||||||
|
def passgpt_attack():
|
||||||
|
return _attacks.passgpt_attack(_attack_ctx())
|
||||||
|
|
||||||
|
|
||||||
# convert hex words for recycling
|
# convert hex words for recycling
|
||||||
def convert_hex(working_file):
|
def convert_hex(working_file):
|
||||||
processed_words = []
|
processed_words = []
|
||||||
@@ -3320,6 +3480,7 @@ def get_main_menu_options():
|
|||||||
"14": loopback_attack,
|
"14": loopback_attack,
|
||||||
"15": ollama_attack,
|
"15": ollama_attack,
|
||||||
"16": omen_attack,
|
"16": omen_attack,
|
||||||
|
"17": passgpt_attack,
|
||||||
"90": download_hashmob_rules,
|
"90": download_hashmob_rules,
|
||||||
"91": analyze_rules,
|
"91": analyze_rules,
|
||||||
"92": download_hashmob_wordlists,
|
"92": download_hashmob_wordlists,
|
||||||
@@ -3730,6 +3891,8 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
ascii_art()
|
ascii_art()
|
||||||
|
if not SKIP_INIT and check_for_updates_enabled:
|
||||||
|
check_for_updates()
|
||||||
menu_loop = True
|
menu_loop = True
|
||||||
while menu_loop:
|
while menu_loop:
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
@@ -3786,6 +3949,8 @@ def main():
|
|||||||
if not hcatHashFileOrig:
|
if not hcatHashFileOrig:
|
||||||
hcatHashFileOrig = hcatHashFile
|
hcatHashFileOrig = hcatHashFile
|
||||||
ascii_art()
|
ascii_art()
|
||||||
|
if not SKIP_INIT and check_for_updates_enabled:
|
||||||
|
check_for_updates()
|
||||||
# Get Initial Input Hash Count
|
# Get Initial Input Hash Count
|
||||||
|
|
||||||
# If LM or NT Mode Selected and pwdump Format Detected, Prompt For LM to NT Attack
|
# If LM or NT Mode Selected and pwdump Format Detected, Prompt For LM to NT Attack
|
||||||
@@ -3973,6 +4138,7 @@ def main():
|
|||||||
print("\t(14) Loopback Attack")
|
print("\t(14) Loopback Attack")
|
||||||
print("\t(15) LLM Attack")
|
print("\t(15) LLM Attack")
|
||||||
print("\t(16) OMEN Attack")
|
print("\t(16) OMEN Attack")
|
||||||
|
print("\t(17) PassGPT Attack")
|
||||||
print("\n\t(90) Download rules from Hashmob.net")
|
print("\n\t(90) Download rules from Hashmob.net")
|
||||||
print("\n\t(91) Analyze Hashcat Rules")
|
print("\n\t(91) Analyze Hashcat Rules")
|
||||||
print("\t(92) Download wordlists from Hashmob.net")
|
print("\t(92) Download wordlists from Hashmob.net")
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""Standalone PassGPT password candidate generator.
|
||||||
|
|
||||||
|
Invokable as ``python -m hate_crack.passgpt_generate``. Outputs one
|
||||||
|
candidate password per line to stdout so it can be piped directly into
|
||||||
|
hashcat. Progress and diagnostic messages go to stderr.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_device() -> str:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
return "cuda"
|
||||||
|
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||||
|
return "mps"
|
||||||
|
return "cpu"
|
||||||
|
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
num: int,
|
||||||
|
model_name: str,
|
||||||
|
batch_size: int,
|
||||||
|
max_length: int,
|
||||||
|
device: str | None,
|
||||||
|
) -> None:
|
||||||
|
import torch
|
||||||
|
from transformers import GPT2LMHeadModel, RobertaTokenizerFast
|
||||||
|
|
||||||
|
if device is None:
|
||||||
|
device = _detect_device()
|
||||||
|
|
||||||
|
print(f"[*] Loading model {model_name} on {device}", file=sys.stderr)
|
||||||
|
tokenizer = RobertaTokenizerFast.from_pretrained(model_name)
|
||||||
|
model = GPT2LMHeadModel.from_pretrained(model_name).to(device)
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
generated = 0
|
||||||
|
seen: set[str] = set()
|
||||||
|
|
||||||
|
print(f"[*] Generating {num} candidates (batch_size={batch_size})", file=sys.stderr)
|
||||||
|
with torch.no_grad():
|
||||||
|
while generated < num:
|
||||||
|
current_batch = min(batch_size, num - generated)
|
||||||
|
input_ids = torch.full(
|
||||||
|
(current_batch, 1),
|
||||||
|
tokenizer.bos_token_id,
|
||||||
|
dtype=torch.long,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
output = model.generate(
|
||||||
|
input_ids,
|
||||||
|
max_length=max_length,
|
||||||
|
do_sample=True,
|
||||||
|
top_k=0,
|
||||||
|
top_p=1.0,
|
||||||
|
num_return_sequences=current_batch,
|
||||||
|
pad_token_id=tokenizer.eos_token_id,
|
||||||
|
)
|
||||||
|
# Strip BOS token
|
||||||
|
output = output[:, 1:]
|
||||||
|
for seq in output:
|
||||||
|
decoded = tokenizer.decode(seq, skip_special_tokens=False)
|
||||||
|
password = decoded.split("</s>")[0]
|
||||||
|
if password and password not in seen:
|
||||||
|
seen.add(password)
|
||||||
|
sys.stdout.write(password + "\n")
|
||||||
|
generated += 1
|
||||||
|
if generated >= num:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.stdout.flush()
|
||||||
|
print(f"[*] Done. Generated {generated} unique candidates.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate password candidates using PassGPT"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--num",
|
||||||
|
type=int,
|
||||||
|
default=1000000,
|
||||||
|
help="Number of candidates to generate (default: 1000000)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
type=str,
|
||||||
|
default="javirandor/passgpt-10characters",
|
||||||
|
help="HuggingFace model name (default: javirandor/passgpt-10characters)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--batch-size",
|
||||||
|
type=int,
|
||||||
|
default=1024,
|
||||||
|
help="Generation batch size (default: 1024)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-length",
|
||||||
|
type=int,
|
||||||
|
default=12,
|
||||||
|
help="Max token length including special tokens (default: 12)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--device",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Device: cuda, mps, or cpu (default: auto-detect)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
generate(
|
||||||
|
num=args.num,
|
||||||
|
model_name=args.model,
|
||||||
|
batch_size=args.batch_size,
|
||||||
|
max_length=args.max_length,
|
||||||
|
device=args.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -12,12 +12,17 @@ dependencies = [
|
|||||||
"requests>=2.31.0",
|
"requests>=2.31.0",
|
||||||
"beautifulsoup4>=4.12.0",
|
"beautifulsoup4>=4.12.0",
|
||||||
"openpyxl>=3.0.0",
|
"openpyxl>=3.0.0",
|
||||||
|
"packaging>=21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
hate_crack = "hate_crack.__main__:main"
|
hate_crack = "hate_crack.__main__:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
ml = [
|
||||||
|
"torch>=2.0.0",
|
||||||
|
"transformers>=4.30.0",
|
||||||
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"mypy>=1.8.0",
|
"mypy>=1.8.0",
|
||||||
"ruff>=0.3.0",
|
"ruff>=0.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import sys
|
||||||
|
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 TestHcatPassGPT:
|
||||||
|
def test_builds_correct_pipe_commands(self, main_module):
|
||||||
|
with 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.object(
|
||||||
|
main_module, "passgptModel", "javirandor/passgpt-10characters"
|
||||||
|
), patch.object(main_module, "passgptBatchSize", 1024), patch(
|
||||||
|
"hate_crack.main.subprocess.Popen"
|
||||||
|
) as mock_popen:
|
||||||
|
mock_gen_proc = MagicMock()
|
||||||
|
mock_gen_proc.stdout = MagicMock()
|
||||||
|
mock_hashcat_proc = MagicMock()
|
||||||
|
mock_hashcat_proc.wait.return_value = None
|
||||||
|
mock_gen_proc.wait.return_value = None
|
||||||
|
mock_popen.side_effect = [mock_gen_proc, mock_hashcat_proc]
|
||||||
|
|
||||||
|
main_module.hcatPassGPT("1000", "/tmp/hashes.txt", 500000)
|
||||||
|
|
||||||
|
assert mock_popen.call_count == 2
|
||||||
|
# First call: passgpt generator
|
||||||
|
gen_cmd = mock_popen.call_args_list[0][0][0]
|
||||||
|
assert gen_cmd[0] == sys.executable
|
||||||
|
assert "-m" in gen_cmd
|
||||||
|
assert "hate_crack.passgpt_generate" in gen_cmd
|
||||||
|
assert "--num" in gen_cmd
|
||||||
|
assert "500000" in gen_cmd
|
||||||
|
assert "--model" in gen_cmd
|
||||||
|
assert "javirandor/passgpt-10characters" in gen_cmd
|
||||||
|
assert "--batch-size" in gen_cmd
|
||||||
|
assert "1024" in gen_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_custom_model_and_batch_size(self, main_module):
|
||||||
|
with 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.object(
|
||||||
|
main_module, "passgptModel", "javirandor/passgpt-10characters"
|
||||||
|
), patch.object(main_module, "passgptBatchSize", 1024), patch(
|
||||||
|
"hate_crack.main.subprocess.Popen"
|
||||||
|
) as mock_popen:
|
||||||
|
mock_gen_proc = MagicMock()
|
||||||
|
mock_gen_proc.stdout = MagicMock()
|
||||||
|
mock_hashcat_proc = MagicMock()
|
||||||
|
mock_hashcat_proc.wait.return_value = None
|
||||||
|
mock_gen_proc.wait.return_value = None
|
||||||
|
mock_popen.side_effect = [mock_gen_proc, mock_hashcat_proc]
|
||||||
|
|
||||||
|
main_module.hcatPassGPT(
|
||||||
|
"1000",
|
||||||
|
"/tmp/hashes.txt",
|
||||||
|
100000,
|
||||||
|
model_name="custom/model",
|
||||||
|
batch_size=512,
|
||||||
|
)
|
||||||
|
|
||||||
|
gen_cmd = mock_popen.call_args_list[0][0][0]
|
||||||
|
assert "custom/model" in gen_cmd
|
||||||
|
assert "512" in gen_cmd
|
||||||
|
|
||||||
|
|
||||||
|
class TestPassGPTAttackHandler:
|
||||||
|
def test_prompts_and_calls_hcatPassGPT(self):
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.HAS_ML_DEPS = True
|
||||||
|
ctx.passgptMaxCandidates = 1000000
|
||||||
|
ctx.passgptModel = "javirandor/passgpt-10characters"
|
||||||
|
ctx.passgptBatchSize = 1024
|
||||||
|
ctx.hcatHashType = "1000"
|
||||||
|
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||||
|
|
||||||
|
with patch("builtins.input", return_value=""):
|
||||||
|
from hate_crack.attacks import passgpt_attack
|
||||||
|
|
||||||
|
passgpt_attack(ctx)
|
||||||
|
|
||||||
|
ctx.hcatPassGPT.assert_called_once_with(
|
||||||
|
"1000",
|
||||||
|
"/tmp/hashes.txt",
|
||||||
|
1000000,
|
||||||
|
model_name="javirandor/passgpt-10characters",
|
||||||
|
batch_size=1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.HAS_ML_DEPS = True
|
||||||
|
ctx.passgptMaxCandidates = 1000000
|
||||||
|
ctx.passgptModel = "javirandor/passgpt-10characters"
|
||||||
|
ctx.passgptBatchSize = 1024
|
||||||
|
ctx.hcatHashType = "1000"
|
||||||
|
ctx.hcatHashFile = "/tmp/hashes.txt"
|
||||||
|
|
||||||
|
inputs = iter(["500000", "custom/model"])
|
||||||
|
with patch("builtins.input", side_effect=inputs):
|
||||||
|
from hate_crack.attacks import passgpt_attack
|
||||||
|
|
||||||
|
passgpt_attack(ctx)
|
||||||
|
|
||||||
|
ctx.hcatPassGPT.assert_called_once_with(
|
||||||
|
"1000",
|
||||||
|
"/tmp/hashes.txt",
|
||||||
|
500000,
|
||||||
|
model_name="custom/model",
|
||||||
|
batch_size=1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ml_deps_missing(self, capsys):
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.HAS_ML_DEPS = False
|
||||||
|
|
||||||
|
from hate_crack.attacks import passgpt_attack
|
||||||
|
|
||||||
|
passgpt_attack(ctx)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "ML dependencies" in captured.out
|
||||||
|
assert "uv pip install" in captured.out
|
||||||
|
ctx.hcatPassGPT.assert_not_called()
|
||||||
@@ -27,6 +27,7 @@ MENU_OPTION_TEST_CASES = [
|
|||||||
("14", CLI_MODULE._attacks, "loopback_attack", "loopback"),
|
("14", CLI_MODULE._attacks, "loopback_attack", "loopback"),
|
||||||
("15", CLI_MODULE._attacks, "ollama_attack", "ollama"),
|
("15", CLI_MODULE._attacks, "ollama_attack", "ollama"),
|
||||||
("16", CLI_MODULE._attacks, "omen_attack", "omen"),
|
("16", CLI_MODULE._attacks, "omen_attack", "omen"),
|
||||||
|
("17", CLI_MODULE._attacks, "passgpt_attack", "passgpt"),
|
||||||
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
|
("90", CLI_MODULE, "download_hashmob_rules", "hashmob-rules"),
|
||||||
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
|
("91", CLI_MODULE, "weakpass_wordlist_menu", "weakpass-menu"),
|
||||||
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
|
("92", CLI_MODULE, "download_hashmob_wordlists", "hashmob-wordlists"),
|
||||||
|
|||||||
Reference in New Issue
Block a user