Class: PEdump::Unpacker::ASPack

Inherits:
Object
  • Object
show all
Defined in:
lib/pedump/unpacker/aspack.rb

Constant Summary collapse

DATA_ROOT =
File.dirname(File.dirname(File.dirname(File.dirname(__FILE__))))
UNLZX_SRC =
File.join(DATA_ROOT, "misc", "aspack", "aspack_unlzx.c")
UNLZXes =
[
  # default path for RVM
  File.join(DATA_ROOT, "misc", "aspack", "aspack_unlzx"),
  # XXX path for normal linux installs
  File.expand_path("~/.pedump_unlzx")
]
OBJ_TBL_CODE =
"  8D B5 (....)            lea     esi, [ebp+442A5Ah]  ; obj_tbl\n  83 3E 00                cmp     dword ptr [esi], 0\n  0F 84 . . 00 00         jz      no_obj_tbl\n  .{0,6}                  lea     esi, [ebp+442A5Ah]  ; obj_tbl\n  6A 04                   push    4\n  68 00 10 00 00          push    1000h\n  68 00 18 00 00          push    1800h\n  6A 00                   push    0\n  FF .{2,5}               call    dword ptr [ebp+4429B9h] ; [41503d]\n  89 85 ....              mov     [ebp+4429B5h], eax      ; [415039]\n  8B 46 04                mov     eax, [esi+4]\n"
VIRTUALPROTECT_RE =
code2re "  50                      push    eax\n  FF .{2,5}               call    dword ptr [ebp+6Ah] ; VirtualProtect\n  59                      pop     ecx\n  AD                      lodsd\n  AD                      lodsd\n  89 47 24                mov     [edi+24h], eax\n"
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
"  8B 06                   mov     eax, [esi]\n  EB (.)                  jmp     short ??                ; ModE8E9\n  80 3E (.)               cmp     byte ptr [esi], ??      ; CmpE8E9\n  75 F3                   jnz     short loc_450141\n  24 00                   and     al, 0\n  C1 C0 18                rol     eax, 18h\n  2B C3                   sub     eax, ebx\n  89 06                   mov     [esi], eax\n  83 C3 05                add     ebx, 5\n  83 C6 04                add     esi, 4\n  83 E9 05                sub     ecx, 5\n  EB                      jmp     short loc_450130\n"
E8_RE =
code2re E8_CODE
E8_FLAG_RE_IMM1 =
code2re "  B3 (.)                  mov     bl, ?\n  80 FB 00                cmp     bl, 0\n  75 .                    jnz     short loc_465163\n  FE 85 ....              inc     byte ptr [ebp+0ECh]\n  8B 3E                   mov     edi, [esi]\n  03 BD ....              add     edi, [ebp+422h]\n  FF 37                   push    dword ptr [edi]\n  C6 07 C3                mov     byte ptr [edi], 0C3h\n  FF D7                   call    edi\n  8F 07                   pop     dword ptr [edi]\n"
E8_FLAG_RE_IMM2 =
code2re "  B3 (.)                  mov     bl, 0\n  80 FB 00                cmp     bl, 0\n  75 .                    jnz     short loc_4C6155\n  FE 85 ....              inc     byte ptr [ebp+0EFh]\n  50                      push    eax\n  51                      push    ecx\n  56                      push    esi\n  53                      push    ebx\n  8B C8                   mov     ecx, eax\n"
E8_FLAG_RE_EBP =
code2re "  80 BD (....) 00         cmp     byte ptr [ebp+443A11h], 0\n  75 .                    jnz     short loc_465163\n  FE 85 ....              inc     byte ptr [ebp+0ECh]\n  8B 3E                   mov     edi, [esi]\n  03 BD ....              add     edi, [ebp+422h]\n  FF 37                   push    dword ptr [edi]\n  C6 07 C3                mov     byte ptr [edi], 0C3h\n  FF D7                   call    edi\n  8F 07                   pop     dword ptr [edi]\n"
OEP_CODE1 =
"  B8 (....)               mov     eax, 101Ah\n  50                      push    eax\n  03 85 ....              add     eax, [ebp+444A28h]\n  59                      pop     ecx\n  0B C9                   or      ecx, ecx\n  89 85 ....              mov     [ebp+443CF1h], eax\n  61                      popa\n  75 08                   jnz     short loc_40A3C0\n  B8 01 00 00 00          mov     eax, 1\n  C2 0C 00                retn    0Ch\n"
OEP_RE1 =
code2re OEP_CODE1
OEP_CODE2 =
"  8B 85 (....)            mov     eax, [ebp+442A4Eh]  ; 004150D2\n  50                      push    eax\n  03 85 ....              add     eax, [ebp+4437E0h]  ; [415e64] = self_base\n  59                      pop     ecx\n  0B C9                   or      ecx, ecx\n  89 85 ....              mov     [ebp+442E7Bh], eax  ; offset of '0' of 'push 0' after 'retn 0Ch'\n  61                      popa\n  75 08                   jnz     short loc_4154FE\n  B8 01 00 00 00          mov     eax, 1\n  C2 0C 00                retn    0Ch\n"
OEP_RE2 =
code2re OEP_CODE2
IMPORTS_CODE1 =
"  EB F1                   jmp ...\n  BE (....)               mov     esi, 55000h       ; immediate imports rva\n  8B 95 ....              mov     edx, [ebp+422h]\n  03 F2                   add     esi, edx\n  8B 46 0C                mov     eax, [esi+0Ch]\n  85 C0                   test    eax, eax\n  0F 84 . . 00 00         jz      ep_rva\n  03 C2                   add     eax, edx\n  8B D8                   mov     ebx, eax\n  50                      push    eax\n  FF 95 (....)            call    dword ptr [ebp+0F4Dh]\n  85 C0                   test    eax, eax\n"
IMPORTS_RE1 =
code2re IMPORTS_CODE1
IMPORTS_CODE2 =
"  EB F1                   jmp ...\n  8B B5 (....)            mov     esi, [ebp+442A4Ah]  ; [0x4150CE] = imports_rva\n  8B 95 ....              mov     edx, [ebp+4437E0h]  ; [0x415e64] = image_base\n  03 F2                   add     esi, edx\n  8B 46 0C                mov     eax, [esi+0Ch]\n  85 C0                   test    eax, eax\n  0F 84 . . 00 00         jz      ep_rva\n  03 C2                   add     eax, edx\n  8B D8                   mov     ebx, eax\n  50                      push    eax\n  FF 95 (....)            call    dword ptr [ebp+4438F4h] ; 415f78 = GetModuleHandleA\n  85 C0                   test    eax, eax\n"
IMPORTS_RE2 =
code2re IMPORTS_CODE2
RELOCS_RE =
code2re "  2B D0                   sub     edx, eax\n  74 79                   jz      short exit_relocs_loop\n  8B C2                   mov     eax, edx\n  C1 E8 10                shr     eax, 10h\n  33 DB                   xor     ebx, ebx\n  8B B5 (....)            mov     esi, [ebp+539h]         ; relocs_rva\n  03 B5 ....              add     esi, [ebp+422h]         ; image_base\n  83 3E 00                cmp     dword ptr [esi], 0\n  74                      jz      short exit_relocs_loop\n"
SECTION_INFO =
PEdump.create_struct 'VlV', :va, :size, :flags
@@xordetect_codes =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, params = {}) ⇒ ASPack

Returns a new instance of ASPack.



24
25
26
27
28
29
30
31
# File 'lib/pedump/unpacker/aspack.rb', line 24

def initialize io, params = {}
  params[:logger] ||= PEdump::Logger.create(params)
  @logger = params[:logger]
  @ldr = PEdump::Loader.new(io, params)
  @io = io

  @e8e9_mode = @e8e9_cmp = @e8e9_flag = @ebp = nil
end

Instance Attribute Details

#loggerObject

Returns the value of attribute logger.



22
23
24
# File 'lib/pedump/unpacker/aspack.rb', line 22

def logger
  @logger
end

Class Method Details

.code2re(code) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/pedump/unpacker/aspack.rb', line 46

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

.unpack(src_fname, dst_fname, log = '') ⇒ Object



10
11
12
13
14
15
16
17
18
19
# 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

Instance Method Details

#_decryptObject



318
319
320
321
322
323
324
# File 'lib/pedump/unpacker/aspack.rb', line 318

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) ⇒ Object



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/pedump/unpacker/aspack.rb', line 326

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_tblObject



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/pedump/unpacker/aspack.rb', line 471

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 = .new(*@ldr[va, record_size].unpack(::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) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/pedump/unpacker/aspack.rb', line 430

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) ⇒ Object



342
343
344
345
346
347
# File 'lib/pedump/unpacker/aspack.rb', line 342

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, &block) ⇒ Object



78
# File 'lib/pedump/unpacker/aspack.rb', line 78

def code2re code, &block; self.class.code2re(code, &block); end

#code2re_dw(code, shift = 0, mode = nil) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/pedump/unpacker/aspack.rb', line 80

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) ⇒ Object



698
699
700
701
702
703
704
705
706
# File 'lib/pedump/unpacker/aspack.rb', line 698

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) ⇒ Object



733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
# File 'lib/pedump/unpacker/aspack.rb', line 733

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

#decryptObject



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/pedump/unpacker/aspack.rb', line 349

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_e8e9Object



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/pedump/unpacker/aspack.rb', line 515

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_importsObject



569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# File 'lib/pedump/unpacker/aspack.rb', line 569

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_tblObject



535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/pedump/unpacker/aspack.rb', line 535

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_oepObject



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/pedump/unpacker/aspack.rb', line 551

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_relocsObject



600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/pedump/unpacker/aspack.rb', line 600

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 imports"
    raise
    return
  end
  @relocs_rva ||= @ldr[(@ebp + a[0]) & 0xffff_ffff, 4].unpack('V').first
  logger.info "[.] relocs RVA = %x" % @relocs_rva
end

#rebuild_importsObject



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/pedump/unpacker/aspack.rb', line 615

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_relocsObject



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/pedump/unpacker/aspack.rb', line 659

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 = {}) ⇒ Object



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/pedump/unpacker/aspack.rb', line 675

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_pathnameObject



708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File 'lib/pedump/unpacker/aspack.rb', line 708

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

#unpackObject



766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# File 'lib/pedump/unpacker/aspack.rb', line 766

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) ⇒ Object



723
724
725
726
727
728
729
730
731
# File 'lib/pedump/unpacker/aspack.rb', line 723

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

#xordetectObject

detects if code is crypted by a dword-xor



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/pedump/unpacker/aspack.rb', line 403

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