class Metasm::PE
Constants
- MAGIC
Attributes
Public Class Methods
# File metasm/exe_format/pe.rb, line 351 def self.imphash(path) pe = decode_file_header(path) pe.decode_imports pe.imphash end
Metasm::COFF::new
# File metasm/exe_format/pe.rb, line 17 def initialize(*a) super(*a) cpu = a.grep(CPU).first @mz = MZ.new(cpu).share_namespace(self) end
# File metasm/exe_format/pe.rb, line 326 def self.pehash(path, digest) decode_file_header(path).pehash(digest) end
Public Instance Methods
# File metasm/exe_format/pe.rb, line 136 def c_set_default_entrypoint return if @optheader.entrypoint if @sections.find { |s| s.encoded.export['main'] } @optheader.entrypoint = 'main' elsif @sections.find { |s| s.encoded.export['DllEntryPoint'] } @optheader.entrypoint = 'DllEntryPoint' elsif @sections.find { |s| s.encoded.export['DllMain'] } case @cpu.shortname when 'ia32' @optheader.entrypoint = 'DllEntryPoint' compile_c <<EOS enum { DLL_PROCESS_DETACH, DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, DLL_PROCESS_VERIFIER }; __stdcall int DllMain(void *handle, unsigned long reason, void *reserved); __stdcall int DllEntryPoint(void *handle, unsigned long reason, void *reserved) { int ret = DllMain(handle, reason, reserved); if (ret == 0 && reason == DLL_PROCESS_ATTACH) DllMain(handle, DLL_PROCESS_DETACH, reserved); return ret; } EOS else @optheader.entrypoint = 'DllMain' end elsif @sections.find { |s| s.encoded.export['WinMain'] } case @cpu.shortname when 'ia32' @optheader.entrypoint = 'main' compile_c <<EOS #define GetCommandLine GetCommandLineA #define GetModuleHandle GetModuleHandleA #define GetStartupInfo GetStartupInfoA #define STARTF_USESHOWWINDOW 0x00000001 #define SW_SHOWDEFAULT 10 typedef unsigned long DWORD; typedef unsigned short WORD; typedef struct { DWORD cb; char *lpReserved, *lpDesktop, *lpTitle; DWORD dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags; WORD wShowWindow, cbReserved2; char *lpReserved2; void *hStdInput, *hStdOutput, *hStdError; } STARTUPINFO; __stdcall void *GetModuleHandleA(const char *lpModuleName); __stdcall void GetStartupInfoA(STARTUPINFO *lpStartupInfo); __stdcall void ExitProcess(unsigned int uExitCode); __stdcall char *GetCommandLineA(void); __stdcall int WinMain(void *hInstance, void *hPrevInstance, char *lpCmdLine, int nShowCmd); int main(void) { STARTUPINFO startupinfo; startupinfo.cb = sizeof(STARTUPINFO); char *cmd = GetCommandLine(); int ret; if (*cmd == '"') { cmd++; while (*cmd && *cmd != '"') { if (*cmd == '\\\\') cmd++; cmd++; } if (*cmd == '"') cmd++; } else while (*cmd && *cmd != ' ') cmd++; while (*cmd == ' ') cmd++; GetStartupInfo(&startupinfo); ret = WinMain(GetModuleHandle(0), 0, cmd, (startupinfo.dwFlags & STARTF_USESHOWWINDOW) ? (int)startupinfo.wShowWindow : (int)SW_SHOWDEFAULT); ExitProcess((DWORD)ret); return ret; } EOS else @optheader.entrypoint = 'WinMain' end end end
overrides COFF#decode_header
simply sets the offset to the PE
pointer before decoding the COFF
header also checks the PE
signature
Metasm::COFF#decode_header
# File metasm/exe_format/pe.rb, line 26 def decode_header @cursection ||= self @encoded.ptr = 0x3c @encoded.ptr = decode_word(@encoded) @signature = @encoded.read(4) raise InvalidExeFormat, "Invalid PE signature #{@signature.inspect}" if @signature != MAGIC @coff_offset = @encoded.ptr if @mz.encoded.empty? @mz.encoded << @encoded[0, @coff_offset-4] @mz.encoded.ptr = 0 @mz.decode_header end super() end
creates a default MZ
file to be used in the PE
header this one is specially crafted to fit in the 0x3c bytes before the signature
# File metasm/exe_format/pe.rb, line 43 def encode_default_mz_header # XXX use single-quoted source, to avoid ruby interpretation of \r\n @mz.cpu = Ia32.new(386, 16) @mz.assemble <<'EOMZSTUB' db "Needs Win32!\r\n$" .entrypoint push cs pop ds xor dx, dx ; ds:dx = addr of $-terminated string mov ah, 9 ; output string int 21h mov ax, 4c01h ; exit with code in al int 21h EOMZSTUB mzparts = @mz.pre_encode # put stuff before 0x3c @mz.encoded << mzparts.shift raise 'OH NOES !!1!!!1!' if @mz.encoded.virtsize > 0x3c # MZ header is too long, cannot happen until mzparts.empty? break if mzparts.first.virtsize + @mz.encoded.virtsize > 0x3c @mz.encoded << mzparts.shift end # set PE signature pointer @mz.encoded.align 0x3c @mz.encoded << encode_word('pesigptr') # put last parts of the MZ program until mzparts.empty? @mz.encoded << mzparts.shift end # ensure the sig will be 8bytes-aligned @mz.encoded.align 8 @mz.encoded.fixup 'pesigptr' => @mz.encoded.virtsize @mz.encoded.fixup @mz.encoded.binding @mz.encoded.fill @mz.encode_fix_checksum end
encodes the PE
header before the COFF
header, uses a default mz header if none defined the MZ
header must have 0x3c pointing just past its last byte which should be 8bytes aligned the 2 1st bytes of the MZ
header should be 'MZ'
Metasm::COFF#encode_header
# File metasm/exe_format/pe.rb, line 89 def encode_header(*a) encode_default_mz_header if @mz.encoded.empty? @encoded << @mz.encoded.dup # append the PE signature @signature ||= MAGIC @encoded << @signature super(*a) end
handles writes to fs: -> dasm SEH handler (first only, does not follow the chain) TODO seh prototype (args => context) TODO hook on (non)resolution of :w xref
Metasm::ExeFormat#get_xrefs_x
# File metasm/exe_format/pe.rb, line 217 def get_xrefs_x(dasm, di) if @cpu.shortname =~ /^ia32|^x64/ and a = di.instruction.args.first and a.kind_of?(Ia32::ModRM) and a.seg and a.seg.val == 4 and w = get_xrefs_rw(dasm, di).find { |type, ptr, len| type == :w and ptr.externals.include? 'segment_base_fs' } and dasm.backtrace(Expression[w[1], :-, 'segment_base_fs'], di.address).to_a.include?(Expression[0]) sehptr = w[1] sz = @cpu.size/8 sehptr = Indirection.new(Expression[Indirection.new(sehptr, sz, di.address), :+, sz], sz, di.address) a = dasm.backtrace(sehptr, di.address, :include_start => true, :origin => di.address, :type => :x, :detached => true) puts "backtrace seh from #{di} => #{a.map { |addr| Expression[addr] }.join(', ')}" if $VERBOSE a.each { |aa| next if aa == Expression::Unknown dasm.auto_label_at(aa, 'seh', 'loc', 'sub') dasm.addrs_todo << [aa] } super(dasm, di) else super(dasm, di) end end
compute Mandiant “importhash”
# File metasm/exe_format/pe.rb, line 331 def imphash lst = [] @imports.to_a.each { |id| ln = id.libname.downcase.sub(/.(dll|sys|ocx)$/, '') id.imports.each { |i| if not i.name and ordtable = WindowsExports::IMPORT_HASH[ln] iname = ordtable[i.ordinal] else iname = i.name end iname ||= "ord#{i.ordinal}" lst << "#{ln}.#{iname}" } } require 'digest/md5' Digest::MD5.hexdigest(lst.join(',').downcase) end
returns a disassembler with a special decodedfunction for GetProcAddress (i386 only), and the default func
Metasm::ExeFormat#init_disassembler
# File metasm/exe_format/pe.rb, line 238 def init_disassembler d = super() d.backtrace_maxblocks_data = 4 case @cpu.shortname when 'ia32', 'x64' old_cp = d.c_parser d.c_parser = nil d.parse_c '__stdcall void *GetProcAddress(int, char *);' d.parse_c '__stdcall void ExitProcess(int) __attribute__((noreturn));' d.c_parser.lexer.define_weak('__MS_X86_64_ABI__') if @cpu.shortname == 'x64' gpa = @cpu.decode_c_function_prototype(d.c_parser, 'GetProcAddress') epr = @cpu.decode_c_function_prototype(d.c_parser, 'ExitProcess') d.c_parser = old_cp d.parse_c '' d.c_parser.lexer.define_weak('__MS_X86_64_ABI__') if @cpu.shortname == 'x64' @getprocaddr_unknown = [] gpa.btbind_callback = lambda { |dasm, bind, funcaddr, calladdr, expr, origin, maxdepth| break bind if @getprocaddr_unknown.include? [dasm, calladdr] or not Expression[expr].externals.include? :eax sz = @cpu.size/8 break bind if not dasm.decoded[calladdr] if @cpu.shortname == 'x64' arg2 = :rdx else arg2 = Indirection[[:esp, :+, 2*sz], sz, calladdr] end fnaddr = dasm.backtrace(arg2, calladdr, :include_start => true, :maxdepth => maxdepth) if fnaddr.kind_of? ::Array and fnaddr.length == 1 and s = dasm.get_section_at(fnaddr.first) and fn = s[0].read(64) and i = fn.index(?\0) and i > sz # try to avoid ordinals bind = bind.merge @cpu.register_symbols[0] => Expression[fn[0, i]] else @getprocaddr_unknown << [dasm, calladdr] puts "unknown func name for getprocaddress from #{Expression[calladdr]}" if $VERBOSE end bind } d.function[Expression['GetProcAddress']] = gpa d.function[Expression['ExitProcess']] = epr d.function[:default] = @cpu.disassembler_default_func end d end
a returns a new PE
with only minimal information copied:
section name/perm/addr/content exports imports (with boundimport cleared) resources
# File metasm/exe_format/pe.rb, line 106 def mini_copy(share_ns=true) ret = self.class.new(@cpu) ret.share_namespace(self) if share_ns ret.header.machine = @header.machine ret.header.characteristics = @header.characteristics ret.optheader.entrypoint = @optheader.entrypoint ret.optheader.image_base = @optheader.image_base ret.optheader.subsystem = @optheader.subsystem ret.optheader.dll_characts = @optheader.dll_characts @sections.each { |s| rs = Section.new rs.name = s.name rs.virtaddr = s.virtaddr rs.characteristics = s.characteristics rs.encoded = s.encoded ret.sections << s } ret.resource = resource ret.tls = tls if imports ret.imports = @imports.map { |id| id.dup } ret.imports.each { |id| id.timestamp = id.firstforwarder = id.ilt_p = id.libname_p = nil } end ret.export = export ret end
# File metasm/exe_format/pe.rb, line 283 def module_address @optheader.image_base end
# File metasm/exe_format/pe.rb, line 279 def module_name export and @export.libname end
# File metasm/exe_format/pe.rb, line 287 def module_size @sections.map { |s_| s_.virtaddr + s_.virtsize }.max || 0 end
# File metasm/exe_format/pe.rb, line 291 def module_symbols syms = [['entrypoint', @optheader.entrypoint]] @export.exports.to_a.each { |e| next if not e.target name = e.name || "ord_#{e.ordinal}" syms << [name, label_rva(e.target)] } if export syms end
compute the pe-sha1 or pe-sha256 of the binary argument should be a Digest::SHA1 (from digest/sha1) or a Digest::SHA256 (from digest/sha2) returns the hex checksum
# File metasm/exe_format/pe.rb, line 304 def pehash(digest) off0 = 0 off1 = @coff_offset + @header.sizeof(self) + @optheader.offsetof(self, :checksum) dir_ct_idx = DIRECTORIES.index('certificate_table') if @optheader.numrva > dir_ct_idx off2 = @coff_offset + @header.sizeof(self) + @optheader.sizeof(self) + 8*dir_ct_idx ct_size = @encoded.data[off2, 8].unpack('V*')[1] off3 = @encoded.length - ct_size else off4 = @encoded.length end digest << @encoded.data[off0 ... off1].to_str digest << @encoded.data[off1+4 ... off2].to_str if off2 digest << @encoded.data[off2+8 ... off3].to_str if off2 and off3 > off2+8 digest << @encoded.data[off1+4 ... off4].to_str if off4 digest << ("\0" * (8 - (@encoded.length & 7))) if @encoded.length & 7 != 0 digest.hexdigest end