Add persistence mode

This commit is contained in:
maride 2021-06-11 18:39:06 +02:00
parent 0f7b756c93
commit 11de20c5f3
7 changed files with 215 additions and 13 deletions

12
barf.py
View File

@ -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

37
barf.sh
View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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("+--------------------------------------------+")

View File

@ -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

109
src/TargetManager.py Normal file
View File

@ -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)