pedump 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +20 -0
- data/README.md +410 -0
- data/Rakefile +179 -0
- data/VERSION +1 -0
- data/bin/pedump +7 -0
- data/data/fs.txt +224 -0
- data/data/jc-userdb.txt +14371 -0
- data/data/sig.bin +0 -0
- data/data/signatures.txt +678 -0
- data/data/userdb.txt +14083 -0
- data/lib/pedump.rb +868 -0
- data/lib/pedump/cli.rb +804 -0
- 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 +351 -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 +173 -0
- 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 +507 -0
- 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 +10 -0
- data/lib/pedump/version_info.rb +171 -0
- 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 +109 -0
- metadata +227 -0
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 relocs"
|
611
|
+
raise
|
612
|
+
return
|
613
|
+
end
|
614
|
+
@relocs_rva ||= @ldr[(@ebp + a[0]) & 0xffff_ffff, 4].unpack('V').first
|
615
|
+
logger.info "[.] relocs RVA = %x" % @relocs_rva
|
616
|
+
end
|
617
|
+
|
618
|
+
########################################################################
|
619
|
+
|
620
|
+
def rebuild_imports
|
621
|
+
return unless @imports_rva
|
622
|
+
|
623
|
+
iids = []
|
624
|
+
|
625
|
+
va = @imports_rva
|
626
|
+
sz = PEdump::IMAGE_IMPORT_DESCRIPTOR::SIZE
|
627
|
+
while true
|
628
|
+
iid = PEdump::IMAGE_IMPORT_DESCRIPTOR.read(@ldr[va,sz])
|
629
|
+
va += sz # increase ptr before breaking, req'd 4 saving total import table size in data dir
|
630
|
+
break if iid.Name.to_i == 0
|
631
|
+
|
632
|
+
[:original_first_thunk, :first_thunk].each do |tbl|
|
633
|
+
camel = tbl.capitalize.to_s.gsub(/_./){ |char| char[1..-1].upcase}
|
634
|
+
iid[tbl] ||= []
|
635
|
+
if (va1 = iid[camel].to_i) != 0
|
636
|
+
while true
|
637
|
+
# intentionally include zero terminator in table to count IAT size
|
638
|
+
t = @ldr[va1,4].unpack('V').first
|
639
|
+
iid[tbl] << t
|
640
|
+
break if t == 0
|
641
|
+
va1 += 4
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
iids << iid
|
646
|
+
end
|
647
|
+
@ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IMPORT].tap do |dd|
|
648
|
+
dd.va = @imports_rva
|
649
|
+
dd.size = va-@imports_rva
|
650
|
+
end
|
651
|
+
if iids.any?
|
652
|
+
iids.sort_by!(&:FirstThunk)
|
653
|
+
@ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::IAT].tap do |dd|
|
654
|
+
# Points to the beginning of the first Import Address Table (IAT).
|
655
|
+
dd.va = iids.first.FirstThunk
|
656
|
+
# The Size field indicates the total size of all the IATs.
|
657
|
+
dd.size = iids.last.FirstThunk - iids.first.FirstThunk + iids.last.first_thunk.size*4
|
658
|
+
# ... to temporarily mark the IATs as read-write during import resolution.
|
659
|
+
# http://msdn.microsoft.com/en-us/magazine/bb985997.aspx
|
660
|
+
end
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
def rebuild_relocs
|
665
|
+
return if @relocs_rva.to_i == 0
|
666
|
+
|
667
|
+
va = @relocs_rva
|
668
|
+
while true
|
669
|
+
a = @ldr[va,4*2].to_s.unpack('V*')
|
670
|
+
break if a[0] == 0 || a[1] == 0
|
671
|
+
va += a[1]
|
672
|
+
end
|
673
|
+
|
674
|
+
@ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::BASERELOC].tap do |dd|
|
675
|
+
dd.va = @relocs_rva
|
676
|
+
dd.size = va-@relocs_rva
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def rebuild_tls h = {}
|
681
|
+
dd = @ldr.pe_hdr.ioh.DataDirectory[PEdump::IMAGE_DATA_DIRECTORY::TLS]
|
682
|
+
return if dd.va.to_i == 0 || dd.size.to_i == 0
|
683
|
+
|
684
|
+
case h[:step]
|
685
|
+
when 1 # store @tls_data
|
686
|
+
@tls_data = @ldr[dd.va, dd.size]
|
687
|
+
when 2 # search in unpacked sections
|
688
|
+
return unless @tls_data if h[:step] == 2
|
689
|
+
# search for original TLS data in all unpacked sections
|
690
|
+
@ldr.sections.each do |section|
|
691
|
+
if offset = section.data.index(@tls_data)
|
692
|
+
# found a TLS section
|
693
|
+
dd.va = section.va + offset
|
694
|
+
return
|
695
|
+
end
|
696
|
+
end
|
697
|
+
logger.error "[!] can't find TLS section"
|
698
|
+
else
|
699
|
+
raise "invalid step"
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
def compile_unlzx dest
|
704
|
+
logger.info "[*] compiling #{File.basename(dest)} .."
|
705
|
+
system("gcc", UNLZX_SRC, "-o", dest)
|
706
|
+
unless File.file?(dest) && File.executable?(dest)
|
707
|
+
logger.fatal "[!] %s compile failed, please compile it yourself at %s" % [
|
708
|
+
File.basename(dest), File.dirname(dest)
|
709
|
+
]
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
def unlzx_pathname
|
714
|
+
UNLZXes.each do |unlzx|
|
715
|
+
return unlzx if File.file?(unlzx) && File.executable?(unlzx)
|
716
|
+
end
|
717
|
+
|
718
|
+
# nothing found, try to compile
|
719
|
+
UNLZXes.each do |unlzx|
|
720
|
+
compile_unlzx unlzx
|
721
|
+
return unlzx if File.file?(unlzx) && File.executable?(unlzx)
|
722
|
+
end
|
723
|
+
|
724
|
+
# all compiles failed
|
725
|
+
raise "no aspack_unlzx binary"
|
726
|
+
end
|
727
|
+
|
728
|
+
def unpack_section data, packed_size, unpacked_size
|
729
|
+
data = IO.popen("#{unlzx_pathname} #{packed_size.to_i} #{unpacked_size.to_i}","r+") do |f|
|
730
|
+
f.write data
|
731
|
+
f.close_write
|
732
|
+
f.read
|
733
|
+
end
|
734
|
+
raise $?.inspect unless $?.success?
|
735
|
+
data
|
736
|
+
end
|
737
|
+
|
738
|
+
def decode_e8e9 data
|
739
|
+
return if !data || data.size < 6
|
740
|
+
return if [@e8e9_flag, @e8e9_mode, @e8e9_cmp].any?(&:nil?)
|
741
|
+
return if @e8e9_flag != 0
|
742
|
+
|
743
|
+
size = data.size - 6
|
744
|
+
offs = 0
|
745
|
+
while size > 0
|
746
|
+
b0 = data[offs]
|
747
|
+
if b0 != "\xE8" && b0 != "\xE9"
|
748
|
+
size-=1; offs+=1
|
749
|
+
next
|
750
|
+
end
|
751
|
+
|
752
|
+
dw = data[offs+1,4].unpack('V').first
|
753
|
+
if @e8e9_mode == 0
|
754
|
+
if (dw & 0xff) != @e8e9_cmp
|
755
|
+
size-=1; offs+=1
|
756
|
+
next
|
757
|
+
end
|
758
|
+
# dw &= 0xffffff00; dw = ROL(dw, 24)
|
759
|
+
dw >>= 8
|
760
|
+
end
|
761
|
+
|
762
|
+
t = (dw-offs) & 0xffffffff # keep value in 32 bits
|
763
|
+
#logger.debug "[d] data[%6x] = %8x" % [offs+1, t]
|
764
|
+
data[offs+1,4] = [t].pack('V')
|
765
|
+
offs += 5; size -= [size, 5].min
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
########################################################################
|
770
|
+
|
771
|
+
def unpack
|
772
|
+
if @section = @ldr.va2section(@ldr.ep)
|
773
|
+
@data = @section.data
|
774
|
+
logger.debug "[.] EP section: #{@section.inspect}"
|
775
|
+
else
|
776
|
+
logger.fatal "[!] cannot determine EP section"
|
777
|
+
return
|
778
|
+
end
|
779
|
+
|
780
|
+
decrypt # must be called before any other finds
|
781
|
+
|
782
|
+
find_imports # also fills @ebp for other finds
|
783
|
+
find_e8e9
|
784
|
+
find_obj_tbl
|
785
|
+
find_oep
|
786
|
+
find_relocs
|
787
|
+
|
788
|
+
###
|
789
|
+
|
790
|
+
rebuild_tls :step => 1
|
791
|
+
sorted_obj_tbl = @obj_tbl.sort_by{ |x| @ldr.pedump.va2file(x.va) }
|
792
|
+
sorted_obj_tbl.each_with_index do |obj,idx|
|
793
|
+
# restore section flags, if any
|
794
|
+
@ldr.va2section(obj.va).flags = obj.flags if obj.flags
|
795
|
+
|
796
|
+
next if obj.size < 0 # empty section
|
797
|
+
#file_offset = @ldr.pedump.va2file(obj.va)
|
798
|
+
#@io.seek file_offset
|
799
|
+
packed_size =
|
800
|
+
if idx == sorted_obj_tbl.size - 1
|
801
|
+
# last obj
|
802
|
+
obj.size
|
803
|
+
else
|
804
|
+
# subtract this file_offset from next object file_offset
|
805
|
+
@ldr.pedump.va2file(sorted_obj_tbl[idx+1].va) - @ldr.pedump.va2file(obj.va)
|
806
|
+
end
|
807
|
+
#packed_data = @io.read packed_size
|
808
|
+
packed_data = @ldr[obj.va, packed_size]
|
809
|
+
unpacked_data = unpack_section(packed_data, packed_data.size, obj.size).force_encoding('binary')
|
810
|
+
# decode e8/e9 only on 1st section?
|
811
|
+
decode_e8e9(unpacked_data) if obj == @obj_tbl.first
|
812
|
+
@ldr[obj.va, unpacked_data.size] = unpacked_data
|
813
|
+
logger.debug "[.] %8x: %8x -> %8x" % [obj.va, packed_size, unpacked_data.size]
|
814
|
+
end
|
815
|
+
|
816
|
+
rebuild_imports
|
817
|
+
rebuild_relocs
|
818
|
+
rebuild_tls :step => 2
|
819
|
+
|
820
|
+
@ldr.pe_hdr.ioh.AddressOfEntryPoint = @oep.to_i
|
821
|
+
@ldr
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
##########################################################################
|
826
|
+
|
827
|
+
if __FILE__ == $0
|
828
|
+
fnames =
|
829
|
+
if ARGV.empty?
|
830
|
+
Dir['samples/*.{dll,exe,bin,ocx}']
|
831
|
+
else
|
832
|
+
ARGV
|
833
|
+
end
|
834
|
+
|
835
|
+
require 'pp'
|
836
|
+
fnames.each do |fname|
|
837
|
+
@fname = fname
|
838
|
+
File.open(fname,"rb") do |f|
|
839
|
+
pedump = PEdump.new :log_level => Logger::DEBUG
|
840
|
+
next unless packer = Array(pedump.packer(f)).first
|
841
|
+
next unless packer.name =~ /aspack/i
|
842
|
+
|
843
|
+
STDERR.puts "\n=== #{fname}".green
|
844
|
+
|
845
|
+
f.rewind
|
846
|
+
unpacker = PEdump::Unpacker::ASPack.new(f,
|
847
|
+
:log_level => Logger::DEBUG,
|
848
|
+
:color => true)
|
849
|
+
if l = unpacker.unpack
|
850
|
+
# returns PEdump::Loader with unpacked data
|
851
|
+
File.open("unpacked.exe","wb") do |f|
|
852
|
+
l.dump(f)
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
end
|
857
|
+
end
|
858
|
+
|