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.
- data/.travis.yml +4 -0
- data/Gemfile +10 -6
- data/Gemfile.lock +27 -19
- data/README.md +37 -25
- data/Rakefile +45 -6
- data/VERSION +1 -1
- data/data/fs.txt +37 -1408
- data/data/jc-userdb.txt +14371 -0
- data/data/sig.bin +0 -0
- data/lib/pedump.rb +355 -618
- data/lib/pedump/cli.rb +214 -113
- data/lib/pedump/comparer.rb +147 -0
- data/lib/pedump/composite_io.rb +56 -0
- data/lib/pedump/core.rb +38 -0
- data/lib/pedump/core_ext/try.rb +57 -0
- data/lib/pedump/loader.rb +393 -0
- data/lib/pedump/loader/minidump.rb +187 -0
- data/lib/pedump/loader/section.rb +57 -0
- data/lib/pedump/logger.rb +67 -0
- data/lib/pedump/ne.rb +425 -0
- data/lib/pedump/ne/version_info.rb +171 -0
- data/lib/pedump/packer.rb +50 -2
- data/lib/pedump/pe.rb +121 -0
- data/lib/pedump/resources.rb +436 -0
- data/lib/pedump/security.rb +58 -0
- data/lib/pedump/sig_parser.rb +145 -24
- data/lib/pedump/tls.rb +17 -0
- data/lib/pedump/unpacker.rb +26 -0
- data/lib/pedump/unpacker/aspack.rb +858 -0
- data/lib/pedump/unpacker/upx.rb +13 -0
- data/lib/pedump/version.rb +1 -1
- data/lib/pedump/version_info.rb +15 -10
- data/misc/aspack/Makefile +3 -0
- data/misc/aspack/aspack_unlzx.c +92 -0
- data/misc/aspack/lzxdec.c +479 -0
- data/misc/aspack/lzxdec.h +56 -0
- data/misc/nedump.c +751 -0
- data/pedump.gemspec +75 -25
- data/samples/bad/68.exe +0 -0
- data/samples/bad/data_dir_15_entries.exe +0 -0
- data/spec/65535sects_spec.rb +8 -0
- data/spec/bad_imports_spec.rb +20 -0
- data/spec/bad_samples_spec.rb +13 -0
- data/spec/composite_io_spec.rb +122 -0
- data/spec/data/calc.exe_sections.yml +49 -0
- data/spec/data/data_dir_15_entries.exe_sections.yml +95 -0
- data/spec/dllord_spec.rb +21 -0
- data/spec/foldedhdr_spec.rb +28 -0
- data/spec/imports_badterm_spec.rb +52 -0
- data/spec/imports_vterm_spec.rb +52 -0
- data/spec/loader/names_spec.rb +24 -0
- data/spec/loader/va_spec.rb +44 -0
- data/spec/manyimportsW7_spec.rb +22 -0
- data/spec/ne_spec.rb +125 -0
- data/spec/packer_spec.rb +17 -0
- data/spec/pe_spec.rb +67 -0
- data/spec/pedump_spec.rb +16 -4
- data/spec/sections_spec.rb +11 -0
- data/spec/sig_all_packers_spec.rb +15 -5
- data/spec/sig_spec.rb +6 -1
- data/spec/spec_helper.rb +15 -3
- data/spec/support/samples.rb +24 -0
- data/spec/unpackers/aspack_spec.rb +69 -0
- data/spec/unpackers/find_spec.rb +21 -0
- data/spec/virtsectblXP_spec.rb +12 -0
- data/tmp/.keep +0 -0
- metadata +146 -35
- data/README.md.tpl +0 -90
- data/samples/calc.7z +0 -0
- 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
|
data/lib/pedump/sig_parser.rb
CHANGED
@@ -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", "
|
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),
|
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,
|
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]
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
data/lib/pedump/tls.rb
ADDED
@@ -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, █ 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
|
+
|