pedump 0.5.3

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