More work on the msfpescan port

git-svn-id: file:///home/svn/incoming/trunk@3599 4d416f70-5f16-0410-b530-b9f4589650da
unstable
HD Moore 2006-04-16 01:56:17 +00:00
parent da41886856
commit 17d2ba798b
7 changed files with 378 additions and 180 deletions

View File

@ -107,6 +107,7 @@ class Pe < PeBase
end
#
# Save the stuffs!
#
@ -116,20 +117,29 @@ class Pe < PeBase
#
#
# You shouldn't need to access these!
# These should not be accessed directly
#
self._isource = isource
self._dos_header = dos_header
self._file_header = file_header
self._optional_header = optional_header
self._section_headers = section_headers
self.image_base = base
self.sections = sections
self.header_section = header_section
self._config_header = _parse_config_header()
# These can be accessed directly
self.hdr = HeaderAccessor.new
self.hdr.dos = self._dos_header
self.hdr.file = self._file_header
self.hdr.opt = self._optional_header
self.hdr.sections = self._section_headers
self.hdr.config = self._config_header
end
#
@ -141,6 +151,5 @@ class Pe < PeBase
[ header_section ] + sections
end
end end end

View File

@ -76,19 +76,40 @@ class PeBase
[ 'uint32v', 'e_lfanew', 0 ]
)
class HeaderAccessor
attr_accessor :dos, :file, :opt, :sections, :config
def initialize
end
end
class GenericHeader
attr_accessor :struct
def initialize(_struct)
self.struct = _struct
end
# this sucks...
# The following methods are just pass-throughs for struct
# Access a value
def v
struct.v
end
# Access a value by array
def [](*args)
struct[*args]
end
# Obtain an array of all fields
def keys
struct.keys
end
def method_missing(meth, *args)
v[meth.to_s] || (raise NoMethodError.new, meth)
end
end
class DosHeader < GenericHeader
@ -350,7 +371,7 @@ class PeBase
[ 'uint32v', 'SizeOfUninitializeData', 0 ],
[ 'uint32v', 'AddressOfEntryPoint', 0 ],
[ 'uint32v', 'BaseOfCode', 0 ],
[ 'uint32v', 'BaseOfdata', 0 ],
[ 'uint32v', 'BaseOfData', 0 ],
[ 'uint32v', 'ImageBase', 0 ],
[ 'uint32v', 'SectionAlignment', 0 ],
[ 'uint32v', 'FileAlignment', 0 ],
@ -563,7 +584,6 @@ class PeBase
IMAGE_LOAD_CONFIG_DIRECTORY32 = Rex::Struct2::CStructTemplate.new(
[ 'uint32v', 'Size', 0 ],
[ 'uint32v', 'TimeDateStamp', 0 ],
[ 'uint32v', 'TimeDateStamp', 0 ],
[ 'uint16v', 'MajorVersion', 0 ],
[ 'uint16v', 'MinorVersion', 0 ],
[ 'uint32v', 'GlobalFlagsClear', 0 ],
@ -584,7 +604,39 @@ class PeBase
[ 'uint32v', 'SEHandlerCount', 0 ]
)
class ConfigHeader < GenericHeader
end
def self._parse_config_header(rawdata)
header = IMAGE_LOAD_CONFIG_DIRECTORY32.make_struct
header.from_s(rawdata)
ConfigHeader.new(header)
end
def _parse_config_header
#
# Get the data directory entry, size, etc
#
exports_entry = _optional_header['DataDirectory'][10]
rva = exports_entry.v['VirtualAddress']
size = exports_entry.v['Size']
return nil if size == 0
#
# Ok, so we have the data directory, now lets parse it
#
dirdata = _isource.read(rva_to_file_offset(rva), size)
header = IMAGE_LOAD_CONFIG_DIRECTORY32.make_struct
header.from_s(dirdata)
ConfigHeader.new(header)
end
#
# Just a stupid routine to round an offset up to it's alignment.
#
@ -603,15 +655,16 @@ class PeBase
attr_accessor :_isource
attr_accessor :_dos_header, :_file_header, :_optional_header,
:_section_headers
:_section_headers, :_config_header
attr_accessor :sections, :header_section, :image_base
attr_accessor :_imports_cache, :_imports_cached
attr_accessor :_exports_cache, :_exports_cached
attr_accessor :_relocations_cache, :_relocations_cached
attr_accessor :_loadconfig_cache, :_loadconfig_cached
attr_accessor :hdr
def self.new_from_file(filename, disk_backed = false)
file = ::File.new(filename)
@ -959,28 +1012,5 @@ class PeBase
return relocdirs
end
def loadconfig
if !_loadconfig_cached
self._loadconfig_cache = _load_loadconfig
self._loadconfig_cached = true
end
return _loadconfig_cache
end
def _load_loadconfig
#
# Get the data directory entry, size, etc
#
exports_entry = _optional_header['DataDirectory'][10]
rva = exports_entry.v['VirtualAddress']
size = exports_entry.v['Size']
return nil if size == 0
return size
end
end end end

9
lib/rex/pescan.rb Normal file
View File

@ -0,0 +1,9 @@
module Rex
module PeScan
end
end
require 'rex/pescan/scanner'
require 'rex/pescan/search'

194
lib/rex/pescan/scanner.rb Normal file
View File

@ -0,0 +1,194 @@
module Rex
module PeScan
module Scanner
class Generic
attr_accessor :pe, :regex
def initialize(pe)
self.pe = pe
end
def config(param)
end
def scan(param)
config(param)
pe.all_sections.each do |section|
hits = scan_section(section, param)
hits.each do |hit|
vma = pe.rva_to_vma(hit[0])
msg = hit[1]
$stdout.puts "0x%08x %s" % [ vma, msg ]
end
end
end
def scan_section(section, param={})
[]
end
end
class JmpRegScanner < Generic
def config(param)
regnums = param['args']
# build a list of the call bytes
calls = _build_byte_list(0xd0, regnums - [4]) # note call esp's don't work..
jmps = _build_byte_list(0xe0, regnums)
pushs1 = _build_byte_list(0x50, regnums)
pushs2 = _build_byte_list(0xf0, regnums)
regexstr = '('
if !calls.empty?
regexstr += "\xff[#{calls}]|"
end
regexstr += "\xff[#{jmps}]|([#{pushs1}]|\xff[#{pushs2}])(\xc3|\xc2..))"
self.regex = Regexp.new(regexstr)
end
# build a list for regex of the possible bytes, based on a base
# byte and a list of register numbers..
def _build_byte_list(base, regnums)
regnums.collect { |regnum| Regexp.escape((base | regnum).chr) }.join('')
end
def _ret_size(section, index)
case section.read(index, 1)
when "\xc3"
return 1
when "\xc2"
return 3
end
raise "wtf"
end
def _parse_ret(data)
if data.length == 1
return "ret"
else
return "retn 0x%04x" % data[1, 2].unpack('v')[0]
end
end
def scan_section(section, param={})
index = 0
hits = [ ]
while (index = section.index(regex, index)) != nil
rva = section.offset_to_rva(index)
message = ''
parse_ret = false
byte1 = section.read(index, 1)[0]
if byte1 == 0xff
byte2 = section.read(index+1, 1)[0]
regname = Rex::Arch::X86.reg_name32(byte2 & 0x7)
case byte2 & 0xf8
when 0xd0
message = "call #{regname}"
index += 2
when 0xe0
message = "jmp #{regname}"
index += 2
when 0xf0
retsize = _ret_size(section, index+2)
message = "push #{regname}; " + _parse_ret(section.read(index+2, retsize))
index += 2 + retsize
else
raise "wtf"
end
else
regname = Rex::Arch::X86.reg_name32(byte1 & 0x7)
retsize = _ret_size(section, index+1)
message = "push #{regname}; " + _parse_ret(section.read(index+1, retsize))
index += 1 + retsize
end
hits << [ rva, message ]
end
return hits
end
end
class PopPopRetScanner < JmpRegScanner
def config(param)
pops = _build_byte_list(0x58, (0 .. 7).to_a - [4]) # we don't want pop esp's...
self.regex = Regexp.new("[#{pops}][#{pops}](\xc3|\xc2..)")
end
def scan_section(section, param={})
index = 0
hits = [ ]
while index < section.size && (index = section.index(regex, index)) != nil
rva = section.offset_to_rva(index)
message = ''
pops = section.read(index, 2)
reg1 = Rex::Arch::X86.reg_name32(pops[0] & 0x7)
reg2 = Rex::Arch::X86.reg_name32(pops[1] & 0x7)
message = "pop #{reg1}; pop #{reg2}; "
retsize = _ret_size(section, index+2)
message += _parse_ret(section.read(index+2, retsize))
index += 2 + retsize
hits << [ rva, message ]
end
return hits
end
end
class RegexScanner < JmpRegScanner
def config(param)
self.regex = Regexp.new(param['args'])
end
def scan_section(section, param={})
index = 0
hits = [ ]
while index < section.size && (index = section.index(regex, index)) != nil
idx = index
buf = ''
mat = nil
while (! (mat = buf.match(regex)))
buf << section.read(idx, 1)
idx += 1
end
rva = section.offset_to_rva(index)
hits << [ rva, buf.unpack("H*") ]
index += buf.length
end
return hits
end
end
end
end
end

40
lib/rex/pescan/search.rb Normal file
View File

@ -0,0 +1,40 @@
module Rex
module PeScan
module Search
class DumpRVA
attr_accessor :pe
def initialize(pe)
self.pe = pe
end
def config(param)
@address = pe.vma_to_rva(param['args'])
end
def scan(param)
config(param)
# Adjust based on -A and -B flags
pre = param['before'] || 0
suf = param['after'] || 16
@address -= pre
@address = 0 if (@address < 0 || ! @address)
buf = pe.read_rva(@address, suf)
$stdout.puts "0x%08x %s" % [ pe.rva_to_vma(@address), buf.unpack("H*") ]
end
end
class DumpOffset < DumpRVA
def config(param)
begin
@address = pe.file_offset_to_rva(param['args'])
rescue Rex::PeParsey::BoundsError
end
end
end
end
end
end

View File

@ -160,6 +160,18 @@ class CStruct < SStruct
return super(index, *other)
end
end
# Produce a list of field names
def keys
@name_table
end
# Iterate through all fields and values
def each_pair(&block)
@name_table.each do |k|
block.call(k, self.v[k])
end
end
end
# end Rex::Struct2

196
msfpescan
View File

@ -3,9 +3,15 @@
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
require 'rex/peparsey'
require 'rex/pescan'
require 'rex/arch/x86'
require 'optparse'
def opt2i(o)
o.index("0x")==0 ? o.hex : o.to_i
end
#
# Right now this program is a bit shakey...
#
@ -16,156 +22,60 @@ require 'optparse'
# - etc etc
#
class JmpRegScanner
attr_accessor :regex
def initialize(regnums)
# build a list of the call bytes
calls = _build_byte_list(0xd0, regnums - [4]) # note call esp's don't work..
jmps = _build_byte_list(0xe0, regnums)
pushs1 = _build_byte_list(0x50, regnums)
pushs2 = _build_byte_list(0xf0, regnums)
regexstr = '('
if !calls.empty?
regexstr += "\xff[#{calls}]|"
end
regexstr += "\xff[#{jmps}]|([#{pushs1}]|\xff[#{pushs2}])(\xc3|\xc2..))"
self.regex = Regexp.new(regexstr)
end
# build a list for regex of the possible bytes, based on a base
# byte and a list of register numbers..
def _build_byte_list(base, regnums)
regnums.collect { |regnum| Regexp.escape((base | regnum).chr) }.join('')
end
def _ret_size(section, index)
case section.read(index, 1)
when "\xc3"
return 1
when "\xc2"
return 3
end
raise "wtf"
end
def _parse_ret(data)
if data.length == 1
return "ret"
else
return "retn 0x%04x" % data[1, 2].unpack('v')[0]
end
end
def scan_section(section)
index = 0
hits = [ ]
while (index = section.index(regex, index)) != nil
rva = section.offset_to_rva(index)
message = ''
parse_ret = false
byte1 = section.read(index, 1)[0]
if byte1 == 0xff
byte2 = section.read(index+1, 1)[0]
regname = Rex::Arch::X86.reg_name32(byte2 & 0x7)
case byte2 & 0xf8
when 0xd0
message = "call #{regname}"
index += 2
when 0xe0
message = "jmp #{regname}"
index += 2
when 0xf0
retsize = _ret_size(section, index+2)
message = "push #{regname}; " + _parse_ret(section.read(index+2, retsize))
index += 2 + retsize
else
raise "wtf"
end
else
regname = Rex::Arch::X86.reg_name32(byte1 & 0x7)
retsize = _ret_size(section, index+1)
message = "push #{regname}; " + _parse_ret(section.read(index+1, retsize))
index += 1 + retsize
end
hits << [ rva, message ]
end
return hits
end
end
class PopPopRetScanner < JmpRegScanner
def initialize
pops = _build_byte_list(0x58, (0 .. 7).to_a - [4]) # we don't want pop esp's...
self.regex = Regexp.new("[#{pops}][#{pops}](\xc3|\xc2..)")
end
def scan_section(section)
index = 0
hits = [ ]
while index < section.size && (index = section.index(regex, index)) != nil
rva = section.offset_to_rva(index)
message = ''
pops = section.read(index, 2)
reg1 = Rex::Arch::X86.reg_name32(pops[0] & 0x7)
reg2 = Rex::Arch::X86.reg_name32(pops[1] & 0x7)
message = "pop #{reg1}; pop #{reg2}; "
retsize = _ret_size(section, index+2)
message += _parse_ret(section.read(index+2, retsize))
index += 2 + retsize
hits << [ rva, message ]
end
return hits
end
end
opt = OptionParser.new
opt.banner = 'Usage: [options] [files]'
opt.banner = "Usage: #{$PROGRAM_NAME} [mode] <options> [targets]"
opt.separator('')
opt.separator('Options:')
opt.separator('Modes:')
worker = nil
param = {}
scanner = nil
pe_klass = Rex::PeParsey::Pe
opt.on('-j', '--jump [REG]', 'Find a jmp/call REG') do |t|
opt.on('-j', '--jump [regA,regB,regC]', 'Search for jump equivalent instructions') do |t|
# take csv of register names (like eax,ebx) and convert
# them to an array of register numbers
regnums = t.split(',').collect { |o| Rex::Arch::X86.reg_number(o) }
scanner = JmpRegScanner.new(regnums)
worker = Rex::PeScan::Scanner::JmpRegScanner
param['args'] = regnums
end
opt.on('-p', '--poppopret', 'Find a pop/pop/ret') do |t|
scanner = PopPopRetScanner.new
opt.on('-p', '--poppopret', 'Search for pop+pop+ret combinations') do |t|
worker = Rex::PeScan::Scanner::PopPopRetScanner
param['args'] = t
end
opt.on('-m', '--memdump', 'The files are memdump.exe images') do |t|
opt.on('-r', '--regex [regex]', 'Search for regex match') do |t|
worker = Rex::PeScan::Scanner::RegexScanner
param['args'] = t
end
opt.on('-a', '--analyze-address [address]', 'Display the code at the specified address') do |t|
worker = Rex::PeScan::Search::DumpRVA
param['args'] = opt2i(t)
end
opt.on('-b', '--analyze-offset [offset]', 'Display the code at the specified offset') do |t|
worker = Rex::PeScan::Search::DumpOffset
param['args'] = opt2i(t)
end
opt.on('-m', '--memdump', 'The targets are memdump.exe directories') do |t|
pe_klass = Rex::PeParsey::PeMemDump
end
opt.separator('')
opt.separator('Options:')
opt.on('-A', '--after [bytes]', 'Search for jump equivalent instructions') do |t|
param['after'] = opt2i(t)
end
opt.on('-B', '--before [bytes]', 'The targets are memdump.exe directories') do |t|
param['before'] = opt2i(t)
end
opt.parse!
ARGV.each do |file|
@ -175,16 +85,10 @@ ARGV.each do |file|
next if $!.message == "Couldn't find the PE magic!"
raise $!
end
pe.all_sections.each do |section|
hits = scanner.scan_section(section)
hits.each do |hit|
vma = pe.rva_to_vma(hit[0])
msg = hit[1]
puts "0x%08x %s" % [ vma, msg ]
end
end
o = worker.new(pe)
o.scan(param)
pe.close
end