class Metasm::PE

Constants

MAGIC

Attributes

coff_offset[RW]
mz[RW]
signature[RW]

Public Class Methods

imphash(path) click to toggle source
# File metasm/exe_format/pe.rb, line 351
def self.imphash(path)
        pe = decode_file_header(path)
        pe.decode_imports
        pe.imphash
end
new(*a) click to toggle source
Calls superclass method 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
pehash(path, digest) click to toggle source
# File metasm/exe_format/pe.rb, line 326
def self.pehash(path, digest)
        decode_file_header(path).pehash(digest)
end

Public Instance Methods

c_set_default_entrypoint() click to toggle source
# 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
decode_header() click to toggle source

overrides COFF#decode_header simply sets the offset to the PE pointer before decoding the COFF header also checks the PE signature

Calls superclass method 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
encode_default_mz_header() click to toggle source

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
encode_header(*a) click to toggle source

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'

Calls superclass method 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
get_xrefs_x(dasm, di) click to toggle source

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

Calls superclass method 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
imphash() click to toggle source

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
init_disassembler() click to toggle source

returns a disassembler with a special decodedfunction for GetProcAddress (i386 only), and the default func

Calls superclass method 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
mini_copy(share_ns=true) click to toggle source

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
module_address() click to toggle source
# File metasm/exe_format/pe.rb, line 283
def module_address
        @optheader.image_base
end
module_name() click to toggle source
# File metasm/exe_format/pe.rb, line 279
def module_name
        export and @export.libname
end
module_size() click to toggle source
# File metasm/exe_format/pe.rb, line 287
def module_size
        @sections.map { |s_| s_.virtaddr + s_.virtsize }.max || 0
end
module_symbols() click to toggle source
# 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
pehash(digest) click to toggle source

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