the COFF object file format mostly used on windows (PE/COFF)
NRELOC_OVFL means there are more than 0xffff reloc the reloc count must be set to 0xffff, and the real reloc count is the VA of the first relocation
PE+ is for 64bits address spaces
lsb of symbol type, unused
boolean, set to true to have decode() ignore the base_relocs directory
computes the checksum for a given COFF file may not work with overlapping sections
# File metasm/exe_format/coff_encode.rb, line 302 def self.checksum(str, endianness = :little) coff = load str coff.endianness = endianness coff.decode_header coff.encoded.ptr = 0 flen = 0 csum = 0 # negate old checksum oldcs = coff.encode_word(coff.optheader.checksum) oldcs.ptr = 0 csum -= coff.decode_half(oldcs) csum -= coff.decode_half(oldcs) # checksum header raw = coff.encoded.read(coff.optheader.headers_size) flen += coff.optheader.headers_size coff.sections.each { |s| coff.encoded.ptr = s.rawaddr raw << coff.encoded.read(s.rawsize) flen += s.rawsize } raw.unpack(endianness == :little ? 'v*' : 'n*').each { |s| csum += s csum = (csum & 0xffff) + (csum >> 16) if (csum >> 16) > 0 } csum + flen end
# File metasm/exe_format/coff.rb, line 404 def initialize(*a) cpu = a.grep(CPU).first @nodecode_relocs = true if a.include? :nodecode_relocs @directory = {} # DIRECTORIES.key => [rva, size] @sections = [] @endianness = cpu ? cpu.endianness : :little @bitsize = cpu ? cpu.size : 32 @header = Header.new @optheader = OptionalHeader.new super(cpu) end
address -> file offset handles LoadedPE
# File metasm/exe_format/coff_decode.rb, line 464 def addr_to_fileoff(addr) addr -= @load_address ||= @optheader.image_base return 0 if addr == 0 # sect_at_rva specialcases 0 if s = sect_at_rva(addr) if s.respond_to? :virtaddr addr - s.virtaddr + s.rawaddr else # header addr end end end
encodes a thunk to imported function
# File metasm/exe_format/coff_encode.rb, line 441 def arch_encode_thunk(edata, import) case @cpu.shortname when 'ia32', 'x64' shellcode = lambda { |c| Shellcode.new(@cpu).share_namespace(self).assemble(c).encoded } if @cpu.generate_PIC if @cpu.shortname == 'x64' edata << shellcode["#{import.thunk}: jmp [rip-$_+#{import.target}]"] return end # sections starts with a helper function that returns the address of metasm_intern_geteip in eax (PIC) if not @sections.find { |s| s.encoded and s.encoded.export['metasm_intern_geteip'] } and edata.empty? edata << shellcode["metasm_intern_geteip: call 42f\n42:\npop eax\nsub eax, 42b-metasm_intern_geteip\nret"] end edata << shellcode["#{import.thunk}: call metasm_intern_geteip\njmp [eax+#{import.target}-metasm_intern_geteip]"] else edata << shellcode["#{import.thunk}: jmp [#{import.target}]"] end else raise EncodeError, 'E: COFF: encode import thunk: unsupported architecture' end end
# File metasm/exe_format/coff_encode.rb, line 952 def assemble(*a) parse(*a) if not a.empty? @source.each { |k, v| raise "no section named #{k} ?" if not s = @sections.find { |s_| s_.name == k } s.encoded << assemble_sequence(v, @cpu) v.clear } end
try to resolve automatically COFF import tables from self.sections.encoded.relocations and WindowsExports::EXPORT if the relocation target is '<symbolname>' or 'iat_<symbolname>, link to the IAT address, if it is '<symbolname> + <expr>', link to a thunk (plt-like) if the relocation is not found, try again after appending 'fallback_append' to the symbol (eg wsprintf => wsprintfA)
# File metasm/exe_format/coff_encode.rb, line 1031 def autoimport(fallback_append='A') WindowsExports rescue return # autorequire autoexports = WindowsExports::EXPORT.dup @sections.each { |s| next if not s.encoded s.encoded.export.keys.each { |e| autoexports.delete e } } @sections.each { |s| next if not s.encoded s.encoded.reloc.each_value { |r| if r.target.op == :+ and not r.target.lexpr and r.target.rexpr.kind_of?(::String) sym = target = r.target.rexpr sym = sym[4..-1] if sym[0, 4] == 'iat_' elsif r.target.op == :- and r.target.rexpr.kind_of?(::String) and r.target.lexpr.kind_of?(::String) sym = thunk = r.target.lexpr end if not dll = autoexports[sym] sym += fallback_append if sym.kind_of?(::String) and fallback_append.kind_of?(::String) next if not dll = autoexports[sym] end @imports ||= [] next if @imports.find { |id| id.imports.find { |ii| ii.name == sym } } if not id = @imports.find { |id_| id_.libname =~ /^#{dll}(\.dll)?$/ } id = ImportDirectory.new id.libname = dll id.imports = [] @imports << id end if not i = id.imports.find { |i_| i_.name == sym } i = ImportDirectory::Import.new i.name = sym id.imports << i end if (target and i.target and (i.target != target or i.thunk == target)) or (thunk and i.thunk and (i.thunk != thunk or i.target == thunk)) puts "autoimport: conflict for #{target} #{thunk} #{i.inspect}" if $VERBOSE else i.target ||= new_label(target || 'iat_' + thunk) i.thunk ||= thunk if thunk end } } end
returns a metasm CPU object corresponding to
header.machine
# File metasm/exe_format/coff_decode.rb, line 795 def cpu_from_headers case @header.machine when 'I386'; Ia32.new when 'AMD64'; X86_64.new when 'R4000'; MIPS.new(:little) else raise "unknown cpu #{@header.machine}" end end
creates the @relocations from sections.encoded.reloc
# File metasm/exe_format/coff_encode.rb, line 493 def create_relocation_tables @relocations = [] # create a fake binding with all exports, to find only-image_base-dependant relocs targets # not foolproof, but works in standard cases startaddr = curaddr = label_at(@encoded, 0, 'coff_start') binding = {} @sections.each { |s| binding.update s.encoded.binding(curaddr) curaddr = Expression[curaddr, :+, s.encoded.virtsize] } # for each section.encoded, make as many RelocationTables as needed @sections.each { |s| # rt.base_addr temporarily holds the offset from section_start, and is fixed up to rva before '@reloc << rt' rt = RelocationTable.new s.encoded.reloc.each { |off, rel| # check that the relocation looks like "program_start + integer" when bound using the fake binding # XXX allow :i32 etc if rel.endianness == @endianness and [:u32, :a32, :u64, :a64].include?(rel.type) and rel.target.bind(binding).reduce.kind_of?(Expression) and Expression[rel.target, :-, startaddr].bind(binding).reduce.kind_of?(::Integer) # winner ! # build relocation r = RelocationTable::Relocation.new r.offset = off & 0xfff r.type = { :u32 => 'HIGHLOW', :u64 => 'DIR64', :a32 => 'HIGHLOW', :a64 => 'DIR64' }[rel.type] # check if we need to start a new relocation table if rt.base_addr and (rt.base_addr & ~0xfff) != (off & ~0xfff) rt.base_addr = Expression[[label_at(s.encoded, 0, 'sect_start'), :-, startaddr], :+, rt.base_addr] @relocations << rt rt = RelocationTable.new end # initialize reloc table base address if needed rt.base_addr ||= off & ~0xfff (rt.relocs ||= []) << r elsif $DEBUG and not rel.target.bind(binding).reduce.kind_of?(Integer) puts "W: COFF: Ignoring weird relocation #{rel.inspect} when building relocation tables" end } if rt and rt.relocs rt.base_addr = Expression[[label_at(s.encoded, 0, 'sect_start'), :-, startaddr], :+, rt.base_addr] @relocations << rt end } end
# File metasm/exe_format/coff_decode.rb, line 418 def curencoded @cursection.encoded end
decodes a COFF file (headers/exports/imports/relocs/sections) starts at encoded.ptr
# File metasm/exe_format/coff_decode.rb, line 780 def decode decode_header decode_exports decode_imports decode_resources decode_certificates decode_debug decode_tls decode_loadconfig decode_delayimports decode_com decode_relocs unless nodecode_relocs or ENV['METASM_NODECODE_RELOCS'] # decode relocs last end
# File metasm/exe_format/coff_decode.rb, line 422 def decode_byte( edata = curencoded) ; edata.decode_imm(:u8, @endianness) end
decodes certificate table
# File metasm/exe_format/coff_decode.rb, line 643 def decode_certificates if ct = @directory['certificate_table'] @certificates = [] @cursection = self if ct[0] > @encoded.length or ct[1] > @encoded.length - ct[0] puts "W: COFF: invalid certificate_table #{'0x%X+0x%0X' % ct}" if $VERBOSE ct = [ct[0], 1] end @encoded.ptr = ct[0] off_end = ct[0]+ct[1] off_end = @encoded.length if off_end > @encoded.length while @encoded.ptr < off_end certlen = decode_word certrev = decode_half certtype = decode_half certdat = @encoded.read(certlen) @certificates << [certrev, certtype, certdat] end end end
decode the COM Cor20 header
# File metasm/exe_format/coff_decode.rb, line 665 def decode_com if @directory['com_runtime'] and sect_at_rva(@directory['com_runtime'][0]) @com_header = Cor20Header.decode(self) if sect_at_rva(@com_header.entrypoint) curencoded.add_export new_label('com_entrypoint') end @com_header.decode_all(self) end end
# File metasm/exe_format/coff_decode.rb, line 741 def decode_debug if dd = @directory['debug'] and sect_at_rva(dd[0]) @debug = [] p0 = curencoded.ptr while curencoded.ptr < p0 + dd[1] @debug << DebugDirectory.decode(self) end @debug.each { |dbg| dbg.decode_inner(self) } end end
# File metasm/exe_format/coff_decode.rb, line 771 def decode_delayimports if di = @directory['delay_import_table'] and sect_at_rva(di[0]) @delayimports = DelayImportDirectory.decode_all(self) end end
decodes COFF export table from directory mark exported names as encoded.export
# File metasm/exe_format/coff_decode.rb, line 587 def decode_exports if @directory['export_table'] and sect_at_rva(@directory['export_table'][0]) @export = ExportDirectory.decode(self) @export.exports.to_a.each { |e| if e.name and sect_at_rva(e.target) name = e.name elsif e.ordinal and sect_at_rva(e.target) name = "ord_#{@export.libname}_#{e.ordinal}" end e.target = curencoded.add_export new_label(name) if name } end end
# File metasm/exe_format/coff_decode.rb, line 423 def decode_half( edata = curencoded) ; edata.decode_imm(:u16, @endianness) end
decodes the COFF header, optional header, section headers marks entrypoint and directories as edata.expord
# File metasm/exe_format/coff_decode.rb, line 506 def decode_header @cursection ||= self @encoded.ptr ||= 0 @sections = [] @header.decode(self) optoff = @encoded.ptr @optheader.decode(self) decode_symbols if @header.num_sym != 0 and not @header.characteristics.include? 'DEBUG_STRIPPED' curencoded.ptr = optoff + @header.size_opthdr decode_sections if sect_at_rva(@optheader.entrypoint) curencoded.add_export new_label('entrypoint') end (DIRECTORIES - ['certificate_table']).each { |d| if @directory[d] and sect_at_rva(@directory[d][0]) curencoded.add_export new_label(d) end } end
decodes COFF import tables from directory mark iat entries as encoded.export
# File metasm/exe_format/coff_decode.rb, line 603 def decode_imports if @directory['import_table'] and sect_at_rva(@directory['import_table'][0]) @imports = ImportDirectory.decode_all(self) iatlen = @bitsize/8 @imports.each { |id| if sect_at_rva(id.iat_p) ptr = curencoded.ptr id.imports.each { |i| if i.name name = new_label i.name elsif i.ordinal name = new_label "ord_#{id.libname}_#{i.ordinal}" end if name i.target ||= name r = Metasm::Relocation.new(Expression[name], "u#@bitsize".to_sym, @endianness) curencoded.reloc[ptr] = r curencoded.add_export new_label('iat_'+name), ptr, true end ptr += iatlen } end } end end
# File metasm/exe_format/coff_decode.rb, line 765 def decode_loadconfig if lc = @directory['load_config'] and sect_at_rva(lc[0]) @loadconfig = LoadConfig.decode(self) end end
# File metasm/exe_format/coff_decode.rb, line 722 def decode_reloc_amd64(r) case r.type when 'ABSOLUTE' when 'HIGHLOW' addr = decode_word if s = sect_at_va(addr) label = label_at(s.encoded, s.encoded.ptr, "xref_#{Expression[addr]}") Metasm::Relocation.new(Expression[label], :u32, @endianness) end when 'DIR64' addr = decode_xword if s = sect_at_va(addr) label = label_at(s.encoded, s.encoded.ptr, "xref_#{Expression[addr]}") Metasm::Relocation.new(Expression[label], :u64, @endianness) end else puts "W: COFF: Unsupported amd64 relocation #{r.inspect}" if $VERBOSE end end
decodes an I386 COFF relocation pointing to encoded.ptr
# File metasm/exe_format/coff_decode.rb, line 703 def decode_reloc_i386(r) case r.type when 'ABSOLUTE' when 'HIGHLOW' addr = decode_word if s = sect_at_va(addr) label = label_at(s.encoded, s.encoded.ptr, "xref_#{Expression[addr]}") Metasm::Relocation.new(Expression[label], :u32, @endianness) end when 'DIR64' addr = decode_xword if s = sect_at_va(addr) label = label_at(s.encoded, s.encoded.ptr, "xref_#{Expression[addr]}") Metasm::Relocation.new(Expression[label], :u64, @endianness) end else puts "W: COFF: Unsupported i386 relocation #{r.inspect}" if $VERBOSE end end
decode COFF relocation tables from directory
# File metasm/exe_format/coff_decode.rb, line 676 def decode_relocs if @directory['base_relocation_table'] and sect_at_rva(@directory['base_relocation_table'][0]) end_ptr = curencoded.ptr + @directory['base_relocation_table'][1] @relocations = [] while curencoded.ptr < end_ptr @relocations << RelocationTable.decode(self) end # interpret as EncodedData relocations relocfunc = ('decode_reloc_' << @header.machine.downcase).to_sym if not respond_to? relocfunc puts "W: COFF: unsupported relocs for architecture #{@header.machine}" if $VERBOSE return end @relocations.each { |rt| rt.relocs.each { |r| if s = sect_at_rva(rt.base_addr + r.offset) e, p = s.encoded, s.encoded.ptr rel = send(relocfunc, r) e.reloc[p] = rel if rel end } } end end
decodes resources from directory
# File metasm/exe_format/coff_decode.rb, line 630 def decode_resources if @directory['resource_table'] and sect_at_rva(@directory['resource_table'][0]) @resource = ResourceDirectory.decode(self) end end
decodes a section content (allows simpler LoadedPE override)
# File metasm/exe_format/coff_decode.rb, line 575 def decode_section_body(s) raw = EncodedData.align_size(s.rawsize, @optheader.file_align) virt = s.virtsize virt = raw = s.rawsize if @header.size_opthdr == 0 virt = raw if virt == 0 virt = EncodedData.align_size(virt, @optheader.sect_align) s.encoded = @encoded[s.rawaddr, [raw, virt].min] || EncodedData.new s.encoded.virtsize = virt end
decode the COFF sections
# File metasm/exe_format/coff_decode.rb, line 543 def decode_sections @header.num_sect.times { @sections << Section.decode(self) } # now decode COFF object relocations @sections.each { |s| next if s.relocnr == 0 curencoded.ptr = s.relocaddr s.relocs = [] s.relocnr.times { s.relocs << RelocObj.decode(self) } new_label 'pcrel' s.relocs.each { |r| next if not r.sym case r.type when 'DIR32' s.encoded.reloc[r.va] = Metasm::Relocation.new(Expression[r.sym.name], :u32, @endianness) when 'REL32' l = new_label('pcrel') s.encoded.add_export(l, r.va+4) s.encoded.reloc[r.va] = Metasm::Relocation.new(Expression[r.sym.name, :-, l], :u32, @endianness) end } } if not @header.characteristics.include?('RELOCS_STRIPPED') symbols.to_a.compact.each { |sym| next if not sym.sec_nr.kind_of? Integer next if sym.storage != 'EXTERNAL' and (sym.storage != 'STATIC' or sym.value == 0) next if not s = @sections[sym.sec_nr-1] s.encoded.add_export new_label(sym.name), sym.value } end
# File metasm/exe_format/coff_decode.rb, line 426 def decode_strz( edata = curencoded) ; super(edata) ; end
decode the COFF symbol table (obj only)
# File metasm/exe_format/coff_decode.rb, line 527 def decode_symbols endptr = @encoded.ptr = @header.ptr_sym + 18*@header.num_sym strlen = decode_word @encoded.ptr = endptr strtab = @encoded.read(strlen) @encoded.ptr = @header.ptr_sym @symbols = [] @header.num_sym.times { break if @encoded.ptr >= endptr or @encoded.ptr >= @encoded.length @symbols << Symbol.decode(self, strtab) # keep the reloc.sym_idx accurate @symbols.last.nr_aux.times { @symbols << nil } } end
decode TLS directory, including tls callback table
# File metasm/exe_format/coff_decode.rb, line 753 def decode_tls if @directory['tls_table'] and sect_at_rva(@directory['tls_table'][0]) @tls = TLSDirectory.decode(self) if s = sect_at_va(@tls.callback_p) s.encoded.add_export 'tls_callback_table' @tls.callbacks.each_with_index { |cb, i| @tls.callbacks[i] = curencoded.add_export "tls_callback_#{i}" if sect_at_rva(cb) } end end end
decode the VERSION information from the resources (file version, os, copyright etc)
# File metasm/exe_format/coff_decode.rb, line 637 def decode_version(lang=0x409) decode_resources if not resource resource.decode_version(self, lang) end
# File metasm/exe_format/coff_decode.rb, line 424 def decode_word( edata = curencoded) ; edata.decode_imm(:u32, @endianness) end
# File metasm/exe_format/coff_decode.rb, line 425 def decode_xword(edata = curencoded) ; edata.decode_imm((@bitsize == 32 ? :u32 : :u64), @endianness) end
# File metasm/exe_format/coff_decode.rb, line 817 def dump_section_header(addr, edata) s = @sections.find { |s_| s_.virtaddr == addr-@optheader.image_base } s ? "\n.section #{s.name.inspect} base=#{Expression[addr]}" : addr == @optheader.image_base ? "// exe header at #{Expression[addr]}" : super(addr, edata) end
# File metasm/exe_format/coff_decode.rb, line 486 def each_section if @header.size_opthdr == 0 and not @header.characteristics.include?('EXECUTABLE_IMAGE') @sections.each { |s| next if not s.encoded l = new_label(s.name) s.encoded.add_export(l, 0) yield s.encoded, l } return end base = @optheader.image_base base = 0 if not base.kind_of? Integer sz = @optheader.headers_size sz = EncodedData.align_size(@optheader.image_size, 4096) if @sections.empty? yield @encoded[0, sz], base @sections.each { |s| yield s.encoded, base + s.virtaddr } end
encode a COFF file, building export/import/reloc tables if needed creates the base relocation tables (need for references to IAT not known before) defaults to generating relocatable files, eg ALSR-aware pass want_relocs=false to avoid the file overhead induced by this
# File metasm/exe_format/coff_encode.rb, line 711 def encode(target='exe', want_relocs=true) @encoded = EncodedData.new label_at(@encoded, 0, 'coff_start') pre_encode_header(target, want_relocs) autoimport encode_exports if export encode_imports if imports encode_resource if resource encode_tls if tls create_relocation_tables if want_relocs encode_relocs if relocations encode_header encode_sections_fixup @encoded.data end
adds a new compiler-generated section
# File metasm/exe_format/coff_encode.rb, line 340 def encode_append_section(s) if (s.virtsize || s.encoded.virtsize) < 4096 # find section to merge with # XXX check following sections for hardcoded base address ? char = s.characteristics.dup secs = @sections.dup # do not merge non-discardable in discardable if not char.delete 'MEM_DISCARDABLE' secs.delete_if { |ss| ss.characteristics.include? 'MEM_DISCARDABLE' } end # do not merge shared w/ non-shared if char.delete 'MEM_SHARED' secs.delete_if { |ss| not ss.characteristics.include? 'MEM_SHARED' } else secs.delete_if { |ss| ss.characteristics.include? 'MEM_SHARED' } end secs.delete_if { |ss| ss.virtsize.kind_of?(::Integer) or ss.rawsize.kind_of?(::Integer) or secs[secs.index(ss)+1..-1].find { |ss_| ss_.virtaddr.kind_of?(::Integer) } } # try to find superset of characteristics if target = secs.find { |ss| (ss.characteristics & char) == char } target.encoded.align 8 puts "PE: merging #{s.name} in #{target.name} (#{target.encoded.virtsize})" if $DEBUG s.encoded = target.encoded << s.encoded else @sections << s end else @sections << s end end
# File metasm/exe_format/coff_encode.rb, line 333 def encode_byte(w) Expression[w].encode(:u8, @endianness, (caller if $DEBUG)) end
encodes the export table as a new section, updates directory
# File metasm/exe_format/coff_encode.rb, line 373 def encode_exports edata = @export.encode self # must include name tables (for forwarders) @directory['export_table'] = [label_at(edata, 0, 'export_table'), edata.virtsize] s = Section.new s.name = '.edata' s.encoded = edata s.characteristics = %w[MEM_READ] encode_append_section s end
# File metasm/exe_format/coff_encode.rb, line 334 def encode_half(w) Expression[w].encode(:u16, @endianness, (caller if $DEBUG)) end
appends the header/optheader/directories/section table to @encoded
# File metasm/exe_format/coff_encode.rb, line 612 def encode_header # encode section table, add CONTAINS_* flags from other characteristics flags s_table = EncodedData.new @sections.each { |s| if s.characteristics.kind_of? Array and s.characteristics.include? 'MEM_READ' if s.characteristics.include? 'MEM_EXECUTE' s.characteristics |= ['CONTAINS_CODE'] elsif s.encoded if s.encoded.rawsize == 0 s.characteristics |= ['CONTAINS_UDATA'] else s.characteristics |= ['CONTAINS_DATA'] end end end s.rawaddr = nil if s.rawaddr.kind_of?(::Integer) # XXX allow to force rawaddr ? s_table << s.encode(self) } # encode optional header @optheader.image_size ||= new_label('image_size') @optheader.image_base ||= label_at(@encoded, 0) @optheader.headers_size ||= new_label('headers_size') @optheader.checksum ||= new_label('checksum') @optheader.subsystem ||= 'WINDOWS_GUI' @optheader.numrva = nil opth = @optheader.encode(self) # encode header @header.machine ||= 'UNKNOWN' @header.num_sect ||= sections.length @header.time ||= Time.now.to_i & -255 @header.size_opthdr ||= opth.virtsize @encoded << @header.encode(self) << opth << s_table end
encodes the import tables as a new section, updates directory and directory
# File metasm/exe_format/coff_encode.rb, line 387 def encode_imports idata, iat = ImportDirectory.encode(self, @imports) @directory['import_table'] = [label_at(idata, 0, 'idata'), idata.virtsize] s = Section.new s.name = '.idata' s.encoded = idata s.characteristics = %w[MEM_READ MEM_WRITE MEM_DISCARDABLE] encode_append_section s if @imports.first and @imports.first.iat_p.kind_of?(Integer) # ordiat = iat.sort_by { @import[x].iat_p } ordiat = @imports.zip(iat).sort_by { |id, it| id.iat_p.kind_of?(Integer) ? id.iat_p : 1<<65 }.map { |id, it| it } else ordiat = iat end @directory['iat'] = [label_at(ordiat.first, 0, 'iat'), Expression[label_at(ordiat.last, ordiat.last.virtsize, 'iat_end'), :-, label_at(ordiat.first, 0)]] if not ordiat.empty? iat_s = nil plt = Section.new plt.name = '.plt' plt.encoded = EncodedData.new plt.characteristics = %w[MEM_READ MEM_EXECUTE] @imports.zip(iat) { |id, it| if id.iat_p.kind_of?(Integer) and @sections.find { |s_| s_.virtaddr <= id.iat_p and s_.virtaddr + (s_.virtsize || s_.encoded.virtsize) > id.iat_p } id.iat = it # will be fixed up after encode_section else # XXX should not be mixed (for @directory['iat'][1]) if not iat_s iat_s = Section.new iat_s.name = '.iat' iat_s.encoded = EncodedData.new iat_s.characteristics = %w[MEM_READ MEM_WRITE] encode_append_section iat_s end iat_s.encoded << it end id.imports.each { |i| if i.thunk arch_encode_thunk(plt.encoded, i) end } } encode_append_section plt if not plt.encoded.empty? end
encodes relocation tables in a new section .reloc, updates @directory
# File metasm/exe_format/coff_encode.rb, line 474 def encode_relocs if @relocations.empty? rt = RelocationTable.new rt.base_addr = 0 rt.relocs = [] @relocations << rt end relocs = @relocations.inject(EncodedData.new) { |edata, rt_| edata << rt_.encode(self) } @directory['base_relocation_table'] = [label_at(relocs, 0, 'reloc_table'), relocs.virtsize] s = Section.new s.name = '.reloc' s.encoded = relocs s.characteristics = %w[MEM_READ MEM_DISCARDABLE] encode_append_section s end
# File metasm/exe_format/coff_encode.rb, line 547 def encode_resource res = @resource.encode self @directory['resource_table'] = [label_at(res, 0, 'resource_table'), res.virtsize] s = Section.new s.name = '.rsrc' s.encoded = res s.characteristics = %w[MEM_READ] encode_append_section s end
append the section bodies to @encoded, and link the resulting binary
# File metasm/exe_format/coff_encode.rb, line 650 def encode_sections_fixup if @optheader.headers_size.kind_of?(::String) @encoded.fixup! @optheader.headers_size => @encoded.virtsize @optheader.headers_size = @encoded.virtsize end @encoded.align @optheader.file_align baseaddr = @optheader.image_base.kind_of?(::Integer) ? @optheader.image_base : 0x400000 binding = @encoded.binding(baseaddr) curaddr = baseaddr + @optheader.headers_size @sections.each { |s| # align curaddr = EncodedData.align_size(curaddr, @optheader.sect_align) if s.rawaddr.kind_of?(::String) @encoded.fixup! s.rawaddr => @encoded.virtsize s.rawaddr = @encoded.virtsize end if s.virtaddr.kind_of?(::Integer) raise "E: COFF: cannot encode section #{s.name}: hardcoded address too short" if curaddr > baseaddr + s.virtaddr curaddr = baseaddr + s.virtaddr end binding.update s.encoded.binding(curaddr) curaddr += s.virtsize pre_sz = @encoded.virtsize @encoded << s.encoded[0, s.encoded.rawsize] @encoded.align @optheader.file_align if s.rawsize.kind_of?(::String) @encoded.fixup! s.rawsize => (@encoded.virtsize - pre_sz) s.rawsize = @encoded.virtsize - pre_sz end } # not aligned ? spec says it is, visual studio does not binding[@optheader.image_size] = curaddr - baseaddr if @optheader.image_size.kind_of?(::String) # patch the iat where iat_p was defined # sort to ensure a 0-terminated will not overwrite an entry # (try to dump notepad.exe, which has a forwarder;) @imports.find_all { |id| id.iat_p.kind_of?(Integer) }.sort_by { |id| id.iat_p }.each { |id| s = sect_at_rva(id.iat_p) @encoded[s.rawaddr + s.encoded.ptr, id.iat.virtsize] = id.iat binding.update id.iat.binding(baseaddr + id.iat_p) } if imports @encoded.fill @encoded.fixup! binding if @optheader.checksum.kind_of?(::String) and @encoded.reloc.length == 1 # won't work if there are other unresolved relocs checksum = self.class.checksum(@encoded.data, @endianness) @encoded.fixup @optheader.checksum => checksum @optheader.checksum = checksum end end
# File metasm/exe_format/coff_encode.rb, line 462 def encode_tls dir, cbtable = @tls.encode(self) @directory['tls_table'] = [label_at(dir, 0, 'tls_table'), dir.virtsize] s = Section.new s.name = '.tls' s.encoded = EncodedData.new << dir << cbtable s.characteristics = %w[MEM_READ MEM_WRITE] encode_append_section s end
# File metasm/exe_format/coff_encode.rb, line 335 def encode_word(w) Expression[w].encode(:u32, @endianness, (caller if $DEBUG)) end
# File metasm/exe_format/coff_encode.rb, line 336 def encode_xword(w) Expression[w].encode((@bitsize == 32 ? :u32 : :u64), @endianness, (caller if $DEBUG)) end
file offset -> memory address handles LoadedPE
# File metasm/exe_format/coff_decode.rb, line 478 def fileoff_to_addr(foff) if s = @sections.find { |s_| s_.rawaddr <= foff and s_.rawaddr + s_.rawsize > foff } s.virtaddr + foff - s.rawaddr + (@load_address ||= @optheader.image_base) elsif foff >= 0 and foff < @optheader.headers_size foff + (@load_address ||= @optheader.image_base) end end
returns an array including the PE entrypoint and the exported functions entrypoints TODO filter out exported data, include safeseh ?
# File metasm/exe_format/coff_decode.rb, line 806 def get_default_entrypoints ep = [] ep.concat @tls.callbacks.to_a if tls ep << (@optheader.image_base + label_rva(@optheader.entrypoint)) @export.exports.to_a.each { |e| next if e.forwarder_lib or not e.target ep << (@optheader.image_base + label_rva(e.target)) } if export ep end
resets the values in the header that may have been modified by your script (eg section count, size, imagesize, etc) call this whenever you decode a file, modify it, and want to reencode it later
# File metasm/exe_format/coff_encode.rb, line 603 def invalidate_header # set those values to nil, they will be # recomputed during encode_header [:code_size, :data_size, :udata_size, :base_of_code, :base_of_data, :sect_align, :file_align, :image_size, :headers_size, :checksum].each { |m| @optheader.send("#{m}=", nil) } [:num_sect, :ptr_sym, :num_sym, :size_opthdr].each { |m| @header.send("#{m}=", nil) } end
# File metasm/exe_format/coff_decode.rb, line 452 def label_rva(name) if name.kind_of? Integer name elsif s = @sections.find { |s_| s_.encoded.export[name] } s.virtaddr + s.encoded.export[name] else @encoded.export[name] end end
# File metasm/exe_format/coff_encode.rb, line 727 def parse_init # ahem... # a fake object, which when appended makes us parse '.text', which creates a real default section # forwards to it this first appendage. # allows the user to specify its own section if he wishes, and to use .text if he doesn't if not defined? @cursource or not @cursource @cursource = ::Object.new class << @cursource attr_accessor :coff def <<(*a) t = Preprocessor::Token.new(nil) t.raw = '.text' coff.parse_parser_instruction t coff.cursource.send(:<<, *a) end end @cursource.coff = self end @source ||= {} super() end
handles compiler meta-instructions
syntax:
.section "<section name>" <perm list> <base> section name is a string (may be quoted) perms are in 'r' 'w' 'x' 'shared' 'discard', may be concatenated (in this order), may be prefixed by 'no' to remove the attribute for an existing section base is the token 'base', the token '=' and an immediate expression default sections: .text = .section '.text' rx .data = .section '.data' rw .rodata = .section '.rodata' r .bss = .section '.bss' rw .entrypoint | .entrypoint <label> defines the label as the program entrypoint without argument, creates a label used as entrypoint .libname "<name>" defines the string to be used as exported library name (should be the same as the file name, may omit extension) .export ["<exported_name>"] [<ordinal>] [<label_name>] exports the specified label with the specified name (label_name defaults to exported_name) if exported_name is an unquoted integer, the export is by ordinal. XXX if the ordinal starts with '0', the integer is interpreted as octal .import "<libname>" "<import_name|ordinal>" [<thunk_name>] [<label_name>] imports a symbol from a library if the thunk name is specified and not 'nil', the compiler will generate a thunk that can be called (in ia32, 'call thunk' == 'call [import_name]') the thunk is position-independent, and should be used instead of the indirect call form, for imported functions label_name is the label to attribute to the location that will receive the address of the imported symbol, defaults to import_name (iat_<import_name> if thunk == iname) .image_base <base> specifies the COFF prefered load address, base is an immediate expression
# File metasm/exe_format/coff_encode.rb, line 777 def parse_parser_instruction(instr) readstr = lambda { @lexer.skip_space raise instr, 'string expected' if not t = @lexer.readtok or (t.type != :string and t.type != :quoted) t.value || t.raw } check_eol = lambda { @lexer.skip_space raise instr, 'eol expected' if t = @lexer.nexttok and t.type != :eol } case instr.raw.downcase when '.text', '.data', '.rodata', '.bss' sname = instr.raw.downcase if not @sections.find { |s| s.name == sname } s = Section.new s.name = sname s.encoded = EncodedData.new s.characteristics = case sname when '.text'; %w[MEM_READ MEM_EXECUTE] when '.data', '.bss'; %w[MEM_READ MEM_WRITE] when '.rodata'; %w[MEM_READ] end @sections << s end @cursource = @source[sname] ||= [] check_eol[] if instr.backtrace # special case for magic @cursource when '.section' # .section <section name|"section name"> [(no)r w x shared discard] [base=<expr>] sname = readstr[] if not s = @sections.find { |s_| s_.name == sname } s = Section.new s.name = sname s.encoded = EncodedData.new s.characteristics = [] @sections << s end loop do @lexer.skip_space break if not tok = @lexer.nexttok or tok.type != :string case @lexer.readtok.raw.downcase when /^(no)?(r)?(w)?(x)?(shared)?(discard)?$/ ar = [] ar << 'MEM_READ' if $2 ar << 'MEM_WRITE' if $3 ar << 'MEM_EXECUTE' if $4 ar << 'MEM_SHARED' if $5 ar << 'MEM_DISCARDABLE' if $6 if $1; s.characteristics -= ar else s.characteristics |= ar end when 'base' @lexer.skip_space @lexer.unreadtok tok if not tok = @lexer.readtok or tok.type != :punct or tok.raw != '=' raise instr, 'invalid base' if not s.virtaddr = Expression.parse(@lexer).reduce or not s.virtaddr.kind_of?(::Integer) if not @optheader.image_base @optheader.image_base = (s.virtaddr-0x80) & 0xfff00000 puts "Warning: no image_base specified, using #{Expression[@optheader.image_base]}" if $VERBOSE end s.virtaddr -= @optheader.image_base else raise instr, 'unknown parameter' end end @cursource = @source[sname] ||= [] check_eol[] when '.libname' # export directory library name # .libname <libname|"libname"> @export ||= ExportDirectory.new @export.libname = readstr[] check_eol[] when '.export' # .export <export name|ordinal|"export name"> [ordinal] [label to export if different] @lexer.skip_space raise instr, 'string expected' if not tok = @lexer.readtok or (tok.type != :string and tok.type != :quoted) exportname = tok.value || tok.raw if tok.type == :string and (0..9).include? tok.raw[0] exportname = Integer(exportname) rescue raise(tok, "bad ordinal value, try quotes #{' or rm leading 0' if exportname[0] == ?0}") end @lexer.skip_space tok = @lexer.readtok if tok and tok.type == :string and (0..9).include? tok.raw[0] (eord = Integer(tok.raw)) rescue @lexer.unreadtok(tok) else @lexer.unreadtok(tok) end @lexer.skip_space tok = @lexer.readtok if tok and tok.type == :string exportlabel = tok.raw else @lexer.unreadtok tok end @export ||= ExportDirectory.new @export.exports ||= [] e = ExportDirectory::Export.new if exportname.kind_of? Integer e.ordinal = exportname else e.name = exportname e.ordinal = eord if eord end e.target = exportlabel || exportname @export.exports << e check_eol[] when '.import' # .import <libname|"libname"> <imported sym|"imported sym"> [label of plt thunk|nil] [label of iat element if != symname] libname = readstr[] i = ImportDirectory::Import.new @lexer.skip_space raise instr, 'string expected' if not tok = @lexer.readtok or (tok.type != :string and tok.type != :quoted) if tok.type == :string and (0..9).include? tok.raw[0] i.ordinal = Integer(tok.raw) else i.name = tok.value || tok.raw end @lexer.skip_space if tok = @lexer.readtok and tok.type == :string i.thunk = tok.raw if tok.raw != 'nil' @lexer.skip_space tok = @lexer.readtok end if tok and tok.type == :string i.target = tok.raw else i.target = ((i.thunk == i.name) ? ('iat_' + i.name) : (i.name ? i.name : (i.thunk ? 'iat_' + i.thunk : raise(instr, 'need iat label')))) @lexer.unreadtok tok end raise tok, 'import target exists' if i.target != new_label(i.target) @imports ||= [] if not id = @imports.find { |id_| id_.libname == libname } id = ImportDirectory.new id.libname = libname id.imports = [] @imports << id end id.imports << i check_eol[] when '.entrypoint' # ".entrypoint <somelabel/expression>" or ".entrypoint" (here) @lexer.skip_space if tok = @lexer.nexttok and tok.type == :string raise instr, 'syntax error' if not entrypoint = Expression.parse(@lexer) else entrypoint = new_label('entrypoint') @cursource << Label.new(entrypoint, instr.backtrace.dup) end @optheader.entrypoint = entrypoint check_eol[] when '.image_base' raise instr if not base = Expression.parse(@lexer) or !(base = base.reduce).kind_of?(::Integer) @optheader.image_base = base check_eol[] when '.subsystem' @lexer.skip_space raise instr if not tok = @lexer.readtok @optheader.subsystem = tok.raw check_eol[] else super(instr) end end
initialize the header from target/cpu/etc, target in ['exe' 'dll' 'kmod' 'obj']
# File metasm/exe_format/coff_encode.rb, line 560 def pre_encode_header(target='exe', want_relocs=true) target = {:bin => 'exe', :lib => 'dll', :obj => 'obj', 'sys' => 'kmod', 'drv' => 'kmod'}.fetch(target, target) @header.machine ||= case @cpu.shortname when 'x64'; 'AMD64' when 'ia32'; 'I386' end @optheader.signature ||= case @cpu.size when 32; 'PE' when 64; 'PE+' end @bitsize = (@optheader.signature == 'PE+' ? 64 : 32) # setup header flags tmp = %w[LINE_NUMS_STRIPPED LOCAL_SYMS_STRIPPED DEBUG_STRIPPED] + case target when 'exe'; %w[EXECUTABLE_IMAGE] when 'dll'; %w[EXECUTABLE_IMAGE DLL] when 'kmod'; %w[EXECUTABLE_IMAGE] when 'obj'; [] end if @cpu.size == 32 tmp << 'x32BIT_MACHINE' else tmp << 'LARGE_ADDRESS_AWARE' end tmp << 'RELOCS_STRIPPED' if not want_relocs @header.characteristics ||= tmp @optheader.subsystem ||= case target when 'exe', 'dll'; 'WINDOWS_GUI' when 'kmod'; 'NATIVE' end tmp = [] tmp << 'NX_COMPAT' tmp << 'DYNAMIC_BASE' if want_relocs @optheader.dll_characts ||= tmp end
honors C attributes: export, export_as(foo), import_from(kernel32), entrypoint import by ordinal: extern __stdcall int anyname(int) __attribute__((import_from(ws2_32:28))); can alias imports with int mygpaddr_alias() attr(import_from(kernel32:GetProcAddr))
# File metasm/exe_format/coff_encode.rb, line 975 def read_c_attrs(cp) cp.toplevel.symbol.each_value { |v| next if not v.kind_of? C::Variable if v.has_attribute 'export' or ea = v.has_attribute_var('export_as') @export ||= ExportDirectory.new @export.exports ||= [] e = ExportDirectory::Export.new begin e.ordinal = Integer(ea || v.name) rescue ArgumentError e.name = ea || v.name end e.target = v.name @export.exports << e end if v.has_attribute('import') or ln = v.has_attribute_var('import_from') ln ||= WindowsExports::EXPORT[v.name] raise "unknown library for #{v.name}" if not ln i = ImportDirectory::Import.new if ln.include? ':' ln, name = ln.split(':') begin i.ordinal = Integer(name) rescue ArgumentError i.name = name end else i.name = v.name end if v.type.kind_of? C::Function i.thunk = v.name i.target = 'iat_'+i.thunk else i.target = v.name end @imports ||= [] if not id = @imports.find { |id_| id_.libname == ln } id = ImportDirectory.new id.libname = ln id.imports = [] @imports << id end id.imports << i end if v.has_attribute 'entrypoint' @optheader.entrypoint = v.name end } end
converts an RVA (offset from base address of file when loaded in memory) to the section containing it using the section table updates @cursection and @cursection.encoded.ptr to point to the specified address may return self when rva points to the coff header returns nil if none match, 0 never matches
# File metasm/exe_format/coff_decode.rb, line 432 def sect_at_rva(rva) return if not rva or not rva.kind_of?(::Integer) or rva <= 0 if sections and not @sections.empty? if s = @sections.find { |s_| s_.virtaddr <= rva and s_.virtaddr + EncodedData.align_size((s_.virtsize == 0 ? s_.rawsize : s_.virtsize), @optheader.sect_align) > rva } s.encoded.ptr = rva - s.virtaddr @cursection = s elsif rva < @sections.map { |s_| s_.virtaddr }.min @encoded.ptr = rva @cursection = self end elsif rva <= @encoded.length @encoded.ptr = rva @cursection = self end end
# File metasm/exe_format/coff_decode.rb, line 448 def sect_at_va(va) sect_at_rva(va - @optheader.image_base) end
returns an array of [name, addr, length, info]
# File metasm/exe_format/coff_decode.rb, line 824 def section_info [['header', @optheader.image_base, @optheader.headers_size, nil]] + @sections.map { |s| [s.name, @optheader.image_base + s.virtaddr, s.virtsize, s.characteristics.join(',')] } end
# File metasm/exe_format/coff.rb, line 417 def shortname; 'coff'; end
# File metasm/exe_format/coff.rb, line 419 def sizeof_byte ; 1 ; end
# File metasm/exe_format/coff.rb, line 420 def sizeof_half ; 2 ; end
# File metasm/exe_format/coff.rb, line 421 def sizeof_word ; 4 ; end
# File metasm/exe_format/coff.rb, line 422 def sizeof_xword ; @bitsize == 32 ? 4 : 8 ; end
# File metasm/exe_format/coff_encode.rb, line 967 def tune_cparser(cp) super(cp) cp.llp64 if @cpu.size == 64 end
defines __PE__
# File metasm/exe_format/coff_encode.rb, line 962 def tune_prepro(l) l.define_weak('__PE__', 1) l.define_weak('__MS_X86_64_ABI__') if @cpu and @cpu.shortname == 'x64' end