From 11de20c5f30640a90e73c09ad86144664a8aca11 Mon Sep 17 00:00:00 2001 From: maride Date: Fri, 11 Jun 2021 18:39:06 +0200 Subject: [PATCH] Add persistence mode --- barf.py | 12 +++- barf.sh | 37 +++++++++++- src/Bruteforce.py | 11 ++-- src/CheckpointBreakpoint.py | 23 ++++++++ src/Helper.py | 6 +- src/PersistenceBreakpoint.py | 30 ++++++++++ src/TargetManager.py | 109 +++++++++++++++++++++++++++++++++++ 7 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 src/CheckpointBreakpoint.py create mode 100644 src/PersistenceBreakpoint.py create mode 100644 src/TargetManager.py diff --git a/barf.py b/barf.py index cb5c9c9..25ce800 100644 --- a/barf.py +++ b/barf.py @@ -25,6 +25,7 @@ sys.path.insert(1, barf_path) # include project files from BreakpointManager import BreakpointManager +from TargetManager import TargetManager from Helper import * from Bruteforce import * @@ -39,11 +40,14 @@ def main(): # Create our breakpoints, managed by the BreakpointManager bm = BreakpointManager(args["positiveAddr"], args["negativeAddr"], args["winAddr"]) + # Manage the target with the TargetManager + tm = TargetManager(args["persistent"], args["startAddr"], args["endAddr"], args["buffAddr"]) + # start the bruteforcing madness ;) - # DisableLogging() - Bruteforce(bm, args["knownPrefix"], args["knownSuffix"], args["chunksize"]) + Bruteforce(bm, tm, args["knownPrefix"], args["knownSuffix"], args["chunksize"]) # g'night, gdb + gdb.execute("set confirm off") gdb.execute("quit") @@ -53,9 +57,13 @@ def getArguments(): a["positiveAddr"] = barf_positive_addr a["negativeAddr"] = barf_negative_addr a["winAddr"] = barf_win_addr + a["startAddr"] = barf_start_addr + a["endAddr"] = barf_end_addr + a["buffAddr"] = barf_buff_addr a["knownPrefix"] = barf_known_prefix a["knownSuffix"] = barf_known_suffix a["chunksize"] = barf_chunksize + a["persistent"] = barf_persistent return a diff --git a/barf.sh b/barf.sh index 3d18e4c..cb8d272 100755 --- a/barf.sh +++ b/barf.sh @@ -9,10 +9,14 @@ POSITIVEADDR="" NEGATIVEADDR="" WINADDR="" +STARTADDR="" +ENDADDR="" +BUFFADDR="" KNOWNPREFIX="" KNOWNSUFFIX="" BARFPATH="$(dirname $(realpath $0))/src" CHUNKSIZE=1 +PERSISTENT="False" # getopt is kind-of unstable across distributions and versions, so we implement it on our own # hat-tip to https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash @@ -32,6 +36,18 @@ while [[ $# -gt 0 ]]; do WINADDR="$2" shift; shift ;; + -s|--start-addr) + STARTADDR="$2" + shift; shift + ;; + -e|--end-addr) + ENDADDR="$2" + shift; shift + ;; + --buff-addr) + BUFFADDR="$2" + shift; shift + ;; -h|--help) SHOWHELP=1 shift @@ -48,6 +64,10 @@ while [[ $# -gt 0 ]]; do CHUNKSIZE="$2" shift; shift ;; + -x|--persistent) + PERSISTENT="1" + shift + ;; *) # unknown option - we assume it is the target literal TARGETFILE="$key" shift @@ -67,15 +87,30 @@ if [ ! -e "$TARGETFILE" ]; then exit 1 fi +# check if the persistent mode can be used +if [[ "$PERSISTENT" == "1" && ("$STARTADDR" == "" || "$ENDADDR" == "" || "$BUFFADDR" == "" ) ]]; then + # missing the end address for persistent mode + echo "You need to specify --start-addr, --end-addr and --buff-addr if you want to use persistent mode." + echo "Set --start-addr to an address before your input reaches the program (e.g. before fgets())" + echo "Set --end-addr to an address after the program has checked if the input is good or not (e.g. somewhere after gets('Yay!') and gets('Nay!'))" + echo "Set --buffer-addr to the address where user input is stored (e.g. the address of b in case of fgets(b, 16, stdin)" + exit 1 +fi + + # see if the user needs our help if [ "$SHOWHELP" == 1 ]; then echo "Usage: ./barf.sh" echo " -p | --positive-addr 0x123456 a location to be counted as good hit" echo " -n | --negative-addr 0x789ABC a location to be counted as bad hit" echo " -w | --win-addr 0xDEF042 a location reached if your input is correct" + echo " -s | --start-addr 0xF0000D a location directly after your input is fed into the target (for persistent mode)" + echo " -e | --end-addr 0x133337 a location where the to-be-fuzzed logic is done (for persistent mode)" + echo " --buff-addr 0x424242 the location where user input is stored (for persistent mode)" echo " -< | --prefix CTF{ a known prefix, e.g. the prefix of your flag" echo " -> | --suffix } a known suffix, e.g. the suffix of your flag" echo " -c | --chunksize 1 amount of characters to try at once" + echo " -x | --persistent enable the experimental (!) persistent mode" echo " -h | --help a great and useful help message, you should try it!" echo " ./path/to/your/crackme the path to the target to be fuzzed" echo "Note that you need to either specify --positive-addr or --negative-addr and your target of course." @@ -83,5 +118,5 @@ if [ "$SHOWHELP" == 1 ]; then fi # ready for take-off -gdb --quiet -nx --eval-command "py barf_positive_addr='$POSITIVEADDR';barf_negative_addr='$NEGATIVEADDR';barf_win_addr='$WINADDR';barf_known_prefix='$KNOWNPREFIX';barf_known_suffix='$KNOWNSUFFIX';barf_path='$BARFPATH';barf_chunksize=$CHUNKSIZE" --command barf.py $TARGETFILE +gdb --quiet -nx --eval-command "py barf_positive_addr='$POSITIVEADDR';barf_negative_addr='$NEGATIVEADDR';barf_win_addr='$WINADDR';barf_start_addr='$STARTADDR';barf_end_addr='$ENDADDR';barf_buff_addr='$BUFFADDR';barf_known_prefix='$KNOWNPREFIX';barf_known_suffix='$KNOWNSUFFIX';barf_path='$BARFPATH';barf_chunksize=$CHUNKSIZE;barf_persistent=$PERSISTENT" --command barf.py $TARGETFILE diff --git a/src/Bruteforce.py b/src/Bruteforce.py index 9e2bd89..030a89c 100644 --- a/src/Bruteforce.py +++ b/src/Bruteforce.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 from Helper import * +from TargetManager import TargetManager # The charset to try, sorted by the likelihood of a character class charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_!?'#%+/ ;[`@-\".<,*|&$(]=)^>\\:~" # bruteforces a single character, sandwiched between the known parts. # Returns the most promising string. -def BruteforceChar(bm, knownPrefix, knownSuffix, chunksize): +def BruteforceChar(bm, tm, knownPrefix, knownSuffix, chunksize): # keyFragment is the variable were we store our found-to-be-correct chars keyFragment = "" @@ -19,7 +20,7 @@ def BruteforceChar(bm, knownPrefix, knownSuffix, chunksize): # the resulting score is the base for the next round of guessing, hopefully with a single solution better than the score of knownPrefix + keyFragment + impossibleChar. # please also note that this will massively fail if the "impossible" character is part of the flag, at the very position it was tested on ... have fun detecting that bm.ResetBreakpoints() - TryInput(knownPrefix + keyFragment + "^" * chunksize + knownSuffix) + tm.Run(knownPrefix + keyFragment + "^" * chunksize + knownSuffix) refScore = bm.PopScore() # iterate over every character in the charset @@ -29,7 +30,7 @@ def BruteforceChar(bm, knownPrefix, knownSuffix, chunksize): # and try it bm.ResetBreakpoints() - TryInput(inp) + tm.Run(inp) score = bm.PopScore() # yay, that's a hit @@ -45,9 +46,9 @@ def BruteforceChar(bm, knownPrefix, knownSuffix, chunksize): # Bruteforce calls BruteforceChar until: # - BruteforceChar was unable to increase the score using any character in the charset, OR # - the "win" breakpoint is hit :) -def Bruteforce(bm, knownPrefix, knownSuffix, chunksize): +def Bruteforce(bm, tm, knownPrefix, knownSuffix, chunksize): while True: - res = BruteforceChar(bm, knownPrefix, knownSuffix, chunksize) + res = BruteforceChar(bm, tm, knownPrefix, knownSuffix, chunksize) if res is False: # no character from the given charset matched. :( EnableLogging() diff --git a/src/CheckpointBreakpoint.py b/src/CheckpointBreakpoint.py new file mode 100644 index 0000000..5de02e4 --- /dev/null +++ b/src/CheckpointBreakpoint.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import gdb + +# A simple breakpoint to set a checkpoint at the given address +# After that, it doesn't do a thing anymore +class CheckpointBreakpoint(gdb.Breakpoint): + # The address to break on + isSet = False + + def __init__(self, startAddr): + # gdb requires address literals to start with a star + if startAddr[0] != "*": + startAddr = "*" + startAddr + + super().__init__(startAddr) + + def stop(self): + if not self.isSet: + gdb.execute("checkpoint") + self.isSet = True + return False + diff --git a/src/Helper.py b/src/Helper.py index 5c92761..eef7ac8 100644 --- a/src/Helper.py +++ b/src/Helper.py @@ -9,16 +9,12 @@ def EnableLogging(): # Disables the typical GDB spam def DisableLogging(): + return gdb.execute("set logging file /dev/null") gdb.execute("set logging redirect on") gdb.execute("set logging on") -# Runs a given input through GDB -def TryInput(inp): - gdb.execute(f"run 2>/dev/null 1>&2 <<< $(echo '{inp}')") - - # Prints a small MOTD, hence the name of the function def MOTD(): print("+--------------------------------------------+") diff --git a/src/PersistenceBreakpoint.py b/src/PersistenceBreakpoint.py new file mode 100644 index 0000000..42c6122 --- /dev/null +++ b/src/PersistenceBreakpoint.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import gdb + +# A breakpoint class with reset-to-checkpoint-magic +# "You hit it? We reset it." +class PersistenceBreakpoint(gdb.Breakpoint): + targetManager = None + + # tm is apointer to TargetManager so we can Reset() the executable + # endAddr is the address on which we want to jump back + def __init__(self, tm, endAddr): + self.targetManager = tm + + # gdb requires address literals to start with a star + if endAddr[0] != "*": + endAddr = "*" + endAddr + + # actually create breakpoint + super().__init__(endAddr) + + # avoid spamming "Breakpoint X, ..." when hit + self.silent = True + + def stop(self): + # do the checkpoint thing + self.targetManager.Reset() + # we return true so we still break - we will 'continue' later, after we wrote memory. + return True + diff --git a/src/TargetManager.py b/src/TargetManager.py new file mode 100644 index 0000000..0130392 --- /dev/null +++ b/src/TargetManager.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import gdb +import base64 + +from CheckpointBreakpoint import CheckpointBreakpoint +from PersistenceBreakpoint import PersistenceBreakpoint + +# TargetManager is a wrapper around the target itself. +# Tasks include: +# - Implementing the persistent mode using gdb checkpoints +# - Feeding input into the binary +# The TargetManager aims to be the one-size-fits-all solution for execution handling. +# That means it is designed to have a unified interface, independent of e.g. persistent mode. +class TargetManager: + usePersistent = False + + # vars used for persistent mode + buffAddr = None + startBreakpoint = None + endBreakpoint = None + checkpointIndex = 1 + isRunning = False + + # usePersistent is a boolean, determing if the experimental persistent mode should be used + # startAddr is the address to start the persistent run + # endAddr is the address to jump back to startAddr + # buffAddr is the address of the target buffer to be written in persistent mode + def __init__(self, usePersistent, startAddr, endAddr, buffAddr): + self.usePersistent = usePersistent + + if usePersistent: + # parse buffer address to int + if buffAddr and isinstance(buffAddr, str): + buffAddr = int(buffAddr, 16) + self.buffAddr = buffAddr + + # set a breakpoint which will set a checkpoint at its address, the beginning + self.startBreakpoint = CheckpointBreakpoint(startAddr) + # breakpoint to reset to the checkpoint on + self.endBreakpoint = PersistenceBreakpoint(self, endAddr) + + # Runs the binary. + # If the persistent mode is used, your input will be written into memory directly. + # If the persistent mode is not used, your input will just be thrown into the binary. + def Run(self, inp): + if not self.usePersistent: + # not running in persistent mode, just feed the input into the binary + if isinstance(inp, str): + inp = inp.encode() + # converting it to base64 is a cheap hack to avoid sanitizing the input ;) + b = base64.b64encode(inp).decode() + gdb.execute("run > /dev/null <<< $(echo %s | base64 -d)" % b, to_string=True) + else: + # we're running in persistent mode, let's see if we need to kickstart the executable + if not self.isRunning: + # the executable doesn't yet run, so we gotta get it groovin ;) + # we want to have a single, clean run, so startBreakpoint can set its checkpoint + # and endBreakpoint can do the first round of resetting. After that, we can + # start filling our buffer and check if we come near the flag (see block below). + # That's why we feed /dev/null into the binary: fgets and other stdin read + # operations should terminate cleanly, avoiding hangs in the executable + gdb.execute("set confirm off") + gdb.execute(f"run > /dev/null < /dev/null", to_string=True) + self.isRunning = True + # Due to the /dev/null magic above, we haven't yet fed inp into the binary. We + # don't want to skip it, so we simply continue with the "isRunning==True" block. + # Please note, as this may cast some confusion, that at this point the binary + # had a full run-thru, is equipped with checkpoints and breakpoints and is + # currently in break mode (not running, so to speak), and is at startAddr. + + # the executable is already running and reset + # means we just need to feed input into the binary, then continue running it + i = gdb.inferiors()[0] + i.write_memory(self.buffAddr, inp + "\n\0") + gdb.execute("continue", to_string=True) + # Reset() will be called by endBreakpoint, we don't need to do that here + + # Resets the target back to the checkpoint set earlier, used for the persistent mode + # Will be called by endBreakpoint + def Reset(self): + # Check if we reached endBreakpoint before hitting startBreakpoint + if not self.startBreakpoint.isSet: + print("[!] Reset() called without a startBreakpoint set.") + print(" Check addresses of start and end!") + return + + # gdb seems to have a problem with more than 41885 checkpoints. + # the ID is rising even if checkpoints are deleted on the way. + # to mitigate this, we start from the beginning and let Run() re-start the executable. + if self.checkpointIndex > 40000: + self.checkpointIndex = 1 + self.isRunning = False + self.startBreakpoint.isSet = False + return + + # jump back to the start + gdb.execute("restart 1", to_string=True) + + # delete old checkpoint if we ran before + if self.checkpointIndex > 1: + gdb.execute(f"delete checkpoint {self.checkpointIndex}", to_string=True) + + # create a fresh copy of checkpoint 1 + gdb.execute("checkpoint", to_string=True) + # ... and use it + self.checkpointIndex += 1 + gdb.execute(f"restart {self.checkpointIndex}", to_string=True) +