pedump 0.4.0 → 0.5.0

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.
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
+