pedump 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ require 'awesome_print' # for colored tty logging
2
+
3
+ class PEdump
4
+ class Logger < ::Logger
5
+ def initialize *args
6
+ super
7
+ @formatter = proc do |severity,_,_,msg|
8
+ # quick and dirty way to remove duplicate messages
9
+ if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
10
+ ''
11
+ else
12
+ @prevmsg = msg
13
+ "#{msg}\n"
14
+ end
15
+ end
16
+ @level = WARN
17
+ end
18
+ end
19
+
20
+ def Logger.create params
21
+ logger =
22
+ if params[:logger]
23
+ params[:logger]
24
+ else
25
+ logdev = params[:logdev] || STDERR
26
+ logger_class =
27
+ if params.key?(:color)
28
+ # forced color or not
29
+ params[:color] ? ColoredLogger : Logger
30
+ else
31
+ # set color if logdev is TTY
32
+ (logdev.respond_to?(:tty?) && logdev.tty?) ? ColoredLogger : Logger
33
+ end
34
+ logger_class.new(logdev)
35
+ end
36
+
37
+ logger.level = params[:log_level] if params[:log_level]
38
+ logger
39
+ end
40
+
41
+ class ColoredLogger < ::Logger
42
+ def initialize *args
43
+ super
44
+ @formatter = proc do |severity,_,_,msg|
45
+ # quick and dirty way to remove duplicate messages
46
+ if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
47
+ ''
48
+ else
49
+ @prevmsg = msg
50
+ color =
51
+ case severity
52
+ when 'FATAL'
53
+ :redish
54
+ when 'ERROR'
55
+ :red
56
+ when 'WARN'
57
+ :yellowish
58
+ when 'DEBUG'
59
+ :gray
60
+ end
61
+ "#{color ? msg.send(color) : msg}\n"
62
+ end
63
+ end
64
+ @level = WARN
65
+ end
66
+ end
67
+ end
@@ -18,6 +18,9 @@ class PEdump
18
18
  ifh && ifh.flags.include?('DLL')
19
19
  end
20
20
 
21
+ def pack
22
+ signature + ifh.pack + ioh.pack
23
+ end
21
24
 
22
25
  def self.read f, args = nil
23
26
  force = args.is_a?(Hash) && args[:force]
@@ -1,6 +1,6 @@
1
1
  class PEdump
2
2
 
3
- def resource_directory f=nil
3
+ def resource_directory f=@io
4
4
  @resource_directory ||= _read_resource_directory_tree(f)
5
5
  end
6
6
 
@@ -181,7 +181,7 @@ class PEdump
181
181
 
182
182
  STRING = Struct.new(:id, :lang, :value)
183
183
 
184
- def strings f=nil
184
+ def strings f=@io
185
185
  r = []
186
186
  Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
187
187
  res.data.each_with_index do |string,idx|
@@ -319,7 +319,7 @@ class PEdump
319
319
  end
320
320
  end
321
321
 
322
- def _scan_resources f=nil, dir=nil
322
+ def _scan_resources f=@io, dir=nil
323
323
  dir ||= resource_directory(f)
324
324
  return nil unless dir
325
325
  dir.entries.map do |entry|
@@ -0,0 +1,26 @@
1
+ require 'pedump'
2
+ require 'pedump/unpacker/aspack'
3
+ require 'pedump/unpacker/upx'
4
+
5
+ module PEdump::Unpacker
6
+ class << self
7
+ def find io
8
+ if io.is_a?(String)
9
+ return File.open(io,"rb"){ |f| find(f) }
10
+ end
11
+
12
+ pedump = PEdump.new(io)
13
+ packer = Array(pedump.packers).first
14
+ return nil unless packer
15
+
16
+ case packer.name
17
+ when /UPX/
18
+ UPX
19
+ when /ASPack/i
20
+ ASPack
21
+ else
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,853 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: binary
3
+ require 'pedump/loader'
4
+ require 'pedump/cli'
5
+
6
+ module PEdump::Unpacker; end
7
+
8
+ class PEdump::Unpacker::ASPack
9
+
10
+ def self.unpack src_fname, dst_fname, log = ''
11
+ File.open(src_fname, "rb") do |f|
12
+ if ldr = new(f).unpack
13
+ File.open(dst_fname,"wb"){ |fo| ldr.dump(fo) }
14
+ return ldr # looks like 'true'
15
+ else
16
+ return false
17
+ end
18
+ end
19
+ end
20
+
21
+ ########################################################################
22
+ attr_accessor :logger
23
+
24
+ def initialize io, params = {}
25
+ params[:logger] ||= PEdump::Logger.create(params)
26
+ @logger = params[:logger]
27
+ @ldr = PEdump::Loader.new(io, params)
28
+ @io = io
29
+
30
+ @e8e9_mode = @e8e9_cmp = @e8e9_flag = @ebp = nil
31
+ end
32
+
33
+ ########################################################################
34
+
35
+ DATA_ROOT = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__))))
36
+ UNLZX_SRC = File.join(DATA_ROOT, "misc", "aspack", "aspack_unlzx.c")
37
+ UNLZXes = [
38
+ # default path for RVM
39
+ File.join(DATA_ROOT, "misc", "aspack", "aspack_unlzx"),
40
+ # XXX path for normal linux installs
41
+ File.expand_path("~/.pedump_unlzx")
42
+ ]
43
+
44
+ ########################################################################
45
+
46
+ def self.code2re code
47
+ idx = -1
48
+ was_any = false
49
+ Regexp.new(
50
+ code.strip.
51
+ split("\n").map{|line| line.strip.split(' ',2).first}.join("\n").
52
+ gsub(/\.{2,}/){ |x| x.split('').join(' ') }.
53
+ split.map do |x|
54
+ idx += 1
55
+ case x
56
+ when /\A[a-f0-9]{2}\Z/i
57
+ x = x.to_i(16)
58
+ if block_given?
59
+ x = yield(x,idx)
60
+ if x == :any
61
+ was_any = true
62
+ '.'
63
+ else
64
+ Regexp.escape(x.chr)
65
+ end
66
+ else
67
+ Regexp.escape(x.chr)
68
+ end
69
+ else
70
+ if was_any && (x.count('.') > 1 || x[/[+*?{}]/])
71
+ raise "[!] cannot use :any with more-than-1-char-long #{x.inspect}"
72
+ end
73
+ x
74
+ end
75
+ end.join, Regexp::MULTILINE
76
+ )
77
+ end
78
+ def code2re code, &block; self.class.code2re(code, &block); end
79
+
80
+ def code2re_dw code, shift=0, mode=nil
81
+ raise "shift must be in 0..3, got #{shift.inspect}" unless (0..3).include?(shift)
82
+ Regexp.new(
83
+ (
84
+ 'X '*shift +
85
+ code.strip.
86
+ split("\n").map{|line| line.strip.split(' ',2).first}.join("\n")
87
+ ).gsub(/\.{2,}/){ |x| x.split('').join(' ') }.
88
+ split.each_slice(4).map do |a|
89
+ a.map! do |x|
90
+ case x
91
+ when /\A[a-f0-9]{2}\Z/i
92
+ x.to_i(16)
93
+ else
94
+ x
95
+ end
96
+ end
97
+ dw = a.reverse.inject(0){ |x,y| (x<<8) + (y.is_a?(Numeric) ? y : 0)}
98
+ dw = yield(dw)
99
+ if dw.is_a?(Array)
100
+ # value + mask, mask = number of exact bytes in dw
101
+ (dw[1]..[3,a.size-1].min).each{ |i| a[i] = '.' }
102
+ dw = dw[0]
103
+ end
104
+ dw <<= 8
105
+
106
+ if mode == :add
107
+ # ADD mode
108
+ if a.all?{ |x| x.is_a?(Numeric)}
109
+ # all bytes are known
110
+ a.map do |x|
111
+ dw >>= 8
112
+ Regexp::escape((dw & 0xff).chr)
113
+ end
114
+ else
115
+ # some bytes are masked
116
+ # => ALL bytes after FIRST MASKED byte should be masked too
117
+ # due to carry flag when doing ADD or SUB
118
+ was_mask = false
119
+ a.map do |x|
120
+ dw >>= 8
121
+ if x.is_a?(Numeric)
122
+ was_mask ? '.' : Regexp::escape((dw & 0xff).chr)
123
+ else
124
+ was_mask = true
125
+ x
126
+ end
127
+ end
128
+ end
129
+ else
130
+ # generic mode, applicable for XOR
131
+ a.map do |x|
132
+ dw >>= 8
133
+ x.is_a?(Numeric) ? Regexp::escape((dw & 0xff).chr) : x
134
+ end
135
+ end
136
+ end.join[shift..-1], Regexp::MULTILINE
137
+ )
138
+ end
139
+
140
+ @@xordetect_codes = []
141
+
142
+ OBJ_TBL_CODE = <<-EOC
143
+ 8D B5 (....) lea esi, [ebp+442A5Ah] ; obj_tbl
144
+ 83 3E 00 cmp dword ptr [esi], 0
145
+ 0F 84 . . 00 00 jz no_obj_tbl
146
+ .{0,6} lea esi, [ebp+442A5Ah] ; obj_tbl
147
+ 6A 04 push 4
148
+ 68 00 10 00 00 push 1000h
149
+ 68 00 18 00 00 push 1800h
150
+ 6A 00 push 0
151
+ FF .{2,5} call dword ptr [ebp+4429B9h] ; [41503d]
152
+ 89 85 .... mov [ebp+4429B5h], eax ; [415039]
153
+ 8B 46 04 mov eax, [esi+4]
154
+ EOC
155
+
156
+ VIRTUALPROTECT_RE = code2re <<-EOC
157
+ 50 push eax
158
+ FF .{2,5} call dword ptr [ebp+6Ah] ; VirtualProtect
159
+ 59 pop ecx
160
+ AD lodsd
161
+ AD lodsd
162
+ 89 47 24 mov [edi+24h], eax
163
+ EOC
164
+
165
+ # CODE1 = <<-EOC
166
+ # 8B 44 24 10 mov eax, [esp+arg_C]
167
+ # 81 EC 54 03 00 00 sub esp, 354h
168
+ # 8D 4C 24 04 lea ecx, [esp+354h+var_350]
169
+ # 50 push eax
170
+ # E8 A8 03 00 00 call sub_465A28
171
+ # 8B 8C 24 5C 03 00 00 mov ecx, [esp+354h+arg_4]
172
+ # 8B 94 24 58 03 00 00 mov edx, [esp+354h+arg_0]
173
+ # 51 push ecx
174
+ # 52 push edx
175
+ # 8D 4C 24 0C lea ecx, [esp+35Ch+var_350]
176
+ # E8 0D 04 00 00 call sub_465AA6
177
+ # 84 C0 test al, al
178
+ # 75 0A jnz short loc_4656A7
179
+ # 83 C8 FF or eax, 0FFFFFFFFh
180
+ # 81 C4 54 03 00 00 add esp, 354h
181
+ # C3 retn
182
+ # EOC
183
+
184
+ E8_CODE = <<-EOC
185
+ 8B 06 mov eax, [esi]
186
+ EB (.) jmp short ?? ; ModE8E9
187
+ 80 3E (.) cmp byte ptr [esi], ?? ; CmpE8E9
188
+ 75 F3 jnz short loc_450141
189
+ 24 00 and al, 0
190
+ C1 C0 18 rol eax, 18h
191
+ 2B C3 sub eax, ebx
192
+ 89 06 mov [esi], eax
193
+ 83 C3 05 add ebx, 5
194
+ 83 C6 04 add esi, 4
195
+ 83 E9 05 sub ecx, 5
196
+ EB jmp short loc_450130
197
+ EOC
198
+ E8_RE = code2re E8_CODE
199
+ @@xordetect_codes << E8_CODE
200
+
201
+ E8_FLAG_RE_IMM1 = code2re <<-EOC
202
+ B3 (.) mov bl, ?
203
+ 80 FB 00 cmp bl, 0
204
+ 75 . jnz short loc_465163
205
+ FE 85 .... inc byte ptr [ebp+0ECh]
206
+ 8B 3E mov edi, [esi]
207
+ 03 BD .... add edi, [ebp+422h]
208
+ FF 37 push dword ptr [edi]
209
+ C6 07 C3 mov byte ptr [edi], 0C3h
210
+ FF D7 call edi
211
+ 8F 07 pop dword ptr [edi]
212
+ EOC
213
+
214
+ E8_FLAG_RE_IMM2 = code2re <<-EOC
215
+ B3 (.) mov bl, 0
216
+ 80 FB 00 cmp bl, 0
217
+ 75 . jnz short loc_4C6155
218
+ FE 85 .... inc byte ptr [ebp+0EFh]
219
+ 50 push eax
220
+ 51 push ecx
221
+ 56 push esi
222
+ 53 push ebx
223
+ 8B C8 mov ecx, eax
224
+ EOC
225
+
226
+ E8_FLAG_RE_EBP = code2re <<-EOC
227
+ 80 BD (....) 00 cmp byte ptr [ebp+443A11h], 0
228
+ 75 . jnz short loc_465163
229
+ FE 85 .... inc byte ptr [ebp+0ECh]
230
+ 8B 3E mov edi, [esi]
231
+ 03 BD .... add edi, [ebp+422h]
232
+ FF 37 push dword ptr [edi]
233
+ C6 07 C3 mov byte ptr [edi], 0C3h
234
+ FF D7 call edi
235
+ 8F 07 pop dword ptr [edi]
236
+ EOC
237
+
238
+ OEP_CODE1 = <<-EOC
239
+ B8 (....) mov eax, 101Ah
240
+ 50 push eax
241
+ 03 85 .... add eax, [ebp+444A28h]
242
+ 59 pop ecx
243
+ 0B C9 or ecx, ecx
244
+ 89 85 .... mov [ebp+443CF1h], eax
245
+ 61 popa
246
+ 75 08 jnz short loc_40A3C0
247
+ B8 01 00 00 00 mov eax, 1
248
+ C2 0C 00 retn 0Ch
249
+ EOC
250
+ OEP_RE1 = code2re OEP_CODE1
251
+ @@xordetect_codes << OEP_CODE1
252
+
253
+ OEP_CODE2 = <<-EOC
254
+ 8B 85 (....) mov eax, [ebp+442A4Eh] ; 004150D2
255
+ 50 push eax
256
+ 03 85 .... add eax, [ebp+4437E0h] ; [415e64] = self_base
257
+ 59 pop ecx
258
+ 0B C9 or ecx, ecx
259
+ 89 85 .... mov [ebp+442E7Bh], eax ; offset of '0' of 'push 0' after 'retn 0Ch'
260
+ 61 popa
261
+ 75 08 jnz short loc_4154FE
262
+ B8 01 00 00 00 mov eax, 1
263
+ C2 0C 00 retn 0Ch
264
+ EOC
265
+ OEP_RE2 = code2re OEP_CODE2
266
+ @@xordetect_codes << OEP_CODE2
267
+
268
+ IMPORTS_CODE1 = <<-EOC
269
+ EB F1 jmp ...
270
+ BE (....) mov esi, 55000h ; immediate imports rva
271
+ 8B 95 .... mov edx, [ebp+422h]
272
+ 03 F2 add esi, edx
273
+ 8B 46 0C mov eax, [esi+0Ch]
274
+ 85 C0 test eax, eax
275
+ 0F 84 . . 00 00 jz ep_rva
276
+ 03 C2 add eax, edx
277
+ 8B D8 mov ebx, eax
278
+ 50 push eax
279
+ FF 95 (....) call dword ptr [ebp+0F4Dh]
280
+ 85 C0 test eax, eax
281
+ EOC
282
+ IMPORTS_RE1 = code2re IMPORTS_CODE1
283
+ @@xordetect_codes << IMPORTS_CODE1
284
+
285
+ IMPORTS_CODE2 = <<-EOC
286
+ EB F1 jmp ...
287
+ 8B B5 (....) mov esi, [ebp+442A4Ah] ; [0x4150CE] = imports_rva
288
+ 8B 95 .... mov edx, [ebp+4437E0h] ; [0x415e64] = image_base
289
+ 03 F2 add esi, edx
290
+ 8B 46 0C mov eax, [esi+0Ch]
291
+ 85 C0 test eax, eax
292
+ 0F 84 . . 00 00 jz ep_rva
293
+ 03 C2 add eax, edx
294
+ 8B D8 mov ebx, eax
295
+ 50 push eax
296
+ FF 95 (....) call dword ptr [ebp+4438F4h] ; 415f78 = GetModuleHandleA
297
+ 85 C0 test eax, eax
298
+ EOC
299
+ IMPORTS_RE2 = code2re IMPORTS_CODE2
300
+ @@xordetect_codes << IMPORTS_CODE2
301
+
302
+ RELOCS_RE = code2re <<-EOC
303
+ 2B D0 sub edx, eax
304
+ 74 79 jz short exit_relocs_loop
305
+ 8B C2 mov eax, edx
306
+ C1 E8 10 shr eax, 10h
307
+ 33 DB xor ebx, ebx
308
+ 8B B5 (....) mov esi, [ebp+539h] ; relocs_rva
309
+ 03 B5 .... add esi, [ebp+422h] ; image_base
310
+ 83 3E 00 cmp dword ptr [esi], 0
311
+ 74 jz short exit_relocs_loop
312
+ EOC
313
+
314
+ SECTION_INFO = PEdump.create_struct 'VlV', :va, :size, :flags
315
+
316
+ ########################################################################
317
+
318
+ def _decrypt
319
+ @data = @data.dup
320
+ @data.size.times do |j|
321
+ @data[j] = (yield(@data[j].ord,j)&0xff).chr
322
+ end
323
+ @data
324
+ end
325
+
326
+ def _decrypt_dw shift=0
327
+ orig_size = @data.size
328
+ @data = @data.dup
329
+ i = shift # FIXME: first 'shift' bytes of data is not decrypted!
330
+ while i < @data.size
331
+ t = @data[i,4]
332
+ t<<"\x00" while t.size < 4
333
+ dw = t.unpack('V').first
334
+ dw = yield(dw)
335
+ @data[i,4] = [dw].pack('V')
336
+ i += 4
337
+ end
338
+ @data = @data[0,orig_size] if @data.size != orig_size
339
+ @data
340
+ end
341
+
342
+ def check_re data, comment = '', re = E8_RE
343
+ if m = data.match(re)
344
+ logger.debug "[.] E8_RE %s found at %4x : %-20s" % [comment, m.begin(0), m[1..-1].inspect]
345
+ m
346
+ end
347
+ end
348
+
349
+ def decrypt
350
+ r=nil
351
+ # check raw
352
+ return r if r=check_re(@data)
353
+
354
+ (1..255).each do |i|
355
+ # check byte add
356
+ if check_re(@data, "[add b,#{i}]", code2re(E8_CODE){ |x| (x+i)&0xff })
357
+ return check_re(_decrypt{|x| x-i})
358
+ end
359
+
360
+ # check byte xor
361
+ if check_re(@data, "[xor b,#{i}]", code2re(E8_CODE){ |x| x^i })
362
+ return check_re(_decrypt{|x| x^i})
363
+ end
364
+ end
365
+
366
+ # check dword dec
367
+ 4.times do |shift|
368
+ re = code2re_dw(E8_CODE,shift){ |dw| dw+1 }
369
+ if r=check_re(@data, "[dec dw:#{shift}]", re)
370
+ shift = (r.begin(0)-shift)%4
371
+ return check_re(_decrypt_dw(shift){ |x| x-1 })
372
+ end
373
+ end
374
+
375
+ # detect dword xor
376
+ h = xordetect
377
+ if h && h.size == 4
378
+ h.keys.permutation.each do |xor_bytes|
379
+ xor_dw = xor_bytes.inject{ |x,y| (x<<8) + y}
380
+ re = code2re_dw(E8_CODE){ |dw| dw^xor_dw }
381
+ if r=check_re(@data, "[xor dw,#{xor_dw.to_s(16)}]", re)
382
+ return check_re(_decrypt_dw(r.begin(0)%4){ |dw| dw^xor_dw })
383
+ end
384
+ end
385
+ end
386
+
387
+ # detect dword add
388
+ if add_dw = add_detect
389
+ 4.times do |shift|
390
+ re = code2re_dw(E8_CODE,shift, :add){ |dw| dw-add_dw }
391
+ if r=check_re(@data, "[add dw:#{shift},#{add_dw.to_s(16)}]", re)
392
+ return check_re(_decrypt_dw((r.begin(0)+shift)%4){ |dw| dw+add_dw })
393
+ end
394
+ end
395
+ end
396
+
397
+ # failed
398
+ false
399
+ end
400
+
401
+ # detects if code is crypted by a dword-xor
402
+ # @data must be original, not modified!
403
+ def xordetect
404
+ logger.info "[*] guessing DWORD-XOR key..."
405
+ h = Hash.new{ |k,v| k[v] = 0 }
406
+ @@xordetect_codes.each do |code|
407
+ 4.times do |shift|
408
+ 0x100.times do |x1|
409
+ re = code2re(code.tr('()','')){ |x,idx| idx%4 == shift ? x^x1 : :any }
410
+ @data.scan(re).each do
411
+ logger.debug "[.] %02x: %2d : %s" % [x1, ($~.begin(0)+shift)%4, re.inspect]
412
+ h[x1] += 1
413
+ end
414
+ end
415
+ end
416
+ end
417
+ case h.size
418
+ when 0
419
+ logger.debug "[?] %s: no matches" % __method__
420
+ when 1..3
421
+ logger.info "[?] %s: not xored, or %d-byte xor key: %s" % [__method__, h.size, h.inspect]
422
+ when 4
423
+ logger.info "[*] %s: FOUND xor key bytes: [%02x %02x %02x %02x]" % [__method__, *h.keys].flatten
424
+ else
425
+ logger.info "[?] %s: %d possible bytes: %s" % [__method__, h.size, h.inspect]
426
+ end
427
+ h
428
+ end
429
+
430
+ def add_detect known_bytes = [], step = 1
431
+ s = known_bytes.map{ |x| "%02x" % x}.join(' ')
432
+ logger.info "[*] guessing DWORD-ADD key... [#{s}]"
433
+ h = Hash.new{ |k,v| k[v] = 0 }
434
+ dec = known_bytes.reverse.inject(0){ |x,y| (x<<8) + y}
435
+ @@xordetect_codes.each do |code|
436
+ 4.times do |shift|
437
+ 0x100.times do |x1|
438
+ #re = code2re_dw(code.tr('()',''),shift){ |x,idx| idx%4 == shift ? ((x-x1)&0xff) : :any }
439
+ re = code2re_dw(code.tr('()',''),shift) do |x|
440
+ [x-dec-(x1<<(known_bytes.size*8)), known_bytes.size+1]
441
+ end
442
+ @data.scan(re).each do
443
+ logger.debug "[.] %02x: %2d : %s" % [x1, ($~.begin(0)+shift)%4, re.inspect[0,75]]
444
+ h[x1] += 1
445
+ end
446
+ end
447
+ end
448
+ end
449
+ if h.any?
450
+ known_bytes << h.sort_by(&:last).last[0] # most frequent byte
451
+ end
452
+ if known_bytes.size == step && step < 4
453
+ add_detect known_bytes, step+1
454
+ else
455
+ kb = known_bytes
456
+ case kb.size
457
+ when 0
458
+ logger.debug "[?] %s: no matches" % __method__
459
+ when 1..3
460
+ logger.info "[?] %s: not 'add' or %d-byte key: %s" % [__method__, kb.size, kb.inspect]
461
+ when 4
462
+ logger.info "[*] %s: FOUND 'add' key bytes: [%02x %02x %02x %02x]" % [__method__, *kb].flatten
463
+ return known_bytes.reverse.inject(0){ |x,y| (x<<8) + y}
464
+ else
465
+ logger.info "[?] %s: %d possible bytes: %s" % [__method__, kb.size, kb.inspect]
466
+ end
467
+ return nil
468
+ end
469
+ end
470
+
471
+ def _scan_obj_tbl
472
+ unless @ebp
473
+ logger.warn "[?] %s: EBP undefined, skipping" % __method__
474
+ return
475
+ end
476
+
477
+ re = code2re OBJ_TBL_CODE
478
+ va = nil
479
+ if m = @data.match(re)
480
+ a = m[1..-1].map{|x| x.unpack('V').first }
481
+ logger.debug "[d] OBJ_TBL_RE found at %4x : %s" % [m.begin(0), a.map{|x| x.to_s(16)}.join(', ')]
482
+ va = (a[0] + @ebp) & 0xffff_ffff
483
+ logger.debug "[.] obj_tbl VA = %4x (using EBP)" % va
484
+ else
485
+ logger.error "[!] cannot find obj_tbl"
486
+ return
487
+ end
488
+
489
+ # obj_tbl contains flags if there is a call to VirtualProtect in loader code
490
+ record_size = (@data['VirtualProtect'] && @data[VIRTUALPROTECT_RE]) ? 4*3 : 4*2
491
+
492
+ # @ldr[va-0x3c,0x3c].unpack('V*').each do |x|
493
+ # printf("%8x\n",x);
494
+ # end
495
+
496
+ r = []
497
+ while true
498
+ obj = SECTION_INFO.new(*@ldr[va, record_size].unpack(SECTION_INFO::FORMAT))
499
+ break if obj.va == 0
500
+ unless @ldr.va2section(obj.va)
501
+ logger.error "[!] can't get section for obj %4x : %4x" % [obj.va, obj.size]
502
+ end
503
+ va += record_size
504
+ r << obj
505
+ if r.size > 0x200
506
+ logger.error "[!] stopped obj_tbl parsing. too many sections!"
507
+ break
508
+ end
509
+ end
510
+ r
511
+ end
512
+
513
+ ########################################################################
514
+
515
+ def find_e8e9
516
+ if m = @data.match(E8_RE)
517
+ @e8e9_mode, @e8e9_cmp = m[1].ord, m[2].ord
518
+ else
519
+ logger.error "[!] can't find E8/E9 patch sub! unpacked code may be invalid!"
520
+ end
521
+
522
+ if m = (@data.match(E8_FLAG_RE_IMM1) || @data.match(E8_FLAG_RE_IMM2))
523
+ @e8e9_flag = m[1].ord
524
+ elsif m = @data.match(E8_FLAG_RE_EBP)
525
+ offset = m[1].unpack('V').first
526
+ @e8e9_flag = @ldr[(@ebp + offset) & 0xffff_ffff, 1].ord
527
+ else
528
+ logger.error "[!] can't find E8/E9 flag! unpacked code may be invalid!"
529
+ raise
530
+ end
531
+
532
+ logger.debug "[.] E8/E9: flag=%s, mode=%s, cmp=%s" % [@e8e9_flag||'???', @e8e9_mode, @e8e9_cmp]
533
+ end
534
+
535
+ def find_obj_tbl
536
+ if @obj_tbl = _scan_obj_tbl
537
+ if logger.level <= ::Logger::INFO
538
+ @obj_tbl.each do |obj|
539
+ if obj.flags
540
+ logger.info "[.] ASP::SECTION va: %8x size: %8x flags: %8x" % [
541
+ obj.va, obj.size&0xffff_ffff, obj.flags]
542
+ else
543
+ logger.info "[.] ASP::SECTION va: %8x size: %8x" % [
544
+ obj.va, obj.size&0xffff_ffff]
545
+ end
546
+ end
547
+ end
548
+ end
549
+ end
550
+
551
+ def find_oep
552
+ @oep = nil
553
+ if m = @data.match(OEP_RE1)
554
+ logger.debug "[.] OEP_RE1 found at %4x" % m.begin(0)
555
+ @oep = m[1].unpack('V').first
556
+ elsif @ebp && m = @data.match(OEP_RE2)
557
+ logger.debug "[.] OEP_RE2 found at %4x (using EBP)" % m.begin(0)
558
+ offset = m[1].unpack('V').first
559
+ @oep = @ldr[(@ebp + offset) & 0xffff_ffff, 4].unpack('V').first
560
+ end
561
+
562
+ if @oep
563
+ logger.info "[.] OEP = %8x" % @oep
564
+ else
565
+ logger.error "[!] cannot find EntryPoint"
566
+ end
567
+ end
568
+
569
+ def find_imports
570
+ @imports_rva = nil
571
+ if m = @data.match(IMPORTS_RE1)
572
+ a = m[1..-1].map{|x| x.unpack('V').first }
573
+ @imports_rva = a[0]
574
+ elsif m = @data.match(IMPORTS_RE2)
575
+ a = m[1..-1].map{|x| x.unpack('V').first }
576
+ else
577
+ logger.error "[!] cannot find imports"
578
+ return
579
+ end
580
+ logger.debug "[d] IMPORTS_REx found at %4x : %s" % [m.begin(0), a.map{|x| x.to_s(16)}.join(', ')]
581
+
582
+ # actually following code is not necessary for IMPORTS_RE1
583
+ # using it to get EBP register value
584
+
585
+ f = @ldr.pedump.imports.map(&:first_thunk).flatten.compact.find{ |x| x.name == "GetModuleHandleA"}
586
+ unless f
587
+ logger.error "[!] GetModuleHandleA not found"
588
+ return
589
+ end
590
+ vaGetModuleHandle = f.va
591
+ logger.debug "[d] GetModuleHandle is at %x" % vaGetModuleHandle
592
+ @ebp = (f.va - a[1]) & 0xffff_ffff
593
+ logger.debug "[d] assume EBP = %x" % @ebp
594
+
595
+ # @imports_rva may already be filled by IMPORTS_RE1
596
+ @imports_rva ||= @data[(@ebp + a[0] - @section.va) & 0xffff_ffff, 4].unpack('V').first
597
+ logger.info "[.] imports RVA = %x" % @imports_rva
598
+ end
599
+
600
+ def find_relocs
601
+ @relocs_rva = nil
602
+ if m = @data.match(RELOCS_RE)
603
+ a = m[1..-1].map{|x| x.unpack('V').first }
604
+ else
605
+ logger.error "[!] cannot find imports"
606
+ raise
607
+ return
608
+ end
609
+ @relocs_rva ||= @ldr[(@ebp + a[0]) & 0xffff_ffff, 4].unpack('V').first
610
+ logger.info "[.] relocs RVA = %x" % @relocs_rva
611
+ end
612
+
613
+ ########################################################################
614
+
615
+ def rebuild_imports
616
+ return unless @imports_rva
617
+
618
+ iids = []
619
+
620
+ va = @imports_rva
621
+ sz = PEdump::IMAGE_IMPORT_DESCRIPTOR::SIZE
622
+ while true
623
+ iid = PEdump::IMAGE_IMPORT_DESCRIPTOR.read(@ldr[va,sz])
624
+ va += sz # increase ptr before breaking, req'd 4 saving total import table size in data dir
625
+ break if iid.Name.to_i == 0
626
+
627
+ [:original_first_thunk, :first_thunk].each do |tbl|
628
+ camel = tbl.capitalize.to_s.gsub(/_./){ |char| char[1..-1].upcase}
629
+ iid[tbl] ||= []
630
+ if (va1 = iid[camel].to_i) != 0
631
+ while true
632
+ # intentionally include zero terminator in table to count IAT size
633
+ t = @ldr[va1,4].unpack('V').first
634
+ iid[tbl] << t
635
+ break if t == 0
636
+ va1 += 4
637
+ end
638
+ end
639
+ end
640
+ iids << iid
641
+ end
642
+ @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IMPORT].tap do |dd|
643
+ dd.va = @imports_rva
644
+ dd.size = va-@imports_rva
645
+ end
646
+ if iids.any?
647
+ iids.sort_by!(&:FirstThunk)
648
+ @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IAT].tap do |dd|
649
+ # Points to the beginning of the first Import Address Table (IAT).
650
+ dd.va = iids.first.FirstThunk
651
+ # The Size field indicates the total size of all the IATs.
652
+ dd.size = iids.last.FirstThunk - iids.first.FirstThunk + iids.last.first_thunk.size*4
653
+ # ... to temporarily mark the IATs as read-write during import resolution.
654
+ # http://msdn.microsoft.com/en-us/magazine/bb985997.aspx
655
+ end
656
+ end
657
+ end
658
+
659
+ def rebuild_relocs
660
+ return if @relocs_rva.to_i == 0
661
+
662
+ va = @relocs_rva
663
+ while true
664
+ a = @ldr[va,4*2].to_s.unpack('V*')
665
+ break if a[0] == 0 || a[1] == 0
666
+ va += a[1]
667
+ end
668
+
669
+ @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::BASERELOC].tap do |dd|
670
+ dd.va = @relocs_rva
671
+ dd.size = va-@relocs_rva
672
+ end
673
+ end
674
+
675
+ def rebuild_tls h = {}
676
+ dd = @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::TLS]
677
+ return if dd.va.to_i == 0 || dd.size.to_i == 0
678
+
679
+ case h[:step]
680
+ when 1 # store @tls_data
681
+ @tls_data = @ldr[dd.va, dd.size]
682
+ when 2 # search in unpacked sections
683
+ return unless @tls_data if h[:step] == 2
684
+ # search for original TLS data in all unpacked sections
685
+ @ldr.sections.each do |section|
686
+ if offset = section.data.index(@tls_data)
687
+ # found a TLS section
688
+ dd.va = section.va + offset
689
+ return
690
+ end
691
+ end
692
+ logger.error "[!] can't find TLS section"
693
+ else
694
+ raise "invalid step"
695
+ end
696
+ end
697
+
698
+ def compile_unlzx dest
699
+ logger.info "[*] compiling #{File.basename(dest)} .."
700
+ system("gcc", UNLZX_SRC, "-o", dest)
701
+ unless File.file?(dest) && File.executable?(dest)
702
+ logger.fatal "[!] %s compile failed, please compile it yourself at %s" % [
703
+ File.basename(dest), File.dirname(dest)
704
+ ]
705
+ end
706
+ end
707
+
708
+ def unlzx_pathname
709
+ UNLZXes.each do |unlzx|
710
+ return unlzx if File.file?(unlzx) && File.executable?(unlzx)
711
+ end
712
+
713
+ # nothing found, try to compile
714
+ UNLZXes.each do |unlzx|
715
+ compile_unlzx unlzx
716
+ return unlzx if File.file?(unlzx) && File.executable?(unlzx)
717
+ end
718
+
719
+ # all compiles failed
720
+ raise "no aspack_unlzx binary"
721
+ end
722
+
723
+ def unpack_section data, packed_size, unpacked_size
724
+ data = IO.popen("#{unlzx_pathname} #{packed_size.to_i} #{unpacked_size.to_i}","r+") do |f|
725
+ f.write data
726
+ f.close_write
727
+ f.read
728
+ end
729
+ raise $?.inspect unless $?.success?
730
+ data
731
+ end
732
+
733
+ def decode_e8e9 data
734
+ return if !data || data.size < 6
735
+ return if [@e8e9_flag, @e8e9_mode, @e8e9_cmp].any?(&:nil?)
736
+ return if @e8e9_flag != 0
737
+
738
+ size = data.size - 6
739
+ offs = 0
740
+ while size > 0
741
+ b0 = data[offs]
742
+ if b0 != "\xE8" && b0 != "\xE9"
743
+ size-=1; offs+=1
744
+ next
745
+ end
746
+
747
+ dw = data[offs+1,4].unpack('V').first
748
+ if @e8e9_mode == 0
749
+ if (dw & 0xff) != @e8e9_cmp
750
+ size-=1; offs+=1
751
+ next
752
+ end
753
+ # dw &= 0xffffff00; dw = ROL(dw, 24)
754
+ dw >>= 8
755
+ end
756
+
757
+ t = (dw-offs) & 0xffffffff # keep value in 32 bits
758
+ #logger.debug "[d] data[%6x] = %8x" % [offs+1, t]
759
+ data[offs+1,4] = [t].pack('V')
760
+ offs += 5; size -= [size, 5].min
761
+ end
762
+ end
763
+
764
+ ########################################################################
765
+
766
+ def unpack
767
+ if @section = @ldr.va2section(@ldr.ep)
768
+ @data = @section.data
769
+ logger.debug "[.] EP section: #{@section.inspect}"
770
+ else
771
+ logger.fatal "[!] cannot determine EP section"
772
+ return
773
+ end
774
+
775
+ decrypt # must be called before any other finds
776
+
777
+ find_imports # also fills @ebp for other finds
778
+ find_e8e9
779
+ find_obj_tbl
780
+ find_oep
781
+ find_relocs
782
+
783
+ ###
784
+
785
+ rebuild_tls :step => 1
786
+ sorted_obj_tbl = @obj_tbl.sort_by{ |x| @ldr.pedump.va2file(x.va) }
787
+ sorted_obj_tbl.each_with_index do |obj,idx|
788
+ # restore section flags, if any
789
+ @ldr.va2section(obj.va).flags = obj.flags if obj.flags
790
+
791
+ next if obj.size < 0 # empty section
792
+ #file_offset = @ldr.pedump.va2file(obj.va)
793
+ #@io.seek file_offset
794
+ packed_size =
795
+ if idx == sorted_obj_tbl.size - 1
796
+ # last obj
797
+ obj.size
798
+ else
799
+ # subtract this file_offset from next object file_offset
800
+ @ldr.pedump.va2file(sorted_obj_tbl[idx+1].va) - @ldr.pedump.va2file(obj.va)
801
+ end
802
+ #packed_data = @io.read packed_size
803
+ packed_data = @ldr[obj.va, packed_size]
804
+ unpacked_data = unpack_section(packed_data, packed_data.size, obj.size).force_encoding('binary')
805
+ # decode e8/e9 only on 1st section?
806
+ decode_e8e9(unpacked_data) if obj == @obj_tbl.first
807
+ @ldr[obj.va, unpacked_data.size] = unpacked_data
808
+ logger.debug "[.] %8x: %8x -> %8x" % [obj.va, packed_size, unpacked_data.size]
809
+ end
810
+
811
+ rebuild_imports
812
+ rebuild_relocs
813
+ rebuild_tls :step => 2
814
+
815
+ @ldr.pe_hdr.ioh.AddressOfEntryPoint = @oep.to_i
816
+ @ldr
817
+ end
818
+ end
819
+
820
+ ##########################################################################
821
+
822
+ if __FILE__ == $0
823
+ fnames =
824
+ if ARGV.empty?
825
+ Dir['samples/*.{dll,exe,bin,ocx}']
826
+ else
827
+ ARGV
828
+ end
829
+
830
+ require 'pp'
831
+ fnames.each do |fname|
832
+ @fname = fname
833
+ File.open(fname,"rb") do |f|
834
+ pedump = PEdump.new :log_level => Logger::DEBUG
835
+ next unless packer = Array(pedump.packer(f)).first
836
+ next unless packer.name =~ /aspack/i
837
+
838
+ STDERR.puts "\n=== #{fname}".green
839
+
840
+ f.rewind
841
+ unpacker = PEdump::Unpacker::ASPack.new(f,
842
+ :log_level => Logger::DEBUG,
843
+ :color => true)
844
+ if l = unpacker.unpack
845
+ # returns PEdump::Loader with unpacked data
846
+ File.open("unpacked.exe","wb") do |f|
847
+ l.dump(f)
848
+ end
849
+ end
850
+ end
851
+ end
852
+ end
853
+