#    This file is part of Metasm, the Ruby assembly manipulation suite
#    Copyright (C) 2006-2009 Yoann GUILLOT
#    Original script and idea by Alexandre GAZET
#
#    Licence is LGPL, see LICENCE in the top-level directory


# 
# this script will load an upx-packed windows executable, find its
# original entrypoint by disassembling the UPX stub, set breakpoint on it,
# run the program, and dump the loaded image to an executable PE.
#
# usage: dump_upx.rb <packed.exe> [<dumped.exe>]
#

require 'metasm'
include Metasm

class UPXUnpacker
	# loads the file
	# find the oep by disassembling
	# run it until the oep
	# dump the memory image
	def initialize(file, dumpfile, iat_rva=nil)
		@dumpfile = dumpfile || 'upx-dumped.exe'
		@iat = iat_rva

		puts 'disassembling UPX loader...'
		pe = PE.decode_file(file)
		@oep = find_oep(pe)
		raise 'cant find oep...' if not @oep
		puts "oep found at #{Expression[@oep]}"
		@baseaddr = pe.optheader.image_base

		@dbg = OS.current.create_process(file).debugger
		puts 'running...'
		debugloop
	end

	# disassemble the upx stub to find a cross-section jump (to the real entrypoint)
	def find_oep(pe)		
		dasm = pe.disassemble_fast 'entrypoint'
		
		return if not jmp = dasm.decoded.find { |addr, di|
			# check only once per basic block
			next if not di.block_head?
			b = di.block
			# our target has only one follower
			next if b.to_subfuncret.to_a.length != 0 or b.to_normal.to_a.length != 1
			to = b.to_normal.first
			# ignore jump to unmmaped address
			next if not s = dasm.get_section_at(to)
			# ignore jump to same section
			next if dasm.get_section_at(di.address) == s

			# gotcha !
			true
		}

		# now jmp is a couple [addr, di], we extract and normalize the oep from there
		dasm.normalize(jmp[1].block.to_normal.first)
	end	

	def debugloop
		# set up a breakpoint on oep
		@dbg.hwbp(@oep) { breakpoint_callback }
		@dbg.run_forever
		puts 'done'
	end

	def breakpoint_callback
		puts 'breakpoint hit !'

		# dump the process
		# create a genuine PE object from the memory image
		dump = LoadedPE.memdump @dbg.memory, @baseaddr, @oep, @iat

		# the UPX loader unpacks everything in sections marked read-only in the PE header, make them writeable
		dump.sections.each { |s| s.characteristics |= ['MEM_WRITE'] }

		# write the PE file to disk
		dump.encode_file @dumpfile

		puts 'dump complete'
	ensure
		# kill the process
		@dbg.kill
	end
end

if __FILE__ == $0
	# args: packed [unpacked] [iat rva]
	UPXUnpacker.new(ARGV.shift, ARGV.shift, (Integer(ARGV.shift) rescue nil))
end
