diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c632d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*~ +/dictionaries/reductor.py +/brute_venv/ +/html/ +/src/__pycache__/ +/test/__pycache__/ +#*# +/targets/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc4a981 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +VENV=brute_venv +INVENV = $(shell pip3 -V | grep $(VENV)) +current_dir = $(shell pwd) + +prereqs: venvcheck FORCE + pip install -r requirements.txt + +venv: FORCE + python3 -m venv $(VENV) + +docs: + pdoc --html ./src/brutus.py --force + +venvcheck: +ifeq ($(INVENV),) + $(error You should only run this from within the venv. Use '. ./$(VENV)/bin/activate') +else + @echo "venv check passed\n" +endif + +test/testbin: test/testbin.c + $(CC) -o test/testbin test/testbin.c + +test: FORCE venvcheck test/testbin + py.test -v test/ + + + + +FORCE: diff --git a/README.md b/README.md new file mode 100644 index 0000000..cebb981 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Brute Campbell + +This document should be edited to include the points below and any +other you think appropriate. You can use this as the report part of +your submission if you convert it to PDF and make sure to include +source code, etc. If you don't want to do this, you can write the +report in Word, LaTeX, etc. + +## User documentation + +## Unit Tests + +## Algorithms diff --git a/dictionaries/base.txt b/dictionaries/base.txt new file mode 100644 index 0000000..d18e5e9 --- /dev/null +++ b/dictionaries/base.txt @@ -0,0 +1,32 @@ +abroad +afford +airport +anchor +andorra +another +coated +romance +rotary +royalty +salmon +season +senator +shadow +social +solaris +station +stomach +storage +tattoo +tobago +tomato +toolbar +totally +totals +tractor +upload +various +vocals +voltage +warrior + diff --git a/download_targets.sh b/download_targets.sh new file mode 100755 index 0000000..8af1d6a --- /dev/null +++ b/download_targets.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +bins=(basic1 basic2 basic3 intermediate1 intermediate2 intermediate3 advanced1 advanced2 advanced3) + +rm -f targets/* + +for b in "${bins[@]}" +do + curl -o targets/$b "https://github.coventry.ac.uk/pages/csx239/naturan_demanto/bins/$b" +done + +chmod u+x targets/* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e67a70d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pdoc3 +pytest +pexpect diff --git a/src/brute_advanced.py b/src/brute_advanced.py new file mode 100755 index 0000000..962555b --- /dev/null +++ b/src/brute_advanced.py @@ -0,0 +1,70 @@ +#!python3 +from brutus import Binary + + +def maxPos(seq): + """ Given a list of numbers, return the **position** of the largest + Args: + seq: the list to be searched + Returns: + an integer position of the largest number in `seq` + """ + maxNum=seq[0] + maxPos=0 + for i in range(len(seq)): + if seq[i]>maxNum: + maxNum=seq[i] + maxPos=i + return maxPos + + +# def averageTry(target, promptText, failText, guess, repeats=2): +# # Provided to assist. If you use it, document it properly... :) +# # Runs multiple attempts at cracking the binary, returning the +# # success AND the average length of time each try took +# results=[] +# success=False +# for i in range(repeats): +# b=Binary(target) +# b.run() +# result=b.timedAttempt(promptText,guess, failText) +# success=result[0] +# results.append(result[1]) +# return (success,sum(results)/len(results)) + +def breakBinary(target, promptText, failText): + + + #Your code here + # Suggested algorithm: + + #1. Use an accumulator for the current guess + #2. in a loop, try the current guess plus each letter of the alphabest and see which one takes longest + #3. If it is the correct password, end + #4. If not, add the current best letter to the guess and repeat... + pass + + + +if __name__=="__main__": + + + # Create a simple menu system to pick the binary we want to force + targets=[] + targets.append(["targets/advanced1","Password:", "Password Incorrect"]) + targets.append(["targets/advanced2","Password:", "Password Incorrect"]) + targets.append(["targets/advanced3","Password:", "Password Incorrect"]) + + print("Intermediate Binary Breaker") + print("Which binary do you want to brute force?") + + for c in range(len(targets)): + print(f"{c}: {targets[c][0]}") + + selection=int(input("Enter the number of the binary to be forced: ")) + + if 0 <= selection < len(targets): + target=targets[selection] + breakBinary(target[0],target[1],target[2]) + else: + print("Invalid selection") diff --git a/src/brute_basic.py b/src/brute_basic.py new file mode 100755 index 0000000..4a815dd --- /dev/null +++ b/src/brute_basic.py @@ -0,0 +1,79 @@ +#!python3 +from brutus import Binary + +# This is a starting-point for your project +# Most of the work is done for the basic task +# Intermediate will require reading from a dictionary file and creating guesses based on the contents +# For the advanced task, see the supporting module "brute_time" + + + +def generateGuesses(): + """ Creates a list of PIN guesses. Needs finishing, testing and documenting... """ + #Here, the guesses are listed explicitly. We *could* write out + #1000 guesses, but the point is to use the computer to do the + #brute force work, not our fingers + + #For the basic task, the only functionality you need is to replace + #the line below with code that can generate all the potential + #guesses + + return ["000","001","002","003","004"] + + + +def breakBinary(target, promptText, failText): + """" Break into the given target binary. + Assumes "basic" level binary, with PIN codes of 000-999 + Args: + target: path to the binary. e.g. "./bins/basic1" + promptText: text to look for in the output that signals a password is required. e.g. "Password:" + failText: text that indicates an attempt failed. e.g. "Password Incorrect" + Returns: + None: if no successful attempt was made + string: a successful password""" + + + guesses=generateGuesses() + + + + for g in guesses: + + #The actual attempt + b=Binary(target) + b.run() + success=b.attempt(promptText,g, failText) + + + if success: + print(f"The Guess '{g}' appears to be correct") + return g #Return the answer. No need to "break" because the return exits the function + else: + print(f"guess: {g} - Password incorrect!") + return None #If we get here, it means we didn't return earlier in the loop with a successful guess + + +if __name__=="__main__": + + + # Create a simple menu system to pick the binary we want to force + targets=[] + targets.append(["targets/basic1","Password:", "Password Incorrect"]) + targets.append(["targets/basic2","Enter the secret code:", "ACCESS DENIED"]) + targets.append(["targets/basic3","Got the PIN?", "NO"]) + + + print("Basic Binary Breaker") + print("Which binary do you want to brute force?") + + for c in range(len(targets)): + print(f"{c}: {targets[c][0]}") + + selection=int(input("Enter the number of the binary to be forced: ")) + + if 0 <= selection < len(targets): + target=targets[selection] + breakBinary(target[0],target[1],target[2]) + else: + print("Invalid selection") diff --git a/src/brute_intermediate.py b/src/brute_intermediate.py new file mode 100755 index 0000000..a72e125 --- /dev/null +++ b/src/brute_intermediate.py @@ -0,0 +1,85 @@ +#!python3 +from brutus import Binary + +def wordsFromFile(filePath): + """ Read lines from a file containing one word per line and return a list of the words + Args: + filePath: the absolute or relative path of the file to be read + Returns: + a list of the words from the file, stripped of whitespace + """ + f=open(filePath,"r") + out=[] + for l in f.readlines(): + w=l.strip() + if len(w)>0: + out.append(w.lower()) + f.close() + return out + +def breakBinary(target, promptText, failText, guesses): + """" Break into the given target binary. + Assumes "intermeduate level binary, with dictionary words + Args: + target: path to the binary. e.g. "./bins/basic1" + promptText: text to look for in the output that signals a password is required. e.g. "Password:" + failText: text that indicates an attempt failed. e.g. "Password Incorrect" + guesses: list of words to try as passwords + Returns: + None: if no successful attempt was made + string: a successful password""" + + for g in guesses: + + #The actual attempt + b=Binary(target) + b.run() + success=b.attempt(promptText,g, failText) + + + if success: + print(f"The Guess '{g}' appears to be correct") + return g #Return the answer. No need to "break" because the return exits the function + else: + print(f"guess: {g} - Password incorrect!") + return None #If we get here, it means we didn't return earlier in the loop with a successful guess + + +if __name__=="__main__": + + #Load the dictionary + words=wordsFromFile("dictionaries/base.txt") + + + ### YOUR CODE HERE + ### Currently it passes in the plain words + ### Change the line "words2=words" so that the list "words2" contains your guesses + ### You need to create a word list that has the dictionary words in PLUS + ### 1. Each word with all 0-9 digits appended (so 'swordfish' would be 'swordfish0', 'swordfish1' etc. + ### 2. Each word turned into "l33t-5p34k" + ### Each o becomes 0, each i becomes 1, each a becomes 4, each s becomes 5, each e becomes 3 + ### 'swordfish' becomes '5w0rdf15h', for example + ### You can assume case (upper/lower) will not need to be changed + + words2=words + + + # Create a simple menu system to pick the binary we want to force + targets=[] + targets.append(["targets/intermediate1","Password:", "Password Incorrect"]) + targets.append(["targets/intermediate2","Secret code:", "Auth Failure"]) + targets.append(["targets/intermediate3","Enter Credentials:", "Invalid Credentials"]) + + print("Intermediate Binary Breaker") + print("Which binary do you want to brute force?") + + for c in range(len(targets)): + print(f"{c}: {targets[c][0]}") + + selection=int(input("Enter the number of the binary to be forced: ")) + + if 0 <= selection < len(targets): + target=targets[selection] + breakBinary(target[0],target[1],target[2], words2) + else: + print("Invalid selection") diff --git a/src/brutus.py b/src/brutus.py new file mode 100644 index 0000000..d68d46a --- /dev/null +++ b/src/brutus.py @@ -0,0 +1,78 @@ +""" Brutus: A simple password-protected password brute-force support tool + +Brutus provides a simple interface for you to run a binary or script and test a password, simplifying the task of breaking passwords with brute force. + +""" +import pexpect +import time + +class Binary: + """ Represents a binary file for cracking and allows it to be run with given input and a test for correct/incorrect password """ + def __init__(self,path,options=[]): + """ Initialises the binary + + Args: + path: the absolute or relative path of the binary + options: an option list of options that are to be given to the binary on execution + """ + self.path=path + self.options=options + self.proc=None + self.queue=[] + def run(self): + """ Begins executing the binary. Returns as soon as it has been launched. """ + argv=[self.path]+self.options + self.proc=child = pexpect.spawn(" ".join(argv)) + def read(self): + """ Reads data from the standard output of the binary. + Returns: + None: if there is no data + A String: containing the next line of output if available.""" + + self.proc.expect(prompt) + self.proc.send(data+"\n") + try: + self.proc.expect(fail, timeout=2) + return False + except pexpect.exceptions.EOF: + return True + + + def attempt(self,prompt,data, fail): + """ Make a guess at a password, when the expected prompt is found + Args: + prompt: a string to idenitfy in the output that signifies WHEN to make the attempt + data: a string to send as the password guess + fail: the text expected on a failed attempt + Returns: + True: If the failure text *IS NOT* found in the output after the attempt + False: If the failure text *IS* found in the output after the attempt + """ + self.proc.expect(prompt) + self.proc.send(data+"\n") + try: + self.proc.expect(fail, timeout=2) + return False + except pexpect.exceptions.EOF: + return True + + + def timedAttempt(self,prompt,data,fail): + """ Make a guess at a password, when the expected prompt is found + Args: + prompt: a string to idenitfy in the output that signifies WHEN to make the attempt + data: a string to send as the password guess + fail: the text expected on a failed attempt + Returns: + a tuple in which the first item is: + + - True: If the failure text *IS NOT* found in the output after the attempt + + - False: If the failure text *IS* found in the output after the attempt + + and the second item is the number of fractional seconds the guess took to make + """ + t1=time.perf_counter() + result=self.attempt(prompt,data,fail) + t2=time.perf_counter() + return (result,t2-t1) diff --git a/targets/.placeholder b/targets/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/test/test_basic.py b/test/test_basic.py new file mode 100644 index 0000000..1303c8f --- /dev/null +++ b/test/test_basic.py @@ -0,0 +1,13 @@ +import pytest +import sys +sys.path.append("./src/") + + +#You will need to write tests for your own functions, or change tests for ones you modify + +import brute_basic + + +def test_guessGenerator(): + #This definitely needs changing and expanding + assert len(brute_basic.generateGuesses())==5 diff --git a/test/test_brutus.py b/test/test_brutus.py new file mode 100644 index 0000000..f240510 --- /dev/null +++ b/test/test_brutus.py @@ -0,0 +1,24 @@ +import pytest +import sys +sys.path.append("./src/") + +#Here the tests are all around the "brutus" module that assists in brute-forcing +#You will need to write tests for your own functions + +import brutus + + +def test_incorrect(): + bad_guesses=["","NO","123","aejfhskdjfhkjsdfh","-1","Swordfish", " swordfish", "swordfish "] + for guess in bad_guesses: + b=brutus.Binary("./test/testbin") + b.run() + result=b.attempt("password:",guess,"Password Incorrect") + assert result==False, f"'{guess}' worked as a guess, but shouldn't have" + + +def test_correct(): + b=brutus.Binary("./test/testbin") + b.run() + result=b.attempt("password:","swordfish","Password Incorrect") + assert result==True, "The correct password was used, but brutus reported it incorrect" diff --git a/test/test_maxPos.py b/test/test_maxPos.py new file mode 100644 index 0000000..34d588e --- /dev/null +++ b/test/test_maxPos.py @@ -0,0 +1,14 @@ +import pytest +import sys +sys.path.append("./src/") + + +#You will need to write tests for your own functions, or change tests for ones you modify + +from brute_advanced import maxPos + + +def test_maxPos(): + assert maxPos([1,2,3])==2 + assert maxPos([1])==0 + assert maxPos([1,-10])==0 diff --git a/test/testbin b/test/testbin new file mode 100755 index 0000000..3a73082 Binary files /dev/null and b/test/testbin differ diff --git a/test/testbin.c b/test/testbin.c new file mode 100644 index 0000000..f5b931c --- /dev/null +++ b/test/testbin.c @@ -0,0 +1,22 @@ +#include +#include +#include +#include +#include + +int main(void) +{ + char* actualPw = "swordfish"; //Of course + char guess[20]; + printf ("Enter the password: "); + fgets ( guess, 80, stdin ); + + //Strip the \n, replace with 0 + guess[strcspn(guess, "\n")] = 0; + printf("You entered [%s]\n",guess); + if(strcmp(guess,actualPw)==0){ + printf("Password Correct\n"); + }else{ + printf("Password Incorrect\n"); + } +}