#!/usr/bin/env python3
#
# MIT License
#
# Copyright (c) 2017 Milton Valencia
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Script name     : objdump2shellcode.py
# Version         : 3.3
# Created date    : 5/27/2017
# Last update     : 10/1/2017
# Author          : wetw0rk
# Architecture	  : x86, and x86-x64
# Python version  : 3.5
# Designed OS     : Linux (preferably a penetration testing distro)
# Dependencies    : none, besides objdump; installed on most OS's,
#		    and even if you don't have objdump installed
#		    raw dump is now supported.
#
# Description     : This is 1000000000 times easier than manually
#                   copying/pasting custom shellcode generated by
#                   objdump. I noticed Black Arch is hosting this
#                   this tool aswell :D so I added more features!
#		    Note: Sometimes 64bit binarys can be finiky I
#		    recommend reading from stdin if you encounter
#		    any issues when dumping, as formatting has not
#		    failed me yet.
#

import os, sys, codecs, subprocess, argparse

instructions	= []
no_junk		= []
op_line		= []

# for our info dump function
lang = {
	"python"	: "#",
	"perl"		: "#",
	"c"		: "//"
}

class colors:

	def __init__(self):
		pass

	RED	= '\033[31m'
	BLUE	= '\033[94m'
	END	= '\033[0m'
	BOLD	= '\033[1m'
	YELLOW	= '\033[93m'

def format_list():

	supported_norm = [
	'hex',
	'hex_space',
	'nasm',
	'c',
	'perl',
	'python',
	'python3',
	'bash',
	'csharp',
	'dword',
	'java',
	'num',
	'powershell',
	'ruby',
	'raw',
	]

	supported_comment = [
	'python',
	'c',
	'perl',
	]

	sn = "\t"
	sc = "\t"

	# normal dumpable languages
	print("Normal output:")
	for i in range(len(supported_norm)):
		sn += "{:s}, ".format(supported_norm[i])

	print(sn[:len(sn)-2])
	# comment supported languages
	print("Comment dump:")
	for i in range(len(supported_comment)):
		sc += "{:s}, ".format(supported_comment[i])

	print(sc[:len(sc)-2])

class format_dump():

	def __init__(self, ops, mode, var_name, badchars):
		self.ops	= ops
		self.mode	= mode
		self.var_name	= var_name
		self.badchars	= badchars

	def character_analysis(self, num, results):

		global op_line

		spotted = []

		if self.badchars != None:
			# if we have bad characters split
			# them in order spot them later.
			sep_chars = self.badchars.split(",")

			for i in range(len(sep_chars)):
				if sep_chars[i] in self.ops:
					spotted += ("{:s}".format(sep_chars[i])),

		# here we begin to spot the badchars should we find one
		# we will replace it with a bold and red opcode, simply
		# making identification an ease
		indiv_byte = len(spotted)-1     	# loop counter for bad characters

		# when debugging shellcode we want to splice differently
		if num == 1337:
			for i in range(len(op_line)):
				while indiv_byte > -1:
					if spotted[indiv_byte] in op_line[i]:
						highlight_byte = "{:s}{:s}{:s}{:s}".format(colors.BOLD, colors.RED, spotted[indiv_byte], colors.END).lstrip()
						op_line[i] = op_line[i].replace(spotted[indiv_byte], highlight_byte)
						results += i,
					indiv_byte -= 1

				indiv_byte = len(spotted)-1	# reset the loop counter
		else:
			# tactical dumping begins here
			splits = [self.ops[x:x+num] for x in range(0,len(self.ops),num)]
			for i in range(len(splits)):
				while indiv_byte > -1:
					if spotted[indiv_byte] in splits[i]:
						highlight_byte = "{:s}{:s}{:s}{:s}".format(colors.BOLD, colors.RED, spotted[indiv_byte], colors.END)
						splits[i] = splits[i].replace(spotted[indiv_byte], highlight_byte)
					indiv_byte -= 1

				indiv_byte = len(spotted)-1	# reset the loop counter

			for i in range(len(splits)):
				results += splits[i],

		return

	def informational_dump(self):

		print("Payload size: {:d} bytes".format(int(len(self.ops)/4)))

		global no_junk, instructions, op_line, done

		num	= 1337
		results = []
		done	= []

		if self.mode == "python":
			for i in range(len(no_junk)):
				op_line += "\\x{:s}".format(no_junk[i].replace(" ", "\\x")),
			self.character_analysis(num, results)
			for i in range(len(op_line)):
				if i in results:
					done += ("{:s} += \"{:s}\"{:s}{:s}\t{:s} {:s} <== WARNING BADCHARS{:s}".format(
						self.var_name,
						op_line[i],
						colors.BOLD,
						colors.YELLOW,
						lang[self.mode],
						instructions[i],
						colors.END)
					).expandtabs(62),
				else:
					done += ("{:s} += \"{:s}\"\t{:s} {:s}".format(
						self.var_name,
						op_line[i],
						lang[self.mode],
						instructions[i])
					),

			print('{:s} = ""'.format(self.var_name))
			for i in range(len(done)):
				print("{:s}".format(done[i]).expandtabs(40))

		if self.mode == "perl":
			for i in range(len(no_junk)):
				op_line += "\\x{:s}".format(no_junk[i].replace(" ", "\\x")),
			self.character_analysis(num, results)
			for i in range(len(op_line)):

				if i == (len(op_line)-1) and i in results:
					done += ("\"{:s}\"{:s};{:s}\t{:s} {:s} <== WARNING BADCHARS{:s}".format(
						op_line[i],
						colors.BOLD,
						colors.YELLOW,
						lang[self.mode],
						instructions[i],
						colors.END)
					).expandtabs(62),

				elif i == (len(op_line)-1):
					done += ("\"{:s}\";\t{:s} {:s}".format(
						op_line[i],
						lang[self.mode],
						instructions[i])
					),

				elif i in results:
					done += ("\"{:s}\"{:s}.{:s}\t{:s} {:s} <== WARNING BADCHARS{:s}".format(
						op_line[i],
						colors.BOLD,
						colors.YELLOW,
						lang[self.mode],
						instructions[i],
						colors.END)
					).expandtabs(62),
				else:
					done += ("\"{:s}\".\t{:s} {:s}".format(
						op_line[i],
						lang[self.mode],
						instructions[i])
					),

			print('{:s} = ""'.format(self.var_name))
			for i in range(len(done)):
				print("{:s}".format(done[i]).expandtabs(40))


		if self.mode == "c":
			for i in range(len(no_junk)):
				op_line += "\\x{:s}".format(no_junk[i].replace(" ", "\\x")),

			self.character_analysis(num, results)
			for i in range(len(op_line)):

				if i == (len(op_line)-1) and i in results:
					done += ("\"{:s}\"{:s};{:s}\t{:s} {:s} <== WARNING BADCHARS{:s}".format(
						op_line[i],
						colors.BOLD,
						colors.YELLOW,
						lang[self.mode],
						instructions[i],
						colors.END)
					).expandtabs(62),

				elif i == (len(op_line)-1):
					done += ("\"{:s}\";\t{:s} {:s}".format(
						op_line[i],
						lang[self.mode],
						instructions[i])
					),

				elif i in results:
					done += ("\"{:s}\"{:s}{:s}\t{:s} {:s} <== WARNING BADCHARS{:s}".format(
						op_line[i],
						colors.BOLD,
						colors.YELLOW,
						lang[self.mode],
						instructions[i],
						colors.END)
					).expandtabs(62),
				else:
					done += ("\"{:s}\"\t{:s} {:s}".format(
						op_line[i],
						lang[self.mode],
						instructions[i])
					),

			print('unsigned char {:s}[] = ""'.format(self.var_name))
			for i in range(len(done)):
				print("{:s}".format(done[i]).expandtabs(40))



	def tactical_dump(self):

		results = []

		if self.mode != "raw":
			print("Payload size: {:d} bytes".format(int(len(self.ops)/4)))

		if self.mode == "raw":
			str_obj = bytes(self.ops, 'ascii')
			raw_ops = codecs.escape_decode(str_obj)[0]
			sys.stdout.buffer.write(raw_ops)

		if self.mode == "python":
			num = 60
			self.character_analysis(num, results)
			print('{:s} = ""'.format(self.var_name))
			for i in range(len(results)):
				print("{:s} += \"{:s}\"".format(self.var_name, results[i]))

		if self.mode == "python3":
			num = 60
			self.character_analysis(num, results)
			print('{:s} = bytearray()'.format(self.var_name))
			for i in range(len(results)):
				print("{:s} += b'{:s}'".format(self.var_name, results[i]))

		if self.mode == "c":
			num = 60
			self.character_analysis(num, results)
			print("unsigned char {:s}[] = ".format(self.var_name))
			for i in range(len(results)):
				if i == (len(results) -1):
					print("\"{:s}\";".format(results[i]))
				else:
					print("\"{:s}\"".format(results[i]))

		if self.mode == "bash":
			num = 56
			self.character_analysis(num, results)
			for i in range(len(results)):
				if i == (len(results) -1):
					print("$'{:s}'".format(results[i]))
				else:
					print("$'{:s}'\\".format(results[i]))

		if self.mode == "ruby":
			num = 56
			self.character_analysis(num, results)
			print("{:s} = ".format(self.var_name))
			for i in range(len(results)):
				if i == (len(results) -1):
					print("\"{:s}\"".format(results[i]))
				else:
					print("\"{:s}\" +".format(results[i]))

		if self.mode == "hex_space":
			op	= []
			num	= 8
			asm_raw = ""
			self.character_analysis(num, results)
			for i in range(len(results)):
				op += results[i].split("\\x")
			for i in range(len(op)):
				if op[i] == '':
					pass
				else:
					asm_raw += "{:s} ".format(op[i])
			print(asm_raw)

		if self.mode == "hex":
			op	= []
			num	= 8
			asm_raw = ""
			self.character_analysis(num, results)
			for i in range(len(results)):
				op += results[i].split("\\x")
			for i in range(len(op)):
				if op[i] == '':
					pass
				else:
					asm_raw += "{:s}".format(op[i])
			print(asm_raw)

		if self.mode == "perl":
			num = 60
			self.character_analysis(num, results)
			print("my ${:s} =".format(self.var_name))
			for i in range(len(results)):
				if i == (len(results) -1):
					print("\"{:s}\";".format(results[i]))
				else:
					print("\"{:s}\" .".format(results[i]))

		if self.mode == "csharp":
			num = 75
			sharp = ""
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					sharp += "0x{:s},".format(asm_ops[i])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with csharp opcodes
			save_length = "{:d}".format(int(len(self.ops)/4)) # save byte length
			self.ops = sharp
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","0x")
			# send it of for character character_analysis
			self.character_analysis(num, results)
			print("byte[] {:s} = new byte[{:s}] {:s}".format(self.var_name, save_length, "{"))
			for i in range(len(results)):
				snip = len(results[i]) - 1
				if i == (len(results)-1):
					print(results[i][:snip] + " };")
				else:
					print(results[i])

		if self.mode == "dword":
			num = 94
			dword = ""
			done = ""
			dlist = []
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					dword += "{:s}".format(asm_ops[i])

			# format into dword format
			splits = [dword[x:x+8] for x in range(0,len(dword),8)]
			for i in range(len(splits)):
				s = splits[i]
				dlist += "0x" + "".join(map(str.__add__, s[-2::-2] ,s[-1::-2])),
			for i in range(int(len(dlist)/8+1)):
				done += ", ".join(dlist[i*8:(i+1)*8])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with hex opcodes
			self.ops = done
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","")
			# send it of for character character_analysis
			self.character_analysis(num, results)
			for i in range(len(results)):
				print(results[i])

		if self.mode == "nasm":
			nasm = ""
			num = 60
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					nasm += "0x{:s},".format(asm_ops[i])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with nasm opcodes
			self.ops = nasm
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","0x")
			# send it of for character character_analysis
			self.character_analysis(num, results)

			for i in range(len(results)):
				snip = len(results[i]) - 1
				print("db " + results[i][:snip])

		if self.mode == "num":
			num = 84
			raw_ops = ""
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					raw_ops += "0x%s, " % (asm_ops[i])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with opcodes
			self.ops = raw_ops
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","0x")
			# send it of for character character_analysis
			self.character_analysis(num, results)

			for i in range(len(results)):
				snip = len(results[i]) - 2
				if i == (len(results)-1):
					print(results[i][:snip])
				else:
					print(results[i])

		if self.mode == "powershell":
			num = 50
			raw_ops = ""
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					raw_ops += "0x%s " % (asm_ops[i])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with opcodes
			self.ops = raw_ops
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","0x")
			# send it of for character character_analysis
			self.character_analysis(num, results)

			for i in range(len(results)):
				snip = len(results[i]) - 1
				if i == 0:
					print("[Byte[]] {:s} = {:s}".format(self.var_name, results[i].replace(" ", ",")[:snip]))
				else:
					print("${:s} += {:s}".format(self.var_name, results[i].replace(" ", ",")[:snip]))

		if self.mode == "java":
			num = 104
			javop = ""
			javlt = ""
			asm_ops = self.ops.split("\\x")
			for i in range(len(asm_ops)):
				if asm_ops[i] == '':
					pass
				else:
					javop += " (byte) 0x{:s},".format(asm_ops[i])

			# overwrite shellcode + badchars (example \xde\xad\xbe\xef) with java opcodes
			self.ops = javop
			if self.badchars != None:
				self.badchars = self.badchars.replace("\\x","(byte) 0x")
			# send it of for character character_analysis
			self.character_analysis(num, results)

			print("byte {:s}[] = new byte[]".format(self.var_name))
			print("{")
			for i in range(len(results)):
				print("\t" + results[i].lstrip(" "))

			print("};")

def compare_dump(op_str, cmp_str):
	done 	= []
	examination = ""

	checking_split = [op_str[x:x+4] for x in range(0,len(op_str),4)]
	original_split = [cmp_str[x:x+4] for x in range(0,len(cmp_str),4)]

	# format anything strange
	for i in range(len(original_split)):
		try:

			if checking_split[i] == original_split[i]:
				examination += "{:s}{:s}{:s}".format(
					colors.BLUE, original_split[i], colors.END
				)

			else:
				examination += "{:s}{:s}{:s}".format(
					colors.RED, original_split[i], colors.END
				)

		except IndexError:
			examination += "{:s}{:s}{:s}".format(colors.RED, original_split[i], colors.END)

	exam_split = [examination[x:x+130] for x in range(0,len(examination),130)]
	checking_split = [op_str[x:x+40] for x in range(0,len(op_str),40)]
	original_split = [cmp_str[x:x+40] for x in range(0,len(cmp_str),40)]

	print("+------------------------------------------+ +------------------------------------------+ +------------------------------------------+")
	print("|    (Differences In Red) Final Results    | |         Shellcode Being Examined         | |          Shellcode Being Dumped          |")
	print("+------------------------------------------+ +------------------------------------------+ +------------------------------------------+")
	for i in range(len(exam_split)):
		try:
			spaces = 13 - int(
				len(exam_split[i])/10
			)
			if spaces < 13:
				spaces = spaces * 3
			# ugly don't look
			if len(exam_split[i]) == 26:
				spaces -= 1
			elif len(exam_split[i]) == 39:
				spaces -= 2
			elif len(exam_split[i]) == 65:
				spaces -= 1
			elif len(exam_split[i]) == 78:
				spaces -= 2
			elif len(exam_split[i]) == 104:
				spaces -= 1
			elif len(exam_split[i]) == 117:
				spaces -= 2

			done += (
				# examination splits
				"| " +
				"{:s}".format(exam_split[i]) +
				" " * spaces +
				" |" +
				# original shellcode
				" | " +
				"{:s}".format(original_split[i]) +
				" " * abs(len(original_split[i]) - 40) +
				" |" +
				# file we are dumping / checking
				" | " +
				"{:s}".format(checking_split[i]) +
				" " * abs(len(checking_split[i]) - 40) +
				" |"
			),
		except IndexError:
			# if the dumped shellcode length is not == examined code
			# truncation has occured
			done += (
				# examination splits
				"| " +
				"{:s}".format(exam_split[i]) +
				" " * spaces +
				" |" +
				# original shellcode
				" | " +
				"{:s}".format(original_split[i]) +
				" " * abs(len(original_split[i]) - 40) +
				" |"
			),

	for i in range(len(done)):
		print(done[i])

def objdump(dumpfile, mode, badchars, comment_code, var_name):

	global no_junk, instructions

	no_addr		= []
	opcodes		= []
	ops		= ""

	# detect if the file exists
	if os.path.isfile(dumpfile) is False:
		print("File non-existent!")
		sys.exit()

	# run objdump to disassemble the binary
	try:
		intel_dump = subprocess.Popen(['objdump', '-D', dumpfile, '-M', 'intel', '--insn-width=15'],
			stdout=subprocess.PIPE).communicate()[0]
	except:
		print("[-] error running command")
		sys.exit()

	# here we begin to clean the output accordingly; this
	# once a function however after consideration ideally
	# we want to reuse the dumping class for stdin, etc
	newline_split = intel_dump.decode().split("\n")

	for i in range(len(newline_split)):
		# split up every line by a [tab] and remove address
		addr_splt = newline_split[i].split('\t')[1:3]
		# get rid of blank lines
		if len(addr_splt) > 0:
			no_addr += addr_splt
		else:
			pass

	# separate opcodes and instructions
	list_len = len(no_addr)
	for i in range(list_len):
		if (i & 1) == 1:
			instructions += no_addr[i],
		else:
			opcodes += no_addr[i],

	# cut off the junk and format (\xde\xad\xbe\xef)
	for i in range(len(opcodes)):
		no_junk  += opcodes[i].rstrip(" "),
	for i in range(len(opcodes)):
		opcodes[i] = opcodes[i].rstrip(" ")
	for i in range(len(opcodes)):
		ops += "\\x%s" % opcodes[i].replace(" ", "\\x")

	# now we send it off for formatting
	if comment_code:
		format = format_dump(ops, mode, var_name, badchars)
		format.informational_dump()
	else:
		format = format_dump(ops, mode, var_name, badchars)
		format.tactical_dump()

def main():

	# handle command line arguments
	parser = argparse.ArgumentParser()
	parser.add_argument("-d", "--dump",     help="binary to use for shellcode extraction (via objdump)")
	parser.add_argument("-rd", "--raw-dump",help="dump the shellcode from provided file without using objdump")
	parser.add_argument("-s", "--stdin",    help="read ops from stdin (EX: echo -ne \"\\xde\\xad\\xbe\\xef\" | objdump2shellcode -s -f python -b \"\\xbe\")", action="store_true")
	parser.add_argument("-f", "--format",   help="output format (use --list for a list)")
	parser.add_argument("-b", "--badchar",  help="seperate badchars like so \"\\x00,\\x0a\"")
	parser.add_argument("-c", "--comment",  help="comments the shellcode output (via objdump)", action="store_true")
	parser.add_argument("-v", "--varname",  required=False, help="alternative variable name")
	parser.add_argument("-e", "--examine",	help="examine a separate file containing original shellcode (works along raw dump). mainly to see if shellcode was recreated successfully")
	parser.add_argument("-l", "--list",     help="list all available formats", action="store_true")
	args = parser.parse_args()

	# assign arguments
	dumpfile	= args.dump
	raw_dump_file	= args.raw_dump
	cmp_dump_file	= args.examine
	mode		= args.format
	badchars	= args.badchar
	comment_code	= args.comment
	stdin		= args.stdin

	# if a list is requested print it
	if args.list == True:
		format_list()
		sys.exit()

	# default variable name if none given
	if args.varname == None:
		var_name = "buf"
	else:
		var_name = args.varname

	# if we are just extracting hex opcodes
	if raw_dump_file:
		op_str = ""
		cmp_str= ""
		with open(raw_dump_file, 'rb') as fd:
			fc = fd.read()
			for i in range(len(fc)):
				op_str += "\\x{:02x}".format(fc[i]).replace('0x','\\x')
		if cmp_dump_file:
			with open(cmp_dump_file, 'rb') as fd:
				fc = fd.read()
				for i in range(len(fc)):
					cmp_str += "\\x{:02x}".format(fc[i]).replace('0x','\\x')

			# send both op strings off to be compared
			compare_dump(op_str, cmp_str)

		else:
			# if we have nothing to compare send it off
			format = format_dump(op_str, mode, var_name, badchars)
			format.tactical_dump()

	# pass to function if objdump 2 be used(thank you @justsam, and greetz)
	elif dumpfile != None:
		objdump(dumpfile, mode, badchars, comment_code, var_name)

	# if requested read from stdin
	elif args.stdin == True:
		raw_shellcode	= []
		ops		= ""

		# integers we will format in hexadecimal this
		# this is what will be sent to format_dump()
		for line in sys.stdin.buffer.raw.read():
			raw_shellcode += line,

		for i in range(len(raw_shellcode)):
			ops += "\\x{:02x}".format(raw_shellcode[i]).replace('0x','\\x')

		format = format_dump(ops, mode, var_name, badchars)
		format.tactical_dump()
	else:
		parser.print_help()

if __name__ == '__main__':
	main()

