#!/usr/bin/python

'''
FuzzDiff
Written by: Dan Rosenberg

This is a simple tool designed to help out with crash analysis during fuzz
testing.  It selectively "un-fuzzes" portions of a fuzzed file that is known to
cause a crash, re-launches the targeted application, and sees if it still
crashes.  Eventually, this will yield a file that still causes the crash, but
contains a minimum set of changes from the original un-fuzzed file.

Copyright (C) 2010 Virtual Security Research, LLC. - All rights reserved

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.

'''

import sys
import os
import random
import shutil
import subprocess
import time

# Probability a fuzzed byte will be reverted
THRESHOLD = .5

# Number of iterations before stopping
ITER = 6000

# Temporary output
TMPFILE = "/tmp/fuzz123"

# Time to wait before killing target program
WAIT = 2

def unfuzz(orig, fuzz, out):

	origstat = os.stat(orig)
	fuzzstat = os.stat(fuzz)

	if(origstat.st_size != fuzzstat.st_size):
		print "[*] Files are not the same size."
		term(-1)

	try:
		origfd = open(orig, "r")
		fuzzfd = open(fuzz, "r")
		outfd = open(out, "w")
	except:
		print "[*] Error opening file - bad arguments"
		term(-1)

	diff = 0
	unchanged = 0

	while(1):
		c = origfd.read(1)
		if not c: break
		d = fuzzfd.read(1)

		# If there's a diff...
		if(c != d):
			diff += 1
			# With some probability, revert it
			if(random.random() > THRESHOLD):
				outfd.write(c)				
			else:
				unchanged += 1
				outfd.write(d)
		else:
			outfd.write(c)

	return (diff, unchanged)

def term(ret):
	print "[*] Terminating..."
	try:
		os.remove(TMPFILE)
	finally:
		sys.exit(ret)

#######################
# Program entry point #
#######################

if(len(sys.argv) < 4):
	print "[*] Usage: fuzzdiff [orig] [fuzzed] [program] [args]"
	sys.exit(-1)

progargs = sys.argv[3:]
orig = sys.argv[1]
best = "fuzz.out" 

try:
	shutil.copy(sys.argv[2], best)
except:
	print "[*] Error opening file - bad fuzzfile argument"
	term(-1)

null = open("/dev/null", "rw")

# Main loop
for i in range(ITER):
	
	(diff, unchanged) = unfuzz(orig, best, TMPFILE)

	# Only bother if we actually reduced the number of diffs
	if(unchanged < diff):

		# Test if the result still crashes the target
		try:
			p = subprocess.Popen(progargs + [TMPFILE], stdout=null, stderr=null)
		except:
			print "[*] Error running program"
			term(-1)

		# Give the program some time to start...
		time.sleep(WAIT)

		# If it segfaulted, keep the changes
		if (p.poll() == -11):
			shutil.copy(TMPFILE, best)
			print "[*] Reduced diffs from", diff, "to", unchanged
			if(unchanged == 1):
				break

		# If the program hasn't terminated, kill it
		elif(p.returncode == None):
			p.terminate()

print "[*] Output written to fuzz.out"
term(1)