Added support for reverse and rotation bruteforce rules to rulegen

This commit is contained in:
iphelix 2013-07-26 13:43:15 -07:00
parent 4ed47777b5
commit 893f96c719
4 changed files with 330 additions and 282 deletions

View File

@ -11,7 +11,7 @@ are permitted provided that the following conditions are met:
list of conditions and the following disclaimer in the documentation and/or list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution. other materials provided with the distribution.
Neither the name of the {organization} nor the names of its Neither the name of the 'The Sprawl' nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.

View File

@ -6,27 +6,9 @@
# VERSION 0.0.2 # VERSION 0.0.2
# #
# Copyright (C) 2013 Peter Kacherginsky # Copyright (C) 2013 Peter Kacherginsky
# All rights reserved. # All rights reserved.#
# #
# Redistribution and use in source and binary forms, with or without # Please see the attached LICENSE file for additiona licensing information.
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. 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.
#
# 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 OWNER 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.
import sys, string, random import sys, string, random
import datetime import datetime

View File

@ -5,33 +5,12 @@
# #
# This tool is part of PACK (Password Analysis and Cracking Kit) # This tool is part of PACK (Password Analysis and Cracking Kit)
# #
# VERSION 0.0.2 # VERSION 0.0.3
# #
# Copyright (C) 2013 Peter Kacherginsky # Copyright (C) 2013 Peter Kacherginsky
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Please see the attached LICENSE file for additiona licensing information.
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. 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.
#
# 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 OWNER 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.
#
# CHANGELOG:
# [*] Fixed greedy substitution issue (thanks smarteam)
import sys import sys
import re import re
@ -41,7 +20,11 @@ import enchant
from optparse import OptionParser, OptionGroup from optparse import OptionParser, OptionGroup
VERSION = "0.0.1" from collections import Counter
from multiprocessing import Queue, Process
VERSION = "0.0.3"
# Testing rules with hashcat --stdout # Testing rules with hashcat --stdout
HASHCAT_PATH = "hashcat/" HASHCAT_PATH = "hashcat/"
@ -52,6 +35,12 @@ class RuleGen:
# Initialize Rule Generator class # Initialize Rule Generator class
def __init__(self,language="en",providers="aspell,myspell",basename='analysis'): def __init__(self,language="en",providers="aspell,myspell",basename='analysis'):
#######################################################################
# Multiprocessing
self.password_queue = multiprocessing.Queue()
self.rule_queue = multiprocessing.Queue()
self.word_queue = multiprocessing.Queue()
self.enchant_broker = enchant.Broker() self.enchant_broker = enchant.Broker()
self.enchant_broker.set_ordering("*",providers) self.enchant_broker.set_ordering("*",providers)
@ -73,19 +62,16 @@ class RuleGen:
self.max_rules = 10 self.max_rules = 10
self.more_rules = False self.more_rules = False
self.simple_rules = False self.simple_rules = False
self.brute_rules = False
# Debugging options # Debugging options
self.verbose = False self.verbose = False
self.debug = False self.debug = False
self.word = None self.word = None # Custom word to use.
self.quiet = False self.quiet = False
######################################################################## ########################################################################
# Word and Rule Statistics # Word and Rule Statistics
self.word_stats = dict()
self.rule_stats = dict()
self.password_stats = dict()
self.numeric_stats_total = 0 self.numeric_stats_total = 0
self.special_stats_total = 0 self.special_stats_total = 0
self.foreign_stats_total = 0 self.foreign_stats_total = 0
@ -176,6 +162,14 @@ class RuleGen:
self.leet["$"] = "s" self.leet["$"] = "s"
self.leet["+"] = "t" self.leet["+"] = "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 # Calculate Levenshtein edit path matrix
def levenshtein(self,word,password): def levenshtein(self,word,password):
@ -205,27 +199,34 @@ class RuleGen:
return matrix return matrix
############################################################################
# Print word X password matrix
def levenshtein_print(self,matrix,word,password): def levenshtein_print(self,matrix,word,password):
""" Print word X password matrix """
print " %s" % " ".join(list(word)) print " %s" % " ".join(list(word))
for i,row in enumerate(matrix): for i,row in enumerate(matrix):
if i == 0: print " ", if i == 0: print " ",
else: print password[i-1], else: print password[i-1],
print " ".join("%2d" % col for col in row) print " ".join("%2d" % col for col in row)
############################################################################ def generate_levenshtein_rules(self, word, password):
# Reverse Levenshtein Path Algorithm by Peter Kacherginsky """ Generates levenshtein rules. Returns a list of lists of levenshtein rules. """
# Generates a list of edit operations necessary to transform a source word
# into a password. Edit operations are recorded in the form: # 1) Generate Levenshtein matrix
# (operation, password_offset, word_offset) matrix = self.levenshtein(word, password)
# Where an operation can be either insertion, deletion or replacement.
def levenshtein_reverse_path(self,matrix,word,password): # 2) Trace reverse paths through the matrix.
paths = self.levenshtein_reverse_recursive(matrix,len(matrix)-1,len(matrix[0])-1,0) 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]] return [path for path in paths if len(path) <= matrix[-1][-1]]
# Calculate reverse Levenshtein paths (recursive, depth first, short-circuited)
def levenshtein_reverse_recursive(self,matrix,i,j,path_len): 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]: if i == 0 and j == 0 or path_len > matrix[-1][-1]:
return [[]] return [[]]
@ -260,91 +261,129 @@ class RuleGen:
return paths return paths
############################################################################
def load_custom_wordlist(self,wordlist_file): def load_custom_wordlist(self,wordlist_file):
self.enchant = enchant.request_pwl_dict(wordlist_file) self.enchant = enchant.request_pwl_dict(wordlist_file)
############################################################################ def generate_words(self,password):
# Generate source words """ Generate source word candidates."""
def generate_words_collection(self,password):
if self.debug: print "[*] Generating source words for %s" % password if self.debug: print "[*] Generating source words for %s" % password
words = [] words = list()
if not self.simple_words: suggestions = self.generate_advanced_words(password) words_collection = list()
else: suggestions = self.generate_simple_words(password)
best_found_distance = sys.maxint # 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
unique_suggestions = [] #######################################################################
for word in suggestions: # Generate words for each preanalysis rule
word = word.replace(' ','') if not self.brute_rules:
word = word.replace('-','') self.preanalysis_rules = self.preanalysis_rules[:1]
if not word in unique_suggestions:
unique_suggestions.append(word)
# NOTE: Enchant already returned a list sorted by edit distance, so for pre_rule, pre_rule_lambda in self.preanalysis_rules:
# we simply need to get the best edit distance of the first word
# and compare the rest with it
for word in unique_suggestions:
matrix = self.levenshtein(word,password) pre_password = pre_rule_lambda(password)
edit_distance = matrix[-1][-1]
# Record best edit distance and skip anything exceeding it # 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(' ','')
suggestion = suggestion.replace('-','')
if not suggestion in suggestions:
suggestions.append(suggestion)
if len(suggestions) != len(set(suggestions)):
print sorted(suggestions)
print sorted(set(suggestions))
for suggestion in suggestions:
distance = enchant.utils.levenshtein(suggestion,pre_password)
word = dict()
word["suggestion"] = suggestion
word["distance"] = distance
word["password"] = pre_password
word["pre_rule"] = pre_rule
word["best_rule_length"] = 9999
words.append(word)
#######################################################################
# Perform Optimization
for word in sorted(words, key=lambda word: word["distance"], reverse=False):
# Optimize for best distance
if not self.more_words: if not self.more_words:
if edit_distance < best_found_distance: if word["distance"] < best_found_distance:
best_found_distance = edit_distance best_found_distance = word["distance"]
elif edit_distance > best_found_distance:
if self.verbose: print "[-] %s => {best distance exceeded: %d (%d)} => %s" % (word,edit_distance,best_found_distance,password)
break
if edit_distance <= self.max_word_dist: elif word["distance"] > best_found_distance:
if self.debug: print "[+] %s => {edit distance: %d (%d)} = > %s" % (word,edit_distance,best_found_distance,password) if self.verbose:
words.append((word,matrix,password)) print "[-] %s => {edit distance suboptimal: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], best_found_distance, word["password"])
break
if not word in self.word_stats: self.word_stats[word] = 1 # Filter words with too big edit distance
else: self.word_stats[word] += 1 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: else:
if self.verbose: print "[-] %s => {max distance exceeded: %d (%d)} => %s" % (word,edit_distance,self.max_word_dist,password) if self.verbose:
print "[-] %s => {max distance exceeded: %d (%d)} => %s" % \
(word["suggestion"], word["distance"], self.max_word_dist, word["password"])
return words if self.max_words:
words_collection = words_collection[:self.max_words]
return words_collection
############################################################################
# Generate simple words
def generate_simple_words(self,password): def generate_simple_words(self,password):
if self.word: """ Generate simple words. A simple spellcheck."""
return [self.word]
else: return self.enchant.suggest(password)
return self.enchant.suggest(password)[:self.max_words]
############################################################################
# Generate advanced words
def generate_advanced_words(self,password): def generate_advanced_words(self,password):
if self.word: """ Generate advanced words.
return [self.word] Perform some additional non-destructive cleaning to help spell-checkers:
else: 1) Remove non-alpha prefixes and appendixes.
# Remove non-alpha prefix and appendix 2) Perform common pattern matches (e.g. email).
insertion_matches = self.password_pattern["insertion"].match(password) 3) Replace non-alpha character substitutions (1337 5p34k)
if insertion_matches: """
password = insertion_matches.group('password')
# Email split # Remove non-alpha prefix and/or appendix
email_matches = self.password_pattern["email"].match(password) insertion_matches = self.password_pattern["insertion"].match(password)
if email_matches: if insertion_matches:
password = email_matches.group('password') password = insertion_matches.group('password')
# Replace common special character replacements (1337 5p34k) # Pattern matches
preanalysis_password = '' email_matches = self.password_pattern["email"].match(password)
for c in password: if email_matches:
if c in self.leet: preanalysis_password += self.leet[c] password = email_matches.group('password')
else: preanalysis_password += c
password = preanalysis_password
if self.debug: "[*] Preanalysis Password: %s" % 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
return self.enchant.suggest(password)[:self.max_words] if self.debug: "[*] Preanalysis Password: %s" % password
return self.enchant.suggest(password)
############################################################################ ############################################################################
# Hashcat specific offset definition 0-9,A-Z # Hashcat specific offset definition 0-9,A-Z
@ -356,56 +395,58 @@ class RuleGen:
if N.isdigit(): return int(N) if N.isdigit(): return int(N)
else: return ord(N)-65+10 else: return ord(N)-65+10
############################################################################ def generate_hashcat_rules(self, suggestion, password):
# Generate hashcat rules """ Generate hashcat rules. Returns a length sorted list of lists of hashcat rules."""
def generate_hashcat_rules_collection(self, lev_rules_collection):
# 2) Generate Levenshtein Rules
lev_rules = self.generate_levenshtein_rules(suggestion, password)
# 3) Generate Hashcat Rules
hashcat_rules = []
hashcat_rules_collection = [] hashcat_rules_collection = []
min_hashcat_rules_length = sys.maxint #######################################################################
for (word,rules,password) in lev_rules_collection: # Generate hashcat rule for each levenshtein rule
for lev_rule in lev_rules:
if self.simple_rules: if self.simple_rules:
hashcat_rules = self.generate_simple_hashcat_rules(word,rules,password) hashcat_rule = self.generate_simple_hashcat_rules(suggestion, lev_rule, password)
else: else:
hashcat_rules = self.generate_advanced_hashcat_rules(word,rules,password) hashcat_rule = self.generate_advanced_hashcat_rules(suggestion, lev_rule, password)
if not hashcat_rules == None: if hashcat_rule == None:
print "[!] Processing FAILED: %s => ;( => %s" % (suggestion,password)
hashcat_rules_length = len(hashcat_rules)
if hashcat_rules_length <= self.max_rule_len:
hashcat_rules_collection.append((word,hashcat_rules,password))
# Determine minimal hashcat rules length
if hashcat_rules_length < min_hashcat_rules_length:
min_hashcat_rules_length = hashcat_rules_length
else:
if self.verbose: print "[!] %s => {max rule length exceeded: %d (%d)} => %s" % (word,hashcat_rules_length,self.max_rule_len,password)
else:
print "[!] Processing FAILED: %s => ;( => %s" % (word,password)
print " Sorry about that, please report this failure to" print " Sorry about that, please report this failure to"
print " the developer: iphelix [at] thesprawl.org" print " the developer: iphelix [at] thesprawl.org"
else:
hashcat_rules.append(hashcat_rule)
# Remove suboptimal rules best_found_rule_length = 9999
if not self.more_rules:
min_hashcat_rules_collection = []
for (word,hashcat_rules,password) in hashcat_rules_collection:
hashcat_rules_length = len(hashcat_rules) #######################################################################
if hashcat_rules_length == min_hashcat_rules_length: # Perform Optimization
min_hashcat_rules_collection.append((word,hashcat_rules,password)) for hashcat_rule in sorted(hashcat_rules, key=lambda hashcat_rule: len(hashcat_rule)):
else:
if self.verbose: print "[!] %s => {rule length suboptimal: %d (%d)} => %s" % (word,hashcat_rules_length,min_hashcat_rules_length,password)
hashcat_rules_collection = min_hashcat_rules_collection 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 return hashcat_rules_collection
############################################################################
# Generate basic hashcat rules using only basic insert,delete,replace rules
def generate_simple_hashcat_rules(self,word,rules,password): def generate_simple_hashcat_rules(self,word,rules,password):
""" Generate basic hashcat rules using only basic insert,delete,replace rules. """
hashcat_rules = [] hashcat_rules = []
if self.debug: print "[*] Simple Processing %s => %s" % (word,password) if self.debug: print "[*] Simple Processing %s => %s" % (word,password)
@ -439,10 +480,8 @@ class RuleGen:
if self.debug: print "[!] Simple Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules) if self.debug: print "[!] Simple Processing FAILED: %s => %s => %s (%s)" % (word," ".join(hashcat_rules),password,word_rules)
return None return None
############################################################################
# Generate advanced hashcat rules using full range of available rules
def generate_advanced_hashcat_rules(self,word,rules,password): def generate_advanced_hashcat_rules(self,word,rules,password):
""" Generate advanced hashcat rules using full range of available rules. """
hashcat_rules = [] hashcat_rules = []
if self.debug: print "[*] Advanced Processing %s => %s" % (word,password) if self.debug: print "[*] Advanced Processing %s => %s" % (word,password)
@ -472,7 +511,7 @@ class RuleGen:
# Detecting global replacement such as sXY, l, u, C, c is a non # Detecting global replacement such as sXY, l, u, C, c is a non
# trivial problem because different characters may be added or # trivial problem because different characters may be added or
# removed from the word by other rules. A reliable way to solve # removed from the word by other rules. A reliable way to solve
# this problem is to apply all of the rules the the source word # 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 # and keep track of its state at any given time. At the same
# time, global replacement rules can be tested by completing # time, global replacement rules can be tested by completing
# the rest of the rules using a simplified engine. # the rest of the rules using a simplified engine.
@ -665,115 +704,146 @@ class RuleGen:
return None return None
############################################################################ ############################################################################
def print_hashcat_rules(self,hashcat_rules_collection): def print_hashcat_rules(self, words, password):
for word,rules,password in hashcat_rules_collection: # sorted(self.masks.keys(), key=lambda mask: self.masks[mask][sorting_mode], reverse=True):
best_found_rule_length = 9999
# Sorted list based on rule length
for word in sorted(words, key=lambda word: len(word["hashcat_rules"][0])):
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)
if self.hashcat:
self.verify_hashcat_rules(word["suggestion"], hashcat_rule + word["pre_rule"], password)
# TODO: Collect statistics later
# if hashcat_rule_str in self.rule_stats: self.rule_stats[hashcat_rule_str] += 1
# else: self.rule_stats[hashcat_rule_str] = 1
self.output_rules_f.write("%s\n" % hashcat_rule_str)
self.output_words_f.write("%s\n" % word["suggestion"])
############################################################################
def verify_hashcat_rules(self,word, rules, password):
import subprocess
f = open("%s/test.rule" % HASHCAT_PATH,'w')
f.write(" ".join(rules))
f.close()
f = open("%s/test.word" % HASHCAT_PATH,'w')
f.write(word)
f.close()
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 [':']) hashcat_rules_str = " ".join(rules or [':'])
if self.verbose: print "[+] %s => %s => %s" % (word, hashcat_rules_str, password) if self.verbose: print "[+] %s => %s => %s" % (word, hashcat_rules_str, password)
if not hashcat_rules_str in self.rule_stats: self.rule_stats[hashcat_rules_str] = 1 else:
else: self.rule_stats[hashcat_rules_str] += 1 print "[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % (word," ".join(rules or [':']),password,out)
self.output_rules_f.write("%s\n" % hashcat_rules_str) def check_reversible_password(self, password):
self.output_words_f.write("%s\n" % word) """ Check whether the password is likely to be reversed successfuly. """
############################################################################
def verify_hashcat_rules(self,hashcat_rules_collection):
for word,rules,password in hashcat_rules_collection:
f = open("%s/test.rule" % HASHCAT_PATH,'w')
f.write(" ".join(rules))
f.close()
f = open("%s/test.word" % HASHCAT_PATH,'w')
f.write(word)
f.close()
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)
if not hashcat_rules_str in self.rule_stats: self.rule_stats[hashcat_rules_str] = 1
else: self.rule_stats[hashcat_rules_str] += 1
self.output_rules_f.write("%s\n" % hashcat_rules_str)
self.output_words_f.write("%s\n" % word)
else:
print "[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % (word," ".join(rules or [':']),password,out)
############################################################################
# Analyze a single password
def analyze_password(self,password):
if self.verbose: print "[*] Analyzing password: %s" % password
if self.verbose: start_time = time.clock()
# Skip all numeric passwords # Skip all numeric passwords
if password.isdigit(): if password.isdigit():
if self.verbose: print "[!] %s => {skipping numeric} => %s" % (password,password) if self.verbose and not self.quiet: print "[!] %s => {skipping numeric} => %s" % (password,password)
self.numeric_stats_total += 1 self.numeric_stats_total += 1
return False
# Skip passwords with less than 25% of alpha character # Skip passwords with less than 25% of alpha character
# TODO: Make random word detection more reliable based on word entropy. # TODO: Make random word detection more reliable based on word entropy.
elif len([c for c in password if c.isalpha()]) < len(password)/4: elif len([c for c in password if c.isalpha()]) < len(password)/4:
print "[!] %s => {skipping alpha less than 25%%} => %s" % (password,password) if self.verbose and not self.quiet:print "[!] %s => {skipping alpha less than 25%%} => %s" % (password,password)
self.special_stats_total += 1 self.special_stats_total += 1
return False
# Only check english ascii passwords for now, add more languages in the next version # Only check english ascii passwords for now
# TODO: Add support for more languages.
elif [c for c in password if ord(c) < 32 or ord(c) > 126]: elif [c for c in password if ord(c) < 32 or ord(c) > 126]:
if self.verbose: print "[!] %s => {skipping non ascii english} => %s" % (password,password) if self.verbose and not self.quiet: print "[!] %s => {skipping non ascii english} => %s" % (password,password)
self.foreign_stats_total += 1 self.foreign_stats_total += 1
return False
# Analyze the password
else: else:
return True
if not password in self.password_stats: self.password_stats[password] = 1 def analyze_password(self,password):
else: self.password_stats[password] += 1 """ Analyze a single password. """
# Short-cut words already in the dictionary if self.verbose: print "[*] Analyzing password: %s" % password
if self.enchant.check(password): if self.verbose: start_time = time.clock()
# Only process passwords likely to be dictionary based.
if self.check_reversible_password(password):
# TODO: Collect statistics later
# if password in self.password_stats: self.password_stats[password] += 1
# else: self.password_stats[password] = 1
words = []
# Short-cut words in the dictionary
if self.enchant.check(password) and not self.word:
# Record password as a source word for stats # Record password as a source word for stats
if not password in self.word_stats: self.word_stats[password] = 1 # TODO: Collect statistics later
else: self.word_stats[password] += 1 # if password in self.word_stats: self.word_stats[password] += 1
# else: self.word_stats[password] = 1
hashcat_rules_collection = [(password,[],password)] 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 # Generate rules for words not in the dictionary
else: else:
# Generate source words list # Generate source words list
words_collection = self.generate_words_collection(password) words = self.generate_words(password)
# Generate levenshtein rules collection for each source word # Generate levenshtein reverse paths for each suggestion
lev_rules_collection = [] for word in words:
for word,matrix,password in words_collection:
# Generate multiple paths to get from word to password # Generate a collection of hashcat_rules lists
lev_rules = self.levenshtein_reverse_path(matrix,word,password) word["hashcat_rules"] = self.generate_hashcat_rules(word["suggestion"],word["password"])
for lev_rule in lev_rules:
lev_rules_collection.append((word,lev_rule,password))
# Generate hashcat rules collection self.print_hashcat_rules(words, password)
hashcat_rules_collection = self.generate_hashcat_rules_collection(lev_rules_collection)
# Print complete for each source word for the original password
if self.hashcat:
self.verify_hashcat_rules(hashcat_rules_collection)
else:
self.print_hashcat_rules(hashcat_rules_collection)
if self.verbose: print "[*] Finished analysis in %.2f seconds" % (time.clock()-start_time) if self.verbose: print "[*] Finished analysis in %.2f seconds" % (time.clock()-start_time)
############################################################################
# Analyze passwords file # Analyze passwords file
def analyze_passwords_file(self,passwords_file): def analyze_passwords_file(self,passwords_file):
print "[*] Analyzing passwords file: %s:" % passwords_file """ Analyze provided passwords file. """
print "[*] Analyzing passwords file: %s:" % passwords_file
f = open(passwords_file,'r') f = open(passwords_file,'r')
password_count = 0 password_count = 0
@ -783,53 +853,60 @@ class RuleGen:
password = password.strip() password = password.strip()
if len(password) > 0: if len(password) > 0:
if password_count != 0 and password_count % 1000 == 0: # Provide analysis time feedback to the user
if password_count != 0 and password_count % 10000 == 0:
current_analysis_time = time.clock() - analysis_start current_analysis_time = time.clock() - analysis_start
if not self.quiet: print "[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % (password_count, current_analysis_time, float(password_count)/current_analysis_time ) if not self.quiet: print "[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % (password_count, current_analysis_time, password_count/current_analysis_time )
password_count += 1 password_count += 1
self.analyze_password(password) self.analyze_password(password)
except (KeyboardInterrupt, SystemExit):
print "\n[*] Rulegen was interrupted."
except (KeyboardInterrupt, SystemExit):
print "\n[!] Rulegen was interrupted."
f.close()
analysis_time = time.clock() - analysis_start analysis_time = time.clock() - 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 "[*] Finished processing %d passwords in %.2f seconds at the rate of %.2f p/sec" % (password_count, analysis_time, float(password_count)/analysis_time )
password_stats_total = sum(self.password_stats.values())
print "[*] Analyzed %d passwords (%0.2f%%)" % (password_stats_total,float(password_stats_total)*100.0/float(password_count))
print "[-] Skipped %d all numeric passwords (%0.2f%%)" % (self.numeric_stats_total, float(self.numeric_stats_total)*100.0/float(password_stats_total))
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_stats_total))
print "[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % (self.foreign_stats_total, float(self.foreign_stats_total)*100.0/float(password_stats_total))
print "\n[*] Top 10 word statistics" print "[*] Generating statistics for [%s] rules and words." % self.basename
top100_f = open("%s-top100.word" % self.basename, 'w') print "[-] Skipped %d all numeric passwords (%0.2f%%)" % \
word_stats_total = sum(self.word_stats.values()) (self.numeric_stats_total, float(self.numeric_stats_total)*100.0/float(password_count))
for i,(word,count) in enumerate(sorted(self.word_stats.iteritems(), key=operator.itemgetter(1), reverse=True)[:100]): print "[-] Skipped %d passwords with less than 25%% alpha characters (%0.2f%%)" % \
if i < 10: print "[+] %s - %d (%0.2f%%)" % (word, count, float(count)*100/float(word_stats_total)) (self.special_stats_total, float(self.special_stats_total)*100.0/float(password_count))
top100_f.write("%s\n" % word) print "[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % \
top100_f.close() (self.foreign_stats_total, float(self.foreign_stats_total)*100.0/float(password_count))
print "[*] Saving Top 100 words in %s-top100.word" % self.basename
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 rule statistics" print "\n[*] Top 10 rule statistics"
top100_f = open("%s-top100.rule" % self.basename, 'w') rules_i = 0
rule_stats_total = sum(self.rule_stats.values()) for (rule, count) in rules_counter.most_common():
for i,(rule,count) in enumerate(sorted(self.rule_stats.iteritems(), key=operator.itemgetter(1), reverse=True)[:100]): rules_sorted_file.write(rule)
if i < 10: print "[+] %s - %d (%0.2f%%)" % (rule, count, float(count)*100/float(rule_stats_total)) if rules_i < 10: print "[+] %s - %d (%0.2f%%)" % (rule.rstrip('\r\n'), count, count*100/rule_counter_total)
top100_f.write("%s\n" % rule) rules_i += 1
top100_f.close()
print "[*] Saving Top 100 rules in %s-top100.rule" % self.basename
print "\n[*] Top 10 password statistics" rules_file.close()
top100_f = open("%s-top100.password" % self.basename, 'w') rules_sorted_file.close()
password_stats_total = sum(self.password_stats.values())
for i,(password,count) in enumerate(sorted(self.password_stats.iteritems(), key=operator.itemgetter(1), reverse=True)[:100]):
if i < 10: print "[+] %s - %d (%0.2f%%)" % (password, count, float(count)*100/float(password_stats_total))
top100_f.write("%s\n" % password)
top100_f.close()
print "[*] Saving Top 100 passwords in %s-top100.password" % self.basename
f.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 word statistics"
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()
if __name__ == "__main__": if __name__ == "__main__":
@ -861,7 +938,8 @@ if __name__ == "__main__":
ruletune.add_option("--maxrulelen", help="Maximum number of operations in a single rule", type="int", default=10, metavar="10") 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("--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("--morerules", help="Generate suboptimal rules", action="store_true", default=False)
ruletune.add_option("--simplerules", help="Generate simple rules insert,delete,hashcat",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) parser.add_option_group(ruletune)
spelltune = OptionGroup(parser, "Fine tune spell checker engine:") spelltune = OptionGroup(parser, "Fine tune spell checker engine:")
@ -899,6 +977,8 @@ if __name__ == "__main__":
rulegen.max_rules=options.maxrules rulegen.max_rules=options.maxrules
rulegen.more_rules=options.morerules rulegen.more_rules=options.morerules
rulegen.simple_rules=options.simplerules rulegen.simple_rules=options.simplerules
rulegen.brute_rules=options.bruterules
if rulegen.brute_rules: print "[!] Bruteforcing reversal and rotation rules. (slower)"
# Debugging options # Debugging options
rulegen.word = options.word rulegen.word = options.word
@ -920,4 +1000,5 @@ if __name__ == "__main__":
# Analyze a single password or several passwords in a file # Analyze a single password or several passwords in a file
if options.password: rulegen.analyze_password(args[0]) if options.password: rulegen.analyze_password(args[0])
else: rulegen.analyze_passwords_file(args[0]) else:
rulegen.analyze_passwords_file(args[0])

View File

@ -8,25 +8,7 @@
# Copyright (C) 2013 Peter Kacherginsky # Copyright (C) 2013 Peter Kacherginsky
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Please see the attached LICENSE file for additiona licensing information.
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. 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.
#
# 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 OWNER 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.
import sys import sys
import re, operator, string import re, operator, string
@ -110,6 +92,7 @@ class StatsGen:
return "".join(mask) return "".join(mask)
def generate_stats(self, filename): def generate_stats(self, filename):
""" Generate password statistics. """
f = open(filename,'r') f = open(filename,'r')
@ -152,6 +135,8 @@ class StatsGen:
f.close() f.close()
def print_stats(self): 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 "[+] Analyzing %d%% (%d/%d) of passwords" % (self.filter_counter*100/self.total_counter, self.filter_counter, self.total_counter)
print " NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords" print " NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords"
print "\n[*] Line Count Statistics..." print "\n[*] Line Count Statistics..."