diff --git a/.gitignore b/.gitignore index 75615ae..7117479 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ working/ hash-cracker.pot -.DS_Store \ No newline at end of file +.DS_Store +input \ No newline at end of file diff --git a/README.md b/README.md index d1c60e8..a7de9b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # hash-cracker +--- + Simple script to get some hash cracking done effectively. In [this blog](https://sensepost.com/blog/2023/hash-cracker-password-cracking-done-effectively/) you can read some background on hash-cracker. +--- + Some sites where you can find wordlists: - @@ -9,10 +13,6 @@ Some sites where you can find wordlists: Want to make the ***$HEX[1234]*** Hashcat output readable? Have a look at [hex-to-readable](https://github.com/crypt0rr/hex-to-readable) or use [CyberChef](https://cyberchef.offsec.nl/). -## Apple Silicon Edition - -There is a separate repo with support for Apple Silicon based systems. Find it over here: [hash-cracker-apple-silicon](https://github.com/crypt0rr/hash-cracker-apple-silicon) - ## Installation ```plain @@ -21,10 +21,16 @@ git clone https://github.com/crypt0rr/hash-cracker ### Requirements for Full Functionality +#### Linux + - Python2 - `pip install pyenchant==3.0.0a1` - [CeWL](https://github.com/digininja/CeWL/) +#### macOS + +- [CeWL](https://github.com/digininja/CeWL/) + ## Usage ```plain diff --git a/VERSION.md b/VERSION.md index a4a81db..8abe983 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,251 +1,255 @@ # Version log +## v4.0 - The Merge + +- Merge of [hash-cracker-apple-silicon](https://github.com/sensepost/hash-cracker-apple-silicon) into hash-cracker + ## v3.9 - Custom Brute Force -* For the heavy lifters, you can now do custom length brute force attacks -* Changed the behavior of `trap` to not only clean temporary files but keep hash-cracker alive when intentionally or unintentionally `CTRL+C` is pressed +- For the heavy lifters, you can now do custom length brute force attacks +- Changed the behavior of `trap` to not only clean temporary files but keep hash-cracker alive when intentionally or unintentionally `CTRL+C` is pressed ## v3.8 - Keep it static -* Even more ability to set static parameters -* Added link to new [blog](https://sensepost.com/blog/2023/hash-cracker-password-cracking-done-effectively/) +- Even more ability to set static parameters +- Added link to new [blog](https://sensepost.com/blog/2023/hash-cracker-password-cracking-done-effectively/) ## v3.7 - Just before -* Introduced `hash-cracker.conf` to set a static config yourself -* Merged `mandatory-checks.sh` and `optional-checks.sh` into `parameters.sh` -* Changed all processors to use the static config file if chosen -* Option to use the static config `--static` +- Introduced `hash-cracker.conf` to set a static config yourself +- Merged `mandatory-checks.sh` and `optional-checks.sh` into `parameters.sh` +- Changed all processors to use the static config file if chosen +- Option to use the static config `--static` ## v3.6 - World Password Day -* New rule - [stacking58](https://github.com/hashcat/hashcat/blob/master/rules/stacking58.rule) -* Added Stacker based on new rule -* Hashcat hardware monitoring can be enabled from now upon +- New rule - [stacking58](https://github.com/hashcat/hashcat/blob/master/rules/stacking58.rule) +- Added Stacker based on new rule +- Hashcat hardware monitoring can be enabled from now upon ## v3.5 - New hashes -* Updated supported hash types, based on hashcat `v6.2.6-420-gdc51a1a97` +- Updated supported hash types, based on hashcat `v6.2.6-420-gdc51a1a97` ## v3.4 - No More tmp_ and More Flexibility -* Moved from `tmp_` files to proper temporary file handling -* Handling of `CTRL+C` - will remove temporary file(s) -* Renamed processors to match with menu option number -* Added `cracklib.txt` -* Added new `parameters.sh` checking script that handles parameters given by the user -* Added option to disable `--loopback` (default: Enabled) +- Moved from `tmp_` files to proper temporary file handling +- Handling of `CTRL+C` - will remove temporary file(s) +- Renamed processors to match with menu option number +- Added `cracklib.txt` +- Added new `parameters.sh` checking script that handles parameters given by the user +- Added option to disable `--loopback` (default: Enabled) ## v3.3.1 - Small Effective Adjustments -* Changes to `digitremover.sh` +- Changes to `digitremover.sh` ## v3.3 - 1234567890 -* Added `digitremover.sh` +- Added `digitremover.sh` ## v3.2 - Custom Word List Generator -* Added Polish [wordlist](https://raw.githubusercontent.com/sigo/polish-dictionary/master/dist/pl.txt) -* Added missing `--hwmon-disable` flag to `markov-generator.sh` -* Replaced OneRuleToRuleThemAll with [OneRuleToRuleThemStill](https://github.com/stealthsploit/OneRuleToRuleThemStill) -* Split `requirements.sh` into two files `mandatory` and `optional` -* Added Custom Word List Generator - [CeWL](https://github.com/digininja/CeWL/) +- Added Polish [wordlist](https://raw.githubusercontent.com/sigo/polish-dictionary/master/dist/pl.txt) +- Added missing `--hwmon-disable` flag to `markov-generator.sh` +- Replaced OneRuleToRuleThemAll with [OneRuleToRuleThemStill](https://github.com/stealthsploit/OneRuleToRuleThemStill) +- Split `requirements.sh` into two files `mandatory` and `optional` +- Added Custom Word List Generator - [CeWL](https://github.com/digininja/CeWL/) ## v3.1 - To speed or not to speed #kernels -* Optimized kernels are used by default but can be disabled with the `-n` or `--no-limit` flag -* Help menu implemented -* Moved search functionality -* Added Markov-chain password generator based on [bujimuji/markov-passwords](https://github.com/bujimuji/markov-passwords) -* Added [Dutch OpenTaal wordlist](https://github.com/OpenTaal/opentaal-wordlist/blob/master/wordlist.txt) +- Optimized kernels are used by default but can be disabled with the `-n` or `--no-limit` flag +- Help menu implemented +- Moved search functionality +- Added Markov-chain password generator based on [bujimuji/markov-passwords](https://github.com/bujimuji/markov-passwords) +- Added [Dutch OpenTaal wordlist](https://github.com/OpenTaal/opentaal-wordlist/blob/master/wordlist.txt) ## v3.0 - 🚀 Besides some small changes you can now chose if you want to run things with a single or multiple word lists. As you can imagine, this will take much longer, but it is worth it. -* Added multi word list support -* Added `german.txt` word list -* Added two new, FaceBook based rules, thanks [@singe](https://twitter.com/singe) -* Disabled hardware monitoring (`--hwmon-disable`) for all processors -* Added word list `brutas-combined` - combining all lists in the 'passwords' category from [brutas](https://github.com/tasooshi/brutas/tree/master/wordlists/passwords) +- Added multi word list support +- Added `german.txt` word list +- Added two new, FaceBook based rules, thanks [@singe](https://twitter.com/singe) +- Disabled hardware monitoring (`--hwmon-disable`) for all processors +- Added word list `brutas-combined` - combining all lists in the 'passwords' category from [brutas](https://github.com/tasooshi/brutas/tree/master/wordlists/passwords) ## v2.9.4 - Fixed issue -* Fixed issue with splitting usernames from source file -* Added Russian word list +- Fixed issue with splitting usernames from source file +- Added Russian word list ## v2.9.3 - Dump Optimization -* Moved from showing cracked hashes with hashcat for use as input to directly taking the potfile - * Performance gain +/- 15% +- Moved from showing cracked hashes with hashcat for use as input to directly taking the potfile + - Performance gain +/- 15% ## v2.9.2 - Additional word lists -* Added two Afrikaans word lists -* Extended public email providers list based on: - * [Kikobeats/free-email-domains](https://github.com/Kikobeats/free-email-domains/blob/master/domains.json) - * [ammarshah/all_email_provider_domains.txt](https://gist.github.com/ammarshah/f5c2624d767f91a7cbdc4e54db8dd0bf) +- Added two Afrikaans word lists +- Extended public email providers list based on: + - [Kikobeats/free-email-domains](https://github.com/Kikobeats/free-email-domains/blob/master/domains.json) + - [ammarshah/all_email_provider_domains.txt](https://gist.github.com/ammarshah/f5c2624d767f91a7cbdc4e54db8dd0bf) ## v2.9.1 - AWK -* Moved from `cut` to `awk` since previously it was not compatible with multiple outputs +- Moved from `cut` to `awk` since previously it was not compatible with multiple outputs ## v2.9 - More better -* Added `keyboardwalk.txt` -* Added `email-domains` -* Added descriptive text for combinator in `showinfo.sh` -* Added `?a` to brute-force pattern -* Added multiple brute-force patterns +- Added `keyboardwalk.txt` +- Added `email-domains` +- Added descriptive text for combinator in `showinfo.sh` +- Added `?a` to brute-force pattern +- Added multiple brute-force patterns ## v2.8 - 3-rule -* Added `3.rule` -* Updated `light.sh` and `iterate.sh` with `3.rule` -* Added COVID-19 related word list (2 parts) -* Added `Robot_MyFavorite.rule` +- Added `3.rule` +- Updated `light.sh` and `iterate.sh` with `3.rule` +- Added COVID-19 related word list (2 parts) +- Added `Robot_MyFavorite.rule` ## v2.7 - Username as Password -* Added support for username as password (currently only NTLM tested) - * Extended the username as password with rules +- Added support for username as password (currently only NTLM tested) + - Extended the username as password with rules ## v2.6 - No more colors -* Added working directory for easy removal after finishing project -* Added new rule -* Removed coloring +- Added working directory for easy removal after finishing project +- Added new rule +- Removed coloring ## v2.5 - Small fixes/extras -* Added extra brute force task -* Fixed issue where the variable was not cleared properly on self restart of the 'selector' scripts -* Fixed issue with hash type selector not restarting on not-known hash type -* Added `multiple-wordlists.sh` processor - option 15 -* Changed starting point after finishing task to 'main' instead of 'menu' -* Added three new word lists +- Added extra brute force task +- Fixed issue where the variable was not cleared properly on self restart of the 'selector' scripts +- Fixed issue with hash type selector not restarting on not-known hash type +- Added `multiple-wordlists.sh` processor - option 15 +- Changed starting point after finishing task to 'main' instead of 'menu' +- Added three new word lists ## v2.4 - Mask Attack -* Removed standalone usage of `rulegen.py` -* Added complete [PACK](https://github.com/iphelix/pack) (Password Analysis and Cracking Kit by Peter Kacherginsky) repo -* Added [Mask Attack](https://hashcat.net/wiki/doku.php?id=mask_attack) -* Added NTLM (-m1000) example hashes +- Removed standalone usage of `rulegen.py` +- Added complete [PACK](https://github.com/iphelix/pack) (Password Analysis and Cracking Kit by Peter Kacherginsky) repo +- Added [Mask Attack](https://hashcat.net/wiki/doku.php?id=mask_attack) +- Added NTLM (-m1000) example hashes ## v2.3 - Fingerprinting -* Added [Fingerprint Attack](https://hashcat.net/wiki/doku.php?id=fingerprint_attack) -* Added [hashcat-utils](https://github.com/hashcat/hashcat-utils) +- Added [Fingerprint Attack](https://hashcat.net/wiki/doku.php?id=fingerprint_attack) +- Added [hashcat-utils](https://github.com/hashcat/hashcat-utils) ## v2.2 - Potfile -* Added specific [potfile](https://hashcat.net/wiki/doku.php?id=frequently_asked_questions#what_is_a_potfile) instead of default +- Added specific [potfile](https://hashcat.net/wiki/doku.php?id=frequently_asked_questions#what_is_a_potfile) instead of default ## v2.1 - Extra rules for iterating -* Added 4 rules -* Extended the iterating processor +- Added 4 rules +- Extended the iterating processor ## v2.0 - Redone way of working -* Took everything from the script and put it into separate scripts -* Changed from hardcoding hashcat path to dynamically with `command -v` -* Minor rearrange of menu options -* Search function showing sorted on number instead of source file -* Removed exporting results for now - will be back in future update +- Took everything from the script and put it into separate scripts +- Changed from hardcoding hashcat path to dynamically with `command -v` +- Minor rearrange of menu options +- Search function showing sorted on number instead of source file +- Removed exporting results for now - will be back in future update ## v1.7 - PACK -* Added [PACK](https://github.com/iphelix/pack) (Password Analysis and Cracking Kit by Peter Kacherginsky) `rulegen.py` -* `rulegen.py` implemented and tested for NTLM (mode 1000) -* Moved some things around +- Added [PACK](https://github.com/iphelix/pack) (Password Analysis and Cracking Kit by Peter Kacherginsky) `rulegen.py` +- `rulegen.py` implemented and tested for NTLM (mode 1000) +- Moved some things around ## v1.6 - Again new rules -* Introducing new rules +- Introducing new rules ## v1.5 - Some new rules -* Introducing some new rules -* Small changes -* New word lists -* Added search function to easily find the right hash type mode you need -* Renamed `processor.sh` to `hash-cracker.sh` +- Introducing some new rules +- Small changes +- New word lists +- Added search function to easily find the right hash type mode you need +- Renamed `processor.sh` to `hash-cracker.sh` ## v1.4 - Lets enter something yourself v2 -* Added function to enter a specific word as input and brute force in front and after it -* Menu rearrange +- Added function to enter a specific word as input and brute force in front and after it +- Menu rearrange ## v1.3 - Lets enter something yourself -* Added function to enter a specific word as input and randomize it -* Extended hybrid processing with non-caps jobs +- Added function to enter a specific word as input and randomize it +- Extended hybrid processing with non-caps jobs ## v1.2 - Optimizing -* Removed `plain processing` -* Removed plain hashes from MD5 example -* Changed `-i` to `--increment` hybrid +- Removed `plain processing` +- Removed plain hashes from MD5 example +- Changed `-i` to `--increment` hybrid ## v1.1 - Added rule -* Added new rule - `OptimizedUpToDate.rule` -* Small changes in list and rule lists +- Added new rule - `OptimizedUpToDate.rule` +- Small changes in list and rule lists ## v1.0 - Release fully working state -* Fully working state +- Fully working state ## v0.9 - Light and heavy lifting -* Made light and heavy rules separate job -* Added `fordyv1.rule` +- Made light and heavy rules separate job +- Added `fordyv1.rule` ## v0.8 - Combinator -* Added combination attack support -* Reduced information directly in the menu moved to separate overview -* Added simple information about the options +- Added combination attack support +- Reduced information directly in the menu moved to separate overview +- Added simple information about the options ## v0.7 - Loop the loop -* Added `--loopback` to rule based cracking -* Extended brute force digits from 8 to 10 +- Added `--loopback` to rule based cracking +- Extended brute force digits from 8 to 10 ## v0.6 - Added Prefix and suffix -* Implemented prefix and suffix processing -* Reordered some functions +- Implemented prefix and suffix processing +- Reordered some functions ## v0.5 - Added Toggle-Case attack -* Adjusted default `bitmap-max` size to 24 -* Added two brute force defaults -* Added the following rules (leetspeak, toggles1, toggles2) -* Added new rules to default processing job -* Added basic Toggle-Case attack +- Adjusted default `bitmap-max` size to 24 +- Added two brute force defaults +- Added the following rules (leetspeak, toggles1, toggles2) +- Added new rules to default processing job +- Added basic Toggle-Case attack ## v0.4 - Added Hybrid attack -* Added hybrid attack support -* Removed line for uniq hashes output because not showing correct numbers depending on input -* Reordered start menu +- Added hybrid attack support +- Removed line for uniq hashes output because not showing correct numbers depending on input +- Reordered start menu ## v0.3 - Added plain processing -* Added the ability to process a plain word/password list against input hashes -* Added tab completion for hash list and word list selector +- Added the ability to process a plain word/password list against input hashes +- Added tab completion for hash list and word list selector ## v0.2 - Multiple changes -* New improved way of performing actions -* Requirements checker -* Iterations of results put into a separate function -* Added function for result processing / output -* Results to `final.txt` removed, must now use option to show results +- New improved way of performing actions +- Requirements checker +- Iterations of results put into a separate function +- Added function for result processing / output +- Results to `final.txt` removed, must now use option to show results ## v0.1 - Initial release -* Initial release +- Initial release diff --git a/hash-cracker.sh b/hash-cracker.sh index fe723bb..97a8d50 100755 --- a/hash-cracker.sh +++ b/hash-cracker.sh @@ -2,7 +2,7 @@ # Author: crypt0rr - https://github.com/crypt0rr/ function hash-cracker () { - echo -e "\nhash-cracker v3.9 by crypt0rr (https://github.com/crypt0rr)" + echo -e "\nhash-cracker v4.0 by crypt0rr (https://github.com/crypt0rr)" } function menu () { diff --git a/scripts/extensions/common-substr b/scripts/extensions/common-substr-linux similarity index 100% rename from scripts/extensions/common-substr rename to scripts/extensions/common-substr-linux diff --git a/scripts/extensions/common-substr-mac b/scripts/extensions/common-substr-mac new file mode 100755 index 0000000..eaa5bc5 Binary files /dev/null and b/scripts/extensions/common-substr-mac differ diff --git a/scripts/extensions/hashcat-utils/CHANGES b/scripts/extensions/hashcat-utils-linux/CHANGES similarity index 100% rename from scripts/extensions/hashcat-utils/CHANGES rename to scripts/extensions/hashcat-utils-linux/CHANGES diff --git a/scripts/extensions/hashcat-utils/LICENSE b/scripts/extensions/hashcat-utils-linux/LICENSE similarity index 100% rename from scripts/extensions/hashcat-utils/LICENSE rename to scripts/extensions/hashcat-utils-linux/LICENSE diff --git a/scripts/extensions/hashcat-utils/README.md b/scripts/extensions/hashcat-utils-linux/README.md similarity index 100% rename from scripts/extensions/hashcat-utils/README.md rename to scripts/extensions/hashcat-utils-linux/README.md diff --git a/scripts/extensions/hashcat-utils/bin/.hold b/scripts/extensions/hashcat-utils-linux/bin/.hold similarity index 100% rename from scripts/extensions/hashcat-utils/bin/.hold rename to scripts/extensions/hashcat-utils-linux/bin/.hold diff --git a/scripts/extensions/hashcat-utils/bin/cap2hccapx.bin b/scripts/extensions/hashcat-utils-linux/bin/cap2hccapx.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cap2hccapx.bin rename to scripts/extensions/hashcat-utils-linux/bin/cap2hccapx.bin diff --git a/scripts/extensions/hashcat-utils/bin/cap2hccapx.exe b/scripts/extensions/hashcat-utils-linux/bin/cap2hccapx.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cap2hccapx.exe rename to scripts/extensions/hashcat-utils-linux/bin/cap2hccapx.exe diff --git a/scripts/extensions/hashcat-utils/bin/cleanup-rules.bin b/scripts/extensions/hashcat-utils-linux/bin/cleanup-rules.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cleanup-rules.bin rename to scripts/extensions/hashcat-utils-linux/bin/cleanup-rules.bin diff --git a/scripts/extensions/hashcat-utils/bin/cleanup-rules.exe b/scripts/extensions/hashcat-utils-linux/bin/cleanup-rules.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cleanup-rules.exe rename to scripts/extensions/hashcat-utils-linux/bin/cleanup-rules.exe diff --git a/scripts/extensions/hashcat-utils/bin/combinator.bin b/scripts/extensions/hashcat-utils-linux/bin/combinator.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combinator.bin rename to scripts/extensions/hashcat-utils-linux/bin/combinator.bin diff --git a/scripts/extensions/hashcat-utils/bin/combinator.exe b/scripts/extensions/hashcat-utils-linux/bin/combinator.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combinator.exe rename to scripts/extensions/hashcat-utils-linux/bin/combinator.exe diff --git a/scripts/extensions/hashcat-utils/bin/combinator3.bin b/scripts/extensions/hashcat-utils-linux/bin/combinator3.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combinator3.bin rename to scripts/extensions/hashcat-utils-linux/bin/combinator3.bin diff --git a/scripts/extensions/hashcat-utils/bin/combinator3.exe b/scripts/extensions/hashcat-utils-linux/bin/combinator3.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combinator3.exe rename to scripts/extensions/hashcat-utils-linux/bin/combinator3.exe diff --git a/scripts/extensions/hashcat-utils/bin/combipow.bin b/scripts/extensions/hashcat-utils-linux/bin/combipow.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combipow.bin rename to scripts/extensions/hashcat-utils-linux/bin/combipow.bin diff --git a/scripts/extensions/hashcat-utils/bin/combipow.exe b/scripts/extensions/hashcat-utils-linux/bin/combipow.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/combipow.exe rename to scripts/extensions/hashcat-utils-linux/bin/combipow.exe diff --git a/scripts/extensions/hashcat-utils/bin/ct3_to_ntlm.bin b/scripts/extensions/hashcat-utils-linux/bin/ct3_to_ntlm.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/ct3_to_ntlm.bin rename to scripts/extensions/hashcat-utils-linux/bin/ct3_to_ntlm.bin diff --git a/scripts/extensions/hashcat-utils/bin/ct3_to_ntlm.exe b/scripts/extensions/hashcat-utils-linux/bin/ct3_to_ntlm.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/ct3_to_ntlm.exe rename to scripts/extensions/hashcat-utils-linux/bin/ct3_to_ntlm.exe diff --git a/scripts/extensions/hashcat-utils/bin/cutb.bin b/scripts/extensions/hashcat-utils-linux/bin/cutb.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cutb.bin rename to scripts/extensions/hashcat-utils-linux/bin/cutb.bin diff --git a/scripts/extensions/hashcat-utils/bin/cutb.exe b/scripts/extensions/hashcat-utils-linux/bin/cutb.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/cutb.exe rename to scripts/extensions/hashcat-utils-linux/bin/cutb.exe diff --git a/scripts/extensions/hashcat-utils/bin/deskey_to_ntlm.pl b/scripts/extensions/hashcat-utils-linux/bin/deskey_to_ntlm.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/deskey_to_ntlm.pl rename to scripts/extensions/hashcat-utils-linux/bin/deskey_to_ntlm.pl diff --git a/scripts/extensions/hashcat-utils/bin/expander.bin b/scripts/extensions/hashcat-utils-linux/bin/expander.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/expander.bin rename to scripts/extensions/hashcat-utils-linux/bin/expander.bin diff --git a/scripts/extensions/hashcat-utils/bin/expander.exe b/scripts/extensions/hashcat-utils-linux/bin/expander.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/expander.exe rename to scripts/extensions/hashcat-utils-linux/bin/expander.exe diff --git a/scripts/extensions/hashcat-utils/bin/gate.bin b/scripts/extensions/hashcat-utils-linux/bin/gate.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/gate.bin rename to scripts/extensions/hashcat-utils-linux/bin/gate.bin diff --git a/scripts/extensions/hashcat-utils/bin/gate.exe b/scripts/extensions/hashcat-utils-linux/bin/gate.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/gate.exe rename to scripts/extensions/hashcat-utils-linux/bin/gate.exe diff --git a/scripts/extensions/hashcat-utils/bin/generate-rules.bin b/scripts/extensions/hashcat-utils-linux/bin/generate-rules.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/generate-rules.bin rename to scripts/extensions/hashcat-utils-linux/bin/generate-rules.bin diff --git a/scripts/extensions/hashcat-utils/bin/generate-rules.exe b/scripts/extensions/hashcat-utils-linux/bin/generate-rules.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/generate-rules.exe rename to scripts/extensions/hashcat-utils-linux/bin/generate-rules.exe diff --git a/scripts/extensions/hashcat-utils/bin/hcstat2gen.bin b/scripts/extensions/hashcat-utils-linux/bin/hcstat2gen.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/hcstat2gen.bin rename to scripts/extensions/hashcat-utils-linux/bin/hcstat2gen.bin diff --git a/scripts/extensions/hashcat-utils/bin/hcstat2gen.exe b/scripts/extensions/hashcat-utils-linux/bin/hcstat2gen.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/hcstat2gen.exe rename to scripts/extensions/hashcat-utils-linux/bin/hcstat2gen.exe diff --git a/scripts/extensions/hashcat-utils/bin/hcstatgen.bin b/scripts/extensions/hashcat-utils-linux/bin/hcstatgen.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/hcstatgen.bin rename to scripts/extensions/hashcat-utils-linux/bin/hcstatgen.bin diff --git a/scripts/extensions/hashcat-utils/bin/hcstatgen.exe b/scripts/extensions/hashcat-utils-linux/bin/hcstatgen.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/hcstatgen.exe rename to scripts/extensions/hashcat-utils-linux/bin/hcstatgen.exe diff --git a/scripts/extensions/hashcat-utils/bin/keyspace.bin b/scripts/extensions/hashcat-utils-linux/bin/keyspace.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/keyspace.bin rename to scripts/extensions/hashcat-utils-linux/bin/keyspace.bin diff --git a/scripts/extensions/hashcat-utils/bin/keyspace.exe b/scripts/extensions/hashcat-utils-linux/bin/keyspace.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/keyspace.exe rename to scripts/extensions/hashcat-utils-linux/bin/keyspace.exe diff --git a/scripts/extensions/hashcat-utils/bin/len.bin b/scripts/extensions/hashcat-utils-linux/bin/len.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/len.bin rename to scripts/extensions/hashcat-utils-linux/bin/len.bin diff --git a/scripts/extensions/hashcat-utils/bin/len.exe b/scripts/extensions/hashcat-utils-linux/bin/len.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/len.exe rename to scripts/extensions/hashcat-utils-linux/bin/len.exe diff --git a/scripts/extensions/hashcat-utils/bin/mli2.bin b/scripts/extensions/hashcat-utils-linux/bin/mli2.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/mli2.bin rename to scripts/extensions/hashcat-utils-linux/bin/mli2.bin diff --git a/scripts/extensions/hashcat-utils/bin/mli2.exe b/scripts/extensions/hashcat-utils-linux/bin/mli2.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/mli2.exe rename to scripts/extensions/hashcat-utils-linux/bin/mli2.exe diff --git a/scripts/extensions/hashcat-utils/bin/morph.bin b/scripts/extensions/hashcat-utils-linux/bin/morph.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/morph.bin rename to scripts/extensions/hashcat-utils-linux/bin/morph.bin diff --git a/scripts/extensions/hashcat-utils/bin/morph.exe b/scripts/extensions/hashcat-utils-linux/bin/morph.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/morph.exe rename to scripts/extensions/hashcat-utils-linux/bin/morph.exe diff --git a/scripts/extensions/hashcat-utils/bin/permute.bin b/scripts/extensions/hashcat-utils-linux/bin/permute.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/permute.bin rename to scripts/extensions/hashcat-utils-linux/bin/permute.bin diff --git a/scripts/extensions/hashcat-utils/bin/permute.exe b/scripts/extensions/hashcat-utils-linux/bin/permute.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/permute.exe rename to scripts/extensions/hashcat-utils-linux/bin/permute.exe diff --git a/scripts/extensions/hashcat-utils/bin/permute_exist.bin b/scripts/extensions/hashcat-utils-linux/bin/permute_exist.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/permute_exist.bin rename to scripts/extensions/hashcat-utils-linux/bin/permute_exist.bin diff --git a/scripts/extensions/hashcat-utils/bin/permute_exist.exe b/scripts/extensions/hashcat-utils-linux/bin/permute_exist.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/permute_exist.exe rename to scripts/extensions/hashcat-utils-linux/bin/permute_exist.exe diff --git a/scripts/extensions/hashcat-utils/bin/prepare.bin b/scripts/extensions/hashcat-utils-linux/bin/prepare.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/prepare.bin rename to scripts/extensions/hashcat-utils-linux/bin/prepare.bin diff --git a/scripts/extensions/hashcat-utils/bin/prepare.exe b/scripts/extensions/hashcat-utils-linux/bin/prepare.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/prepare.exe rename to scripts/extensions/hashcat-utils-linux/bin/prepare.exe diff --git a/scripts/extensions/hashcat-utils/bin/remaining.pl b/scripts/extensions/hashcat-utils-linux/bin/remaining.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/remaining.pl rename to scripts/extensions/hashcat-utils-linux/bin/remaining.pl diff --git a/scripts/extensions/hashcat-utils/bin/req-exclude.bin b/scripts/extensions/hashcat-utils-linux/bin/req-exclude.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/req-exclude.bin rename to scripts/extensions/hashcat-utils-linux/bin/req-exclude.bin diff --git a/scripts/extensions/hashcat-utils/bin/req-exclude.exe b/scripts/extensions/hashcat-utils-linux/bin/req-exclude.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/req-exclude.exe rename to scripts/extensions/hashcat-utils-linux/bin/req-exclude.exe diff --git a/scripts/extensions/hashcat-utils/bin/req-include.bin b/scripts/extensions/hashcat-utils-linux/bin/req-include.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/req-include.bin rename to scripts/extensions/hashcat-utils-linux/bin/req-include.bin diff --git a/scripts/extensions/hashcat-utils/bin/req-include.exe b/scripts/extensions/hashcat-utils-linux/bin/req-include.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/req-include.exe rename to scripts/extensions/hashcat-utils-linux/bin/req-include.exe diff --git a/scripts/extensions/hashcat-utils/bin/rli.bin b/scripts/extensions/hashcat-utils-linux/bin/rli.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rli.bin rename to scripts/extensions/hashcat-utils-linux/bin/rli.bin diff --git a/scripts/extensions/hashcat-utils/bin/rli.exe b/scripts/extensions/hashcat-utils-linux/bin/rli.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rli.exe rename to scripts/extensions/hashcat-utils-linux/bin/rli.exe diff --git a/scripts/extensions/hashcat-utils/bin/rli2.bin b/scripts/extensions/hashcat-utils-linux/bin/rli2.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rli2.bin rename to scripts/extensions/hashcat-utils-linux/bin/rli2.bin diff --git a/scripts/extensions/hashcat-utils/bin/rli2.exe b/scripts/extensions/hashcat-utils-linux/bin/rli2.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rli2.exe rename to scripts/extensions/hashcat-utils-linux/bin/rli2.exe diff --git a/scripts/extensions/hashcat-utils/bin/rules_optimize.bin b/scripts/extensions/hashcat-utils-linux/bin/rules_optimize.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rules_optimize.bin rename to scripts/extensions/hashcat-utils-linux/bin/rules_optimize.bin diff --git a/scripts/extensions/hashcat-utils/bin/rules_optimize.exe b/scripts/extensions/hashcat-utils-linux/bin/rules_optimize.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/rules_optimize.exe rename to scripts/extensions/hashcat-utils-linux/bin/rules_optimize.exe diff --git a/scripts/extensions/hashcat-utils/bin/seprule.pl b/scripts/extensions/hashcat-utils-linux/bin/seprule.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/seprule.pl rename to scripts/extensions/hashcat-utils-linux/bin/seprule.pl diff --git a/scripts/extensions/hashcat-utils/bin/splitlen.bin b/scripts/extensions/hashcat-utils-linux/bin/splitlen.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/splitlen.bin rename to scripts/extensions/hashcat-utils-linux/bin/splitlen.bin diff --git a/scripts/extensions/hashcat-utils/bin/splitlen.exe b/scripts/extensions/hashcat-utils-linux/bin/splitlen.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/splitlen.exe rename to scripts/extensions/hashcat-utils-linux/bin/splitlen.exe diff --git a/scripts/extensions/hashcat-utils/bin/strip-bsn.bin b/scripts/extensions/hashcat-utils-linux/bin/strip-bsn.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/strip-bsn.bin rename to scripts/extensions/hashcat-utils-linux/bin/strip-bsn.bin diff --git a/scripts/extensions/hashcat-utils/bin/strip-bsn.exe b/scripts/extensions/hashcat-utils-linux/bin/strip-bsn.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/strip-bsn.exe rename to scripts/extensions/hashcat-utils-linux/bin/strip-bsn.exe diff --git a/scripts/extensions/hashcat-utils/bin/strip-bsr.bin b/scripts/extensions/hashcat-utils-linux/bin/strip-bsr.bin similarity index 100% rename from scripts/extensions/hashcat-utils/bin/strip-bsr.bin rename to scripts/extensions/hashcat-utils-linux/bin/strip-bsr.bin diff --git a/scripts/extensions/hashcat-utils/bin/strip-bsr.exe b/scripts/extensions/hashcat-utils-linux/bin/strip-bsr.exe similarity index 100% rename from scripts/extensions/hashcat-utils/bin/strip-bsr.exe rename to scripts/extensions/hashcat-utils-linux/bin/strip-bsr.exe diff --git a/scripts/extensions/hashcat-utils/bin/tmesis-dynamic.pl b/scripts/extensions/hashcat-utils-linux/bin/tmesis-dynamic.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/tmesis-dynamic.pl rename to scripts/extensions/hashcat-utils-linux/bin/tmesis-dynamic.pl diff --git a/scripts/extensions/hashcat-utils/bin/tmesis.pl b/scripts/extensions/hashcat-utils-linux/bin/tmesis.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/tmesis.pl rename to scripts/extensions/hashcat-utils-linux/bin/tmesis.pl diff --git a/scripts/extensions/hashcat-utils/bin/topmorph.pl b/scripts/extensions/hashcat-utils-linux/bin/topmorph.pl similarity index 100% rename from scripts/extensions/hashcat-utils/bin/topmorph.pl rename to scripts/extensions/hashcat-utils-linux/bin/topmorph.pl diff --git a/scripts/extensions/hashcat-utils/src/Makefile b/scripts/extensions/hashcat-utils-linux/src/Makefile similarity index 100% rename from scripts/extensions/hashcat-utils/src/Makefile rename to scripts/extensions/hashcat-utils-linux/src/Makefile diff --git a/scripts/extensions/hashcat-utils/src/cap2hccapx.c b/scripts/extensions/hashcat-utils-linux/src/cap2hccapx.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/cap2hccapx.c rename to scripts/extensions/hashcat-utils-linux/src/cap2hccapx.c diff --git a/scripts/extensions/hashcat-utils/src/cleanup-rules.c b/scripts/extensions/hashcat-utils-linux/src/cleanup-rules.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/cleanup-rules.c rename to scripts/extensions/hashcat-utils-linux/src/cleanup-rules.c diff --git a/scripts/extensions/hashcat-utils/src/combinator.c b/scripts/extensions/hashcat-utils-linux/src/combinator.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/combinator.c rename to scripts/extensions/hashcat-utils-linux/src/combinator.c diff --git a/scripts/extensions/hashcat-utils/src/combinator3.c b/scripts/extensions/hashcat-utils-linux/src/combinator3.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/combinator3.c rename to scripts/extensions/hashcat-utils-linux/src/combinator3.c diff --git a/scripts/extensions/hashcat-utils/src/combipow.c b/scripts/extensions/hashcat-utils-linux/src/combipow.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/combipow.c rename to scripts/extensions/hashcat-utils-linux/src/combipow.c diff --git a/scripts/extensions/hashcat-utils/src/cpu_rules.c b/scripts/extensions/hashcat-utils-linux/src/cpu_rules.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/cpu_rules.c rename to scripts/extensions/hashcat-utils-linux/src/cpu_rules.c diff --git a/scripts/extensions/hashcat-utils/src/cpu_rules.h b/scripts/extensions/hashcat-utils-linux/src/cpu_rules.h similarity index 100% rename from scripts/extensions/hashcat-utils/src/cpu_rules.h rename to scripts/extensions/hashcat-utils-linux/src/cpu_rules.h diff --git a/scripts/extensions/hashcat-utils/src/ct3_to_ntlm.c b/scripts/extensions/hashcat-utils-linux/src/ct3_to_ntlm.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/ct3_to_ntlm.c rename to scripts/extensions/hashcat-utils-linux/src/ct3_to_ntlm.c diff --git a/scripts/extensions/hashcat-utils/src/cutb.c b/scripts/extensions/hashcat-utils-linux/src/cutb.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/cutb.c rename to scripts/extensions/hashcat-utils-linux/src/cutb.c diff --git a/scripts/extensions/hashcat-utils/src/deskey_to_ntlm.pl b/scripts/extensions/hashcat-utils-linux/src/deskey_to_ntlm.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/deskey_to_ntlm.pl rename to scripts/extensions/hashcat-utils-linux/src/deskey_to_ntlm.pl diff --git a/scripts/extensions/hashcat-utils/src/expander.c b/scripts/extensions/hashcat-utils-linux/src/expander.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/expander.c rename to scripts/extensions/hashcat-utils-linux/src/expander.c diff --git a/scripts/extensions/hashcat-utils/src/gate.c b/scripts/extensions/hashcat-utils-linux/src/gate.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/gate.c rename to scripts/extensions/hashcat-utils-linux/src/gate.c diff --git a/scripts/extensions/hashcat-utils/src/generate-rules.c b/scripts/extensions/hashcat-utils-linux/src/generate-rules.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/generate-rules.c rename to scripts/extensions/hashcat-utils-linux/src/generate-rules.c diff --git a/scripts/extensions/hashcat-utils/src/hcstat2gen.c b/scripts/extensions/hashcat-utils-linux/src/hcstat2gen.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/hcstat2gen.c rename to scripts/extensions/hashcat-utils-linux/src/hcstat2gen.c diff --git a/scripts/extensions/hashcat-utils/src/hcstatgen.c b/scripts/extensions/hashcat-utils-linux/src/hcstatgen.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/hcstatgen.c rename to scripts/extensions/hashcat-utils-linux/src/hcstatgen.c diff --git a/scripts/extensions/hashcat-utils/src/keyspace.c b/scripts/extensions/hashcat-utils-linux/src/keyspace.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/keyspace.c rename to scripts/extensions/hashcat-utils-linux/src/keyspace.c diff --git a/scripts/extensions/hashcat-utils/src/len.c b/scripts/extensions/hashcat-utils-linux/src/len.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/len.c rename to scripts/extensions/hashcat-utils-linux/src/len.c diff --git a/scripts/extensions/hashcat-utils/src/mli2.c b/scripts/extensions/hashcat-utils-linux/src/mli2.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/mli2.c rename to scripts/extensions/hashcat-utils-linux/src/mli2.c diff --git a/scripts/extensions/hashcat-utils/src/morph.c b/scripts/extensions/hashcat-utils-linux/src/morph.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/morph.c rename to scripts/extensions/hashcat-utils-linux/src/morph.c diff --git a/scripts/extensions/hashcat-utils/src/permute.c b/scripts/extensions/hashcat-utils-linux/src/permute.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/permute.c rename to scripts/extensions/hashcat-utils-linux/src/permute.c diff --git a/scripts/extensions/hashcat-utils/src/permute_exist.c b/scripts/extensions/hashcat-utils-linux/src/permute_exist.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/permute_exist.c rename to scripts/extensions/hashcat-utils-linux/src/permute_exist.c diff --git a/scripts/extensions/hashcat-utils/src/prepare.c b/scripts/extensions/hashcat-utils-linux/src/prepare.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/prepare.c rename to scripts/extensions/hashcat-utils-linux/src/prepare.c diff --git a/scripts/extensions/hashcat-utils/src/remaining.pl b/scripts/extensions/hashcat-utils-linux/src/remaining.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/remaining.pl rename to scripts/extensions/hashcat-utils-linux/src/remaining.pl diff --git a/scripts/extensions/hashcat-utils/src/req-exclude.c b/scripts/extensions/hashcat-utils-linux/src/req-exclude.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/req-exclude.c rename to scripts/extensions/hashcat-utils-linux/src/req-exclude.c diff --git a/scripts/extensions/hashcat-utils/src/req-include.c b/scripts/extensions/hashcat-utils-linux/src/req-include.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/req-include.c rename to scripts/extensions/hashcat-utils-linux/src/req-include.c diff --git a/scripts/extensions/hashcat-utils/src/rli.c b/scripts/extensions/hashcat-utils-linux/src/rli.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/rli.c rename to scripts/extensions/hashcat-utils-linux/src/rli.c diff --git a/scripts/extensions/hashcat-utils/src/rli2.c b/scripts/extensions/hashcat-utils-linux/src/rli2.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/rli2.c rename to scripts/extensions/hashcat-utils-linux/src/rli2.c diff --git a/scripts/extensions/hashcat-utils/src/rp_cpu.h b/scripts/extensions/hashcat-utils-linux/src/rp_cpu.h similarity index 100% rename from scripts/extensions/hashcat-utils/src/rp_cpu.h rename to scripts/extensions/hashcat-utils-linux/src/rp_cpu.h diff --git a/scripts/extensions/hashcat-utils/src/rules_optimize.c b/scripts/extensions/hashcat-utils-linux/src/rules_optimize.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/rules_optimize.c rename to scripts/extensions/hashcat-utils-linux/src/rules_optimize.c diff --git a/scripts/extensions/hashcat-utils/src/seprule.pl b/scripts/extensions/hashcat-utils-linux/src/seprule.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/seprule.pl rename to scripts/extensions/hashcat-utils-linux/src/seprule.pl diff --git a/scripts/extensions/hashcat-utils/src/splitlen.c b/scripts/extensions/hashcat-utils-linux/src/splitlen.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/splitlen.c rename to scripts/extensions/hashcat-utils-linux/src/splitlen.c diff --git a/scripts/extensions/hashcat-utils/src/strip-bsn.c b/scripts/extensions/hashcat-utils-linux/src/strip-bsn.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/strip-bsn.c rename to scripts/extensions/hashcat-utils-linux/src/strip-bsn.c diff --git a/scripts/extensions/hashcat-utils/src/strip-bsr.c b/scripts/extensions/hashcat-utils-linux/src/strip-bsr.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/strip-bsr.c rename to scripts/extensions/hashcat-utils-linux/src/strip-bsr.c diff --git a/scripts/extensions/hashcat-utils/src/tmesis-dynamic.pl b/scripts/extensions/hashcat-utils-linux/src/tmesis-dynamic.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/tmesis-dynamic.pl rename to scripts/extensions/hashcat-utils-linux/src/tmesis-dynamic.pl diff --git a/scripts/extensions/hashcat-utils/src/tmesis.pl b/scripts/extensions/hashcat-utils-linux/src/tmesis.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/tmesis.pl rename to scripts/extensions/hashcat-utils-linux/src/tmesis.pl diff --git a/scripts/extensions/hashcat-utils/src/topmorph.pl b/scripts/extensions/hashcat-utils-linux/src/topmorph.pl similarity index 100% rename from scripts/extensions/hashcat-utils/src/topmorph.pl rename to scripts/extensions/hashcat-utils-linux/src/topmorph.pl diff --git a/scripts/extensions/hashcat-utils/src/utils.c b/scripts/extensions/hashcat-utils-linux/src/utils.c similarity index 100% rename from scripts/extensions/hashcat-utils/src/utils.c rename to scripts/extensions/hashcat-utils-linux/src/utils.c diff --git a/scripts/extensions/hashcat-utils-mac/bin/expander.bin b/scripts/extensions/hashcat-utils-mac/bin/expander.bin new file mode 100755 index 0000000..92bf969 Binary files /dev/null and b/scripts/extensions/hashcat-utils-mac/bin/expander.bin differ diff --git a/scripts/extensions/mkpass b/scripts/extensions/mkpass-linux similarity index 100% rename from scripts/extensions/mkpass rename to scripts/extensions/mkpass-linux diff --git a/scripts/extensions/mkpass-mac b/scripts/extensions/mkpass-mac new file mode 100755 index 0000000..13fa060 Binary files /dev/null and b/scripts/extensions/mkpass-mac differ diff --git a/scripts/extensions/pack/LICENSE b/scripts/extensions/pack-linux/LICENSE similarity index 100% rename from scripts/extensions/pack/LICENSE rename to scripts/extensions/pack-linux/LICENSE diff --git a/scripts/extensions/pack/README b/scripts/extensions/pack-linux/README similarity index 100% rename from scripts/extensions/pack/README rename to scripts/extensions/pack-linux/README diff --git a/scripts/extensions/pack/maskgen.py b/scripts/extensions/pack-linux/maskgen.py similarity index 100% rename from scripts/extensions/pack/maskgen.py rename to scripts/extensions/pack-linux/maskgen.py diff --git a/scripts/extensions/pack/policygen.py b/scripts/extensions/pack-linux/policygen.py similarity index 100% rename from scripts/extensions/pack/policygen.py rename to scripts/extensions/pack-linux/policygen.py diff --git a/scripts/extensions/pack/rulegen.py b/scripts/extensions/pack-linux/rulegen.py similarity index 100% rename from scripts/extensions/pack/rulegen.py rename to scripts/extensions/pack-linux/rulegen.py diff --git a/scripts/extensions/pack/statsgen.py b/scripts/extensions/pack-linux/statsgen.py similarity index 100% rename from scripts/extensions/pack/statsgen.py rename to scripts/extensions/pack-linux/statsgen.py diff --git a/scripts/extensions/pack-mac/LICENSE b/scripts/extensions/pack-mac/LICENSE new file mode 100644 index 0000000..1b563eb --- /dev/null +++ b/scripts/extensions/pack-mac/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Peter Kacherginsky +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the 'The Sprawl' nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/scripts/extensions/pack-mac/README b/scripts/extensions/pack-mac/README new file mode 100644 index 0000000..304fc69 --- /dev/null +++ b/scripts/extensions/pack-mac/README @@ -0,0 +1,677 @@ +Password Analysis and Cracking Kit by Peter Kacherginsky (iphelix) +================================================================== + +PACK (Password Analysis and Cracking Toolkit) is a collection of utilities developed to aid in analysis of password lists in order to enhance password cracking through pattern detection of masks, rules, character-sets and other password characteristics. The toolkit generates valid input files for Hashcat family of password crackers. + +NOTE: The toolkit itself is not able to crack passwords, but instead designed to make operation of password crackers more efficient. + +Selecting passwords lists for analysis +====================================== + +Before we can begin using the toolkit we must establish a selection criteria of password lists. Since we are looking to analyze the way people create their passwords, we must obtain as large of a sample of leaked passwords as possible. One such excellent list is based on RockYou.com compromise. This list both provides large and diverse enough collection that provides a good results for common passwords used by similar sites (e.g. social networking). The analysis obtained from this list may not work for organizations with specific password policies. As such, selecting sample input should be as close to your target as possible. In addition, try to avoid obtaining lists based on already cracked passwords as it will generate statistics bias of rules and masks used by individual(s) cracking the list and not actual users. + +StatsGen +======================================= + +The most basic analysis that you can perform is simply obtaining most common length, character-set and other characteristics of passwords in the provided list. In the example below, we will use 'rockyou.txt' containing approximately 14 million passwords. Launch `statsgen.py` with the following command line: + + $ python statsgen.py rockyou.txt + +Below is the output from the above command: + + _ + StatsGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Analyzing passwords in [rockyou.txt] + [+] Analyzing 100% (14344390/14344390) of passwords + NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords + + [*] Length: + [+] 8: 20% (2966037) + [+] 7: 17% (2506271) + [+] 9: 15% (2191039) + [+] 10: 14% (2013695) + [+] 6: 13% (1947798) + ... + + [*] Character-set: + [+] loweralphanum: 42% (6074867) + [+] loweralpha: 25% (3726129) + [+] numeric: 16% (2346744) + [+] loweralphaspecialnum: 02% (426353) + [+] upperalphanum: 02% (407431) + ... + + [*] Password complexity: + [+] digit: min(0) max(255) + [+] lower: min(0) max(255) + [+] upper: min(0) max(187) + [+] special: min(0) max(255) + + [*] Simple Masks: + [+] stringdigit: 37% (5339556) + [+] string: 28% (4115314) + [+] digit: 16% (2346744) + [+] digitstring: 04% (663951) + [+] othermask: 04% (576324) + ... + + [*] Advanced Masks: + [+] ?l?l?l?l?l?l?l?l: 04% (687991) + [+] ?l?l?l?l?l?l: 04% (601152) + [+] ?l?l?l?l?l?l?l: 04% (585013) + [+] ?l?l?l?l?l?l?l?l?l: 03% (516830) + [+] ?d?d?d?d?d?d?d: 03% (487429) + ... + + +NOTE: You can reduce the number of outliers displayed by including the --hiderare flag which will not show any items with occurrence of less than 1%. + +Here is what we can immediately learn from the above list: + + * Most of the passwords have length 6 to 10 characters. + * The majority of passwords have loweralphanumeric character-set. + * There is no obvious minimum or maximum password complexity. + * Analyzed passwords tend to follow a simple masks "string followed by digits". + +The last section, "Advanced Masks", contains most frequently occurring masks using the Hashcat format. Individual symbols can be interpreted as follows: + + ?l - a single lowercase character + ?u - a single uppercase character + ?d - a single digit + ?s - a single special character + +For example, the very first mask, "?l?l?l?l?l?l?l?l", will match all of the lowercase alpha passwords. Given the sample size you will be able to crack approximately 4% of passwords. However, after generating the initial output, you may be interested in using filters to narrow down on password data. + +Using filters +------------- + +Let's see how RockYou users tend to select their passwords using the "stringdigit" simple mask (a string followed by numbers): + + $ python statsgen.py ../PACK-0.0.3/archive/rockyou.txt --simplemask stringdigit -q --hiderare + + [*] Analyzing passwords in [rockyou.txt] + [+] Analyzing 37% (5339556/14344390) of passwords + NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords + + [*] Length: + [+] 8: 23% (1267260) + [+] 7: 18% (981432) + [+] 9: 17% (939971) + [+] 10: 14% (750938) + [+] 6: 11% (618983) + [+] 11: 05% (294869) + [+] 12: 03% (175875) + [+] 13: 01% (103047) + [+] 14: 01% (65958) + + [*] Character-set: + [+] loweralphanum: 88% (4720184) + [+] upperalphanum: 06% (325941) + [+] mixedalphanum: 05% (293431) + + [*] Password complexity: + [+] digit: min(1) max(252) + [+] lower: min(0) max(46) + [+] upper: min(0) max(31) + [+] special: min(0) max(0) + + [*] Simple Masks: + [+] stringdigit: 100% (5339556) + + [*] Advanced Masks: + [+] ?l?l?l?l?l?l?d?d: 07% (420318) + [+] ?l?l?l?l?l?d?d: 05% (292306) + [+] ?l?l?l?l?l?l?l?d?d: 05% (273624) + [+] ?l?l?l?l?d?d?d?d: 04% (235360) + [+] ?l?l?l?l?d?d: 04% (215074) + ... + +The very top of the output specifies what percentage of total passwords was analyzed. In this case, by cracking only passwords matching the "stringdigit" mask it is only possible to recover about 37% of the total set just as was displayed in the original output. Next, it appears that only 11% of this password type use anything other than lowercase. So it would be smart to concentrate on only lowercase strings matching this mask. At last, in the "Advanced Mask" section we can see that the majority of "stringdigit" passwords consist of a string with two or four digits following it. With the information gained from the above output, we can begin creating a mental image of target users' password generation patterns. + +There are a few other filters available for password length, mask, and character sets: + +**Length:** --minlength and/or --maxlength + +**Simple Mask:** --simplemask [numeric, loweralpha, upperalpha, mixedalpha, loweralphanum, etc.] + +**Character sets:** --charset [digit, string, stringdigit, digitstring, digitstringdigit, etc.] + +NOTE: More than one filter of the same class can be specified as a comma-separated list: + + --simplemask="stringdigit,digitstring" + +Saving advanced masks +--------------------- + +While the "Advanced Mask" section only displays patterns matching greater than 1% of all passwords, you can obtain and save a full list of password masks matching a given dictionary by using the following command: + + $ python statsgen.py rockyou.txt -o rockyou.masks + +All of the password masks and their frequencies will be saved into the specified file in the CSV format. Naturally, you can provide filters to only generate masks file matching specified parameters. The output file can be used as an input to MaskGen tool covered in the next section. + +MaskGen +================== + +MaskGen allows you to craft pattern-based mask attacks for input into Hashcat family of password crackers. The tool uses output produced by statsgen above with the '-o' flag in order to produce the most optimal mask attack sorted by mask complexity, mask occurrence or ratio of the two (optimal index). + +Let's run MaskGen with only StatGen's output as an argument: + + $ python maskgen.py rockyou.masks + + _ + MaskGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [*] Finished generating masks: + Masks generated: 146578 + Masks coverage: 100% (14344390/14344390) + Masks runtime: >1 year + +There are several pieces of information that you should observe: + + * Default cracking speed used for calculations is 1,000,000,000 keys/sec + * Default sorting mode is [optindex] equivalent to --optindex flag. + * 146,578 unique masks were generated which have 100% coverage + * Total runtime of all generated masks is more than 1 year. + +Specifying target time +---------------------- + +Since you are usually limited in time to perform and craft attacks, maskgen allows you to specify how much time you have to perform mask attacks and will generate the most optimal collection of masks based on the sorting mode. Let's play a bit with different sorting modes and target times: + + $ python maskgen.py rockyou.masks --targettime 600 --optindex -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 779 + Masks coverage: 56% (8116195/14344390) + Masks runtime: 0:11:36 + + + $ python maskgen.py rockyou.masks --targettime 600 --complexity -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [complexity]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 5163 + Masks coverage: 31% (4572346/14344390) + Masks runtime: 0:10:01 + + + $ python maskgen.py rockyou.masks --targettime 600 --occurrence -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [occurrence]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 4 + Masks coverage: 16% (2390986/14344390) + Masks runtime: 1:34:05 + +All of the above runs have target time of 600 seconds (or 10 minutes) with different sorting modes. Based on our experiments, masks generated using OptIndex sorting mode can crack 56% of RockYou passwords in about 10 minutes. At the same time masks generated using Occurrence sorting mode not only have pretty weak coverage of only 16%, but also exceeded specified target time by more than an hour. + +NOTE: Masks sorted by complexity can be very effective when attacking policy based lists. + +Let's see some of the masks generated by maskgen in optindex mode using the --showmasks flag: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q --showmasks + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [L:] Mask: [ Occ: ] [ Time: ] + ... + [ 7] ?l?d?s?l?l?d?d [6 ] [ 0:00:00] + [ 8] ?s?l?l?l?l?l?l?s [3480 ] [ 0:05:36] + [ 9] ?l?l?l?l?d?d?d?d?s [1553 ] [ 0:02:30] + [ 8] ?d?l?d?d?d?l?l?l [47 ] [ 0:00:04] + [ 8] ?d?l?l?d?l?d?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?l?d?d?l?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?d?l?d?d?l?l [47 ] [ 0:00:04] + [ 8] ?d?d?l?l?d?l?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?d?d?l?l?l?l [122 ] [ 0:00:11] + [ 8] ?u?u?d?u?d?d?d?d [18 ] [ 0:00:01] + [ 6] ?d?s?s?s?s?s [4 ] [ 0:00:00] + [10] ?l?l?l?l?l?l?l?l?d?d [213109 ] [ 5:48:02] + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 3970 + Masks coverage: 74% (10620959/14344390) + Masks runtime: 16:10:38 + +Displayed masks follow a pretty intuitive format: + + + [ 9] ?l?l?l?l?d?d?d?d?s [1553 ] [ 0:02:30] + \ \ \ \ + \ \_ generated mask \ \_ mask runtime + \ \ + \_ mask length \_ mask occurrence + + +In the above sample you can see some of the logic that goes into mask generation. For example, while '?s?l?l?l?l?l?l?s' mask has one of the longest runtimes in the sample (5 minutes), it still has higher priority because of its relatively higher occurrence to '?l?l?l?l?d?d?d?d?s'. At the same time, while '?l?d?s?l?l?d?d' has pretty low coverage it still gets a higher priority than other masks because as only a six character mask it executes very quickly. + +Specifying mask filters +----------------------- + +You can further optimize your generated mask attacks by using filters. For example, you may have sufficiently powerful hardware where you can simple bruteforce all of the passwords up to 8 characters. In this case, you can generate masks only greater than 8 characters using the --minlength flag as follows: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q --minlength 8 + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 585 + Masks coverage: 41% (5905182/14344390) + Masks runtime: 15:50:36 + +Naturally the generated mask coverage was reduced, but these filters become useful when preparing a collection of masks when attacking password lists other than the one used to generate them. + +The list below shows additional filters you can use: + + Individual Mask Filter Options: + --minlength=8 Minimum password length + --maxlength=8 Maximum password length + --mintime=3600 Minimum mask runtime (seconds) + --maxtime=3600 Maximum mask runtime (seconds) + --mincomplexity=1 Minimum complexity + --maxcomplexity=100 + Maximum complexity + --minoccurrence=1 Minimum occurrence + --maxoccurrence=100 + Maximum occurrence + +Occurrrence and complexity flags can be particularly powerful to fine-tune generated masks using different sorting modes. + +Saving generated masks +---------------------- + +Once you are satisfied with the above generated masks, you can save them using the -o flag: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q -o rockyou.hcmask + [*] Analyzing masks in [rockyou.masks] + [*] Saving generated masks to [rockyou.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 3970 + Masks coverage: 74% (10620959/14344390) + Masks runtime: 16:10:38 + +This will produce 'rockyou.hcmask' file which can be directly used by Hashcat suite of tools or as part of a custom script that loops through them. + +Checking mask coverage +---------------------- + +It is often useful to see how well generated masks perform against already cracked lists. Maskgen can compare a collection of masks against others to see how well they would perform if masks from one password list would be attempted against another. Let's compare how well masks generated from RockYou list will perform against another compromised list such as Gawker: + + $ python statsgen.py ../PACK-0.0.3/archive/gawker.dic -o gawker.masks + + $ python maskgen.py gawker.masks --checkmasksfile rockyou.hcmask -q + [*] Analyzing masks in [gawker.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Checking coverage of masks in [rockyou.hcmask] + [*] Finished matching masks: + Masks matched: 1775 + Masks coverage: 96% (1048889/1084394) + Masks runtime: 16:25:44 + +Using the '--checkmasksfile' parameter we attempted to run masks inside 'rockyou.hcmask' file generated earlier against masks from a sample leaked list 'gawker.masks'. This results in a good 96% coverage where 1775 of the 3970 total generated RockYou-based masks matched masks in Gawker list. + +It is also possible to see the coverage of one or more masks by specifying them directly on the command-line as follows: + + $ python maskgen.py gawker.masks --checkmasks="?u?l?l?l?l?l?d,?l?l?l?l?l?d?d" -q + [*] Analyzing masks in [gawker.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Checking coverage of the these masks [?u?l?l?l?l?l?d, ?l?l?l?l?l?d?d] + [*] Finished matching masks: + Masks matched: 2 + Masks coverage: 1% (18144/1084394) + Masks runtime: 0:00:04 + +Both of the specified masks matched with only 1% coverage. + +Specifying speed +---------------- + +Depending on your exact hardware specs and target hash you may want to increase or decrease keys/sec speed used during calculations using the '--pps' parameter: + + $ python maskgen.py rockyou.masks --targettime 43200 --pps 50000000 -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 50,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 1192 + Masks coverage: 61% (8754548/14344390) + Masks runtime: 12:17:31 + +Using the '--pps' parameter to match you actual performance makes target time more meaningful. + +PolicyGen +========= + +A lot of the mask and dictionary attacks will fail in the corporate environment with minimum password complexity requirements. Instead of resorting to a pure bruteforcing attack, we can leverage known or guessed password complexity rules to avoid trying password candidates that are not compliant with the policy or inversely only audit for noncompliant passwords. Using PolicyGen, you will be able to generate a collection of masks following the password complexity in order to significantly reduce the cracking time. + +Below is a sample session where we generate all valid password masks for an environment requiring at least one digit, one upper, and one special characters. + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o complexity.hcmask + _ + PolicyGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Saving generated masks to [complexity.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 40824 Time: 35 days, 0:33:09 + +From the above output you can see that we have generated 40824 masks matching the specified complexity that will take about 35 days to run at the speed of 1,000,000,000 keys/sec. + +In case you are simply performing a password audit and tasked to discover only non-compliant passwords you can specify '--noncompliant' flag to invert generated masks: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o noncompliant.hcmask -q --noncompliant + [*] Saving generated masks to [noncompliant.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [non-compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 24712 Time: 41 days, 18:16:55 + +Let's see some of the non-compliant masks generated above using the '--showmasks' flag: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o noncompliant.hcmask -q --noncompliant --showmasks + [*] Saving generated masks to [noncompliant.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [non-compliant] masks. + [*] Generating 8 character password masks. + [ 8] ?d?d?d?d?d?d?d?d [l: 0 u: 0 d: 8 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?l [l: 1 u: 0 d: 7 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?u [l: 0 u: 1 d: 7 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?s [l: 0 u: 0 d: 7 s: 1] [ 0:00:00] + ... + [ 8] ?s?s?s?s?s?s?s?d [l: 0 u: 0 d: 1 s: 7] [ 0:07:06] + [ 8] ?s?s?s?s?s?s?s?l [l: 1 u: 0 d: 0 s: 7] [ 0:18:28] + [ 8] ?s?s?s?s?s?s?s?u [l: 0 u: 1 d: 0 s: 7] [ 0:18:28] + [ 8] ?s?s?s?s?s?s?s?s [l: 0 u: 0 d: 0 s: 8] [ 0:23:26] + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 24712 Time: 41 days, 18:16:55 + +As you can see all of the masks have at least one missing password complexity requirement. Interestingly with fewer generated masks it takes longer to attack because of long running masks like '?s?s?s?s?s?s?s?s'. + +Specifying maximum complexity +----------------------------- + +It is also possible to specify maximum password complexity using --maxlower, --maxupper, --maxdigit and --maxspecial flags in order to fine-tune you attack. For example, below is a sample site which enforces password policy but does not allow any special characters: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --maxspecial 0 -o maxcomplexity.hcmask -q + [*] Saving generated masks to [maxcomplexity.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:None + Max strength: l:None u:None d:None s:0 + [*] Generating [compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 5796 Time: 1 day, 20:20:55 + +Rules Analysis +================== + +`rulegen.py` implements password analysis and rule generation for the Hashcat password cracker as described in the [Automatic Password Rule Analysis and Generation](http://thesprawl.org/research/automatic-password-rule-analysis-generation/) paper. Please review this document for detailed discussion on the theory of rule analysis and generation. + +Reversing source words and word mangling rules from already cracked passwords can be very effective in performing attacks against still encrypted hashes. By continuously recycling/expanding generated rules and words you may be able to crack a greater number of passwords. + +Prerequisites +----------------- +There are several prerequisites for the effective use of `rulegen.py`. The tool utilizes Enchant spell-checking library to interface with a number of spell-checking engines such as Aspell, MySpell, etc. You must install these tools prior to use. It is also critical to install dictionaries for whatever spell-checking engine you end up using (alternatively it is possible to use a custom wordlist). At last, I have bundled PyEnchant for convenience which should interface directly with Enchant's shared libraries; however, should there be any issues, simply remove the bundled 'enchant' directory and install PyEnchant for your distribution. + +For additional details on specific Hashcat rule syntax see [Hashcat Rule Based Attack](http://hashcat.net/wiki/doku.php?id=rule_based_attack). + +Analyzing a Single Password +------------------------------- + +The most basic use of `rulegen.py` involves analysis of a single password to automatically detect rules. Let's detect rules and potential source word used to generate a sample password `P@55w0rd123`: + + $ python rulegen.py --verbose --password P@55w0rd123 + _ + RuleGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: P@55w0rd123 + [-] Pas sword => {edit distance suboptimal: 8 (7)} => P@55w0rd123 + [+] Password => sa@ ss5 so0 $1 $2 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 o81 $2 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 i81 o92 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 i81 i92 oA3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 o81 o92 $3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 o81 i92 oA3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 i81 o92 oA3 => P@55w0rd123 + +There are several flags that we have used for this example: + + * --password - specifies a single password to analyze. + * --verbose - prints out verbose information such as generated rules and performance statistics. + +Processing password files is covered in a section below; however, let's first discuss some of the available fine tuning options using a single password as an example. + +Spell-checking provider +--------------------------- + +Notice that we are using the `aspell` Enchant module for source word detection. The exact spell-checking engine can be changed using the `--provider` flag as follows: + + $ python rulegen.py --verbose --provider myspell --password P@55w0rd123 -q + [*] Using Enchant 'myspell' module. For best results please install + 'myspell' module language dictionaries. + ... + + +NOTE: Provider engine priority can be specified using a comma-separated list (e.g. --provider aspell,myspell). + +Forcing source word +----------------------- + +The use of the source word detection engine can be completely disabled by specifying a source word with the `--word` flag: + + $ python rulegen.py -q --verbose --word word --password P@55w0rd123 + [*] Analyzing password: P@55w0rd123 + [+] word => ^5 ^5 ^@ ^P so0 $1 $2 $3 => P@55w0rd123 + +By specifying different source words you can have a lot of fun experimenting with the rule generation engine. + +Defining Custom Dictionary +------------------------------ + +Inevitably you will come across a point where generating rules using the standard spelling-engine wordlist is no longer sufficient. You can specify a custom wordlist using the `--wordlist` flag. This is particularly useful when reusing source words from a previous analysis session: + + $ python rulegen.py -q --verbose --wordlist rockyou.txt --password 1pa55w0rd1 + [*] Using Enchant 'Personal Wordlist' module. For best results please install + 'Personal Wordlist' module language dictionaries. + [*] Analyzing password: 1pa55w0rd1 + [+] password => ^1 ss5 so0 $1 => 1pa55w0rd1 + +Custom wordlist can be particularly useful when using not normally found words such as slang as well as using already cracked passwords. + +Generating Suboptimal Rules and Words +----------------------------------------- + +While `rulegen.py` attempts to generate and record only the best source words and passwords, there may be cases when you are interested in more results. Use `--morewords` and `--morerules` flags to generate words and rules which may exceed optimal edit distance: + + $ python rulegen.py -q --verbose --password '$m0n3y$' --morerules --morewords + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: $m0n3y$ + [+] money => ^$ so0 se3 $$ => $m0n3y$ + [+] moneys => ^$ so0 se3 o6$ => $m0n3y$ + [+] mingy => ^$ si0 sg3 $$ => $m0n3y$ + [+] many => ^$ sa0 i43 $$ => $m0n3y$ + [+] Mooney => sM$ o1m so0 se3 $$ => $m0n3y$ + +It is possible to further expand generated words using `--maxworddist` and `--maxwords` flags. Similarly, you can produce more rules using `--maxrulelen` and `--maxrules` flags. + +Disabling Advanced Engines +------------------------------ + +`rulegen.py` includes a number of advanced engines to generate better quality words and rules. It is possible to disable them to observe the difference (or if they are causing issues) using `--simplewords` and `--simplerules` flags. Let's observe how both source words and rules change with these flags on: + + $ python rulegen.py -q --verbose --password '$m0n3y$' --simplewords --simplerules + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: $m0n3y$ + [-] Meany => {edit distance suboptimal: 5 (4)} => $m0n3y$ + [+] many => i0$ o20 i43 i6$ => $m0n3y$ + [+] mingy => i0$ o20 o43 i6$ => $m0n3y$ + [+] money => i0$ o20 o43 i6$ => $m0n3y$ + [+] mangy => i0$ o20 o43 i6$ => $m0n3y$ + [+] manky => i0$ o20 o43 i6$ => $m0n3y$ + +Notice the quality of generated words and rules was reduced significantly with words like 'manky' having less relationship to the actual source word 'money'. At the same time, generated rules were reduced to simple insertions, deletions and replacements. + +Processing password lists +----------------------------- + +Now that you have mastered all of the different flags and switches, we can attempt to generate words and rules for a collection of passwords. Let's generate a text file `korelogic.txt` containing the following fairly complex test passwords: + + &~defcon + '#(4)\ + August19681 + '&a123456 + 10-D'Ann + ~|Bailey + Krist0f3r + f@cebOOK + Nuclear$( + zxcvbn2010! + 13Hark's + NjB3qqm + Sydney93? + antalya%] + Annl05de + ;-Fluffy + +Now let's observe `rulegen.py` analysis by simply specifying the password file as the first argument: + + $ python rulegen.py korelogic.txt -q + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing passwords file: korelogic.txt: + [*] Press Ctrl-C to end execution and generate statistical analysis. + [*] Saving rules to analysis.rule + [*] Saving words to analysis.word + [*] Finished processing 16 passwords in 1.00 seconds at the rate of 15.94 p/sec + [*] Generating statistics for [analysis] rules and words. + [-] Skipped 0 all numeric passwords (0.00%) + [-] Skipped 2 passwords with less than 25% alpha characters (12.50%) + [-] Skipped 0 passwords with non ascii characters (0.00%) + + [*] Top 10 rules + [+] ^3 ^1 o4r - 3 (2.00%) + [+] i61 i79 i86 i98 oA1 - 2 (1.00%) + [+] ^- ^0 ^1 i4' o5A - 2 (1.00%) + [+] sS1 i13 T2 - 1 (0.00%) + [+] i61 se9 i86 i98 oA1 - 1 (0.00%) + [+] o61 i79 i86 i98 oA1 - 1 (0.00%) + [+] ^- ^0 ^1 so' i5A - 1 (0.00%) + [+] D3 si0 i55 $e - 1 (0.00%) + [+] i61 i79 se6 i98 oA1 - 1 (0.00%) + [+] i3a o5y o6a i7% o8] - 1 (0.00%) + + [*] Top 10 words + [+] Analyze - 1 (0.00%) + [+] defcon - 1 (0.00%) + [+] Kristen - 1 (0.00%) + [+] Bailey - 1 (0.00%) + [+] Augusts - 1 (0.00%) + [+] Annelid - 1 (0.00%) + [+] Hack's - 1 (0.00%) + [+] antlers - 1 (0.00%) + [+] antelope - 1 (0.00%) + [+] xxxv - 1 (0.00%) + +Using all default settings we were able to produce several high quality rules. The application displays some basic Top 10 rules and words statistics. All of the generated rules and words are saved using basename 'analysis' by default: + +* analysis.word - unsorted and ununiqued source words +* analysis-sorted.word - occurrence sorted and unique source words +* analysis.rule - unsorted and ununiqued rules +* analysis-sorted.rule - occurrence sorted and unique rules + +Notice that several passwords such as '#(4)\ and '&a123456 were skipped because they do not have sufficient characteristics to be processed. Other than alpha character count, the program will skip all numeric passwords and passwords containing non-ASCII characters. The latter is due to a bug in the Enchant engine which I hope to fix in the future thus allowing word processing of many languages. + +Specifying output basename +------------------------------ + +As previously mentioned `rulegen.py` saves output files using the 'analysis' basename by default. You can change file basename with the `--basename` or `-b` flag as follows: + + $ python rulegen.py korelogic.txt -q -b korelogic + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing passwords file: korelogic.txt: + [*] Press Ctrl-C to end execution and generate statistical analysis. + [*] Saving rules to korelogic.rule + [*] Saving words to korelogic.word + + +Debugging rules +-------------------- + +There may be situations where you run into issues generating rules for the Hashcat password cracker. `rulegen.py` includes the `--hashcat` flag to validate generated words and rules using hashcat itself running in --stdout mode. In order for this mode to work correctly, you must download the latest version of hashcat-cli and edit the `HASHCAT_PATH` variable in the source. For example, at the time of this writing I have placed the hashcat-0.## folder in the PACK directory and defined `HASHCAT_PATH` as 'hashcat-0.##/'. + +You can also observe the inner workings of the rule generation engine with the `--debug` flag. Don't worry about messages of certain rule failings, this is the result of the halting problem solver trying to find an optimal and valid solution. + +Conclusion +============== + +While this guide introduces a number of methods to analyze passwords, reverse rules and generate masks, there are a number of other tricks that are waiting for you to discover. I would be excited if you told me about some unusual use or suggestions for any of the covered tools. + +Happy Cracking! + + -Peter \ No newline at end of file diff --git a/scripts/extensions/pack-mac/maskgen.py b/scripts/extensions/pack-mac/maskgen.py new file mode 100755 index 0000000..2147a87 --- /dev/null +++ b/scripts/extensions/pack-mac/maskgen.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# MaskGen - Generate Password Masks +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import csv +import datetime +from optparse import OptionParser, OptionGroup + +VERSION = "0.0.3" + + +class MaskGen: + def __init__(self): + # Masks collections with meta data + self.masks = dict() + + self.target_time = None + self.output_file = None + + self.minlength = None + self.maxlength = None + self.mintime = None + self.maxtime = None + self.mincomplexity = None + self.maxcomplexity = None + self.minoccurrence = None + self.maxoccurrence = None + + # PPS (Passwords per Second) Cracking Speed + self.pps = 1000000000 + self.showmasks = False + + # Counter for total masks coverage + self.total_occurrence = 0 + + @staticmethod + def getcomplexity(mask): + """ Return mask complexity. """ + count = 1 + for char in mask[1:].split("?"): + if char == "l": + count *= 26 + elif char == "u": + count *= 26 + elif char == "d": + count *= 10 + elif char == "s": + count *= 33 + elif char == "a": + count *= 95 + else: + print("[!] Error, unknown mask ?%s in a mask %s" % (char, mask)) + + return count + + def loadmasks(self, filename): + """ Load masks and apply filters. """ + mask_reader = csv.reader(open(filename, 'r'), delimiter=',', quotechar='"') + + for (mask, occurrence) in mask_reader: + + if not mask: + continue + + mask_occurrence = int(occurrence) + mask_length = len(mask) // 2 + mask_complexity = self.getcomplexity(mask) + mask_time = mask_complexity // self.pps + + self.total_occurrence += mask_occurrence + + # Apply filters based on occurrence, length, complexity and time + if (self.minoccurrence is None or mask_occurrence >= self.minoccurrence) and \ + (self.maxoccurrence is None or mask_occurrence <= self.maxoccurrence) and \ + (self.mincomplexity is None or mask_complexity <= self.mincomplexity) and \ + (self.maxcomplexity is None or mask_complexity <= self.maxcomplexity) and \ + (self.mintime is None or mask_time <= self.mintime) and \ + (self.maxtime is None or mask_time <= self.maxtime) and \ + (self.maxlength is None or mask_length <= self.maxlength) and \ + (self.minlength is None or mask_length >= self.minlength): + + self.masks[mask] = dict() + self.masks[mask]['length'] = mask_length + self.masks[mask]['occurrence'] = mask_occurrence + self.masks[mask]['complexity'] = 1 - mask_complexity + self.masks[mask]['time'] = mask_time + self.masks[mask]['optindex'] = 1 - (mask_complexity // mask_occurrence) + + def generate_masks(self, sorting_mode): + """ Generate optimal password masks sorted by occurrence, complexity or optindex """ + sample_count = 0 + sample_time = 0 + sample_occurrence = 0 + + # TODO Group by time here 1 minutes, 1 hour, 1 day, 1 month, 1 year.... + # Group by length 1,2,3,4,5,6,7,8,9,10.... + # Group by occurrence 10%, 20%, 30%, 40%, 50%.... + + if self.showmasks: + print("[L:] Mask: [ Occ: ] [ Time: ]") + + for mask in sorted(self.masks.keys(), key=lambda m: self.masks[m][sorting_mode], reverse=True): + + if self.showmasks: + time_human = ">1 year" if self.masks[mask]['time'] > 60*60*24*365 \ + else str(datetime.timedelta(seconds=self.masks[mask]['time'])) + print("[{:>2}] {:<30} [{:<7}] [{:>8}] ".format(self.masks[mask]['length'], mask, + self.masks[mask]['occurrence'], time_human)) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + sample_occurrence += self.masks[mask]['occurrence'] + sample_time += self.masks[mask]['time'] + sample_count += 1 + + if self.target_time and sample_time > self.target_time: + print("[!] Target time exceeded.") + break + + print("[*] Finished generating masks:") + print(" Masks generated: %s" % sample_count) + print(" Masks coverage: %d%% (%d/%d)" % (sample_occurrence * 100 // self.total_occurrence, + sample_occurrence, self.total_occurrence)) + time_human = ">1 year" if sample_time > 60*60*24*365 else str(datetime.timedelta(seconds=sample_time)) + print(" Masks runtime: %s" % time_human) + + def getmaskscoverage(self, checkmasks): + + sample_count = 0 + sample_occurrence = 0 + + total_complexity = 0 + + if self.showmasks: + print("[L:] Mask: [ Occ: ] [ Time: ]") + + for mask in checkmasks: + mask = mask.strip() + mask_complexity = self.getcomplexity(mask) + + total_complexity += mask_complexity + + if mask in self.masks: + + if self.showmasks: + time_human = ">1 year" if self.masks[mask]['time'] > 60*60*24*365 \ + else str(datetime.timedelta(seconds=self.masks[mask]['time'])) + print("[{:>2}] {:<30} [{:<7}] [{:>8}] ".format(self.masks[mask]['length'], mask, + self.masks[mask]['occurrence'], time_human)) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + sample_occurrence += self.masks[mask]['occurrence'] + sample_count += 1 + + if self.target_time and total_complexity / self.pps > self.target_time: + print("[!] Target time exceeded.") + break + + # TODO: Something wrong here, complexity and time doesn't match with estimated from policygen + total_time = total_complexity / self.pps + time_human = ">1 year" if total_time > 60*60*24*365 else str(datetime.timedelta(seconds=total_time)) + print("[*] Finished matching masks:") + print(" Masks matched: %s" % sample_count) + print(" Masks coverage: %d%% (%d/%d)" % (sample_occurrence * 100 / self.total_occurrence, + sample_occurrence, self.total_occurrence)) + print(" Masks runtime: %s" % time_human) + + +if __name__ == "__main__": + + header = " _ \n" + header += " MaskGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog pass0.masks [pass1.masks ...] [options]", version="%prog "+VERSION) + + parser.add_option("-t", "--targettime", dest="target_time", type="int", metavar="86400", + help="Target time of all masks (seconds)") + parser.add_option("-o", "--outputmasks", dest="output_masks", metavar="masks.hcmask", + help="Save masks to a file") + + filters = OptionGroup(parser, "Individual Mask Filter Options") + filters.add_option("--minlength", dest="minlength", type="int", metavar="8", help="Minimum password length") + filters.add_option("--maxlength", dest="maxlength", type="int", metavar="8", help="Maximum password length") + filters.add_option("--mintime", dest="mintime", type="int", metavar="3600", help="Minimum mask runtime (seconds)") + filters.add_option("--maxtime", dest="maxtime", type="int", metavar="3600", help="Maximum mask runtime (seconds)") + filters.add_option("--mincomplexity", dest="mincomplexity", type="int", metavar="1", help="Minimum complexity") + filters.add_option("--maxcomplexity", dest="maxcomplexity", type="int", metavar="100", help="Maximum complexity") + filters.add_option("--minoccurrence", dest="minoccurrence", type="int", metavar="1", help="Minimum occurrence") + filters.add_option("--maxoccurrence", dest="maxoccurrence", type="int", metavar="100", help="Maximum occurrence") + parser.add_option_group(filters) + + sorting = OptionGroup(parser, "Mask Sorting Options") + sorting.add_option("--optindex", action="store_true", dest="optindex", help="sort by mask optindex (default)", + default=False) + sorting.add_option("--occurrence", action="store_true", dest="occurrence", help="sort by mask occurrence", + default=False) + sorting.add_option("--complexity", action="store_true", dest="complexity", help="sort by mask complexity", + default=False) + parser.add_option_group(sorting) + + coverage = OptionGroup(parser, "Check mask coverage") + coverage.add_option("--checkmasks", dest="checkmasks", help="check mask coverage", + metavar="?u?l?l?l?l?l?d,?l?l?l?l?l?d?d") + coverage.add_option("--checkmasksfile", dest="checkmasks_file", help="check mask coverage in a file", + metavar="masks.hcmask") + parser.add_option_group(coverage) + + parser.add_option("--showmasks", dest="showmasks", help="Show matching masks", action="store_true", default=False) + + misc = OptionGroup(parser, "Miscellaneous options") + misc.add_option("--pps", dest="pps", help="Passwords per Second", type="int", metavar="1000000000") + misc.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + parser.add_option_group(misc) + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print(header) + + if len(args) < 1: + parser.error("no masks file specified! Please provide statsgen output.") + exit(1) + + print("[*] Analyzing masks in [%s]" % args[0]) + + maskgen = MaskGen() + + # Settings + if options.target_time: + maskgen.target_time = options.target_time + if options.output_masks: + print("[*] Saving generated masks to [%s]" % options.output_masks) + maskgen.output_file = open(options.output_masks, 'w') + + # Filters + if options.minlength: + maskgen.minlength = options.minlength + if options.maxlength: + maskgen.maxlength = options.maxlength + if options.mintime: + maskgen.mintime = options.mintime + if options.maxtime: + maskgen.maxtime = options.maxtime + if options.mincomplexity: + maskgen.mincomplexity = options.mincomplexity + if options.maxcomplexity: + maskgen.maxcomplexity = options.maxcomplexity + if options.minoccurrence: + maskgen.minoccurrence = options.minoccurrence + if options.maxoccurrence: + maskgen.maxoccurrence = options.maxoccurrence + + # Misc + if options.pps: + maskgen.pps = options.pps + if options.showmasks: + maskgen.showmasks = options.showmasks + + print("[*] Using {:,d} keys/sec for calculations.".format(maskgen.pps)) + + # Load masks + for arg in args: + maskgen.loadmasks(arg) + + # Matching masks from the command-line + if options.checkmasks: + checkmasks = [m.strip() for m in options.checkmasks.split(',')] + print("[*] Checking coverage of the these masks [%s]" % ", ".join(checkmasks)) + maskgen.getmaskscoverage(checkmasks) + + # Matching masks from a file + elif options.checkmasks_file: + checkmasks_file = open(options.checkmasks_file, 'r') + print("[*] Checking coverage of masks in [%s]" % options.checkmasks_file) + maskgen.getmaskscoverage(checkmasks_file) + + # Printing masks in a file + else: + # Process masks according to specified sorting algorithm + if options.occurrence: + sort_mode = "occurrence" + elif options.complexity: + sort_mode = "complexity" + else: + sort_mode = "optindex" + + print("[*] Sorting masks by their [%s]." % sort_mode) + maskgen.generate_masks(sort_mode) diff --git a/scripts/extensions/pack-mac/policygen.py b/scripts/extensions/pack-mac/policygen.py new file mode 100755 index 0000000..108d759 --- /dev/null +++ b/scripts/extensions/pack-mac/policygen.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# PolicyGen - Analyze and Generate password masks according to a password policy +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.2 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import datetime +from optparse import OptionParser, OptionGroup +import itertools + +VERSION = "0.0.2" + + +class PolicyGen: + def __init__(self): + self.output_file = None + + self.minlength = 8 + self.maxlength = 8 + self.mindigit = None + self.minlower = None + self.minupper = None + self.minspecial = None + self.maxdigit = None + self.maxlower = None + self.maxupper = None + self.maxspecial = None + + # PPS (Passwords per Second) Cracking Speed + self.pps = 1000000000 + self.showmasks = False + + @staticmethod + def getcomplexity(mask): + """ Return mask complexity. """ + count = 1 + for char in mask[1:].split("?"): + if char == "l": + count *= 26 + elif char == "u": + count *= 26 + elif char == "d": + count *= 10 + elif char == "s": + count *= 33 + elif char == "a": + count *= 95 + else: + print("[!] Error, unknown mask ?%s in a mask %s" % (char, mask)) + + return count + + def generate_masks(self, noncompliant): + """ Generate all possible password masks matching the policy """ + + total_count = 0 + sample_count = 0 + + # NOTE: It is better to collect total complexity + # not to lose precision when dividing by pps + total_complexity = 0 + sample_complexity = 0 + + # TODO: Randomize or even statistically arrange matching masks + for length in range(self.minlength, self.maxlength + 1): + print("[*] Generating %d character password masks." % length) + total_length_count = 0 + sample_length_count = 0 + + total_length_complexity = 0 + sample_length_complexity = 0 + + for masklist in itertools.product(['?d', '?l', '?u', '?s'], repeat=length): + + mask = ''.join(masklist) + + lowercount = 0 + uppercount = 0 + digitcount = 0 + specialcount = 0 + + mask_complexity = self.getcomplexity(mask) + + total_length_count += 1 + total_length_complexity += mask_complexity + + # Count character types in a mask + for char in mask[1:].split("?"): + if char == "l": + lowercount += 1 + elif char == "u": + uppercount += 1 + elif char == "d": + digitcount += 1 + elif char == "s": + specialcount += 1 + + # Filter according to password policy + # NOTE: Perform exact opposite (XOR) operation if noncompliant + # flag was set when calling the function. + if ((self.minlower is None or lowercount >= self.minlower) and + (self.maxlower is None or lowercount <= self.maxlower) and + (self.minupper is None or uppercount >= self.minupper) and + (self.maxupper is None or uppercount <= self.maxupper) and + (self.mindigit is None or digitcount >= self.mindigit) and + (self.maxdigit is None or digitcount <= self.maxdigit) and + (self.minspecial is None or specialcount >= self.minspecial) and + (self.maxspecial is None or specialcount <= self.maxspecial)) ^ noncompliant: + + sample_length_count += 1 + sample_length_complexity += mask_complexity + + if self.showmasks: + mask_time = mask_complexity // self.pps + time_human = ">1 year" if mask_time > 60 * 60 * 24 * 365 \ + else str(datetime.timedelta(seconds=mask_time)) + print("[{:>2}] {:<30} [l:{:>2} u:{:>2} d:{:>2} s:{:>2}] [{:>8}] ".format(length, mask, + lowercount, + uppercount, + digitcount, + specialcount, + time_human)) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + total_count += total_length_count + sample_count += sample_length_count + + total_complexity += total_length_complexity + sample_complexity += sample_length_complexity + + total_time = total_complexity // self.pps + total_time_human = ">1 year" if total_time > 60 * 60 * 24 * 365 else str(datetime.timedelta(seconds=total_time)) + print("[*] Total Masks: %d Time: %s" % (total_count, total_time_human)) + + sample_time = sample_complexity // self.pps + sample_time_human = ">1 year" if sample_time > 60 * 60 * 24 * 365 else str( + datetime.timedelta(seconds=sample_time)) + print("[*] Policy Masks: %d Time: %s" % (sample_count, sample_time_human)) + + +if __name__ == "__main__": + + header = " _ \n" + header += " PolicyGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + # parse command line arguments + parser = OptionParser("%prog [options]\n\nType --help for more options", version="%prog " + VERSION) + parser.add_option("-o", "--outputmasks", dest="output_masks", help="Save masks to a file", metavar="masks.hcmask") + parser.add_option("--pps", dest="pps", help="Passwords per Second", type="int", metavar="1000000000") + parser.add_option("--showmasks", dest="showmasks", help="Show matching masks", action="store_true", default=False) + parser.add_option("--noncompliant", dest="noncompliant", help="Generate masks for noncompliant passwords", + action="store_true", default=False) + + group = OptionGroup(parser, "Password Policy", + "Define the minimum (or maximum) password strength policy that you would like to test") + group.add_option("--minlength", dest="minlength", type="int", metavar="8", default=8, + help="Minimum password length") + group.add_option("--maxlength", dest="maxlength", type="int", metavar="8", default=8, + help="Maximum password length") + group.add_option("--mindigit", dest="mindigit", type="int", metavar="1", help="Minimum number of digits") + group.add_option("--minlower", dest="minlower", type="int", metavar="1", + help="Minimum number of lower-case characters") + group.add_option("--minupper", dest="minupper", type="int", metavar="1", + help="Minimum number of upper-case characters") + group.add_option("--minspecial", dest="minspecial", type="int", metavar="1", + help="Minimum number of special characters") + group.add_option("--maxdigit", dest="maxdigit", type="int", metavar="3", help="Maximum number of digits") + group.add_option("--maxlower", dest="maxlower", type="int", metavar="3", + help="Maximum number of lower-case characters") + group.add_option("--maxupper", dest="maxupper", type="int", metavar="3", + help="Maximum number of upper-case characters") + group.add_option("--maxspecial", dest="maxspecial", type="int", metavar="3", + help="Maximum number of special characters") + parser.add_option_group(group) + + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print(header) + + policygen = PolicyGen() + + # Settings + if options.output_masks: + print("[*] Saving generated masks to [%s]" % options.output_masks) + policygen.output_file = open(options.output_masks, 'w') + + # Password policy + if options.minlength is not None: + policygen.minlength = options.minlength + if options.maxlength is not None: + policygen.maxlength = options.maxlength + if options.mindigit is not None: + policygen.mindigit = options.mindigit + if options.minlower is not None: + policygen.minlower = options.minlower + if options.minupper is not None: + policygen.minupper = options.minupper + if options.minspecial is not None: + policygen.minspecial = options.minspecial + if options.maxdigit is not None: + policygen.maxdigit = options.maxdigit + if options.maxlower is not None: + policygen.maxlower = options.maxlower + if options.maxupper is not None: + policygen.maxupper = options.maxupper + if options.maxspecial is not None: + policygen.maxspecial = options.maxspecial + + # Misc + if options.pps: + policygen.pps = options.pps + if options.showmasks: + policygen.showmasks = options.showmasks + + print("[*] Using {:,d} keys/sec for calculations.".format(policygen.pps)) + + # Print current password policy + print("[*] Password policy:") + print(" Pass Lengths: min:%d max:%d" % (policygen.minlength, policygen.maxlength)) + print(" Min strength: l:%s u:%s d:%s s:%s" % ( + policygen.minlower, policygen.minupper, policygen.mindigit, policygen.minspecial)) + print(" Max strength: l:%s u:%s d:%s s:%s" % ( + policygen.maxlower, policygen.maxupper, policygen.maxdigit, policygen.maxspecial)) + + print("[*] Generating [%s] masks." % ("compliant" if not options.noncompliant else "non-compliant")) + policygen.generate_masks(options.noncompliant) diff --git a/scripts/extensions/pack-mac/rulegen.py b/scripts/extensions/pack-mac/rulegen.py new file mode 100755 index 0000000..94bf12a --- /dev/null +++ b/scripts/extensions/pack-mac/rulegen.py @@ -0,0 +1,1233 @@ +#!/usr/bin/env python3 +# Rulegen.py - Advanced automated password rule and wordlist generator for the +# Hashcat password cracker using the Levenshtein Reverse Path +# algorithm and Enchant spell checking library. +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import sys +import re +import time + +import multiprocessing +import subprocess +import enchant +from optparse import OptionParser, OptionGroup +from collections import Counter + +VERSION = "0.0.4" + +# Testing rules with hashcat --stdout +HASHCAT_PATH = "hashcat/" + + +# Rule Generator class responsible for the complete cycle of rule generation +class RuleGen: + # Initialize Rule Generator class + def __init__(self, language="en", providers="aspell,myspell", basename='analysis', + threads=multiprocessing.cpu_count()): + + self.threads = threads + + self.enchant_broker = enchant.Broker() + self.enchant_broker.set_ordering("*", providers) + + self.enchant = enchant.Dict(language, self.enchant_broker) + + # Output options + self.basename = basename + + # Finetuning word generation + self.max_word_dist = 10 + self.max_words = 10 + self.more_words = False + self.simple_words = False + + # Finetuning rule generation + self.max_rule_len = 10 + self.max_rules = 10 + self.more_rules = False + self.simple_rules = False + self.brute_rules = False + + # Debugging options + self.verbose = False + self.debug = False + self.word = None # Custom word to use. + self.quiet = False + + ######################################################################## + # Word and Rule Statistics + self.numeric_stats_total = 0 + self.special_stats_total = 0 + self.foreign_stats_total = 0 + + ######################################################################## + # Preanalysis Password Patterns + self.password_pattern = dict() + self.password_pattern["insertion"] = re.compile('^[^a-z]*(?P.+?)[^a-z]*$', re.IGNORECASE) + self.password_pattern["email"] = re.compile('^(?P.+?)@[A-Z0-9.-]+\.[A-Z]{2,4}', re.IGNORECASE) + self.password_pattern["alldigits"] = re.compile('^(\d+)$', re.IGNORECASE) + self.password_pattern["allspecial"] = re.compile('^([^a-z0-9]+)$', re.IGNORECASE) + + ######################################################################## + # Hashcat Rules Engine + self.hashcat_rule = dict() + + ###################### + # Dummy rule # + ###################### + + # Do nothing + self.hashcat_rule[':'] = lambda x: x + + ###################### + # Case rules # + ###################### + + # Lowercase all letters + self.hashcat_rule["l"] = lambda x: x.lower() + # Capitalize all letters + self.hashcat_rule["u"] = lambda x: x.upper() + # Capitalize the first letter + self.hashcat_rule["c"] = lambda x: x.capitalize() + # Lowercase the first found character, uppercase the rest + self.hashcat_rule["C"] = lambda x: x[0].lower() + x[1:].upper() + # Toggle the case of all characters in word + self.hashcat_rule["t"] = lambda x: x.swapcase() + # Toggle the case of characters at position N + self.hashcat_rule["T"] = lambda x, y: x[:y] + x[y].swapcase() + x[y + 1:] + # Upper case the first letter and every letter after a space + self.hashcat_rule["E"] = lambda x: " ".join([i[0].upper() + i[1:] for i in x.split(" ")]) + + ###################### + # Rotation rules # + ###################### + + # Reverse the entire word + self.hashcat_rule["r"] = lambda x: x[::-1] + # Rotate the word left + self.hashcat_rule["{"] = lambda x: x[1:] + x[0] + # Rotate the word right + self.hashcat_rule["}"] = lambda x: x[-1] + x[:-1] + + ###################### + # Duplication rules # + ###################### + + # Duplicate entire word + self.hashcat_rule["d"] = lambda x: x + x + # Duplicate entire word N times + self.hashcat_rule["p"] = lambda x, y: x * y + # Duplicate word reversed + self.hashcat_rule["f"] = lambda x: x + x[::-1] + # Duplicate first character N times + self.hashcat_rule["z"] = lambda x, y: x[0] * y + x + # Duplicate last character N times + self.hashcat_rule["Z"] = lambda x, y: x + x[-1] * y + # Duplicate every character + self.hashcat_rule["q"] = lambda x: "".join([i + i for i in x]) + # Duplicate first N characters + self.hashcat_rule["y"] = lambda x, y: x[:y] + x + # Duplicate last N characters + self.hashcat_rule["Y"] = lambda x, y: x + x[-y:] + + ###################### + # Cutting rules # + ###################### + + # Delete first character + self.hashcat_rule["["] = lambda x: x[1:] + # Delete last character + self.hashcat_rule["]"] = lambda x: x[:-1] + # Deletes character at position N + self.hashcat_rule["D"] = lambda x, y: x[:y] + x[y + 1:] + # Truncate word at position N + self.hashcat_rule["'"] = lambda x, y: x[:y] + # Delete M characters, starting at position N + self.hashcat_rule["O"] = lambda x, y, z: x[:y] + x[y + z:] + # Extracts M characters, starting at position N + self.hashcat_rule["'"] = lambda x, y, z: x[y:y+z] + # Purge all instances of X + self.hashcat_rule["@"] = lambda x, y: x.replace(y, '') + + ###################### + # Insertion rules # + ###################### + + # Append character to end + self.hashcat_rule["$"] = lambda x, y: x + y + # Prepend character to front + self.hashcat_rule["^"] = lambda x, y: y + x + # Insert character X at position N + self.hashcat_rule["i"] = lambda x, y, z: x[:y] + z + x[y:] + + ###################### + # Replacement rules # + ###################### + + # Overwrite character at position N with X + self.hashcat_rule["o"] = lambda x, y, z: x[:y] + z + x[y + 1:] + # Replace all instances of X with Y + self.hashcat_rule["s"] = lambda x, y, z: x.replace(y, z) + # Bitwise shift left character @ N + self.hashcat_rule["L"] = lambda x, y: x[:y] + chr(ord(x[y]) << 1) + x[y + 1:] + # Bitwise shift right character @ N + self.hashcat_rule["R"] = lambda x, y: x[:y] + chr(ord(x[y]) >> 1) + x[y + 1:] + # Increment character @ N by 1 ascii value + self.hashcat_rule["+"] = lambda x, y: x[:y] + chr(ord(x[y]) + 1) + x[y + 1:] + # Decrement character @ N by 1 ascii value + self.hashcat_rule["-"] = lambda x, y: x[:y] + chr(ord(x[y]) - 1) + x[y + 1:] + # Replace character @ N with value at @ N plus 1 + self.hashcat_rule["."] = lambda x, y: x[:y] + x[y + 1] + x[y + 1:] + # Replace character @ N with value at @ N minus 1 + self.hashcat_rule[","] = lambda x, y: x[:y] + x[y - 1] + x[y + 1:] + + ###################### + # Swapping rules # + ###################### + + # Swap first two characters + self.hashcat_rule["k"] = lambda x: x[1] + x[0] + x[2:] + # Swap last two characters + self.hashcat_rule["K"] = lambda x: x[:-2] + x[-1] + x[-2] + # Swap character X with Y + self.hashcat_rule["*"] = lambda x, y, z: x[:y] + x[z] + x[y + 1:z] + x[y] + x[z + 1:] if z > y \ + else x[:z] + x[y] + x[z + 1:y] + x[z] + x[y + 1:] + + ######################################################################## + # Common numeric and special character substitutions (1337 5p34k) + self.leet = { + '1': 'i', + '2': 'z', + '3': 'e', + '4': 'a', + '5': 's', + '6': 'b', + '7': 't', + '8': 'b', + '9': 'g', + '0': 'o', + '!': 'i', + '|': 'i', + '@': 'a', + '$': 's', + '+': 't' + } + + ######################################################################## + # Preanalysis rules to bruteforce for each word + self.preanalysis_rules = [] + self.preanalysis_rules.append(([], self.hashcat_rule[':'])) # Blank rule + self.preanalysis_rules.append((['r'], self.hashcat_rule['r'])) # Reverse rule + # self.preanalysis_rules.append((['{'],self.hashcat_rule['}'])) # Rotate left + # self.preanalysis_rules.append((['}'],self.hashcat_rule['{'])) # Rotate right + + ############################################################################ + # Calculate Levenshtein edit path matrix + @staticmethod + def levenshtein(word, password): + matrix = [] + + # Generate and populate the initial matrix + for i in range(len(password) + 1): + matrix.append([]) + for j in range(len(word) + 1): + if i == 0: + matrix[i].append(j) + elif j == 0: + matrix[i].append(i) + else: + matrix[i].append(0) + + # Calculate edit distance for each substring + for i in range(1, len(password) + 1): + for j in range(1, len(word) + 1): + if password[i - 1] == word[j - 1]: + matrix[i][j] = matrix[i - 1][j - 1] + else: + insertion = matrix[i - 1][j] + 1 + deletion = matrix[i][j - 1] + 1 + substitution = matrix[i - 1][j - 1] + 1 + matrix[i][j] = min(insertion, deletion, substitution) + + return matrix + + def levenshtein_distance(self, s1, s2): + """Calculate the Levenshtein distance between two strings. + + This is straight from Wikipedia. + """ + if len(s1) < len(s2): + return self.levenshtein_distance(s2, s1) + if not s1: + return len(s2) + + previous_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] + + @staticmethod + def levenshtein_print(matrix, word, password): + """ Print word X password matrix """ + print(" %s" % " ".join(list(word))) + for i, row in enumerate(matrix): + if i == 0: + print(" ", end=' ') + else: + print(password[i - 1], end=' ') + print(" ".join("%2d" % col for col in row)) + + def generate_levenshtein_rules(self, word, password): + """ Generates levenshtein rules. Returns a list of lists of levenshtein rules. """ + + # 1) Generate Levenshtein matrix + matrix = self.levenshtein(word, password) + + # 2) Trace reverse paths through the matrix. + paths = self.levenshtein_reverse_recursive(matrix, len(matrix) - 1, len(matrix[0]) - 1, 0) + + # 3) Return a collection of reverse paths. + return [path for path in paths if len(path) <= matrix[-1][-1]] + + def levenshtein_reverse_recursive(self, matrix, i, j, path_len): + """ Calculate reverse Levenshtein paths. + Recursive, Depth First, Short-circuited algorithm by Peter Kacherginsky + Generates a list of edit operations necessary to transform a source word + into a password. Edit operations are recorded in the form: + (operation, password_offset, word_offset) + Where an operation can be either insertion, deletion or replacement. + """ + + if i == 0 and j == 0 or path_len > matrix[-1][-1]: + return [[]] + else: + paths = list() + + cost = matrix[i][j] + + # Calculate minimum cost of each operation + cost_delete = cost_insert = cost_equal_or_replace = sys.maxsize + if i > 0: + cost_insert = matrix[i - 1][j] + if j > 0: + cost_delete = matrix[i][j - 1] + if i > 0 and j > 0: + cost_equal_or_replace = matrix[i - 1][j - 1] + cost_min = min(cost_delete, cost_insert, cost_equal_or_replace) + + # Recurse through reverse path for each operation + if cost_insert == cost_min: + insert_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j, path_len + 1) + for insert_path in insert_paths: + paths.append(insert_path + [('insert', i - 1, j)]) + + if cost_delete == cost_min: + delete_paths = self.levenshtein_reverse_recursive(matrix, i, j - 1, path_len + 1) + for delete_path in delete_paths: + paths.append(delete_path + [('delete', i, j - 1)]) + + if cost_equal_or_replace == cost_min: + if cost_equal_or_replace == cost: + equal_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j - 1, path_len) + for equal_path in equal_paths: + paths.append(equal_path) + else: + replace_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j - 1, path_len + 1) + for replace_path in replace_paths: + paths.append(replace_path + [('replace', i - 1, j - 1)]) + + return paths + + def load_custom_wordlist(self, wordlist_file): + self.enchant = enchant.request_pwl_dict(wordlist_file) + + def generate_words(self, password): + """ Generate source word candidates.""" + + if self.debug: + print("[*] Generating source words for %s" % password) + + words = list() + words_collection = list() + + # Let's collect best edit distance as soon as possible to prevent + # less efficient pre_rules like reversal and rotation from slowing + # us down with garbage + best_found_distance = 9999 + + ####################################################################### + # Generate words for each preanalysis rule + if not self.brute_rules: + self.preanalysis_rules = self.preanalysis_rules[:1] + + for pre_rule, pre_rule_lambda in self.preanalysis_rules: + + pre_password = pre_rule_lambda(password) + + # Generate word suggestions + if self.word: + suggestions = [self.word] + elif self.simple_words: + suggestions = self.generate_simple_words(pre_password) + else: + suggestions = self.generate_advanced_words(pre_password) + + # HACK: Perform some additional expansion on multi-word suggestions + # TODO: May be I should split these two and see if I can generate + # rules for each of the suggestions + for suggestion in suggestions[:self.max_words]: + suggestion = suggestion.replace(' ', '').replace('-', '') + + if suggestion not in suggestions: + suggestions.append(suggestion) + + if len(suggestions) != len(set(suggestions)): + print(sorted(suggestions)) + print(sorted(set(suggestions))) + + for suggestion in suggestions: + distance = self.levenshtein_distance(suggestion, pre_password) + + words.append({ + 'suggestion': suggestion, + 'distance': distance, + 'password': pre_password, + 'pre_rule': pre_rule, + 'best_rule_length': 9999 + }) + + ####################################################################### + # Perform Optimization + for word in sorted(words, key=lambda w: w["distance"], reverse=False): + + # Optimize for best distance + if not self.more_words: + if word["distance"] < best_found_distance: + best_found_distance = word["distance"] + + elif word["distance"] > best_found_distance: + if self.verbose: + print("[-] %s => {edit distance suboptimal: %d (%d)} => %s" % + (word["suggestion"], word["distance"], best_found_distance, word["password"])) + break + + # Filter words with too big edit distance + if word["distance"] <= self.max_word_dist: + if self.debug: + print("[+] %s => {edit distance: %d (%d)} = > %s" % + (word["suggestion"], word["distance"], best_found_distance, word["password"])) + + words_collection.append(word) + + else: + if self.verbose: + print("[-] %s => {max distance exceeded: %d (%d)} => %s" % + (word["suggestion"], word["distance"], self.max_word_dist, word["password"])) + + if self.max_words: + words_collection = words_collection[:self.max_words] + + return words_collection + + def generate_simple_words(self, password): + """ Generate simple words. A simple spellcheck.""" + + return self.enchant.suggest(password) + + def generate_advanced_words(self, password): + """ Generate advanced words. + Perform some additional non-destructive cleaning to help spell-checkers: + 1) Remove non-alpha prefixes and appendixes. + 2) Perform common pattern matches (e.g. email). + 3) Replace non-alpha character substitutions (1337 5p34k) + """ + + # Remove non-alpha prefix and/or appendix + insertion_matches = self.password_pattern["insertion"].match(password) + if insertion_matches: + password = insertion_matches.group('password') + + # Pattern matches + email_matches = self.password_pattern["email"].match(password) + if email_matches: + password = email_matches.group('password') + + # Replace common special character replacements (1337 5p34k) + preanalysis_password = '' + for c in password: + if c in self.leet: + preanalysis_password += self.leet[c] + else: + preanalysis_password += c + password = preanalysis_password + + if self.debug: + "[*] Preanalysis Password: %s" % password + + return self.enchant.suggest(password) + + ############################################################################ + # Hashcat specific offset definition 0-9,A-Z + @staticmethod + def int_to_hashcat(n): + if n < 10: + return n + else: + return chr(65 + n - 10) + + @staticmethod + def hashcat_to_int(n): + if n.isdigit(): + return int(n) + else: + return ord(n) - 65 + 10 + + def generate_hashcat_rules(self, suggestion, password): + """ Generate hashcat rules. Returns a length sorted list of lists of hashcat rules.""" + + # 2) Generate Levenshtein Rules + lev_rules = self.generate_levenshtein_rules(suggestion, password) + + # 3) Generate Hashcat Rules + hashcat_rules = [] + hashcat_rules_collection = [] + + ####################################################################### + # Generate hashcat rule for each levenshtein rule + for lev_rule in lev_rules: + + if self.simple_rules: + hashcat_rule = self.generate_simple_hashcat_rules(suggestion, lev_rule, password) + else: + hashcat_rule = self.generate_advanced_hashcat_rules(suggestion, lev_rule, password) + + if hashcat_rule is None: + print("[!] Processing FAILED: %s => ;( => %s" % (suggestion, password)) + print(" Sorry about that, please report this failure to") + print(" the developer: iphelix [at] thesprawl.org") + + else: + hashcat_rules.append(hashcat_rule) + + best_found_rule_length = 9999 + + ####################################################################### + # Perform Optimization + for hashcat_rule in sorted(hashcat_rules, key=lambda rule: len(rule)): + + rule_length = len(hashcat_rule) + + if not self.more_rules: + if rule_length < best_found_rule_length: + best_found_rule_length = rule_length + + elif rule_length > best_found_rule_length: + if self.verbose: + print("[-] %s => {best rule length exceeded: %d (%d)} => %s" % + (suggestion, rule_length, best_found_rule_length, password)) + break + + if rule_length <= self.max_rule_len: + hashcat_rules_collection.append(hashcat_rule) + + return hashcat_rules_collection + + def generate_simple_hashcat_rules(self, word, rules, password): + """ Generate basic hashcat rules using only basic insert,delete,replace rules. """ + hashcat_rules = [] + + if self.debug: + print("[*] Simple Processing %s => %s" % (word, password)) + + # Dynamically apply rules to the source word + # NOTE: Special case were word == password this would work as well. + word_rules = word + + for (op, p, w) in rules: + + if self.debug: + print("\t[*] Simple Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules))) + + if op == 'insert': + hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['i'](word_rules, p, password[p]) + + elif op == 'delete': + hashcat_rules.append("D%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['D'](word_rules, p) + + elif op == 'replace': + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + if self.debug: + print("\t[*] Simple Processing Ended: %s => %s => %s" % (word_rules, " ".join(hashcat_rules), password)) + + # Check if rules result in the correct password + if word_rules == password: + return hashcat_rules + else: + if self.debug: + print("[!] Simple Processing FAILED: %s => %s => %s (%s)" % + (word, " ".join(hashcat_rules), password, word_rules)) + return None + + def generate_advanced_hashcat_rules(self, word, rules, password): + """ Generate advanced hashcat rules using full range of available rules. """ + hashcat_rules = [] + + if self.debug: + print("[*] Advanced Processing %s => %s" % (word, password)) + + # Dynamically apply and store rules in word_rules variable. + # NOTE: Special case where word == password this would work as well. + word_rules = word + + # Generate case statistics + password_lower = sum(c.islower() for c in password) + password_upper = sum(c.isupper() for c in password) + + for i, (op, p, w) in enumerate(rules): + + if self.debug: + print("\t[*] Advanced Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules))) + + if op == 'insert': + hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['i'](word_rules, p, password[p]) + + elif op == 'delete': + hashcat_rules.append("D%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['D'](word_rules, p) + + elif op == 'replace': + + # Detecting global replacement such as sXY, l, u, C, c is a non + # trivial problem because different characters may be added or + # removed from the word by other rules. A reliable way to solve + # this problem is to apply all of the rules the source word + # and keep track of its state at any given time. At the same + # time, global replacement rules can be tested by completing + # the rest of the rules using a simplified engine. + + # The sequence of if statements determines the priority of rules + + # This rule was made obsolete by a prior global replacement + if word_rules[p] == password[p]: + if self.debug: + print("\t[*] Advanced Processing Obsolete Rule: %s - %s" % + (word_rules, " ".join(hashcat_rules))) + + # Swapping rules + elif p < len(password) - 1 and p < len(word_rules) - 1 \ + and word_rules[p] == password[p + 1] and word_rules[p + 1] == password[p]: + # Swap first two characters + if p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['k'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("k") + word_rules = self.hashcat_rule['k'](word_rules) + # Swap last two characters + elif p == len(word_rules) - 2 and self.generate_simple_hashcat_rules( + self.hashcat_rule['K'](word_rules), rules[i + 1:], password): + hashcat_rules.append("K") + word_rules = self.hashcat_rule['K'](word_rules) + # Swap any two characters (only adjacent swapping is supported) + elif self.generate_simple_hashcat_rules(self.hashcat_rule['*'](word_rules, p, p + 1), rules[i + 1:], + password): + hashcat_rules.append("*%s%s" % (self.int_to_hashcat(p), self.int_to_hashcat(p + 1))) + word_rules = self.hashcat_rule['*'](word_rules, p, p + 1) + else: + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + # Case Toggle: Uppercased a letter + elif word_rules[p].islower() and word_rules[p].upper() == password[p]: + + # Toggle the case of all characters in word (mixed cases) + if password_upper and password_lower and self.generate_simple_hashcat_rules( + self.hashcat_rule['t'](word_rules), rules[i + 1:], password): + hashcat_rules.append("t") + word_rules = self.hashcat_rule['t'](word_rules) + + # Capitalize all letters + elif self.generate_simple_hashcat_rules(self.hashcat_rule['u'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("u") + word_rules = self.hashcat_rule['u'](word_rules) + + # Capitalize the first letter + elif p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['c'](word_rules), + rules[i + 1:], password): + hashcat_rules.append("c") + word_rules = self.hashcat_rule['c'](word_rules) + + # Toggle the case of characters at position N + else: + hashcat_rules.append("T%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['T'](word_rules, p) + + # Case Toggle: Lowercased a letter + elif word_rules[p].isupper() and word_rules[p].lower() == password[p]: + + # Toggle the case of all characters in word (mixed cases) + if password_upper and password_lower and self.generate_simple_hashcat_rules( + self.hashcat_rule['t'](word_rules), rules[i + 1:], password): + hashcat_rules.append("t") + word_rules = self.hashcat_rule['t'](word_rules) + + # Lowercase all letters + elif self.generate_simple_hashcat_rules(self.hashcat_rule['l'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("l") + word_rules = self.hashcat_rule['l'](word_rules) + + # Lowercase the first found character, uppercase the rest + elif p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['C'](word_rules), + rules[i + 1:], password): + hashcat_rules.append("C") + word_rules = self.hashcat_rule['C'](word_rules) + + # Toggle the case of characters at position N + else: + hashcat_rules.append("T%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['T'](word_rules, p) + + # Special case substitution of 'all' instances (1337 $p34k) + elif word_rules[p].isalpha() and not password[p].isalpha() and self.generate_simple_hashcat_rules( + self.hashcat_rule['s'](word_rules, word_rules[p], password[p]), rules[i + 1:], password): + + # If we have already detected this rule, then skip it thus + # reducing total rule count. + # BUG: Elisabeth => sE3 sl1 u o3Z sE3 => 31IZAB3TH + # if not "s%s%s" % (word_rules[p],password[p]) in hashcat_rules: + hashcat_rules.append("s%s%s" % (word_rules[p], password[p])) + word_rules = self.hashcat_rule['s'](word_rules, word_rules[p], password[p]) + + # Replace next character with current + elif p < len(password) - 1 and p < len(word_rules) - 1 and password[p] == password[p + 1] \ + and password[p] == word_rules[p + 1]: + hashcat_rules.append(".%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['.'](word_rules, p) + + # Replace previous character with current + elif p > 0 and w > 0 and password[p] == password[p - 1] and password[p] == word_rules[p - 1]: + hashcat_rules.append(",%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule[','](word_rules, p) + + # ASCII increment + elif ord(word_rules[p]) + 1 == ord(password[p]): + hashcat_rules.append("+%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['+'](word_rules, p) + + # ASCII decrement + elif ord(word_rules[p]) - 1 == ord(password[p]): + hashcat_rules.append("-%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['-'](word_rules, p) + + # SHIFT left + elif ord(word_rules[p]) << 1 == ord(password[p]): + hashcat_rules.append("L%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['L'](word_rules, p) + + # SHIFT right + elif ord(word_rules[p]) >> 1 == ord(password[p]): + hashcat_rules.append("R%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['R'](word_rules, p) + + # Position based replacements. + else: + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + if self.debug: + print("\t[*] Advanced Processing Ended: %s %s" % (word_rules, " ".join(hashcat_rules))) + + ######################################################################## + # Prefix rules + last_prefix = 0 + prefix_rules = list() + for hashcat_rule in hashcat_rules: + if hashcat_rule[0] == "i" and self.hashcat_to_int(hashcat_rule[1]) == last_prefix: + prefix_rules.append("^%s" % hashcat_rule[2]) + last_prefix += 1 + elif len(prefix_rules): + hashcat_rules = prefix_rules[::-1] + hashcat_rules[len(prefix_rules):] + break + else: + break + else: + hashcat_rules = prefix_rules[::-1] + hashcat_rules[len(prefix_rules):] + + #################################################################### + # Appendix rules + last_appendix = len(password) - 1 + appendix_rules = list() + for hashcat_rule in hashcat_rules[::-1]: + if hashcat_rule[0] == "i" and self.hashcat_to_int(hashcat_rule[1]) == last_appendix: + appendix_rules.append("$%s" % hashcat_rule[2]) + last_appendix -= 1 + elif len(appendix_rules): + hashcat_rules = hashcat_rules[:-len(appendix_rules)] + appendix_rules[::-1] + break + else: + break + else: + hashcat_rules = hashcat_rules[:-len(appendix_rules)] + appendix_rules[::-1] + + #################################################################### + # Truncate left rules + last_precut = 0 + precut_rules = list() + for hashcat_rule in hashcat_rules: + if hashcat_rule[0] == "D" and self.hashcat_to_int(hashcat_rule[1]) == last_precut: + precut_rules.append("[") + elif len(precut_rules): + hashcat_rules = precut_rules[::-1] + hashcat_rules[len(precut_rules):] + break + else: + break + else: + hashcat_rules = precut_rules[::-1] + hashcat_rules[len(precut_rules):] + + #################################################################### + # Truncate right rules + last_postcut = len(password) + postcut_rules = list() + for hashcat_rule in hashcat_rules[::-1]: + + if hashcat_rule[0] == "D" and self.hashcat_to_int(hashcat_rule[1]) >= last_postcut: + postcut_rules.append("]") + elif len(postcut_rules): + hashcat_rules = hashcat_rules[:-len(postcut_rules)] + postcut_rules[::-1] + break + else: + break + else: + hashcat_rules = hashcat_rules[:-len(postcut_rules)] + postcut_rules[::-1] + + # Check if rules result in the correct password + if word_rules == password: + return hashcat_rules + else: + if self.debug: + print("[!] Advanced Processing FAILED: %s => %s => %s (%s)" % + (word, " ".join(hashcat_rules), password, word_rules)) + return None + + def check_reversible_password(self, password): + """ Check whether the password is likely to be reversed successfuly. """ + + # Skip all numeric passwords + if password.isdigit(): + if self.verbose and not self.quiet: + print("[!] %s => {skipping numeric} => %s" % (password, password)) + self.numeric_stats_total += 1 + return False + + # Skip passwords with less than 25% of alpha character + # TODO: Make random word detection more reliable based on word entropy. + elif sum(c.isalpha() for c in password) < len(password) // 4: + if self.verbose and not self.quiet: + print("[!] %s => {skipping alpha less than 25%%} => %s" % (password, password)) + self.special_stats_total += 1 + return False + + # Only check english ascii passwords for now + # TODO: Add support for more languages. + elif any(ord(c) < 32 or ord(c) > 126 for c in password): + if self.verbose and not self.quiet: + print("[!] %s => {skipping non ascii english} => %s" % (password, password)) + self.foreign_stats_total += 1 + return False + + else: + return True + + def analyze_password(self, password, rules_queue=multiprocessing.Queue(), words_queue=multiprocessing.Queue()): + """ Analyze a single password. """ + + if self.verbose: + print("[*] Analyzing password: %s" % password) + + words = [] + + # Short-cut words in the dictionary + if self.enchant.check(password) and not self.word: + + word = dict() + word["password"] = password + word["suggestion"] = password + word["hashcat_rules"] = [[], ] + word["pre_rule"] = [] + word["best_rule_length"] = 9999 + + words.append(word) + + # Generate rules for words not in the dictionary + else: + + # Generate source words list + words = self.generate_words(password) + + # Generate levenshtein reverse paths for each suggestion + for word in words: + # Generate a collection of hashcat_rules lists + word["hashcat_rules"] = self.generate_hashcat_rules(word["suggestion"], word["password"]) + + self.print_hashcat_rules(words, password, rules_queue, words_queue) + + def print_hashcat_rules(self, words, password, rules_queue, words_queue): + + best_found_rule_length = 9999 + + # Sorted list based on rule length + for word in sorted(words, key=lambda w: len(w["hashcat_rules"][0])): + words_queue.put(word["suggestion"]) + + for hashcat_rule in word["hashcat_rules"]: + rule_length = len(hashcat_rule) + + if not self.more_rules: + if rule_length < best_found_rule_length: + best_found_rule_length = rule_length + + elif rule_length > best_found_rule_length: + if self.verbose: + print("[-] %s => {best rule length exceeded: %d (%d)} => %s" % + (word["suggestion"], rule_length, best_found_rule_length, password)) + break + + if rule_length <= self.max_rule_len: + hashcat_rule_str = " ".join(hashcat_rule + word["pre_rule"] or [':']) + + if self.verbose: + print("[+] %s => %s => %s" % (word["suggestion"], hashcat_rule_str, password)) + + rules_queue.put(hashcat_rule_str) + + def password_worker(self, i, passwords_queue, rules_queue, words_queue): + if self.debug: + print("[*] Password analysis worker [%d] started." % i) + try: + while True: + password = passwords_queue.get() + + # Interrupted by a Death Pill + if password is None: + break + + self.analyze_password(password, rules_queue, words_queue) + except (KeyboardInterrupt, SystemExit): + if self.debug: + print("[*] Password analysis worker [%d] terminated." % i) + + if self.debug: + print("[*] Password analysis worker [%d] stopped." % i) + + def rule_worker(self, rules_queue, output_rules_filename): + """ Worker to store generated rules. """ + print("[*] Saving rules to %s" % output_rules_filename) + + with open(output_rules_filename, 'w') as f: + if self.debug: + print("[*] Rule worker started.") + try: + while True: + rule = rules_queue.get() + + # Interrupted by a Death Pill + if rule is None: + break + + f.write("%s\n" % rule) + f.flush() + + except (KeyboardInterrupt, SystemExit): + if self.debug: + print("[*] Rule worker terminated.") + + if self.debug: + print("[*] Rule worker stopped.") + + def word_worker(self, words_queue, output_words_filename): + """ Worker to store generated rules. """ + print("[*] Saving words to %s" % output_words_filename) + + with open(output_words_filename, 'w') as f: + if self.debug: + print("[*] Word worker started.") + try: + while True: + word = words_queue.get() + + # Interrupted by a Death Pill + if word is None: + break + + f.write("%s\n" % word) + f.flush() + + except (KeyboardInterrupt, SystemExit): + if self.debug: + print("[*] Word worker terminated.") + + if self.debug: + print("[*] Word worker stopped.") + + # Analyze passwords file + def analyze_passwords_file(self, passwords_file): + """ Analyze provided passwords file. """ + + print("[*] Analyzing passwords file: %s:" % passwords_file) + print("[*] Press Ctrl-C to end execution and generate statistical analysis.") + + # Setup queues + passwords_queue = multiprocessing.Queue(self.threads) + rules_queue = multiprocessing.Queue() + words_queue = multiprocessing.Queue() + + # Start workers + for i in range(self.threads): + multiprocessing.Process(target=self.password_worker, + args=(i, passwords_queue, rules_queue, words_queue)).start() + multiprocessing.Process(target=self.rule_worker, args=(rules_queue, "%s.rule" % self.basename)).start() + multiprocessing.Process(target=self.word_worker, args=(words_queue, "%s.word" % self.basename)).start() + + # Continue with the main thread + password_count = 0 + + with open(passwords_file, 'r', encoding="latin-1", errors='ignore') as f: + analysis_start = time.time() + segment_start = analysis_start + + try: + for password in f: + password = password.rstrip('\r\n') + + if len(password) > 0: + # Provide analysis time feedback to the user + if not self.quiet and password_count != 0 and password_count % 5000 == 0: + segment_time = time.time() - segment_start + print("[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % + (password_count, segment_start - analysis_start, 5000 / segment_time)) + segment_start = time.time() + + password_count += 1 + + # Perform preliminary checks and add password to the queue + if self.check_reversible_password(password): + passwords_queue.put(password) + + except (KeyboardInterrupt, SystemExit): + print("\n[!] Rulegen was interrupted.") + + else: + # Signal workers to stop. + for i in range(self.threads): + passwords_queue.put(None) + + # Wait for all of the queued passwords to finish. + while not passwords_queue.empty(): + time.sleep(1) + + # Signal writers to stop. + rules_queue.put(None) + words_queue.put(None) + + analysis_time = time.time() - analysis_start + print("[*] Finished processing %d passwords in %.2f seconds at the rate of %.2f p/sec" % + (password_count, analysis_time, float(password_count) / analysis_time)) + + print("[*] Generating statistics for [%s] rules and words." % self.basename) + print("[-] Skipped %d all numeric passwords (%0.2f%%)" % + (self.numeric_stats_total, float(self.numeric_stats_total) * 100.0 / float(password_count))) + print("[-] Skipped %d passwords with less than 25%% alpha characters (%0.2f%%)" % + (self.special_stats_total, float(self.special_stats_total) * 100.0 / float(password_count))) + print("[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % + (self.foreign_stats_total, float(self.foreign_stats_total) * 100.0 / float(password_count))) + + # TODO: Counter breaks on large files. uniq -c | sort -rn is still the most + # optimal way. + rules_file = open("%s.rule" % self.basename, 'r') + rules_sorted_file = open("%s-sorted.rule" % self.basename, 'w') + rules_counter = Counter(rules_file) + rule_counter_total = sum(rules_counter.values()) + + print("\n[*] Top 10 rules") + rules_i = 0 + for (rule, count) in rules_counter.most_common(): + rules_sorted_file.write(rule) + if rules_i < 10: + print("[+] %s - %d (%0.2f%%)" % (rule.rstrip('\r\n'), count, count * 100 / rule_counter_total)) + rules_i += 1 + + rules_file.close() + rules_sorted_file.close() + + words_file = open("%s.word" % self.basename, 'r') + words_sorted_file = open("%s-sorted.word" % self.basename, 'w') + words_counter = Counter(words_file) + word_counter_total = sum(rules_counter.values()) + + print("\n[*] Top 10 words") + words_i = 0 + for (word, count) in words_counter.most_common(): + words_sorted_file.write(word) + if words_i < 10: + print("[+] %s - %d (%0.2f%%)" % (word.rstrip('\r\n'), count, count * 100 / word_counter_total)) + words_i += 1 + + words_file.close() + words_sorted_file.close() + + ############################################################################ + def verify_hashcat_rules(self, word, rules, password): + + with open("%s/test.rule" % HASHCAT_PATH, 'w') as f: + f.write(" ".join(rules)) + + with open("%s/test.word" % HASHCAT_PATH, 'w') as f: + f.write(word) + + p = subprocess.Popen(["%s/hashcat-cli64.bin" % HASHCAT_PATH, "-r", "%s/test.rule" % HASHCAT_PATH, "--stdout", + "%s/test.word" % HASHCAT_PATH], stdout=subprocess.PIPE) + out, err = p.communicate() + out = out.strip() + + if out == password: + hashcat_rules_str = " ".join(rules or [':']) + if self.verbose: + print("[+] %s => %s => %s" % (word, hashcat_rules_str, password)) + + else: + print("[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % + (word, " ".join(rules or [':']), password, out)) + + +if __name__ == "__main__": + + header = " _ \n" + header += " RuleGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog [options] passwords.txt", version="%prog " + VERSION) + + parser.add_option("-b", "--basename", + help="Output base name. The following files will be generated: " + + "basename.words, basename.rules and basename.stats", + default="analysis", metavar="rockyou") + parser.add_option("-w", "--wordlist", help="Use a custom wordlist for rule analysis.", metavar="wiki.dict") + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + parser.add_option("--threads", type="int", default=multiprocessing.cpu_count(), + help="Parallel threads to use for processing.") + + wordtune = OptionGroup(parser, "Fine tune source word generation:") + wordtune.add_option("--maxworddist", help="Maximum word edit distance (Levenshtein)", type="int", default=10, + metavar="10") + wordtune.add_option("--maxwords", help="Maximum number of source word candidates to consider", type="int", + default=5, metavar="5") + wordtune.add_option("--morewords", help="Consider suboptimal source word candidates", action="store_true", + default=False) + wordtune.add_option("--simplewords", help="Generate simple source words for given passwords", action="store_true", + default=False) + parser.add_option_group(wordtune) + + ruletune = OptionGroup(parser, "Fine tune rule generation:") + ruletune.add_option("--maxrulelen", help="Maximum number of operations in a single rule", type="int", default=10, + metavar="10") + ruletune.add_option("--maxrules", help="Maximum number of rules to consider", type="int", default=5, metavar="5") + ruletune.add_option("--morerules", help="Generate suboptimal rules", action="store_true", default=False) + ruletune.add_option("--simplerules", help="Generate simple rules insert,delete,replace", action="store_true", + default=False) + ruletune.add_option("--bruterules", help="Bruteforce reversal and rotation rules (slow)", action="store_true", + default=False) + parser.add_option_group(ruletune) + + spelltune = OptionGroup(parser, "Fine tune spell checker engine:") + spelltune.add_option("--providers", help="Comma-separated list of provider engines", default="aspell,myspell", + metavar="aspell,myspell") + parser.add_option_group(spelltune) + + debug = OptionGroup(parser, "Debuggin options:") + debug.add_option("-v", "--verbose", help="Show verbose information.", action="store_true", default=False) + debug.add_option("-d", "--debug", help="Debug rules.", action="store_true", default=False) + debug.add_option("--password", help="Process the last argument as a password not a file.", action="store_true", + default=False) + debug.add_option("--word", help="Use a custom word for rule analysis", metavar="Password") + debug.add_option("--hashcat", help="Test generated rules with hashcat-cli", action="store_true", default=False) + parser.add_option_group(debug) + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print(header) + + if len(args) < 1: + parser.error("no passwords file specified") + exit(1) + + try: + rulegen = RuleGen(language="en", providers=options.providers, basename=options.basename, + threads=options.threads) + except enchant.errors.DictNotFoundError: + print("[-] Cannot find a dictionary for specified language. Please install it and try again.") + print("[*] Hint: Usually this dictionary resides within an aspell / myspell package.") + exit(-1) + + # Finetuning word generation + rulegen.max_word_dist = options.maxworddist + rulegen.max_words = options.maxwords + rulegen.more_words = options.morewords + rulegen.simple_words = options.simplewords + + # Finetuning rule generation + rulegen.max_rule_len = options.maxrulelen + rulegen.max_rules = options.maxrules + rulegen.more_rules = options.morerules + rulegen.simple_rules = options.simplerules + rulegen.brute_rules = options.bruterules + if rulegen.brute_rules: + print("[!] Bruteforcing reversal and rotation rules. (slower)") + + # Debugging options + rulegen.word = options.word + rulegen.verbose = options.verbose + rulegen.debug = options.debug + rulegen.hashcat = options.hashcat + rulegen.quiet = options.quiet + + # Custom wordlist + if not options.word: + if options.wordlist: + rulegen.load_custom_wordlist(options.wordlist) + print("[*] Using Enchant '%s' module. For best results please install" % rulegen.enchant.provider.name) + print(" '%s' module language dictionaries." % rulegen.enchant.provider.name) + + # Analyze a single password or several passwords in a file + if options.password: + rulegen.analyze_password(args[0]) + else: + rulegen.analyze_passwords_file(args[0]) diff --git a/scripts/extensions/pack-mac/statsgen.py b/scripts/extensions/pack-mac/statsgen.py new file mode 100755 index 0000000..d6f174a --- /dev/null +++ b/scripts/extensions/pack-mac/statsgen.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +# StatsGen - Password Statistical Analysis tool +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import operator +import string +from optparse import OptionParser, OptionGroup + +VERSION = "0.0.3" + + +class StatsGen: + def __init__(self): + self.output_file = None + + # Filters + self.minlength = None + self.maxlength = None + self.simplemasks = None + self.charsets = None + self.quiet = False + self.debug = True + + # Stats dictionaries + self.stats_length = dict() + self.stats_simplemasks = dict() + self.stats_advancedmasks = dict() + self.stats_charactersets = dict() + + # Ignore stats with less than 1% coverage + self.hiderare = False + + self.filter_counter = 0 + self.total_counter = 0 + + # Minimum password complexity counters + self.mindigit = None + self.minupper = None + self.minlower = None + self.minspecial = None + + self.maxdigit = None + self.maxupper = None + self.maxlower = None + self.maxspecial = None + + @staticmethod + def analyze_password(password): + + # Password length + pass_length = len(password) + + # Character-set and policy counters + digit = 0 + lower = 0 + upper = 0 + special = 0 + + simplemask = list() + advancedmask_string = "" + + # Detect simple and advanced masks + for letter in password: + + if letter in string.digits: + digit += 1 + advancedmask_string += "?d" + if not simplemask or not simplemask[-1] == 'digit': + simplemask.append('digit') + + elif letter in string.ascii_lowercase: + lower += 1 + advancedmask_string += "?l" + if not simplemask or not simplemask[-1] == 'string': + simplemask.append('string') + + elif letter in string.ascii_uppercase: + upper += 1 + advancedmask_string += "?u" + if not simplemask or not simplemask[-1] == 'string': + simplemask.append('string') + + else: + special += 1 + advancedmask_string += "?s" + if not simplemask or not simplemask[-1] == 'special': + simplemask.append('special') + + # String representation of masks + simplemask_string = ''.join(simplemask) if len(simplemask) <= 3 else 'othermask' + + # Policy + policy = (digit, lower, upper, special) + + # Determine character-set + if digit and not lower and not upper and not special: + charset = 'numeric' + elif not digit and lower and not upper and not special: + charset = 'loweralpha' + elif not digit and not lower and upper and not special: + charset = 'upperalpha' + elif not digit and not lower and not upper and special: + charset = 'special' + elif not digit and lower and upper and not special: + charset = 'mixedalpha' + elif digit and lower and not upper and not special: + charset = 'loweralphanum' + elif digit and not lower and upper and not special: + charset = 'upperalphanum' + elif not digit and lower and not upper and special: + charset = 'loweralphaspecial' + elif not digit and not lower and upper and special: + charset = 'upperalphaspecial' + elif digit and not lower and not upper and special: + charset = 'specialnum' + elif not digit and lower and upper and special: + charset = 'mixedalphaspecial' + elif digit and not lower and upper and special: + charset = 'upperalphaspecialnum' + elif digit and lower and not upper and special: + charset = 'loweralphaspecialnum' + elif digit and lower and upper and not special: + charset = 'mixedalphanum' + else: + charset = 'all' + + return pass_length, charset, simplemask_string, advancedmask_string, policy + + def generate_stats(self, filename): + """ Generate password statistics. """ + + f = open(filename, 'r', encoding="latin-1", errors='ignore') + for password in f: + password = password.rstrip('\r\n') + + # if the password is empty, continue + if not password: + continue + + self.total_counter += 1 + + (pass_length, characterset, simplemask, advancedmask, policy) = self.analyze_password(password) + (digit, lower, upper, special) = policy + + if (self.charsets is None or characterset in self.charsets) and \ + (self.simplemasks is None or simplemask in self.simplemasks) and \ + (self.maxlength is None or pass_length <= self.maxlength) and \ + (self.minlength is None or pass_length >= self.minlength): + + self.filter_counter += 1 + + if self.mindigit is None or digit < self.mindigit: + self.mindigit = digit + if self.maxdigit is None or digit > self.maxdigit: + self.maxdigit = digit + + if self.minupper is None or upper < self.minupper: + self.minupper = upper + if self.maxupper is None or upper > self.maxupper: + self.maxupper = upper + + if self.minlower is None or lower < self.minlower: + self.minlower = lower + if self.maxlower is None or lower > self.maxlower: + self.maxlower = lower + + if self.minspecial is None or special < self.minspecial: + self.minspecial = special + if self.maxspecial is None or special > self.maxspecial: + self.maxspecial = special + + if pass_length in self.stats_length: + self.stats_length[pass_length] += 1 + else: + self.stats_length[pass_length] = 1 + + if characterset in self.stats_charactersets: + self.stats_charactersets[characterset] += 1 + else: + self.stats_charactersets[characterset] = 1 + + if simplemask in self.stats_simplemasks: + self.stats_simplemasks[simplemask] += 1 + else: + self.stats_simplemasks[simplemask] = 1 + + if advancedmask in self.stats_advancedmasks: + self.stats_advancedmasks[advancedmask] += 1 + else: + self.stats_advancedmasks[advancedmask] = 1 + + f.close() + + def print_stats(self): + """ Print password statistics. """ + + print("[+] Analyzing %d%% (%d/%d) of passwords" % (self.filter_counter*100//self.total_counter, + self.filter_counter, self.total_counter)) + print("[*] Statistics below is relative to the number of analyzed passwords, not total number of passwords") + print("\n[*] Length:") + for (length, count) in sorted(iter(self.stats_length.items()), key=operator.itemgetter(1), reverse=True): + if self.hiderare and not count*100//self.filter_counter > 0: + continue + print("[+] %25d: %02d%% (%d)" % (length, count*100/self.filter_counter, count)) + + print("\n[*] Character-set:") + for (char, count) in sorted(iter(self.stats_charactersets.items()), key=operator.itemgetter(1), reverse=True): + if self.hiderare and not count*100//self.filter_counter > 0: + continue + print("[+] %25s: %02d%% (%d)" % (char, count*100/self.filter_counter, count)) + + print("\n[*] Password complexity:") + print("[+] digit: min(%s) max(%s)" % (self.mindigit, self.maxdigit)) + print("[+] lower: min(%s) max(%s)" % (self.minlower, self.maxlower)) + print("[+] upper: min(%s) max(%s)" % (self.minupper, self.maxupper)) + print("[+] special: min(%s) max(%s)" % (self.minspecial, self.maxspecial)) + + print("\n[*] Simple Masks:") + for (simplemask, count) in sorted(iter(self.stats_simplemasks.items()), key=operator.itemgetter(1), + reverse=True): + if self.hiderare and not count*100//self.filter_counter > 0: + continue + print("[+] %25s: %02d%% (%d)" % (simplemask, count*100//self.filter_counter, count)) + + print("\n[*] Advanced Masks:") + for (advancedmask, count) in sorted(iter(self.stats_advancedmasks.items()), key=operator.itemgetter(1), + reverse=True): + if count*100//self.filter_counter > 0: + print("[+] %25s: %02d%% (%d)" % (advancedmask, count*100//self.filter_counter, count)) + + if self.output_file: + self.output_file.write("%s,%d\n" % (advancedmask, count)) + +if __name__ == "__main__": + + header = " _ \n" + header += " StatsGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog [options] passwords.txt\n\nType --help for more options", version="%prog "+VERSION) + + filters = OptionGroup(parser, "Password Filters") + filters.add_option("--minlength", dest="minlength", type="int", metavar="8", help="Minimum password length") + filters.add_option("--maxlength", dest="maxlength", type="int", metavar="8", help="Maximum password length") + filters.add_option("--charset", dest="charsets", help="Password charset filter (comma separated)", + metavar="loweralpha,numeric") + filters.add_option("--simplemask", dest="simplemasks", help="Password mask filter (comma separated)", + metavar="stringdigit,allspecial") + parser.add_option_group(filters) + + parser.add_option("-o", "--output", dest="output_file", help="Save masks and stats to a file", + metavar="password.masks") + parser.add_option("--hiderare", action="store_true", dest="hiderare", default=False, + help="Hide statistics covering less than 1% of the sample") + + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print(header) + + if len(args) != 1: + parser.error("no passwords file specified") + exit(1) + + print("[*] Analyzing passwords in [%s]" % args[0]) + + statsgen = StatsGen() + + if options.minlength is not None: + statsgen.minlength = options.minlength + if options.maxlength is not None: + statsgen.maxlength = options.maxlength + if options.charsets is not None: + statsgen.charsets = [x.strip() for x in options.charsets.split(',')] + if options.simplemasks is not None: + statsgen.simplemasks = [x.strip() for x in options.simplemasks.split(',')] + + if options.hiderare: + statsgen.hiderare = options.hiderare + + if options.output_file: + print("[*] Saving advanced masks and occurrences to [%s]" % options.output_file) + statsgen.output_file = open(options.output_file, 'w') + + statsgen.generate_stats(args[0]) + statsgen.print_stats() diff --git a/scripts/linux.sh b/scripts/linux.sh new file mode 100644 index 0000000..3b14fd6 --- /dev/null +++ b/scripts/linux.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo -e "\nOptional modules:" +if [[ -x "scripts/extensions/common-substr-linux" ]]; then + echo '[+] common-substr-linux is executable' +else + echo '[-] common-substr-linux is not available/executable (option 10 / 11)' +fi +if [[ -x "$(command -v python2)" ]]; then + echo '[+] Python2 is executable' +else + echo '[-] Python2 is not available/executable (option 12 / 13)' +fi +if [[ -x "scripts/extensions/hashcat-utils-linux/bin/expander.bin" ]]; then + echo '[+] Expander is executable' +else + echo '[-] Expander is not available/executable (option 14)' +fi +if [[ -x "$(command -v cewl)" ]]; then + echo '[+] CeWL is executable' + CEWL=$(command -v cewl) +else + echo '[-] CeWL is not available/executable (option 18)' +fi diff --git a/scripts/mac.sh b/scripts/mac.sh new file mode 100644 index 0000000..b8cc852 --- /dev/null +++ b/scripts/mac.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo -e "\nOptional modules:" +if [[ -x "scripts/extensions/common-substr-mac" ]]; then + echo '[+] common-substr-mac is executable' +else + echo '[-] common-substr-mac is not executable or found (option 10 / 11)' +fi +if [[ -x "scripts/extensions/hashcat-utils-mac/bin/expander.bin" ]]; then + echo '[+] Expander is executable' +else + echo '[-] Expander is not available/executable or found, this is needed for fingerprint cracking' +fi +if [[ -x "scripts/extensions/cewl/cewl.rb" ]]; then + echo '[+] CeWL is executable' + CEWL="scripts/extensions/cewl/cewl.rb" +else + echo '[-] CeWL is not executable or found (option 18)' +fi \ No newline at end of file diff --git a/scripts/parameters.sh b/scripts/parameters.sh index 3bf30cb..c2189c3 100644 --- a/scripts/parameters.sh +++ b/scripts/parameters.sh @@ -86,27 +86,21 @@ if [ "$COUNTER" \> 0 ]; then echo -e "\nNot all mandatory requirements are met. Please fix and try again."; exit 1 fi -echo -e "\nOptional modules:" -if [[ -x "scripts/extensions/common-substr" ]]; then - echo '[+] Common-substr is executable' -else - echo '[-] Common-substr is not available/executable (option 10 / 11)' -fi -if [[ -x "$(command -v python2)" ]]; then - echo '[+] Python2 is executable' -else - echo '[-] Python2 is not available/executable (option 12 / 13)' -fi -if [[ -x "scripts/extensions/hashcat-utils/bin/expander.bin" ]]; then - echo '[+] Expander is executable' -else - echo '[-] Expander is not available/executable (option 14)' -fi -if [[ -x "$(command -v cewl)" ]]; then - echo '[+] CeWL is executable' - CEWL=$(command -v cewl) +# Apple macOS vs Linux +UNAMEOUT="$(uname -s)" +case "${UNAMEOUT}" in + Linux*) MACHINE=Linux;; + Darwin*) MACHINE=Mac;; + *) MACHINE="UNKNOWN:${UNAMEOUT}" +esac + +if [ "$MACHINE" == "Mac" ]; then + source scripts/mac.sh +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + source scripts/linux.sh else - echo '[-] CeWL is not available/executable (option 18)' + echo "PLEASE OPEN ISSUE with output of 'uname -s'. Fallback to Linux" + source scripts/linux.sh fi echo -e "\nVariable Parameters:" diff --git a/scripts/processors/10-prefixsuffix.sh b/scripts/processors/10-prefixsuffix.sh index 431e278..3a184d2 100644 --- a/scripts/processors/10-prefixsuffix.sh +++ b/scripts/processors/10-prefixsuffix.sh @@ -22,10 +22,16 @@ tmp=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp2=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp3=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp4=$(mktemp /tmp/hash-cracker-tmp.XXXX) +cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null # Logic -cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null -cat $tmp | awk -F: '{print $NF}' | sort | tee $tmp2 &>/dev/null && ./scripts/extensions/common-substr -n -p -f $tmp2 > $tmp3 && ./scripts/extensions/common-substr -n -s -f $tmp2 > $tmp4 && rm $tmp2 $tmp +if [ "$MACHINE" == "Mac" ]; then + cat $tmp | awk -F: '{print $NF}' | sort | tee $tmp2 &>/dev/null && ./scripts/extensions/common-substr-mac -n -p -f $tmp2 > $tmp3 && ./scripts/extensions/common-substr-mac -n -s -f $tmp2 > $tmp4 && rm $tmp2 $tmp +else + cat $tmp | awk -F: '{print $NF}' | sort | tee $tmp2 &>/dev/null && ./scripts/extensions/common-substr-linux -n -p -f $tmp2 > $tmp3 && ./scripts/extensions/common-substr-linux -n -s -f $tmp2 > $tmp4 && rm $tmp2 $tmp +fi + $HASHCAT $KERNEL --bitmap-max=24 $HWMON --potfile-path=$POTFILE -m$HASHTYPE $HASHLIST -a1 $tmp3 $tmp4 $HASHCAT $KERNEL --bitmap-max=24 $HWMON --potfile-path=$POTFILE -m$HASHTYPE $HASHLIST -a1 $tmp4 $tmp3 -rm $tmp3 $tmp4; echo -e "\nPrefix suffix processing done\n" \ No newline at end of file +rm $tmp3 $tmp4 +echo -e "\nPrefix suffix processing done\n" \ No newline at end of file diff --git a/scripts/processors/11-commonsubstring.sh b/scripts/processors/11-commonsubstring.sh index a7d268f..d803bc2 100644 --- a/scripts/processors/11-commonsubstring.sh +++ b/scripts/processors/11-commonsubstring.sh @@ -22,9 +22,15 @@ tmp=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp2=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp3=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp4=$(mktemp /tmp/hash-cracker-tmp.XXXX) +cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null > $tmp2; rm $tmp # Logic -cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null > $tmp2; rm $tmp -cat $tmp2 | awk -F: '{print $NF}' | sort | tee $tmp3 &>/dev/null && ./scripts/extensions/common-substr -n -f $tmp3 > $tmp4 && rm $tmp3 $tmp2 +if [ "$MACHINE" == "Mac" ]; then + cat $tmp2 | awk -F: '{print $NF}' | sort | tee $tmp3 &>/dev/null && ./scripts/extensions/common-substr-mac -n -f $tmp3 > $tmp4 && rm $tmp3 $tmp2 +else + cat $tmp2 | awk -F: '{print $NF}' | sort | tee $tmp3 &>/dev/null && ./scripts/extensions/common-substr-linux -n -f $tmp3 > $tmp4 && rm $tmp3 $tmp2 +fi + $HASHCAT $KERNEL --bitmap-max=24 $HWMON --potfile-path=$POTFILE -m$HASHTYPE $HASHLIST -a1 $tmp4 $tmp4 -rm $tmp4; echo -e "\nSubstring processing done\n" \ No newline at end of file +rm $tmp4 +echo -e "\nSubstring processing done\n" \ No newline at end of file diff --git a/scripts/processors/12-pack-rule.sh b/scripts/processors/12-pack-rule.sh index 7b762c9..7cb2133 100644 --- a/scripts/processors/12-pack-rule.sh +++ b/scripts/processors/12-pack-rule.sh @@ -19,11 +19,16 @@ fi # Temporary Files tmp=$(mktemp /tmp/hash-cracker-tmp.XXXX) +cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null # Logic -cat $POTFILE | awk -F: '{print $NF}' | tee $tmp &>/dev/null -python2 scripts/extensions/pack/rulegen.py $tmp -rm analysis-sorted.word analysis.word analysis-sorted.rule +if [ "$MACHINE" == "Mac" ]; then + echo "This option is currently unavailable on Mac." + source hash-cracker.sh +else + python2 scripts/extensions/pack-linux/rulegen.py $tmp + rm analysis-sorted.word analysis.word analysis-sorted.rule +fi source scripts/selectors/wordlist.sh diff --git a/scripts/processors/13-pack-mask.sh b/scripts/processors/13-pack-mask.sh index 0cd8850..580b1a1 100644 --- a/scripts/processors/13-pack-mask.sh +++ b/scripts/processors/13-pack-mask.sh @@ -21,11 +21,17 @@ fi tmp=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp2=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp3=$(mktemp /tmp/hash-cracker-tmp.XXXX) +cat $POTFILE | awk -F: '{print $NF}' | sort -u | tee $tmp &>/dev/null # Logic -cat $POTFILE | awk -F: '{print $NF}' | sort -u | tee $tmp &>/dev/null -python2 scripts/extensions/pack/statsgen.py $tmp -o $tmp2 -python2 scripts/extensions/pack/maskgen.py $tmp2 --targettime 1000 --optindex -q --pps 14000000000 --minlength=2 -o $tmp3 +if [ "$MACHINE" == "Mac" ]; then + python3 scripts/extensions/pack-mac/statsgen.py $tmp -o $tmp2 + python3 scripts/extensions/pack-mac/maskgen.py $tmp2 --targettime 1000 --optindex -q --pps 14000000000 --minlength=2 -o $tmp3 +else + python2 scripts/extensions/pack-linux/statsgen.py $tmp -o $tmp2 + python2 scripts/extensions/pack-linux/maskgen.py $tmp2 --targettime 1000 --optindex -q --pps 14000000000 --minlength=2 -o $tmp3 +fi + $HASHCAT $KERNEL --bitmap-max=24 $HWMON --potfile-path=$POTFILE -m$HASHTYPE $HASHLIST -a 3 $tmp3 rm $tmp $tmp2 $tmp3 echo -e "\nPACK mask processing done\n" \ No newline at end of file diff --git a/scripts/processors/14-fingerprint.sh b/scripts/processors/14-fingerprint.sh index 901e2da..251c5f1 100644 --- a/scripts/processors/14-fingerprint.sh +++ b/scripts/processors/14-fingerprint.sh @@ -20,10 +20,15 @@ fi # Temporary Files tmp=$(mktemp /tmp/hash-cracker-tmp.XXXX) tmp2=$(mktemp /tmp/hash-cracker-tmp.XXXX) +cat $POTFILE | awk -F: '{print $NF}' | sort -u | tee $tmp &>/dev/null # Logic -cat $POTFILE | awk -F: '{print $NF}' | sort -u | tee $tmp &>/dev/null -./scripts/extensions/hashcat-utils/bin/expander.bin < $tmp | sort -u > $tmp2 && rm $tmp +if [ "$MACHINE" == "Mac" ]; then + ./scripts/extensions/hashcat-utils-mac/bin/expander.bin < $tmp | iconv -f ISO-8859-1 -t UTF-8//TRANSLIT | sort -u > $tmp2 && rm $tmp +else + ./scripts/extensions/hashcat-utils-linux/bin/expander.bin < $tmp | sort -u > $tmp2 && rm $tmp +fi + $HASHCAT $KERNEL --bitmap-max=24 $HWMON --potfile-path=$POTFILE -m$HASHTYPE $HASHLIST -a 1 $tmp2 $tmp2 rm $tmp2 echo -e "\nFingerprint attack done\n" \ No newline at end of file diff --git a/scripts/processors/17-markov-generator.sh b/scripts/processors/17-markov-generator.sh index ded9116..d0ad6cb 100644 --- a/scripts/processors/17-markov-generator.sh +++ b/scripts/processors/17-markov-generator.sh @@ -41,7 +41,12 @@ read -p "Minimum password (length) character limit: " NGRAM read -p "Amount of passwords to create: " AMOUNT cat $LIST | awk -F: '{print $NF}' | sort -u | tee $tmp &>/dev/null -./scripts/extensions/mkpass -infile $tmp -ngram $NGRAM -m $AMOUNT | tee $tmp2 &>/dev/null && rm $tmp + +if [ "$MACHINE" == "Mac" ]; then + ./scripts/extensions/mkpass-mac -infile $tmp -ngram $NGRAM -m $AMOUNT | tee $tmp2 &>/dev/null && rm $tmp +else + ./scripts/extensions/mkpass-linux -infile $tmp -ngram $NGRAM -m $AMOUNT | tee $tmp2 &>/dev/null && rm $tmp +fi read -p "Use rules? (y/n): " USERULES