# Copyright (c) 2014 FireEye, Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# 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 AUTHOR 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 AUTHOR 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 string
import re
import os
import argparse
import binascii

def getString(buf,pos):
	strpattern = r"([^\x00]+)"
	match = re.search(strpattern, buf[pos:])
	if match is not None:
		return match.group(1)
	else:
		return ""
		
def getPassword(buf,pos):
	strpattern = r"([^\x00]+)"
	prpattern = r"^[ -~]{4,}\x00"
	match = re.search(prpattern, buf[pos:pos+32])
	if match is None:
		return "0x%s" % binascii.hexlify(buf[pos:pos+32])
	match = re.search(strpattern, buf[pos:pos+32])
	if match is not None:
		return match.group(1)
	else:
		return ""

def getConfig(f,buf):
	pattern = r"StubPath.SOFTWARE\\Classes\\http\\shell\\open\\command.{22}Software\\Microsoft\\Active Setup\\Installed Components\\"
	
	match = re.search(pattern, buf)
	if match is not None:
		print "pivy config located in %s" % f
		pos = string.find(buf,match.group(0))
		configpos = pos - 0x40f
		id = getString(buf,configpos + 0xafa)
		group = getString(buf,configpos + 0xbf9)
		if re.search(r"^[ -~]+$",group) is None:
			group = "not configured"
		
		password = getPassword(buf,configpos + 0x145)
		mutex = getString(buf,configpos + 0x3fb)
		print "ID: %s" % id
		print "Group: %s" % group
		print "Password: %s" % password
		print "Mutex: %s" % mutex
		return (id,group,password,mutex)
	else:
		print "Could not locate pivy config.."
		return None

def getFile(f):
	rp = open(f, "rb")
	buf = rp.read()
	rp.close()
	return buf

if __name__ == "__main__":
	parser=argparse.ArgumentParser(description="finds and prints Poison Ivy RAT configuration details from a process memory dump")
	parser.add_argument('i', metavar='Input', help='a path to a process dump or a directory of process dumps containing Poison Ivy RAT configuration data')
	parser.add_argument('-csv', metavar="CSV", help='write results to CSV instead of printing to console')
		
	args = parser.parse_args()
	
	if os.path.isfile(args.i):
		f = args.i
		buf = getFile(f)
		getConfig(f,buf)
	elif os.path.isdir(args.i):
		if args.csv:
			csv = open(args.csv,"w")
			headers = "file,id,group,password,mutex\n"
			csv.write(headers)
		else:
			csv = None
			
		d = args.i
		for f in os.listdir(d):
			path = os.path.join(d, f)
			if os.path.isfile(path):
				buf = getFile(path)
				res = getConfig(path,buf)
				if csv is not None:
					if res is not None:
						csv.write("%s,%s,%s,%s,%s\n" % (f, res[0], res[1], res[2], res[3]))
		
		if csv is not None:
			csv.close()


