class PEdump::Unpacker::ASPack
Constants
- AddressOfEntryPoint
- DATA_ROOT
- E8_CODE
CODE1 = <<-EOC
8B 44 24 10 mov eax, [esp+arg_C] 81 EC 54 03 00 00 sub esp, 354h 8D 4C 24 04 lea ecx, [esp+354h+var_350] 50 push eax E8 A8 03 00 00 call sub_465A28 8B 8C 24 5C 03 00 00 mov ecx, [esp+354h+arg_4] 8B 94 24 58 03 00 00 mov edx, [esp+354h+arg_0] 51 push ecx 52 push edx 8D 4C 24 0C lea ecx, [esp+35Ch+var_350] E8 0D 04 00 00 call sub_465AA6 84 C0 test al, al 75 0A jnz short loc_4656A7 83 C8 FF or eax, 0FFFFFFFFh 81 C4 54 03 00 00 add esp, 354h C3 retn
EOC
- E8_FLAG_RE_EBP
- E8_FLAG_RE_IMM1
- E8_FLAG_RE_IMM2
- E8_RE
- IMPORTS_CODE1
- IMPORTS_CODE2
- IMPORTS_RE1
- IMPORTS_RE2
- OBJ_TBL_CODE
- OEP_CODE1
- OEP_CODE2
- OEP_RE1
- OEP_RE2
- RELOCS_RE
- SECTION_INFO
- UNLZX_SRC
- UNLZXes
- VIRTUALPROTECT_RE
Attributes
logger[RW]
Public Class Methods
code2re(code) { |x,idx| ... }
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 51 def self.code2re code idx = -1 was_any = false Regexp.new( code.strip. split("\n").map{|line| line.strip.split(' ',2).first}.join("\n"). gsub(/\.{2,}/){ |x| x.split('').join(' ') }. split.map do |x| idx += 1 case x when /\A[a-f0-9]{2}\Z/i x = x.to_i(16) if block_given? x = yield(x,idx) if x == :any was_any = true '.' else Regexp.escape(x.chr) end else Regexp.escape(x.chr) end else if was_any && (x.count('.') > 1 || x[/[+*?{}]/]) raise "[!] cannot use :any with more-than-1-char-long #{x.inspect}" end x end end.join, Regexp::MULTILINE ) end
new(io, params = {})
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 24 def initialize io, params = {} params[:logger] ||= PEdump::Logger.create(params) # XXX aspack unpacker code does not distinguish RVA from VA, so set # image base to zero for RVA be equal VA params[:image_base] ||= 0 @logger = params[:logger] @ldr = PEdump::Loader.new(io, params) @io = io @e8e9_mode = @e8e9_cmp = @e8e9_flag = @ebp = nil end
unpack(src_fname, dst_fname, log = '')
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 10 def self.unpack src_fname, dst_fname, log = '' File.open(src_fname, "rb") do |f| if ldr = new(f).unpack File.open(dst_fname,"wb"){ |fo| ldr.dump(fo) } return ldr # looks like 'true' else return false end end end
Public Instance Methods
_decrypt() { |ord,j| ... }
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 323 def _decrypt @data = @data.dup @data.size.times do |j| @data[j] = (yield(@data[j].ord,j)&0xff).chr end @data end
_decrypt_dw(shift=0) { |dw| ... }
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 331 def _decrypt_dw shift=0 orig_size = @data.size @data = @data.dup i = shift # FIXME: first 'shift' bytes of data is not decrypted! while i < @data.size t = @data[i,4] t<<"\x00" while t.size < 4 dw = t.unpack('V').first dw = yield(dw) @data[i,4] = [dw].pack('V') i += 4 end @data = @data[0,orig_size] if @data.size != orig_size @data end
_scan_obj_tbl()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 476 def _scan_obj_tbl unless @ebp logger.warn "[?] %s: EBP undefined, skipping" % __method__ return end re = code2re OBJ_TBL_CODE va = nil if m = @data.match(re) a = m[1..-1].map{|x| x.unpack('V').first } logger.debug "[d] OBJ_TBL_RE found at %4x : %s" % [m.begin(0), a.map{|x| x.to_s(16)}.join(', ')] va = (a[0] + @ebp) & 0xffff_ffff logger.debug "[.] obj_tbl VA = %4x (using EBP)" % va else logger.error "[!] cannot find obj_tbl" return end # obj_tbl contains flags if there is a call to VirtualProtect in loader code record_size = (@data['VirtualProtect'] && @data[VIRTUALPROTECT_RE]) ? 4*3 : 4*2 # @ldr[va-0x3c,0x3c].unpack('V*').each do |x| # printf("%8x\n",x); # end r = [] while true obj = SECTION_INFO.new(*@ldr[va, record_size].unpack(SECTION_INFO::FORMAT)) break if obj.va == 0 unless @ldr.va2section(obj.va) logger.error "[!] can't get section for obj %4x : %4x" % [obj.va, obj.size] end va += record_size r << obj if r.size > 0x200 logger.error "[!] stopped obj_tbl parsing. too many sections!" break end end r end
add_detect(known_bytes = [], step = 1)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 435 def add_detect known_bytes = [], step = 1 s = known_bytes.map{ |x| "%02x" % x}.join(' ') logger.info "[*] guessing DWORD-ADD key... [#{s}]" h = Hash.new{ |k,v| k[v] = 0 } dec = known_bytes.reverse.inject(0){ |x,y| (x<<8) + y} @@xordetect_codes.each do |code| 4.times do |shift| 0x100.times do |x1| #re = code2re_dw(code.tr('()',''),shift){ |x,idx| idx%4 == shift ? ((x-x1)&0xff) : :any } re = code2re_dw(code.tr('()',''),shift) do |x| [x-dec-(x1<<(known_bytes.size*8)), known_bytes.size+1] end @data.scan(re).each do logger.debug "[.] %02x: %2d : %s" % [x1, ($~.begin(0)+shift)%4, re.inspect[0,75]] h[x1] += 1 end end end end if h.any? known_bytes << h.sort_by(&:last).last[0] # most frequent byte end if known_bytes.size == step && step < 4 add_detect known_bytes, step+1 else kb = known_bytes case kb.size when 0 logger.debug "[?] %s: no matches" % __method__ when 1..3 logger.info "[?] %s: not 'add' or %d-byte key: %s" % [__method__, kb.size, kb.inspect] when 4 logger.info "[*] %s: FOUND 'add' key bytes: [%02x %02x %02x %02x]" % [__method__, *kb].flatten return known_bytes.reverse.inject(0){ |x,y| (x<<8) + y} else logger.info "[?] %s: %d possible bytes: %s" % [__method__, kb.size, kb.inspect] end return nil end end
check_re(data, comment = '', re = E8_RE)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 347 def check_re data, comment = '', re = E8_RE if m = data.match(re) logger.debug "[.] E8_RE %s found at %4x : %-20s" % [comment, m.begin(0), m[1..-1].inspect] m end end
code2re(code, █)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 83 def code2re code, █ self.class.code2re(code, &block); end
code2re_dw(code, shift=0, mode=nil) { |dw| ... }
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 85 def code2re_dw code, shift=0, mode=nil raise "shift must be in 0..3, got #{shift.inspect}" unless (0..3).include?(shift) Regexp.new( ( 'X '*shift + code.strip. split("\n").map{|line| line.strip.split(' ',2).first}.join("\n") ).gsub(/\.{2,}/){ |x| x.split('').join(' ') }. split.each_slice(4).map do |a| a.map! do |x| case x when /\A[a-f0-9]{2}\Z/i x.to_i(16) else x end end dw = a.reverse.inject(0){ |x,y| (x<<8) + (y.is_a?(Numeric) ? y : 0)} dw = yield(dw) if dw.is_a?(Array) # value + mask, mask = number of exact bytes in dw (dw[1]..[3,a.size-1].min).each{ |i| a[i] = '.' } dw = dw[0] end dw <<= 8 if mode == :add # ADD mode if a.all?{ |x| x.is_a?(Numeric)} # all bytes are known a.map do |x| dw >>= 8 Regexp::escape((dw & 0xff).chr) end else # some bytes are masked # => ALL bytes after FIRST MASKED byte should be masked too # due to carry flag when doing ADD or SUB was_mask = false a.map do |x| dw >>= 8 if x.is_a?(Numeric) was_mask ? '.' : Regexp::escape((dw & 0xff).chr) else was_mask = true x end end end else # generic mode, applicable for XOR a.map do |x| dw >>= 8 x.is_a?(Numeric) ? Regexp::escape((dw & 0xff).chr) : x end end end.join[shift..-1], Regexp::MULTILINE ) end
compile_unlzx(dest)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 703 def compile_unlzx dest logger.info "[*] compiling #{File.basename(dest)} .." system("gcc", UNLZX_SRC, "-o", dest) unless File.file?(dest) && File.executable?(dest) logger.fatal "[!] %s compile failed, please compile it yourself at %s" % [ File.basename(dest), File.dirname(dest) ] end end
decode_e8e9(data)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 738 def decode_e8e9 data return if !data || data.size < 6 return if [@e8e9_flag, @e8e9_mode, @e8e9_cmp].any?(&:nil?) return if @e8e9_flag != 0 size = data.size - 6 offs = 0 while size > 0 b0 = data[offs] if b0 != "\xE8" && b0 != "\xE9" size-=1; offs+=1 next end dw = data[offs+1,4].unpack('V').first if @e8e9_mode == 0 if (dw & 0xff) != @e8e9_cmp size-=1; offs+=1 next end # dw &= 0xffffff00; dw = ROL(dw, 24) dw >>= 8 end t = (dw-offs) & 0xffffffff # keep value in 32 bits #logger.debug "[d] data[%6x] = %8x" % [offs+1, t] data[offs+1,4] = [t].pack('V') offs += 5; size -= [size, 5].min end end
decrypt()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 354 def decrypt r=nil # check raw return r if r=check_re(@data) (1..255).each do |i| # check byte add if check_re(@data, "[add b,#{i}]", code2re(E8_CODE){ |x| (x+i)&0xff }) return check_re(_decrypt{|x| x-i}) end # check byte xor if check_re(@data, "[xor b,#{i}]", code2re(E8_CODE){ |x| x^i }) return check_re(_decrypt{|x| x^i}) end end # check dword dec 4.times do |shift| re = code2re_dw(E8_CODE,shift){ |dw| dw+1 } if r=check_re(@data, "[dec dw:#{shift}]", re) shift = (r.begin(0)-shift)%4 return check_re(_decrypt_dw(shift){ |x| x-1 }) end end # detect dword xor h = xordetect if h && h.size == 4 h.keys.permutation.each do |xor_bytes| xor_dw = xor_bytes.inject{ |x,y| (x<<8) + y} re = code2re_dw(E8_CODE){ |dw| dw^xor_dw } if r=check_re(@data, "[xor dw,#{xor_dw.to_s(16)}]", re) return check_re(_decrypt_dw(r.begin(0)%4){ |dw| dw^xor_dw }) end end end # detect dword add if add_dw = add_detect 4.times do |shift| re = code2re_dw(E8_CODE,shift, :add){ |dw| dw-add_dw } if r=check_re(@data, "[add dw:#{shift},#{add_dw.to_s(16)}]", re) return check_re(_decrypt_dw((r.begin(0)+shift)%4){ |dw| dw+add_dw }) end end end # failed false end
find_e8e9()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 520 def find_e8e9 if m = @data.match(E8_RE) @e8e9_mode, @e8e9_cmp = m[1].ord, m[2].ord else logger.error "[!] can't find E8/E9 patch sub! unpacked code may be invalid!" end if m = (@data.match(E8_FLAG_RE_IMM1) || @data.match(E8_FLAG_RE_IMM2)) @e8e9_flag = m[1].ord elsif m = @data.match(E8_FLAG_RE_EBP) offset = m[1].unpack('V').first @e8e9_flag = @ldr[(@ebp + offset) & 0xffff_ffff, 1].ord else logger.error "[!] can't find E8/E9 flag! unpacked code may be invalid!" raise end logger.debug "[.] E8/E9: flag=%s, mode=%s, cmp=%s" % [@e8e9_flag||'???', @e8e9_mode, @e8e9_cmp] end
find_imports()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 574 def find_imports @imports_rva = nil if m = @data.match(IMPORTS_RE1) a = m[1..-1].map{|x| x.unpack('V').first } @imports_rva = a[0] elsif m = @data.match(IMPORTS_RE2) a = m[1..-1].map{|x| x.unpack('V').first } else logger.error "[!] cannot find imports" return end logger.debug "[d] IMPORTS_REx found at %4x : %s" % [m.begin(0), a.map{|x| x.to_s(16)}.join(', ')] # actually following code is not necessary for IMPORTS_RE1 # using it to get EBP register value f = @ldr.pedump.imports.map(&:first_thunk).flatten.compact.find{ |x| x.name == "GetModuleHandleA"} unless f logger.error "[!] GetModuleHandleA not found" return end vaGetModuleHandle = f.va logger.debug "[d] GetModuleHandle is at %x" % vaGetModuleHandle @ebp = (f.va - a[1]) & 0xffff_ffff logger.debug "[d] assume EBP = %x" % @ebp # @imports_rva may already be filled by IMPORTS_RE1 @imports_rva ||= @data[(@ebp + a[0] - @section.va) & 0xffff_ffff, 4].unpack('V').first logger.info "[.] imports RVA = %x" % @imports_rva end
find_obj_tbl()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 540 def find_obj_tbl if @obj_tbl = _scan_obj_tbl if logger.level <= ::Logger::INFO @obj_tbl.each do |obj| if obj.flags logger.info "[.] ASP::SECTION va: %8x size: %8x flags: %8x" % [ obj.va, obj.size&0xffff_ffff, obj.flags] else logger.info "[.] ASP::SECTION va: %8x size: %8x" % [ obj.va, obj.size&0xffff_ffff] end end end end end
find_oep()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 556 def find_oep @oep = nil if m = @data.match(OEP_RE1) logger.debug "[.] OEP_RE1 found at %4x" % m.begin(0) @oep = m[1].unpack('V').first elsif @ebp && m = @data.match(OEP_RE2) logger.debug "[.] OEP_RE2 found at %4x (using EBP)" % m.begin(0) offset = m[1].unpack('V').first @oep = @ldr[(@ebp + offset) & 0xffff_ffff, 4].unpack('V').first end if @oep logger.info "[.] OEP = %8x" % @oep else logger.error "[!] cannot find EntryPoint" end end
find_relocs()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 605 def find_relocs @relocs_rva = nil if m = @data.match(RELOCS_RE) a = m[1..-1].map{|x| x.unpack('V').first } else logger.error "[!] cannot find relocs" raise return end @relocs_rva ||= @ldr[(@ebp + a[0]) & 0xffff_ffff, 4].unpack('V').first logger.info "[.] relocs RVA = %x" % @relocs_rva end
rebuild_imports()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 620 def rebuild_imports return unless @imports_rva iids = [] va = @imports_rva sz = PEdump::IMAGE_IMPORT_DESCRIPTOR::SIZE while true iid = PEdump::IMAGE_IMPORT_DESCRIPTOR.read(@ldr[va,sz]) va += sz # increase ptr before breaking, req'd 4 saving total import table size in data dir break if iid.Name.to_i == 0 [:original_first_thunk, :first_thunk].each do |tbl| camel = tbl.capitalize.to_s.gsub(/_./){ |char| char[1..-1].upcase} iid[tbl] ||= [] if (va1 = iid[camel].to_i) != 0 while true # intentionally include zero terminator in table to count IAT size t = @ldr[va1,4].unpack('V').first iid[tbl] << t break if t == 0 va1 += 4 end end end iids << iid end @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IMPORT].tap do |dd| dd.va = @imports_rva dd.size = va-@imports_rva end if iids.any? iids.sort_by!(&:FirstThunk) @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IAT].tap do |dd| # Points to the beginning of the first Import Address Table (IAT). dd.va = iids.first.FirstThunk # The Size field indicates the total size of all the IATs. dd.size = iids.last.FirstThunk - iids.first.FirstThunk + iids.last.first_thunk.size*4 # ... to temporarily mark the IATs as read-write during import resolution. # http://msdn.microsoft.com/en-us/magazine/bb985997.aspx end end end
rebuild_relocs()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 664 def rebuild_relocs return if @relocs_rva.to_i == 0 va = @relocs_rva while true a = @ldr[va,4*2].to_s.unpack('V*') break if a[0] == 0 || a[1] == 0 va += a[1] end @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::BASERELOC].tap do |dd| dd.va = @relocs_rva dd.size = va-@relocs_rva end end
rebuild_tls(h = {})
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 680 def rebuild_tls h = {} dd = @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::TLS] return if dd.va.to_i == 0 || dd.size.to_i == 0 case h[:step] when 1 # store @tls_data @tls_data = @ldr[dd.va, dd.size] when 2 # search in unpacked sections return unless @tls_data if h[:step] == 2 # search for original TLS data in all unpacked sections @ldr.sections.each do |section| if offset = section.data.index(@tls_data) # found a TLS section dd.va = section.va + offset return end end logger.error "[!] can't find TLS section" else raise "invalid step" end end
unlzx_pathname()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 713 def unlzx_pathname UNLZXes.each do |unlzx| return unlzx if File.file?(unlzx) && File.executable?(unlzx) end # nothing found, try to compile UNLZXes.each do |unlzx| compile_unlzx unlzx return unlzx if File.file?(unlzx) && File.executable?(unlzx) end # all compiles failed raise "no aspack_unlzx binary" end
unpack()
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 771 def unpack if @section = @ldr.va2section(@ldr.ep) @data = @section.data logger.debug "[.] EP section: #{@section.inspect}" else logger.fatal "[!] cannot determine EP section" return end decrypt # must be called before any other finds find_imports # also fills @ebp for other finds find_e8e9 find_obj_tbl find_oep find_relocs ### rebuild_tls :step => 1 sorted_obj_tbl = @obj_tbl.sort_by{ |x| @ldr.pedump.va2file(x.va) } sorted_obj_tbl.each_with_index do |obj,idx| # restore section flags, if any @ldr.va2section(obj.va).flags = obj.flags if obj.flags next if obj.size < 0 # empty section #file_offset = @ldr.pedump.va2file(obj.va) #@io.seek file_offset packed_size = if idx == sorted_obj_tbl.size - 1 # last obj obj.size else # subtract this file_offset from next object file_offset @ldr.pedump.va2file(sorted_obj_tbl[idx+1].va) - @ldr.pedump.va2file(obj.va) end #packed_data = @io.read packed_size packed_data = @ldr[obj.va, packed_size] unpacked_data = unpack_section(packed_data, packed_data.size, obj.size).force_encoding('binary') # decode e8/e9 only on 1st section? decode_e8e9(unpacked_data) if obj == @obj_tbl.first @ldr[obj.va, unpacked_data.size] = unpacked_data logger.debug "[.] %8x: %8x -> %8x" % [obj.va, packed_size, unpacked_data.size] end rebuild_imports rebuild_relocs rebuild_tls :step => 2 @ldr.pe_hdr.ioh.AddressOfEntryPoint = @oep.to_i @ldr end
unpack_section(data, packed_size, unpacked_size)
click to toggle source
# File lib/pedump/unpacker/aspack.rb, line 728 def unpack_section data, packed_size, unpacked_size data = IO.popen("#{unlzx_pathname} #{packed_size.to_i} #{unpacked_size.to_i}","r+") do |f| f.write data f.close_write f.read end raise $?.inspect unless $?.success? data end
xordetect()
click to toggle source
detects if code is crypted by a dword-xor @data must be original, not modified!
# File lib/pedump/unpacker/aspack.rb, line 408 def xordetect logger.info "[*] guessing DWORD-XOR key..." h = Hash.new{ |k,v| k[v] = 0 } @@xordetect_codes.each do |code| 4.times do |shift| 0x100.times do |x1| re = code2re(code.tr('()','')){ |x,idx| idx%4 == shift ? x^x1 : :any } @data.scan(re).each do logger.debug "[.] %02x: %2d : %s" % [x1, ($~.begin(0)+shift)%4, re.inspect] h[x1] += 1 end end end end case h.size when 0 logger.debug "[?] %s: no matches" % __method__ when 1..3 logger.info "[?] %s: not xored, or %d-byte xor key: %s" % [__method__, h.size, h.inspect] when 4 logger.info "[*] %s: FOUND xor key bytes: [%02x %02x %02x %02x]" % [__method__, *h.keys].flatten else logger.info "[?] %s: %d possible bytes: %s" % [__method__, h.size, h.inspect] end h end