"""
20JUN2018 - cs beacon config decoder
By <NAME> (@sysopfb) and <NAME>

Free to use, attribute properly.

Will attempt to work on beacons from memory with already decoded configs as well


TODO
Add logic for other XOR keys
Transform parsing code needs work as well
"""
import sys
import re
import binascii
import struct

BeaconSettings = {1: "PROTOCOL", 2: "PORT", 3: "SLEEPTIME", 4: "MAXGET", 5: "ITTER", 6: "MAXDNS", 7: "PUBKEY", 8: "DOMAINS", 9: "USERAGENT", 10: "SUBMITURI", 11: "C2_RECOVER", 12: "C2_REQUEST", 13: "C2_POSTREQ", 14: "SPAWNTO", 15: "PIPENAME", 16: "KILLDATE_YEAR", 17: "KILLDATE_MONTH", 18: "KILLDATE_DAY", 19: "DNS_IDLE", 20: "DNS_SLEEP", 21: "SSH_HOST", 22: "SSH_PORT", 23: "SSH_USERNAME", 24: "SSH_PASSWORD", 25: "SSH_KEY", 26: "C2_VERB_GET", 27: "C2_VERB_POST", 28: "C2_CHUNK_POST", 29: "SPAWNTO_X86", 30: "SPAWNTO_X64", 31: "CRYPTO_sCHEME", 32: "PROXY_CONFIG", 33: "PROXY_USER", 34: "PROXY_PASSWORD", 35: "PROXY_BEHAVIOR", 36: "INJECT_OPTIONS", 37: "WATERMARK", 54: "C2_HOST_HEADER"}

SettingTypes = {0: "TYPE_NONE", 1: "TYPE_SHORT", 2: "TYPE_INT", 3: "TYPE_PTR"}


TransformStep = {1: "APPEND", 2: "PREPEND", 3: "BASE64", 4: "PRINT", 5: "PARAMETER", 6: "HEADER", 7: "BUILD", 8: "NETBIOS", 9: "_PARAMETER", 10: "_HEADER", 11: "NETBIOSU", 12: "URI_APPEND", 13: "BASE64URL", 14: "STRREP", 15: "MASK"}

test_data = '\n\x00\x00\x00\x0bAccept: */*\x00\x00\x00\n\x00\x00\x00\x16Content-Type: text/xml\x00\x00\x00\n\x00\x00\x00 X-Requested-With: XMLHttpRequest\x00\x00\x00\n\x00\x00\x00\x14Host: www.amazon.com\x00\x00\x00\t\x00\x00\x00\nsz=160x600\x00\x00\x00\t\x00\x00\x00\x11oe=oe=ISO-8859-1;\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x02sn\x00\x00\x00\t\x00\x00\x00\x06s=3717\x00\x00\x00\t\x00\x00\x00"dc_ref=http%3A%2F%2Fwww.amazon.com\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x04'

def parse_transformdata(data):
	config = []
	while len(data) > 5:
		(tstep,val,l,) = struct.unpack_from('>BHH',data)
		data = data[5:]
		if tstep in TransformStep.keys():
			if tstep != 7:
				temp = (TransformStep[tstep],val)
				if l > 0:
					s = str(data[:l])
					temp += (s,)
				data = data[l+3:]
			else:
				(a,b,) = struct.unpack_from('>HH', data)
				data = data[4:]
				temp = ()
				if val != 0:
					temp += (TransformStep[val],l)
				if a != 0:
					temp += (TransformStep[a],)
				if b != 0:
					if b in [5,6]:
						(c,d) = struct.unpack_from('>HH', data)
						data = data[4:]
						temp += (TransformStep[b],str(data[:d]))
						data = data[l+3:]
					elif b in [3]:
						(c,d) = struct.unpack_from('>HH', data)
						data = data[4:]
						if d in [5,6]:
							(e,f) = struct.unpack_from('>HH', data)
							data = data[4:]
							temp += ((TransformStep[b],TransformStep[d],str(data[:f])))
							data = data[f+3:]

					else:
						temp += (TransformStep[b],)
				temp = ('BUILD',temp)
			config.append(temp)
	return config



class conf_item:
	def __init__(self,beacon_index, setting_type, length, value):
		if beacon_index in BeaconSettings.keys():
			self.beaconSetting = BeaconSettings[beacon_index]
		else:
			self.beaconSetting = "UNKNOWN"+str(beacon_index)
		if setting_type in SettingTypes.keys():
			self.settingType = SettingTypes[setting_type]
		else:
			self.settingType = "UNKNOWN"+str(beacon_index)
		self.length = length
		if beacon_index == 7 or "UNKNOWN" in self.beaconSetting:
			if setting_type in [2,3] and beacon_index != 7:
				self.data = value
			else:
				self.data = binascii.hexlify(str(value))
		elif beacon_index in [12,13]:
			try:
				self.data = parse_transformdata(value)
			except:
				self.data = str(value)
		else:
			self.data = str(value)

	def get_jsonify(self):
		return({self.beaconSetting:str(self.data)})

class beaconSettings:
	def __init__(self, blob):
		self.items = []
		(bsetting, stype, l,) = struct.unpack_from('>HHH', blob)
		while bsetting < 60 and stype < 10 and l < 1000 and len(blob) > 7:
			blob = blob[6:]
			data = blob[:l]
			blob = blob[l:]
			if stype == 3:
				data = data.strip('\x00')
			elif stype == 2:
				data = struct.unpack('>I', data)[0]
			elif stype == 1:
				data = struct.unpack('>H', data)[0]
			self.items.append(conf_item(bsetting, stype, l, data))
			if len(blob) < 8:
				break
			(bsetting, stype, l,) = struct.unpack_from('>HHH', blob)


	def get_jsonify(self):
		ret_val = {}
		for item in self.items:
			ret_val.update(item.get_jsonify())
		return ret_val


def decoder(data):
	config = {}
	blob = bytearray(data)
	key = 0
	#bruteforces xor key by searching for known plaintext
	for x in range(1, 0xff):
		start1 = re.escape(''.join([chr(ord(c) ^ x) for c in '\x00\x01\x00\x01\x00\x02\x00']))
		start2 = re.escape(''.join([chr(ord(c) ^ x) for c in '\x00\x02\x00\x01']))
		start = start1 + '.' + start2
		if re.search(start, data):
			key = x
			break
	for i in range(len(blob)):
		blob[i] ^= x
	start = re.findall('''\x00\x01\x00\x01\x00\x02\x00.\x00\x02\x00\x01''',blob)
	if len(start) > 0:
		start_offset = blob.find(start[0])
		bs = beaconSettings(blob[start_offset:])
		return bs.get_jsonify()
	pub_matches = re.findall('''\x30[\x00-\xff]{100,200}\x02\x03\x01\x00\x01\x00\x00''', blob)
	if pub_matches:
		config['PUBKEY'] = binascii.hexlify(pub_matches[0])

	pipe_matches = re.findall('''\\\\\\\\%s\\\\pipe\\\\[^\x00]+''', blob)
	if pipe_matches:
		config['PIPE'] = map(str,pipe_matches)

	ua_matches = re.findall('''Mozilla[^\x00]+''', blob)
	if ua_matches:
		config['UserAgents'] = map(str,ua_matches)

	ip_matches = re.findall('''\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}''', blob)
	if ip_matches:
		config['C2'] = map(str,ip_matches)
	uri_matches = re.findall('''\/[^\x2e\x00]+\.php''',blob)
	if uri_matches:
		config['URI'] = map(str,uri_matches)

	#Check if this is a mem dumped DLL with already decoded config
	if config == {}:
		start = re.findall('''\x00\x01\x00\x01\x00\x02\x00.\x00\x02\x00\x01''',data)
		if len(start) > 0:
			start_offset = data.find(start[0])
			bs = beaconSettings(data[start_offset:])
			return bs.get_jsonify()


	return config

if __name__ == "__main__":
	data = open(sys.argv[1], 'rb').read()
	t = decoder(data)
	print(t)
