metasploit-framework/lib/metasm/samples/peldr.rb

495 lines
19 KiB
Ruby

# This file is part of Metasm, the Ruby assembly manipulation suite
# Copyright (C) 2006-2009 Yoann GUILLOT
#
# Licence is LGPL, see LICENCE in the top-level directory
#
# Map a PE file under another OS using DynLdr, API imports are redirected to ruby callback for emulation
#
require 'metasm'
class PeLdr
attr_accessor :pe, :load_address
DL = Metasm::DynLdr
# load a PE file, setup basic IAT hooks (raises "unhandled lib!import")
def initialize(file, hooktype=:iat)
if file.kind_of? Metasm::PE
@pe = file
elsif file[0, 2] == 'MZ' and file.length > 0x3c
@pe = Metasm::PE.decode(file)
else # filename
@pe = Metasm::PE.decode_file(file)
end
@load_address = DL.memory_alloc(@pe.optheader.image_size)
raise 'malloc' if @load_address == 0xffff_ffff
puts "map sections" if $DEBUG
DL.memory_write(@load_address, @pe.encoded.data[0, @pe.optheader.headers_size].to_str)
@pe.sections.each { |s|
DL.memory_write(@load_address+s.virtaddr, s.encoded.data.to_str)
}
puts "fixup sections" if $DEBUG
off = @load_address - @pe.optheader.image_base
@pe.relocations.to_a.each { |rt|
base = rt.base_addr
rt.relocs.each { |r|
if r.type == 'HIGHLOW'
ptr = @load_address + base + r.offset
old = DL.memory_read(ptr, 4).unpack('V').first
DL.memory_write_int(ptr, old + off)
end
}
}
@iat_cb = {}
@eat_cb = {}
case hooktype
when :iat
puts "hook IAT" if $DEBUG
@pe.imports.to_a.each { |id|
ptr = @load_address + id.iat_p
id.imports.each { |i|
n = "#{id.libname}!#{i.name}"
cb = DL.callback_alloc_c('void x(void)') { raise "unemulated import #{n}" }
DL.memory_write_int(ptr, cb)
@iat_cb[n] = cb
ptr += 4
}
}
when :eat, :exports
puts "hook EAT" if $DEBUG
ptr = @load_address + @pe.export.func_p
@pe.export.exports.each { |e|
n = e.name || e.ordinal
cb = DL.callback_alloc_c('void x(void)') { raise "unemulated export #{n}" }
DL.memory_write_int(ptr, cb - @load_address) # RVA
@eat_cb[n] = cb
ptr += 4
}
end
end
# reset original expected memory protections for the sections
# the IAT may reside in a readonly section, so call this only after all hook_imports
def reprotect_sections
@pe.sections.each { |s|
p = ''
p << 'r' if s.characteristics.include? 'MEM_READ'
p << 'w' if s.characteristics.include? 'MEM_WRITE'
p << 'x' if s.characteristics.include? 'MEM_EXECUTE'
DL.memory_perm(@load_address + s.virtaddr, s.virtsize, p)
}
end
# add a specific hook for an IAT function
# accepts a function pointer in proto
# exemple: hook_import('KERNEL32.dll', 'GetProcAddress', '__stdcall int f(int, char*)') { |h, name| puts "getprocaddr #{name}" ; 0 }
def hook_import(libname, impname, proto, &b)
@pe.imports.to_a.each { |id|
next if id.libname != libname
ptr = @load_address + id.iat_p
id.imports.each { |i|
if i.name == impname
DL.callback_free(@iat_cb["#{libname}!#{impname}"])
if proto.kind_of? Integer
cb = proto
else
cb = DL.callback_alloc_c(proto, &b)
@iat_cb["#{libname}!#{impname}"] = cb
end
DL.memory_write_int(ptr, cb)
end
ptr += 4
}
}
end
# add a specific hook in the export table
# exemple: hook_export('ExportedFunc', '__stdcall int f(int, char*)') { |i, p| blabla ; 1 }
def hook_export(name, proto, &b)
ptr = @load_address + @pe.export.func_p
@pe.export.exports.each { |e|
n = e.name || e.ordinal
if n == name
DL.callback_free(@eat_cb[name])
if proto.kind_of? Integer
cb = proto
else
cb = DL.callback_alloc_c(proto, &b)
@eat_cb[name] = cb
end
DL.memory_write_int(ptr, cb - @load_address) # RVA
end
ptr += 4
}
end
# run the loaded PE entrypoint
def run_init
ptr = @pe.optheader.entrypoint
if ptr != 0
ptr += @load_address
DL.raw_invoke(ptr, [@load_address, 1, 1], 1)
end
end
# similar to DL.new_api_c for the mapped PE
def new_api_c(proto)
proto += ';' # allow 'int foo()'
cp = DL.host_cpu.new_cparser
cp.parse(proto)
cp.toplevel.symbol.each_value { |v|
next if not v.kind_of? Metasm::C::Variable # enums
if e = pe.export.exports.find { |e_| e_.name == v.name and e_.target }
DL.new_caller_for(cp, v, v.name.downcase, @load_address + pe.label_rva(e.target))
end
}
cp.numeric_constants.each { |k, v, f|
n = k.upcase
n = "C#{n}" if n !~ /^[A-Z]/
DL.const_set(n, v) if not DL.const_defined?(n) and v.kind_of? Integer
}
end
# maps a TEB/PEB in the current process, sets the fs register to point to it
def self.setup_teb
@@teb = DL.memory_alloc(4096)
@@peb = DL.memory_alloc(4096)
populate_teb
populate_peb
fs = allocate_ldt_entry_teb
DL.new_func_c('__fastcall void set_fs(int i) { asm("mov fs, ecx"); }') { DL.set_fs(fs) }
end
# fills a fake TEB structure
def self.populate_teb
DL.memory_write(@@teb, 0.chr*4096)
set = lambda { |off, val| DL.memory_write_int(@@teb+off, val) }
# the stack will probably never go higher than that whenever in the dll...
set[0x4, DL.new_func_c('int get_sp(void) { asm("mov eax, esp and eax, ~0xfff"); }') { DL.get_sp }] # stack_base
set[0x8, 0x10000] # stack_limit
set[0x18, @@teb] # teb
set[0x30, @@peb] # peb
end
def self.populate_peb
DL.memory_write(@@peb, 0.chr*4096)
#set = lambda { |off, val| DL.memory_write_int(@@peb+off, val) }
end
def self.teb ; @@teb ; end
def self.peb ; @@peb ; end
# allocate an LDT entry for the teb, returns a value suitable for the fs selector
def self.allocate_ldt_entry_teb
entry = 1
# ldt_entry base_addr size_in_pages
# 32bits:1 type:2 (0=data) readonly:1 limit_in_pages:1 seg_not_present:1 usable:1
struct = [entry, @@teb, 1, 0b1_0_1_0_00_1].pack('VVVV')
Kernel.syscall(123, 1, DL.str_ptr(struct), struct.length) # __NR_modify_ldt
(entry << 3) | 7
end
setup_teb
end
# generate a fake PE which exports stuff found in k32/ntdll
# so that other lib may directly scan this PE with their own getprocaddr
class FakeWinAPI < PeLdr
attr_accessor :win_version
attr_accessor :exports
def initialize(elist=nil)
@exports = []
@win_version = { :major => 5, :minor => 1, :build => 2600, :sp => 'Service pack 3', :sp_major => 3, :sp_minor => 0 }
# if you know the exact list you need, put it there (much faster)
if not elist
elist = Metasm::WindowsExports::EXPORT.map { |k, v| k if v =~ /kernel32|ntdll/i }.compact
elist |= ['free', 'malloc', 'memset', '??2@YAPAXI@Z', '_initterm', '_lock', '_unlock', '_wcslwr', '_wcsdup', '__dllonexit']
end
src = ".libname 'emu_winapi'\ndummy: int 3\n" + elist.map { |e| ".export #{e.inspect} dummy" }.join("\n")
super(Metasm::PE.assemble(Metasm::Ia32.new, src).encode_string(:lib), :eat) # put 'nil' instead of :eat if all exports are emu
@heap = {}
malloc = lambda { |sz| str = 0.chr*sz ; ptr = DL.str_ptr(str) ; @heap[ptr] = str ; ptr }
lasterr = 0
# kernel32
hook_export('CloseHandle', '__stdcall int f(int)') { |a| 1 }
hook_export('DuplicateHandle', '__stdcall int f(int, int, int, void*, int, int, int)') { |*a| DL.memory_write_int(a[3], a[1]) ; 1 }
hook_export('EnterCriticalSection', '__stdcall int f(void*)') { 1 }
hook_export('GetCurrentProcess', '__stdcall int f(void)') { -1 }
hook_export('GetCurrentProcessId', '__stdcall int f(void)') { Process.pid }
hook_export('GetCurrentThreadId', '__stdcall int f(void)') { Process.pid }
hook_export('GetEnvironmentVariableW', '__stdcall int f(void*, void*, int)') { |name, resp, sz|
next 0 if name == 0
s = DL.memory_read_wstrz(name)
s = s.unpack('v*').pack('C*') rescue nil
puts "GetEnv #{s.inspect}" if $VERBOSE
v = ENV[s].to_s
if resp and v.length*2+2 <= sz
DL.memory_write(resp, (v.unpack('C*') << 0).pack('v*'))
v.length*2 # 0 if not found
else
v.length*2+2
end
}
hook_export('GetLastError', '__stdcall int f(void)') { lasterr }
hook_export('GetProcAddress', '__stdcall int f(int, char*)') { |h, v|
v = DL.memory_read_strz(v) if v >= 0x10000
puts "GetProcAddr #{'0x%x' % h}, #{v.inspect}" if $VERBOSE
@eat_cb[v] or raise "unemulated getprocaddr #{v}"
}
hook_export('GetSystemInfo', '__stdcall void f(void*)') { |ptr|
DL.memory_write(ptr, [0, 0x1000, 0x10000, 0x7ffeffff, 1, 1, 586, 0x10000, 0].pack('V*'))
1
}
hook_export('GetSystemTimeAsFileTime', '__stdcall void f(void*)') { |ptr|
v = ((Time.now - Time.mktime(1971, 1, 1, 0, 0, 0) + 370*365.25*24*60*60) * 1000 * 1000 * 10).to_i
DL.memory_write(ptr, [v & 0xffffffff, (v >> 32 & 0xffffffff)].pack('VV'))
1
}
hook_export('GetTickCount', '__stdcall int f(void)') { (Time.now.to_i * 1000) & 0xffff_ffff }
hook_export('GetVersion', '__stdcall int f(void)') { (@win_version[:build] << 16) | (@win_version[:major] << 8) | @win_version[:minor] }
hook_export('GetVersionExA', '__stdcall int f(void*)') { |ptr|
sz = DL.memory_read(ptr, 4).unpack('V').first
data = [@win_version[:major], @win_version[:minor], @win_version[:build], 2, @win_version[:sp], @win_version[:sp_major], @win_version[:sp_minor]].pack('VVVVa128VV')
DL.memory_write(ptr+4, data[0, sz-4])
1
}
hook_export('HeapAlloc', '__stdcall int f(int, int, int)') { |h, f, sz| malloc[sz] }
hook_export('HeapCreate', '__stdcall int f(int, int, int)') { 1 }
hook_export('HeapFree', '__stdcall int f(int, int, int)') { |h, f, p| @heap.delete p ; 1 }
hook_export('InterlockedCompareExchange', '__stdcall int f(int*, int, int)'+
'{ asm("mov eax, [ebp+16] mov ecx, [ebp+12] mov edx, [ebp+8] lock cmpxchg [edx], ecx"); }')
hook_export('InterlockedExchange', '__stdcall int f(int*, int)'+
'{ asm("mov eax, [ebp+12] mov ecx, [ebp+8] lock xchg [ecx], eax"); }')
hook_export('InitializeCriticalSectionAndSpinCount', '__stdcall int f(int, int)') { 1 }
hook_export('InitializeCriticalSection', '__stdcall int f(void*)') { 1 }
hook_export('LeaveCriticalSection', '__stdcall int f(void*)') { 1 }
hook_export('QueryPerformanceCounter', '__stdcall int f(void*)') { |ptr|
v = (Time.now.to_f * 1000 * 1000).to_i
DL.memory_write(ptr, [v & 0xffffffff, (v >> 32 & 0xffffffff)].pack('VV'))
1
}
hook_export('SetLastError', '__stdcall int f(int)') { |i| lasterr = i ; 1 }
hook_export('TlsAlloc', '__stdcall int f(void)') { 1 }
# ntdll
readustring = lambda { |p| DL.memory_read(*DL.memory_read(p, 8).unpack('vvV').values_at(2, 0)) }
hook_export('RtlEqualUnicodeString', '__stdcall int f(void*, void*, int)') { |s1, s2, cs|
s1 = readustring[s1]
s2 = readustring[s2]
puts "RtlEqualUnicodeString #{s1.unpack('v*').pack('C*').inspect}, #{s2.unpack('v*').pack('C*').inspect}, #{cs}" if $VERBOSE
if cs == 1
s1 = s1.downcase
s2 = s2.downcase
end
s1 == s2 ? 1 : 0
}
hook_export('MultiByteToWideChar', '__stdcall int f(int, int, void*, int, void*, int)') { |cp, fl, ip, is, op, os|
is = DL.memory_read_strz(ip).length if is == 0xffff_ffff
if os == 0
is
elsif os >= is*2 # not sure with this..
DL.memory_write(op, DL.memory_read(ip, is).unpack('C*').pack('v*'))
is
else 0
end
}
hook_export('LdrUnloadDll', '__stdcall int f(int)') { 0 }
# msvcrt
hook_export('free', 'void f(int)') { |i| @heap.delete i ; 0}
hook_export('malloc', 'int f(int)') { |i| malloc[i] }
hook_export('memset', 'char* f(char* p, char c, int n) { while (n--) p[n] = c; return p; }')
hook_export('??2@YAPAXI@Z', 'int f(int)') { |i| raise 'fuuu' if i > 0x100000 ; malloc[i] } # at some point we're called with a ptr as arg, may be a peldr bug
hook_export('__dllonexit', 'int f(int, int, int)') { |i, ii, iii| i }
hook_export('_initterm', 'void f(void (**p)(void), void*p2) { while(p < p2) { if (*p) (**p)(); p++; } }')
hook_export('_lock', 'void f(int)') { 0 }
hook_export('_unlock', 'void f(int)') { 0 }
hook_export('_wcslwr', '__int16* f(__int16* p) { int i=-1; while (p[++i]) p[i] |= 0x20; return p; }')
hook_export('_wcsdup', 'int f(__int16* p)') { |p|
cp = DL.memory_read_wstrz(p) + "\0\0"
p = DL.str_ptr(cp)
@heap[p] = cp
p
}
end
def hook_export(*a, &b)
@exports |= [a.first]
super(*a, &b)
end
# take another PeLdr and patch its IAT with functions from our @exports (eg our explicit export hooks)
def intercept_iat(ldr)
ldr.pe.imports.to_a.each { |id|
id.imports.each { |i|
next if not @exports.include? i.name or not @eat_cb[i.name]
ldr.hook_import(id.libname, i.name, @eat_cb[i.name])
}
}
end
end
if $0 == __FILE__
dl = Metasm::DynLdr
l = PeLdr.new('dbghelp.dll')
#dl.memory_write(l.load_address + 0x33b10, "\x90\xcc") # break on SymInitializeW
puts 'dbghelp@%x' % l.load_address if $VERBOSE
wapi = FakeWinAPI.new %w[CloseHandle DuplicateHandle EnterCriticalSection GetCurrentProcess GetCurrentProcessId GetCurrentThreadId GetEnvironmentVariableW GetLastError GetProcAddress GetSystemInfo GetSystemTimeAsFileTime GetTickCount GetVersion GetVersionExA HeapAlloc HeapCreate HeapFree InterlockedCompareExchange InterlockedExchange InitializeCriticalSectionAndSpinCount InitializeCriticalSection LeaveCriticalSection QueryPerformanceCounter SetLastError TlsAlloc RtlEqualUnicodeString MultiByteToWideChar free malloc memset ??2@YAPAXI@Z __dllonexit _initterm _lock _unlock _wcslwr _wcsdup GetModuleHandleA LoadLibraryA NtQueryObject NtQueryInformationProcess LdrUnloadDll]
puts 'wapi@%x' % wapi.load_address if $VERBOSE
wapi.hook_export('GetModuleHandleA', '__stdcall int f(char*)') { |ptr|
s = dl.memory_read_strz(ptr) if ptr
case s
when /kernel32|ntdll/i; wapi.load_address
else 0
end
}
wapi.hook_export('LoadLibraryA', '__stdcall int f(char*)') { |ptr|
s = dl.memory_read_strz(ptr)
case s
when /kernel32|ntdll/i; wapi.load_address
else puts "LoadLibrary #{s.inspect}" ; 0
end
}
wapi.hook_export('NtQueryObject', '__stdcall int f(int, int, void*, int, int*)') { |h, type, resp, sz, psz|
puts "NtQueryObject #{h}, #{type}, #{sz}" if $VERBOSE
if h == 42 and type == 2 and sz >= 24
dl.memory_write(resp, [14, 16, resp+8].pack('vvV') + "Process\0".unpack('C*').pack('v*'))
dl.memory_write_int(psz, 24) if psz
0
else
0x8000_0000
end
}
wapi.hook_export('NtQueryInformationProcess', '__stdcall int f(int, int, void*, int, int*)') { |h, type, resp, sz, psz|
puts "NtQueryInformationProcess #{h}, #{type}, #{sz}" if $VERBOSE
if h == 42 and type == 0
# reservd peb res res ptr_to_pid
peb = 0xdead0000
dl.memory_write(resp, [42, peb, 0, 0, resp, 0].pack('V*'))
dl.memory_write_int(psz, 24) if psz
0
else
0x8000_0000
end
}
#puts wapi.exports.join(' ') # generate arglist for FakeWinAPI.new
# TODO hook the resolv function of dbghelp to list what it checks
wapi.intercept_iat(l)
l.reprotect_sections
l.new_api_c <<EOS
#define SYMOPT_CASE_INSENSITIVE 0x00000001
#define SYMOPT_UNDNAME 0x00000002
#define SYMOPT_DEFERRED_LOADS 0x00000004
#define SYMOPT_NO_CPP 0x00000008
#define SYMOPT_LOAD_LINES 0x00000010
#define SYMOPT_OMAP_FIND_NEAREST 0x00000020
#define SYMOPT_LOAD_ANYTHING 0x00000040
#define SYMOPT_IGNORE_CVREC 0x00000080
#define SYMOPT_NO_UNQUALIFIED_LOADS 0x00000100
#define SYMOPT_FAIL_CRITICAL_ERRORS 0x00000200
#define SYMOPT_EXACT_SYMBOLS 0x00000400
#define SYMOPT_ALLOW_ABSOLUTE_SYMBOLS 0x00000800
#define SYMOPT_IGNORE_NT_SYMPATH 0x00001000
#define SYMOPT_INCLUDE_32BIT_MODULES 0x00002000
#define SYMOPT_PUBLICS_ONLY 0x00004000
#define SYMOPT_NO_PUBLICS 0x00008000
#define SYMOPT_AUTO_PUBLICS 0x00010000
#define SYMOPT_NO_IMAGE_SEARCH 0x00020000
#define SYMOPT_SECURE 0x00040000
#define SYMOPT_NO_PROMPTS 0x00080000
#define SYMOPT_DEBUG 0x80000000
typedef int BOOL;
typedef char CHAR;
typedef unsigned long DWORD;
typedef unsigned __int64 DWORD64;
typedef void *HANDLE;
typedef unsigned __int64 *PDWORD64;
typedef void *PVOID;
typedef unsigned long ULONG;
typedef unsigned __int64 ULONG64;
typedef const CHAR *PCSTR;
typedef CHAR *PSTR;
struct _SYMBOL_INFO {
ULONG SizeOfStruct;
ULONG TypeIndex;
ULONG64 Reserved[2];
ULONG info;
ULONG Size;
ULONG64 ModBase;
ULONG Flags;
ULONG64 Value;
ULONG64 Address;
ULONG Register;
ULONG Scope;
ULONG Tag;
ULONG NameLen;
ULONG MaxNameLen;
CHAR Name[1];
};
typedef struct _SYMBOL_INFO *PSYMBOL_INFO;
typedef __stdcall BOOL (*PSYM_ENUMERATESYMBOLS_CALLBACK)(PSYMBOL_INFO pSymInfo, ULONG SymbolSize, PVOID UserContext);
__stdcall DWORD SymGetOptions(void);
__stdcall DWORD SymSetOptions(DWORD SymOptions __attribute__((in)));
__stdcall BOOL SymInitialize(HANDLE hProcess __attribute__((in)), PSTR UserSearchPath __attribute__((in)), BOOL fInvadeProcess __attribute__((in)));
__stdcall DWORD64 SymLoadModule64(HANDLE hProcess __attribute__((in)), HANDLE hFile __attribute__((in)), PSTR ImageName __attribute__((in)), PSTR ModuleName __attribute__((in)), DWORD64 BaseOfDll __attribute__((in)), DWORD SizeOfDll __attribute__((in)));
__stdcall BOOL SymSetSearchPath(HANDLE hProcess __attribute__((in)), PSTR SearchPathA __attribute__((in)));
__stdcall BOOL SymFromAddr(HANDLE hProcess __attribute__((in)), DWORD64 Address __attribute__((in)), PDWORD64 Displacement __attribute__((out)), PSYMBOL_INFO Symbol __attribute__((in)) __attribute__((out)));
__stdcall BOOL SymEnumSymbols(HANDLE hProcess __attribute__((in)), ULONG64 BaseOfDll __attribute__((in)), PCSTR Mask __attribute__((in)), PSYM_ENUMERATESYMBOLS_CALLBACK EnumSymbolsCallback __attribute__((in)), PVOID UserContext __attribute__((in)));
EOS
puts 'run_init'
l.run_init
puts 'sym_init'
dl.syminitialize(42, 0, 0)
puts 'sym_setopt'
dl.symsetoptions(dl.symgetoptions|dl::SYMOPT_DEFERRED_LOADS|dl::SYMOPT_NO_PROMPTS)
puts 'sym_setsearch'
sympath = ENV['_NT_SYMBOL_PATH'] || 'srv**/tmp/symbols*http://msdl.microsoft.com/download/symbols'
dl.symsetsearchpath(42, sympath)
puts 'sym_loadmod'
tg = PeLdr.new('kernel32.dll')
dl.symloadmodule64(42, 0, 0, 0, tg.load_address, 0)
puts 'walk'
symstruct = [0x58].pack('L') + 0.chr*4*19 + [512].pack('L') # sizeofstruct, ..., nameszmax
text = tg.pe.sections.find { |s| s.name == '.text' }
# SymEnumSymbols
text.rawsize.times { |o|
sym = symstruct + 0.chr*512 # name concat'ed after the struct
off = 0.chr*8
if dl.symfromaddr(42, tg.load_address+text.virtaddr+o, off, sym) and off.unpack('L').first == 0
symnamelen = sym[19*4, 4].unpack('L').first
puts ' %x %s' % [text.virtaddr+o, sym[0x54, symnamelen].inspect]
break
end
puts ' %x/%x' % [o, text.rawsize] if $VERBOSE and o & 0xffff == 0
}
puts
end