pedump 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.travis.yml +4 -0
  2. data/Gemfile +10 -6
  3. data/Gemfile.lock +27 -19
  4. data/README.md +37 -25
  5. data/Rakefile +45 -6
  6. data/VERSION +1 -1
  7. data/data/fs.txt +37 -1408
  8. data/data/jc-userdb.txt +14371 -0
  9. data/data/sig.bin +0 -0
  10. data/lib/pedump.rb +355 -618
  11. data/lib/pedump/cli.rb +214 -113
  12. data/lib/pedump/comparer.rb +147 -0
  13. data/lib/pedump/composite_io.rb +56 -0
  14. data/lib/pedump/core.rb +38 -0
  15. data/lib/pedump/core_ext/try.rb +57 -0
  16. data/lib/pedump/loader.rb +393 -0
  17. data/lib/pedump/loader/minidump.rb +187 -0
  18. data/lib/pedump/loader/section.rb +57 -0
  19. data/lib/pedump/logger.rb +67 -0
  20. data/lib/pedump/ne.rb +425 -0
  21. data/lib/pedump/ne/version_info.rb +171 -0
  22. data/lib/pedump/packer.rb +50 -2
  23. data/lib/pedump/pe.rb +121 -0
  24. data/lib/pedump/resources.rb +436 -0
  25. data/lib/pedump/security.rb +58 -0
  26. data/lib/pedump/sig_parser.rb +145 -24
  27. data/lib/pedump/tls.rb +17 -0
  28. data/lib/pedump/unpacker.rb +26 -0
  29. data/lib/pedump/unpacker/aspack.rb +858 -0
  30. data/lib/pedump/unpacker/upx.rb +13 -0
  31. data/lib/pedump/version.rb +1 -1
  32. data/lib/pedump/version_info.rb +15 -10
  33. data/misc/aspack/Makefile +3 -0
  34. data/misc/aspack/aspack_unlzx.c +92 -0
  35. data/misc/aspack/lzxdec.c +479 -0
  36. data/misc/aspack/lzxdec.h +56 -0
  37. data/misc/nedump.c +751 -0
  38. data/pedump.gemspec +75 -25
  39. data/samples/bad/68.exe +0 -0
  40. data/samples/bad/data_dir_15_entries.exe +0 -0
  41. data/spec/65535sects_spec.rb +8 -0
  42. data/spec/bad_imports_spec.rb +20 -0
  43. data/spec/bad_samples_spec.rb +13 -0
  44. data/spec/composite_io_spec.rb +122 -0
  45. data/spec/data/calc.exe_sections.yml +49 -0
  46. data/spec/data/data_dir_15_entries.exe_sections.yml +95 -0
  47. data/spec/dllord_spec.rb +21 -0
  48. data/spec/foldedhdr_spec.rb +28 -0
  49. data/spec/imports_badterm_spec.rb +52 -0
  50. data/spec/imports_vterm_spec.rb +52 -0
  51. data/spec/loader/names_spec.rb +24 -0
  52. data/spec/loader/va_spec.rb +44 -0
  53. data/spec/manyimportsW7_spec.rb +22 -0
  54. data/spec/ne_spec.rb +125 -0
  55. data/spec/packer_spec.rb +17 -0
  56. data/spec/pe_spec.rb +67 -0
  57. data/spec/pedump_spec.rb +16 -4
  58. data/spec/sections_spec.rb +11 -0
  59. data/spec/sig_all_packers_spec.rb +15 -5
  60. data/spec/sig_spec.rb +6 -1
  61. data/spec/spec_helper.rb +15 -3
  62. data/spec/support/samples.rb +24 -0
  63. data/spec/unpackers/aspack_spec.rb +69 -0
  64. data/spec/unpackers/find_spec.rb +21 -0
  65. data/spec/virtsectblXP_spec.rb +12 -0
  66. data/tmp/.keep +0 -0
  67. metadata +146 -35
  68. data/README.md.tpl +0 -90
  69. data/samples/calc.7z +0 -0
  70. data/samples/zlib.dll +0 -0
@@ -0,0 +1,58 @@
1
+ class PEdump
2
+ def security f=@io
3
+ return nil unless pe(f) && pe(f).ioh && f
4
+ dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::SECURITY]
5
+ return nil if !dir || dir.va == 0
6
+
7
+ # IMAGE_DIRECTORY_ENTRY_SECURITY
8
+ # Points to a list of WIN_CERTIFICATE structures, defined in WinTrust.H.
9
+ # Not mapped into memory as part of the image.
10
+ # Therefore, the VirtualAddress field is a file offset, rather than an RVA.
11
+ #
12
+ # http://msdn.microsoft.com/en-us/magazine/bb985997.aspx
13
+
14
+ f.seek dir.va
15
+ r = []
16
+ ofs = f.tell
17
+ while !f.eof? && (f.tell-ofs < dir.size)
18
+ r << WIN_CERTIFICATE.read(f)
19
+ end
20
+ r
21
+ end
22
+ alias :signature :security
23
+
24
+ # WIN_CERT_TYPE_X509 (0x0001) bCertificate contains an X.509 certificate.
25
+ # WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) bCertificate contains a PKCS SignedData structure.
26
+ # WIN_CERT_TYPE_RESERVED_1 (0x0003) Reserved.
27
+ # WIN_CERT_TYPE_PKCS1_SIGN (0x0009) bCertificate contains PKCS1_MODULE_SIGN fields.
28
+
29
+ # http://msdn.microsoft.com/en-us/library/aa447037.aspx
30
+ class WIN_CERTIFICATE < IOStruct.new 'Vvv',
31
+ :dwLength,
32
+ :wRevision,
33
+ :wCertificateType,
34
+ # manual
35
+ :data
36
+
37
+ def self.read f
38
+ super.tap do |x|
39
+ if x.dwLength.to_i < 8
40
+ PEdump.logger.error "[!] #{x.class}: too small length #{x.dwLength}"
41
+ elsif x.dwLength.to_i > 0x100_000
42
+ PEdump.logger.error "[!] #{x.class}: too big length #{x.dwLength}"
43
+ else
44
+ x.data = f.read(x.dwLength - 8)
45
+ begin
46
+ case x.wCertificateType
47
+ when 2
48
+ require 'openssl'
49
+ x.data = OpenSSL::PKCS7.new(x.data)
50
+ end
51
+ rescue
52
+ PEdump.logger.error "[!] #{$!}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -6,9 +6,14 @@ class PEdump
6
6
  TEXT_SIGS_FILES = [
7
7
  File.join(DATA_ROOT, "data", "userdb.txt"),
8
8
  File.join(DATA_ROOT, "data", "signatures.txt"),
9
- File.join(DATA_ROOT, "data", "fs.txt")
9
+ File.join(DATA_ROOT, "data", "jc-userdb.txt"),
10
+ File.join(DATA_ROOT, "data", "fs.txt"), # has special parse options!
10
11
  ]
11
12
 
13
+ SPECIAL_PARSE_OPTIONS = {
14
+ File.join(DATA_ROOT, "data", "fs.txt") => {:fix1 => true}
15
+ }
16
+
12
17
  class OrBlock < Array; end
13
18
 
14
19
  class << self
@@ -19,19 +24,20 @@ class PEdump
19
24
  sigs = {}; sig = nil
20
25
 
21
26
  args[:fnames].each do |fname|
22
- n0 = sigs.size
27
+ n0 = sigs.size; add_sig_args = args.dup
28
+ add_sig_args.merge!(SPECIAL_PARSE_OPTIONS[fname] || {})
23
29
  File.open(fname,'r:utf-8') do |f|
24
30
  while line = f.gets
25
31
  case line.strip
26
32
  when /^[<;#]/, /^$/ # comments & blank lines
27
33
  next
28
34
  when /^\[(.+)=(.+)\]$/
29
- _add_sig(sigs, Packer.new($1, $2, true), args )
30
- when /^\[([^=]+)\]$/
35
+ _add_sig(sigs, Packer.new($1, $2, true), add_sig_args )
36
+ when /^\[([^=]+)\](\s+\/\/.+)?$/
31
37
  sig = Packer.new($1)
32
38
  when /^signature = (.+)$/
33
39
  sig.re = $1
34
- _add_sig(sigs, sig, args)
40
+ _add_sig(sigs, sig, add_sig_args)
35
41
  when /^ep_only = (.+)$/
36
42
  sig.ep_only = ($1.strip.downcase == 'true')
37
43
  else raise line
@@ -61,7 +67,13 @@ class PEdump
61
67
  when /\A[a-f0-9]{2}\Z/i
62
68
  x = x.to_i(16).chr
63
69
  bins[sig] << x
64
- args[:raw] ? x : Regexp::escape(x)
70
+ if args[:raw]
71
+ x
72
+ elsif args[:raword]
73
+ x.ord
74
+ else
75
+ Regexp::escape(x)
76
+ end
65
77
  else
66
78
  puts "[?] unknown re element: #{x.inspect} in #{sig.inspect}" if args[:verbose]
67
79
  "BAD_RE"
@@ -72,10 +84,10 @@ class PEdump
72
84
  a = sig.name.split(/-+>/,2).map(&:strip)
73
85
  sig.name = "#{a[0]} (#{a[1]})"
74
86
  end
75
- sig.re.pop while sig.re.last == '??'
87
+ sig.re.pop while sig.re && sig.re.last == '??'
76
88
  end
77
89
  sigs.delete_if{ |sig| !sig.re || sig.re.index('BAD_RE') }
78
- return sigs if args[:raw]
90
+ return sigs if args[:raw] || args[:raword]
79
91
 
80
92
  # require 'awesome_print'
81
93
  # bins.each do |bin_sig, bin|
@@ -125,6 +137,9 @@ class PEdump
125
137
  # bad sigs
126
138
  return if sig.re[/\A538BD833C0A30:::::/]
127
139
  return if sig.name == "Name of the Packer v1.0"
140
+ return if sig.name == "Alias PIX/Vivid IMG Graphics format"
141
+ return if sig.name == "JAR Archive"
142
+ return if sig.name == "Turbo / Borland Pascal v7.x Unit"
128
143
  return if sig.re == "54 68 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 6D 6F" # dos stub
129
144
 
130
145
  sig.name.sub!(/^\*\s+/, '')
@@ -135,10 +150,47 @@ class PEdump
135
150
  sig.name.sub! 'RLP ','RLPack '
136
151
  sig.name.sub! '.beta', ' beta'
137
152
  sig.name.sub! '(com)','[com]'
153
+ sig.name.gsub!(/ V(\d)/, " v\\1") # V1.1 -> v1.1
138
154
  sig.name = sig.name.split(/\s*-+>\s*/).join(' -> ') # fix spaces around '->'
155
+ sig.name = sig.name.split(' ').delete_if do |x|
156
+ # delete words: vX.X, v?.?, ?.?, x.x
157
+ x =~ /\Av?[?x]\.[?x]\Z/i
158
+ end.join(' ')
139
159
 
140
160
  sig.re = sig.re.strip.upcase.tr(':','?')
141
161
  sig.re = sig.re.scan(/../).join(' ') if sig.re.split.first.size > 2
162
+
163
+ # sig contains entirely zeroes or masks or only both
164
+ a_bad = [%w'00', %w'??', %w'00 ??', %w'90', %w'90 ??']
165
+ # ?a, 0? => ??, ??
166
+ a_cur = sig.re.split.map{ |x| x['?'] ? '??' : x }.uniq.sort
167
+ return if a_bad.include?(a_cur)
168
+
169
+ # first byte is unique and all others are zeroes or masks
170
+ a_cur = sig.re.split[1..-1].map{ |x| x['?'] ? '??' : x }.uniq.sort
171
+ return if a_bad.include?(a_cur)
172
+
173
+ # too short signatures
174
+ if sig.re.split.delete_if{ |x| x['?'] }.size < 3
175
+ require 'awesome_print'
176
+ puts sig.inspect.red
177
+ end
178
+
179
+ # fs.txt contains a lot of signatures that copied from other sources
180
+ # BUT have all 01 replaced with '??'
181
+ # // replaced the file with filtered one (see 'fs-good' below) // zzz
182
+ if args[:fix1]
183
+ sigs.keys.each do |re|
184
+ if re.gsub("01","??") == sig.re
185
+ puts "[.] fix1: rejecting #{sig.name} - already present with 01 in place" if args[:verbose]
186
+ return
187
+ end
188
+ end
189
+ # File.open("fs-good.txt","a") do |f|
190
+ # f << "[#{sig.name}=#{sig.re.tr(' ','')}]\n"
191
+ # end
192
+ end
193
+
142
194
  if sigs[sig.re]
143
195
  a = [sig, sigs[sig.re]].map{ |x| x.name.upcase.split('->').first.tr('V ','') }
144
196
  return if a[0][a[1]] || a[1][a[0]]
@@ -161,14 +213,21 @@ class PEdump
161
213
  d.map! do |x|
162
214
  x - [
163
215
  'EXE','[EXE]',
164
- 'vx.x','v?.?',
165
216
  'DLL','(DLL)','[DLL]',
166
217
  '[LZMA]','(LZMA)','LZMA',
167
218
  '-','~','(pack)','(1)','(2)',
168
- '19??'
219
+ '19??',
220
+ 'with:', 'with?'
169
221
  ]
170
222
  end
171
- return if d.all?(&:empty?) # no different words
223
+ return if d.all?(&:empty?) # no different words => can keep ANY name
224
+
225
+
226
+ # if name1 =~ /pecompact/i
227
+ # require 'awesome_print'
228
+ # puts "[d] #{name1}".yellow
229
+ # puts "[d] #{name2}".yellow
230
+ # end
172
231
 
173
232
  # [["v1.14/v1.20"], ["v1.14,", "v1.20"]]]
174
233
  # [["EXEShield", "v0.3b/v0.3", "v0.6"], ["Shield", "v0.3b,", "v0.3"]]]
@@ -182,21 +241,53 @@ class PEdump
182
241
  end
183
242
  end
184
243
 
244
+ # require 'awesome_print'
245
+ # puts "[d] #{name1.yellow} #{name2.green}"
246
+
185
247
  a = name1.split
186
248
  b = name2.split
249
+
250
+ # merge common head
187
251
  new_name_head = []
188
252
  while a.any? && b.any? && a.first.upcase == b.first.upcase
189
253
  new_name_head << a.shift
190
254
  b.shift
191
255
  end
256
+
257
+ # merge common tail
192
258
  new_name_tail = []
193
259
  while a.any? && b.any? && a.last.upcase == b.last.upcase
194
260
  new_name_tail.unshift a.pop
195
261
  b.pop
196
262
  end
263
+
264
+ # rm common words from middle
265
+ separators = [ "/", "->" ]
266
+ was = true
267
+ while was
268
+ was = false
269
+ b.each do |bw|
270
+ next if bw == "/" || bw == "->"
271
+ if a.include?(bw) || a.include?(bw+")") || a.include?("("+bw) || a.include?("(#{bw})")
272
+ b -= [bw]
273
+ was = true
274
+ break
275
+ end
276
+ end
277
+ end
278
+ while separators.include?(b.last)
279
+ b.pop
280
+ end
281
+
197
282
  new_name = new_name_head
198
283
  new_name << [a.join(' '), b.join(' ')].delete_if{|x| x.empty?}.join(' / ')
199
284
  new_name += new_name_tail
285
+ # if name1 =~ /pecompact/i
286
+ # p a
287
+ # p b
288
+ # p new_name_tail
289
+ # puts "[=] #{new_name.inspect}".red
290
+ # end
200
291
  new_name = new_name.join(' ')
201
292
  end
202
293
 
@@ -240,14 +331,16 @@ class PEdump
240
331
  if rd = _re_diff(sig1.re, sig2.re, max_diff)
241
332
  if rd.all?{ |x| x[0].nil? || x[0] == '.' } && sig2.re.size >= sig1.re.size
242
333
  if new_name = _merge_names(sig2.name, sig1.name)
243
- #pp ["FIRST", sig1.name, sig2.name, new_name, sig1.re.join, sig2.re.join] if new_name
334
+ # require 'pp'
335
+ # pp ["FIRST", sig1.name, sig2.name, new_name, sig1.re.join, sig2.re.join] if new_name =~ /pecompact/i
244
336
  sig1.name = new_name
245
337
  end
246
338
  sig2.ep_only ||= sig1.ep_only
247
339
  sig2.re = []
248
340
  elsif rd.all?{ |x| x[1].nil? || x[1] == '.' } && sig1.re.size >= sig2.re.size
249
341
  if new_name = _merge_names(sig2.name, sig1.name)
250
- #pp ["SECOND", sig1.name, sig2.name, new_name, sig1.re.join, sig2.re.join] if new_name
342
+ # require 'pp'
343
+ # pp ["SECOND", sig1.name, sig2.name, new_name, sig1.re.join, sig2.re.join] if new_name =~ /pecompact/i
251
344
  sig2.name = new_name
252
345
  end
253
346
  sig1.re = []
@@ -264,26 +357,53 @@ class PEdump
264
357
  sigs.delete_if{ |sig| sig.re.empty? }
265
358
  end
266
359
 
267
- def optimize sigs
360
+ def _name2wordonly name
361
+ name.downcase.split(/[^a-z0-9_.]+/).join(' ').strip
362
+ end
363
+
364
+ def optimize_names sigs
268
365
  # replaces all duplicate names with references to one name
269
366
  # saves ~30k out of ~200k mem
270
367
  h = {}
368
+
369
+ # find shortest names
271
370
  sigs.each do |sig|
272
- sig.name = (h[sig.name] ||= sig.name)
371
+ t = _name2wordonly(sig.name)
372
+ if h[t]
373
+ # keep shortest name
374
+ if h[t] != sig.name
375
+ #print "[d] #{[h[t], sig.name].inspect} -> "
376
+ h[t] = [h[t], sig.name].sort_by(&:size).first
377
+ #puts h[t]
378
+ else
379
+ # fully identical names
380
+ end
381
+ else
382
+ h[t] = sig.name
383
+ end
273
384
  end
274
385
 
275
- print "[.] sigs merge: #{sigs.size}"; _optimize(sigs); puts " -> #{sigs.size}"
386
+ # assign names back to sigs
387
+ sigs.each{ |sig| sig.name = h[_name2wordonly(sig.name)] }
388
+
389
+ puts "[.] sigs merge: #{h.size} unique names"
390
+ end
391
+
392
+ def optimize sigs
393
+ optimize_names sigs
394
+
395
+ # XXX no optimize from now, prefer more precise sigs
396
+ #print "[.] sigs merge: #{sigs.size}"; _optimize(sigs); puts " -> #{sigs.size}"
276
397
 
277
398
  # try to merge signatures with same name, size & ep_only
278
- sigs.group_by{ |sig|
279
- [sig.re.size, sig.name, sig.ep_only]
280
- }.values.each do |a|
281
- next if a.size == 1
282
- if merged_re = _merge(a)
283
- a.first.re = merged_re
284
- a[1..-1].each{ |sig| sig.re = nil }
399
+ sigs.group_by{ |sig| [sig.re.size, sig.name, sig.ep_only] }.
400
+ values.each do |a|
401
+ next if a.size == 1
402
+ if merged_re = _merge(a)
403
+ a.first.re = merged_re
404
+ a[1..-1].each{ |sig| sig.re = nil }
405
+ end
285
406
  end
286
- end
287
407
  print "[.] sigs merge: #{sigs.size}"; sigs.delete_if{ |x| x.re.nil? }; puts " -> #{sigs.size}"
288
408
 
289
409
 
@@ -361,6 +481,7 @@ class PEdump
361
481
  end
362
482
  prev_eq = eq
363
483
  end
484
+ dstart ||= 0
364
485
  r = dstart..dend
365
486
  r == (0..(size-1)) ? nil : r
366
487
  end
@@ -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 imports"
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
+