pedump 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+