pedump 0.3.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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +32 -0
- data/LICENSE.txt +20 -0
- data/README.md +56 -0
- data/README.md.tpl +35 -0
- data/Rakefile +152 -0
- data/VERSION +1 -0
- data/bin/pedump +7 -0
- data/data/sig.bin +0 -0
- data/data/sig.txt +14083 -0
- data/lib/pedump.rb +1044 -0
- data/lib/pedump/cli.rb +593 -0
- data/lib/pedump/packer.rb +124 -0
- data/lib/pedump/version.rb +10 -0
- data/pedump.gemspec +76 -0
- data/samples/calc.7z +0 -0
- data/spec/pedump_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +138 -0
data/lib/pedump.rb
ADDED
@@ -0,0 +1,1044 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'logger'
|
3
|
+
require 'pedump/version'
|
4
|
+
|
5
|
+
# pedump.rb by zed_0xff
|
6
|
+
#
|
7
|
+
# http://zed.0xff.me
|
8
|
+
# http://github.com/zed-0xff
|
9
|
+
|
10
|
+
class String
|
11
|
+
def xor x
|
12
|
+
if x.is_a?(String)
|
13
|
+
r = ''
|
14
|
+
j = 0
|
15
|
+
0.upto(self.size-1) do |i|
|
16
|
+
r << (self[i].ord^x[j].ord).chr
|
17
|
+
j+=1
|
18
|
+
j=0 if j>= x.size
|
19
|
+
end
|
20
|
+
r
|
21
|
+
else
|
22
|
+
r = ''
|
23
|
+
0.upto(self.size-1) do |i|
|
24
|
+
r << (self[i].ord^x).chr
|
25
|
+
end
|
26
|
+
r
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class File
|
32
|
+
def checked_seek newpos
|
33
|
+
@file_range ||= (0..size)
|
34
|
+
@file_range.include?(newpos) && (seek(newpos) || true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class PEdump
|
39
|
+
attr_accessor :fname, :logger, :force
|
40
|
+
|
41
|
+
VERSION = Version::STRING
|
42
|
+
|
43
|
+
def initialize fname, params = {}
|
44
|
+
@fname = fname
|
45
|
+
@force = params[:force]
|
46
|
+
@logger = @@logger = params[:logger] || PEdump::Logger.new(STDERR)
|
47
|
+
end
|
48
|
+
|
49
|
+
class Logger < ::Logger
|
50
|
+
def initialize *args
|
51
|
+
super
|
52
|
+
@formatter = proc do |severity,_,_,msg|
|
53
|
+
# quick and dirty way to remove duplicate messages
|
54
|
+
if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
|
55
|
+
''
|
56
|
+
else
|
57
|
+
@prevmsg = msg
|
58
|
+
"#{msg}\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
@level = Logger::WARN
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class << self
|
66
|
+
def logger; @@logger; end
|
67
|
+
def logger= l; @@logger=l; end
|
68
|
+
|
69
|
+
def create_struct fmt, *args
|
70
|
+
size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
|
71
|
+
[len.to_i, 1].max *
|
72
|
+
case f
|
73
|
+
when /[aAC]/ then 1
|
74
|
+
when 'v' then 2
|
75
|
+
when 'V' then 4
|
76
|
+
when 'Q' then 8
|
77
|
+
else raise "unknown fmt #{f.inspect}"
|
78
|
+
end
|
79
|
+
end.inject(&:+)
|
80
|
+
|
81
|
+
Struct.new( *args ).tap do |x|
|
82
|
+
x.const_set 'FORMAT', fmt
|
83
|
+
x.const_set 'SIZE', size
|
84
|
+
x.class_eval do
|
85
|
+
def pack
|
86
|
+
to_a.pack self.class.const_get('FORMAT')
|
87
|
+
end
|
88
|
+
def empty?
|
89
|
+
to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
def x.read file, size = nil
|
93
|
+
size ||= const_get 'SIZE'
|
94
|
+
data = file.read(size).to_s
|
95
|
+
if data.size < size && PEdump.logger
|
96
|
+
PEdump.logger.error "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
|
97
|
+
end
|
98
|
+
new(*data.unpack(const_get('FORMAT')))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# http://www.delorie.com/djgpp/doc/exe/
|
106
|
+
MZ = create_struct( "a2v13Qv2V6",
|
107
|
+
:signature,
|
108
|
+
:bytes_in_last_block,
|
109
|
+
:blocks_in_file,
|
110
|
+
:num_relocs,
|
111
|
+
:header_paragraphs,
|
112
|
+
:min_extra_paragraphs,
|
113
|
+
:max_extra_paragraphs,
|
114
|
+
:ss,
|
115
|
+
:sp,
|
116
|
+
:checksum,
|
117
|
+
:ip,
|
118
|
+
:cs,
|
119
|
+
:reloc_table_offset,
|
120
|
+
:overlay_number,
|
121
|
+
:reserved0, # 8 reserved bytes
|
122
|
+
:oem_id,
|
123
|
+
:oem_info,
|
124
|
+
:reserved2, # 20 reserved bytes
|
125
|
+
:reserved3,
|
126
|
+
:reserved4,
|
127
|
+
:reserved5,
|
128
|
+
:reserved6,
|
129
|
+
:lfanew
|
130
|
+
)
|
131
|
+
|
132
|
+
class PE < Struct.new(
|
133
|
+
:signature, # "PE\x00\x00"
|
134
|
+
:image_file_header,
|
135
|
+
:image_optional_header,
|
136
|
+
:section_table
|
137
|
+
)
|
138
|
+
alias :ifh :image_file_header
|
139
|
+
alias :ioh :image_optional_header
|
140
|
+
def x64?
|
141
|
+
ifh && ifh.Machine == 0x8664
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# http://msdn.microsoft.com/en-us/library/ms809762.aspx
|
146
|
+
IMAGE_FILE_HEADER = create_struct( 'v2V3v2',
|
147
|
+
:Machine, # w
|
148
|
+
:NumberOfSections, # w
|
149
|
+
:TimeDateStamp, # dw
|
150
|
+
:PointerToSymbolTable, # dw
|
151
|
+
:NumberOfSymbols, # dw
|
152
|
+
:SizeOfOptionalHeader, # w
|
153
|
+
:Characteristics # w
|
154
|
+
)
|
155
|
+
class IMAGE_FILE_HEADER
|
156
|
+
def initialize *args
|
157
|
+
super
|
158
|
+
self.TimeDateStamp = Time.at(self.TimeDateStamp)
|
159
|
+
end
|
160
|
+
def method_missing mname
|
161
|
+
mname = mname.to_s.capitalize
|
162
|
+
self.send(mname) if self.respond_to?(mname)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# http://msdn.microsoft.com/en-us/library/ms809762.aspx
|
167
|
+
IMAGE_OPTIONAL_HEADER = create_struct( 'vC2V9v6V4v2V6',
|
168
|
+
:Magic, # w
|
169
|
+
:MajorLinkerVersion, :MinorLinkerVersion, # 2b
|
170
|
+
:SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, # 9dw
|
171
|
+
:BaseOfCode, :BaseOfData, :ImageBase, :SectionAlignment, :FileAlignment,
|
172
|
+
:MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
|
173
|
+
:MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
|
174
|
+
:Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
|
175
|
+
:Subsystem, :DllCharacteristics, # 2w
|
176
|
+
:SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 6dw
|
177
|
+
:LoaderFlags, :NumberOfRvaAndSizes,
|
178
|
+
:DataDirectory # readed manually
|
179
|
+
)
|
180
|
+
|
181
|
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx)
|
182
|
+
IMAGE_OPTIONAL_HEADER64 = create_struct( 'vC2V5QV2v6V4v2Q4V2',
|
183
|
+
:Magic, # w
|
184
|
+
:MajorLinkerVersion, :MinorLinkerVersion, # 2b
|
185
|
+
:SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, :BaseOfCode, # 5dw
|
186
|
+
:ImageBase, # qw
|
187
|
+
:SectionAlignment, :FileAlignment, # 2dw
|
188
|
+
:MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
|
189
|
+
:MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
|
190
|
+
:Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
|
191
|
+
:Subsystem, :DllCharacteristics, # 2w
|
192
|
+
:SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 4qw
|
193
|
+
:LoaderFlags, :NumberOfRvaAndSizes, #2dw
|
194
|
+
:DataDirectory # readed manually
|
195
|
+
)
|
196
|
+
|
197
|
+
class IMAGE_OPTIONAL_HEADER
|
198
|
+
def self.read file, size = SIZE
|
199
|
+
usual_size = 224
|
200
|
+
PEdump.logger.warn "[?] unusual size of IMAGE_OPTIONAL_HEADER = #{size} (must be #{usual_size})" if size != usual_size
|
201
|
+
new(*file.read([size,SIZE].min).to_s.unpack(FORMAT)).tap do |ioh|
|
202
|
+
ioh.DataDirectory = []
|
203
|
+
|
204
|
+
# check if "...this address is outside the memory mapped file and is zeroed by the OS"
|
205
|
+
# see http://www.phreedom.org/solar/code/tinype/, section "Removing the data directories"
|
206
|
+
ioh.each_pair{ |k,v| ioh[k] = 0 if v.nil? }
|
207
|
+
|
208
|
+
# http://opcode0x90.wordpress.com/2007/04/22/windows-loader-does-it-differently/
|
209
|
+
# maximum of 0x10 entries, even if bigger
|
210
|
+
[0x10,ioh.NumberOfRvaAndSizes].min.times do |idx|
|
211
|
+
ioh.DataDirectory << IMAGE_DATA_DIRECTORY.read(file)
|
212
|
+
ioh.DataDirectory.last.type = IMAGE_DATA_DIRECTORY::TYPES[idx]
|
213
|
+
end
|
214
|
+
#ioh.DataDirectory.pop while ioh.DataDirectory.last.empty?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class IMAGE_OPTIONAL_HEADER64
|
220
|
+
def self.read file, size = SIZE
|
221
|
+
usual_size = 240
|
222
|
+
PEdump.logger.warn "[?] unusual size of IMAGE_OPTIONAL_HEADER = #{size} (must be #{usual_size})" if size != usual_size
|
223
|
+
new(*file.read([size,SIZE].min).unpack(FORMAT)).tap do |ioh|
|
224
|
+
ioh.DataDirectory = []
|
225
|
+
|
226
|
+
# check if "...this address is outside the memory mapped file and is zeroed by the OS"
|
227
|
+
# see http://www.phreedom.org/solar/code/tinype/, section "Removing the data directories"
|
228
|
+
ioh.each_pair{ |k,v| ioh[k] = 0 if v.nil? }
|
229
|
+
|
230
|
+
# http://opcode0x90.wordpress.com/2007/04/22/windows-loader-does-it-differently/
|
231
|
+
# maximum of 0x10 entries, even if bigger
|
232
|
+
[0x10,ioh.NumberOfRvaAndSizes].min.times do |idx|
|
233
|
+
ioh.DataDirectory << IMAGE_DATA_DIRECTORY.read(file)
|
234
|
+
ioh.DataDirectory.last.type = IMAGE_DATA_DIRECTORY::TYPES[idx]
|
235
|
+
end
|
236
|
+
#ioh.DataDirectory.pop while ioh.DataDirectory.last.empty?
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
IMAGE_DATA_DIRECTORY = create_struct( "VV", :va, :size, :type )
|
242
|
+
IMAGE_DATA_DIRECTORY::TYPES =
|
243
|
+
%w'EXPORT IMPORT RESOURCE EXCEPTION SECURITY BASERELOC DEBUG ARCHITECTURE GLOBALPTR TLS LOAD_CONFIG
|
244
|
+
Bound_IAT IAT Delay_IAT CLR_Header'
|
245
|
+
IMAGE_DATA_DIRECTORY::TYPES.each_with_index do |type,idx|
|
246
|
+
IMAGE_DATA_DIRECTORY.const_set(type,idx)
|
247
|
+
end
|
248
|
+
|
249
|
+
IMAGE_SECTION_HEADER = create_struct( 'A8V6v2V',
|
250
|
+
:Name, # A8 6dw
|
251
|
+
:VirtualSize, :VirtualAddress, :SizeOfRawData, :PointerToRawData, :PointerToRelocations, :PointerToLinenumbers,
|
252
|
+
:NumberOfRelocations, :NumberOfLinenumbers, # 2w
|
253
|
+
:Characteristics # dw
|
254
|
+
)
|
255
|
+
class IMAGE_SECTION_HEADER
|
256
|
+
alias :flags :Characteristics
|
257
|
+
def flags_desc
|
258
|
+
r = ''
|
259
|
+
f = self.flags.to_i
|
260
|
+
r << (f & 0x4000_0000 > 0 ? 'R' : '-')
|
261
|
+
r << (f & 0x8000_0000 > 0 ? 'W' : '-')
|
262
|
+
r << (f & 0x2000_0000 > 0 ? 'X' : '-')
|
263
|
+
r << ' CODE' if f & 0x20 > 0
|
264
|
+
|
265
|
+
# section contains initialized data. Almost all sections except executable and the .bss section have this flag set
|
266
|
+
r << ' IDATA' if f & 0x40 > 0
|
267
|
+
|
268
|
+
# section contains uninitialized data (for example, the .bss section)
|
269
|
+
r << ' UDATA' if f & 0x80 > 0
|
270
|
+
|
271
|
+
r << ' DISCARDABLE' if f & 0x02000000 > 0
|
272
|
+
r << ' SHARED' if f & 0x10000000 > 0
|
273
|
+
r
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx
|
278
|
+
IMAGE_SUBSYSTEMS = %w'UNKNOWN NATIVE WINDOWS_GUI WINDOWS_CUI' + [nil,'OS2_CUI',nil,'POSIX_CUI',nil] +
|
279
|
+
%w'WINDOWS_CE_GUI EFI_APPLICATION EFI_BOOT_SERVICE_DRIVER EFI_RUNTIME_DRIVER EFI_ROM XBOX' +
|
280
|
+
[nil, 'WINDOWS_BOOT_APPLICATION']
|
281
|
+
|
282
|
+
# http://ntcore.com/files/richsign.htm
|
283
|
+
class RichHdr < String
|
284
|
+
attr_accessor :offset, :key # xor key
|
285
|
+
|
286
|
+
class Entry < Struct.new(:version,:id,:times)
|
287
|
+
def inspect
|
288
|
+
"<id=#{id}, version=#{version}, times=#{times}>"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.from_dos_stub stub
|
293
|
+
key = stub[stub.index('Rich')+4,4]
|
294
|
+
start_idx = stub.index(key.xor('DanS'))
|
295
|
+
end_idx = stub.index('Rich')+8
|
296
|
+
if stub[end_idx..-1].tr("\x00",'') != ''
|
297
|
+
t = stub[end_idx..-1]
|
298
|
+
t = "#{t[0,0x100]}..." if t.size > 0x100
|
299
|
+
PEdump.logger.error "[!] non-zero dos stub after rich_hdr: #{t.inspect}"
|
300
|
+
return nil
|
301
|
+
end
|
302
|
+
RichHdr.new(stub[start_idx, end_idx-start_idx]).tap do |x|
|
303
|
+
x.key = key
|
304
|
+
x.offset = stub.offset + start_idx
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def dexor
|
309
|
+
self[4..-9].sub(/\A(#{Regexp::escape(key)}){3}/,'').xor(key)
|
310
|
+
end
|
311
|
+
|
312
|
+
def decode
|
313
|
+
x = dexor
|
314
|
+
if x.size%8 == 0
|
315
|
+
x.unpack('vvV'*(x.size/8)).each_slice(3).map{ |slice| Entry.new(*slice)}
|
316
|
+
else
|
317
|
+
PEdump.logger.error "[?] #{self.class}: dexored size(#{x.size}) must be a multiple of 8"
|
318
|
+
nil
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
class DOSStub < String
|
324
|
+
attr_accessor :offset
|
325
|
+
end
|
326
|
+
|
327
|
+
def logger= l
|
328
|
+
@logger = @@logger = l
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.dump fname
|
332
|
+
new(fname).dump
|
333
|
+
end
|
334
|
+
|
335
|
+
def mz f=nil
|
336
|
+
@mz ||= f && MZ.read(f).tap do |mz|
|
337
|
+
if mz.signature != 'MZ' && mz.signature != 'ZM'
|
338
|
+
if @force
|
339
|
+
logger.warn "[?] no MZ signature. want: 'MZ' or 'ZM', got: #{mz.signature.inspect}"
|
340
|
+
else
|
341
|
+
logger.error "[!] no MZ signature. want: 'MZ' or 'ZM', got: #{mz.signature.inspect}. (not forced)"
|
342
|
+
return nil
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def dos_stub f=nil
|
349
|
+
@dos_stub ||=
|
350
|
+
begin
|
351
|
+
return nil unless mz = mz(f)
|
352
|
+
dos_stub_offset = mz.header_paragraphs.to_i * 0x10
|
353
|
+
dos_stub_size = mz.lfanew.to_i - dos_stub_offset
|
354
|
+
if dos_stub_offset <= 0
|
355
|
+
logger.warn "[?] invalid DOS stub offset #{dos_stub_offset}"
|
356
|
+
nil
|
357
|
+
elsif f && dos_stub_offset > f.size
|
358
|
+
logger.warn "[?] DOS stub offset beyond EOF: #{dos_stub_offset}"
|
359
|
+
nil
|
360
|
+
elsif dos_stub_size < 0
|
361
|
+
logger.warn "[?] invalid DOS stub size #{dos_stub_size}"
|
362
|
+
nil
|
363
|
+
elsif dos_stub_size == 0
|
364
|
+
# no DOS stub, it's ok
|
365
|
+
nil
|
366
|
+
elsif !f
|
367
|
+
# no open file, it's ok
|
368
|
+
nil
|
369
|
+
else
|
370
|
+
if dos_stub_size > 0x1000
|
371
|
+
logger.warn "[?] DOS stub size too big (#{dos_stub_size}), limiting to 0x1000"
|
372
|
+
dos_stub_size = 0x1000
|
373
|
+
end
|
374
|
+
f.seek dos_stub_offset
|
375
|
+
DOSStub.new(f.read(dos_stub_size)).tap do |dos_stub|
|
376
|
+
dos_stub.offset = dos_stub_offset
|
377
|
+
if dos_stub['Rich']
|
378
|
+
if @rich_hdr = RichHdr.from_dos_stub(dos_stub)
|
379
|
+
dos_stub[dos_stub.index(@rich_hdr)..-1] = ''
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def rich_hdr f=nil
|
388
|
+
dos_stub(f) && @rich_hdr
|
389
|
+
end
|
390
|
+
alias :rich_header :rich_hdr
|
391
|
+
alias :rich :rich_hdr
|
392
|
+
|
393
|
+
def pe f=nil
|
394
|
+
@pe ||=
|
395
|
+
begin
|
396
|
+
pe_offset = mz(f) && mz(f).lfanew
|
397
|
+
if pe_offset.nil?
|
398
|
+
logger.fatal "[!] NULL PE offset (e_lfanew). cannot continue."
|
399
|
+
nil
|
400
|
+
elsif pe_offset > f.size
|
401
|
+
logger.fatal "[!] PE offset beyond EOF. cannot continue."
|
402
|
+
nil
|
403
|
+
else
|
404
|
+
f.seek pe_offset
|
405
|
+
pe_sig = f.read 4
|
406
|
+
logger.error "[!] 'NE' format is not supported!" if pe_sig == "NE\x00\x00"
|
407
|
+
if pe_sig != "PE\x00\x00"
|
408
|
+
if @force
|
409
|
+
logger.warn "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect})"
|
410
|
+
else
|
411
|
+
logger.error "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect}). (not forced)"
|
412
|
+
return nil
|
413
|
+
end
|
414
|
+
end
|
415
|
+
PE.new(pe_sig).tap do |pe|
|
416
|
+
pe.image_file_header = IMAGE_FILE_HEADER.read(f)
|
417
|
+
if pe.ifh.SizeOfOptionalHeader > 0
|
418
|
+
if pe.x64?
|
419
|
+
pe.image_optional_header = IMAGE_OPTIONAL_HEADER64.read(f, pe.ifh.SizeOfOptionalHeader)
|
420
|
+
else
|
421
|
+
pe.image_optional_header = IMAGE_OPTIONAL_HEADER.read(f, pe.ifh.SizeOfOptionalHeader)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
if (nToRead=pe.ifh.NumberOfSections) > 32
|
426
|
+
if @force.is_a?(Numeric) && @force > 1
|
427
|
+
logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). forced. reading all"
|
428
|
+
else
|
429
|
+
logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). not forced, reading first 32"
|
430
|
+
nToRead = 32
|
431
|
+
end
|
432
|
+
end
|
433
|
+
pe.section_table = nToRead.times.map do
|
434
|
+
IMAGE_SECTION_HEADER.read(f)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def resource_directory f=nil
|
442
|
+
@resource_directory ||= _read_resource_directory_tree(f)
|
443
|
+
end
|
444
|
+
|
445
|
+
# OPTIONAL: assigns @mz, @rich_hdr, @pe, etc
|
446
|
+
def dump f=nil
|
447
|
+
f ? _dump_handle(f) : File.open(@fname){ |f| _dump_handle(f) }
|
448
|
+
self
|
449
|
+
end
|
450
|
+
|
451
|
+
def _dump_handle h
|
452
|
+
rich_hdr(h) # includes mz(h)
|
453
|
+
resources(h) # includes pe(h)
|
454
|
+
imports h
|
455
|
+
exports h
|
456
|
+
end
|
457
|
+
|
458
|
+
def data_directory f=nil
|
459
|
+
pe(f) && pe.ioh && pe.ioh.DataDirectory
|
460
|
+
end
|
461
|
+
|
462
|
+
def sections f=nil
|
463
|
+
pe(f) && pe.section_table
|
464
|
+
end
|
465
|
+
alias :section_table :sections
|
466
|
+
|
467
|
+
##############################################################################
|
468
|
+
# imports
|
469
|
+
##############################################################################
|
470
|
+
|
471
|
+
# http://sandsprite.com/CodeStuff/Understanding_imports.html
|
472
|
+
# http://stackoverflow.com/questions/5631317/import-table-it-vs-import-address-table-iat
|
473
|
+
IMAGE_IMPORT_DESCRIPTOR = create_struct 'V5',
|
474
|
+
:OriginalFirstThunk,
|
475
|
+
:TimeDateStamp,
|
476
|
+
:ForwarderChain,
|
477
|
+
:Name,
|
478
|
+
:FirstThunk,
|
479
|
+
# manual:
|
480
|
+
:module_name,
|
481
|
+
:original_first_thunk,
|
482
|
+
:first_thunk
|
483
|
+
|
484
|
+
ImportedFunction = Struct.new(:hint, :name, :ordinal)
|
485
|
+
|
486
|
+
def imports f=nil
|
487
|
+
return @imports if @imports
|
488
|
+
return nil unless pe(f) && pe(f).ioh && f
|
489
|
+
dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::IMPORT]
|
490
|
+
return [] if !dir || (dir.va == 0 && dir.size == 0)
|
491
|
+
va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::IMPORT].va
|
492
|
+
file_offset = va2file(va)
|
493
|
+
return nil unless file_offset
|
494
|
+
f.seek file_offset
|
495
|
+
r = []
|
496
|
+
until (t=IMAGE_IMPORT_DESCRIPTOR.read(f)).empty?
|
497
|
+
r << t
|
498
|
+
end
|
499
|
+
@imports = r.each do |x|
|
500
|
+
if x.Name.to_i != 0 && (va = va2file(x.Name))
|
501
|
+
f.seek va
|
502
|
+
x.module_name = f.gets("\x00").chop
|
503
|
+
end
|
504
|
+
[:original_first_thunk, :first_thunk].each do |tbl|
|
505
|
+
camel = tbl.capitalize.to_s.gsub(/_./){ |char| char[1..-1].upcase}
|
506
|
+
if x[camel].to_i != 0 && (va = va2file(x[camel]))
|
507
|
+
f.seek va
|
508
|
+
x[tbl] ||= []
|
509
|
+
if pe.x64?
|
510
|
+
x[tbl] << t while (t = f.read(8).unpack('Q').first) != 0
|
511
|
+
else
|
512
|
+
x[tbl] << t while (t = f.read(4).unpack('V').first) != 0
|
513
|
+
end
|
514
|
+
end
|
515
|
+
cache = {}
|
516
|
+
bits = pe.x64? ? 64 : 32
|
517
|
+
x[tbl] && x[tbl].map! do |t|
|
518
|
+
cache[t] ||=
|
519
|
+
if t & (2**(bits-1)) > 0 # 0x8000_0000(_0000_0000)
|
520
|
+
ImportedFunction.new(nil,nil,t & (2**(bits-1)-1)) # 0x7fff_ffff(_ffff_ffff)
|
521
|
+
elsif va=va2file(t)
|
522
|
+
f.seek va
|
523
|
+
ImportedFunction.new(f.read(2).unpack('v').first, f.gets("\x00").chop)
|
524
|
+
else
|
525
|
+
nil
|
526
|
+
end
|
527
|
+
end
|
528
|
+
x[tbl] && x[tbl].compact!
|
529
|
+
end
|
530
|
+
if x.original_first_thunk && !x.first_thunk
|
531
|
+
logger.warn "[?] import table: empty FirstThunk of #{x.module_name}"
|
532
|
+
elsif !x.original_first_thunk && x.first_thunk
|
533
|
+
logger.warn "[?] import table: empty OriginalFirstThunk of #{x.module_name}"
|
534
|
+
elsif x.original_first_thunk != x.first_thunk
|
535
|
+
logger.warn "[?] import table: OriginalFirstThunk != FirstThunk of #{x.module_name}"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
##############################################################################
|
541
|
+
# exports
|
542
|
+
##############################################################################
|
543
|
+
|
544
|
+
#http://msdn.microsoft.com/en-us/library/ms809762.aspx
|
545
|
+
IMAGE_EXPORT_DIRECTORY = create_struct 'V2v2V7',
|
546
|
+
:Characteristics,
|
547
|
+
:TimeDateStamp,
|
548
|
+
:MajorVersion, # These fields appear to be unused and are set to 0.
|
549
|
+
:MinorVersion, # These fields appear to be unused and are set to 0.
|
550
|
+
:Name,
|
551
|
+
:Base, # The starting ordinal number for exported functions
|
552
|
+
:NumberOfFunctions,
|
553
|
+
:NumberOfNames,
|
554
|
+
:AddressOfFunctions,
|
555
|
+
:AddressOfNames,
|
556
|
+
:AddressOfNameOrdinals,
|
557
|
+
# manual:
|
558
|
+
:name, :entry_points, :names, :name_ordinals
|
559
|
+
|
560
|
+
def exports f=nil
|
561
|
+
return @exports if @exports
|
562
|
+
return nil unless pe(f) && pe(f).ioh && f
|
563
|
+
dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::EXPORT]
|
564
|
+
return [] if !dir || (dir.va == 0 && dir.size == 0)
|
565
|
+
va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::EXPORT].va
|
566
|
+
file_offset = va2file(va)
|
567
|
+
return nil unless file_offset
|
568
|
+
f.seek file_offset
|
569
|
+
@exports = IMAGE_EXPORT_DIRECTORY.read(f).tap do |x|
|
570
|
+
x.entry_points = []
|
571
|
+
x.name_ordinals = []
|
572
|
+
x.names = []
|
573
|
+
if x.Name.to_i != 0 && (va = va2file(x.Name))
|
574
|
+
f.seek va
|
575
|
+
x.name = f.gets("\x00").chop
|
576
|
+
end
|
577
|
+
if x.NumberOfFunctions.to_i != 0
|
578
|
+
if x.AddressOfFunctions.to_i !=0 && (va = va2file(x.AddressOfFunctions))
|
579
|
+
f.seek va
|
580
|
+
x.entry_points = f.read(x.NumberOfFunctions*4).unpack('V*')
|
581
|
+
end
|
582
|
+
if x.AddressOfNameOrdinals.to_i !=0 && (va = va2file(x.AddressOfNameOrdinals))
|
583
|
+
f.seek va
|
584
|
+
x.name_ordinals = f.read(x.NumberOfNames*2).unpack('v*').map{ |o| o+x.Base }
|
585
|
+
end
|
586
|
+
end
|
587
|
+
if x.NumberOfNames.to_i != 0 && x.AddressOfNames.to_i !=0 && (va = va2file(x.AddressOfNames))
|
588
|
+
f.seek va
|
589
|
+
x.names = f.read(x.NumberOfNames*4).unpack('V*').map do |va|
|
590
|
+
f.seek va2file(va)
|
591
|
+
f.gets("\x00").chop
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
##############################################################################
|
598
|
+
# resources
|
599
|
+
##############################################################################
|
600
|
+
|
601
|
+
IMAGE_RESOURCE_DIRECTORY = create_struct 'V2v4',
|
602
|
+
:Characteristics, :TimeDateStamp, # 2dw
|
603
|
+
:MajorVersion, :MinorVersion, :NumberOfNamedEntries, :NumberOfIdEntries, # 4w
|
604
|
+
:entries # manual
|
605
|
+
class IMAGE_RESOURCE_DIRECTORY
|
606
|
+
class << self
|
607
|
+
attr_accessor :base
|
608
|
+
alias :read_without_children :read
|
609
|
+
def read f, root=true
|
610
|
+
if root
|
611
|
+
@@loopchk1 = Hash.new(0)
|
612
|
+
@@loopchk2 = Hash.new(0)
|
613
|
+
@@loopchk3 = Hash.new(0)
|
614
|
+
elsif (@@loopchk1[f.tell] += 1) > 1
|
615
|
+
PEdump.logger.error "[!] #{self}: loop1 detected at file pos #{f.tell}" if @@loopchk1[f.tell] < 2
|
616
|
+
return nil
|
617
|
+
end
|
618
|
+
read_without_children(f).tap do |r|
|
619
|
+
nToRead = r.NumberOfNamedEntries.to_i + r.NumberOfIdEntries.to_i
|
620
|
+
r.entries = []
|
621
|
+
nToRead.times do |i|
|
622
|
+
if f.eof?
|
623
|
+
PEdump.logger.error "[!] #{self}: #{nToRead} entries in directory, but got EOF on #{i}-th."
|
624
|
+
break
|
625
|
+
end
|
626
|
+
if (@@loopchk2[f.tell] += 1) > 1
|
627
|
+
PEdump.logger.error "[!] #{self}: loop2 detected at file pos #{f.tell}" if @@loopchk2[f.tell] < 2
|
628
|
+
next
|
629
|
+
end
|
630
|
+
r.entries << IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
|
631
|
+
end
|
632
|
+
#r.entries.uniq!
|
633
|
+
r.entries.each do |entry|
|
634
|
+
entry.name =
|
635
|
+
if entry.Name.to_i & 0x8000_0000 > 0
|
636
|
+
# Name is an address of unicode string
|
637
|
+
f.seek base + entry.Name & 0x7fff_ffff
|
638
|
+
nChars = f.read(2).to_s.unpack("v").first.to_i
|
639
|
+
begin
|
640
|
+
f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
|
641
|
+
rescue
|
642
|
+
PEdump.logger.error "[!] #{self} failed to read entry name: #{$!}"
|
643
|
+
"???"
|
644
|
+
end
|
645
|
+
else
|
646
|
+
# Name is a numeric id
|
647
|
+
"##{entry.Name}"
|
648
|
+
end
|
649
|
+
if entry.OffsetToData && f.checked_seek(base + entry.OffsetToData & 0x7fff_ffff)
|
650
|
+
if (@@loopchk3[f.tell] += 1) > 1
|
651
|
+
PEdump.logger.error "[!] #{self}: loop3 detected at file pos #{f.tell}" if @@loopchk3[f.tell] < 2
|
652
|
+
next
|
653
|
+
end
|
654
|
+
entry.data =
|
655
|
+
if entry.OffsetToData & 0x8000_0000 > 0
|
656
|
+
# child is a directory
|
657
|
+
IMAGE_RESOURCE_DIRECTORY.read(f,false)
|
658
|
+
else
|
659
|
+
# child is a resource
|
660
|
+
IMAGE_RESOURCE_DATA_ENTRY.read(f)
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|
664
|
+
@@loopchk1 = @@loopchk2 = @@loopchk3 = nil if root # save some memory
|
665
|
+
end
|
666
|
+
end
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
IMAGE_RESOURCE_DIRECTORY_ENTRY = create_struct 'V2',
|
671
|
+
:Name, :OffsetToData,
|
672
|
+
:name, :data
|
673
|
+
|
674
|
+
IMAGE_RESOURCE_DATA_ENTRY = create_struct 'V4',
|
675
|
+
:OffsetToData, :Size, :CodePage, :Reserved
|
676
|
+
|
677
|
+
def va2file va
|
678
|
+
sections.each do |s|
|
679
|
+
if (s.VirtualAddress...(s.VirtualAddress+s.VirtualSize)).include?(va)
|
680
|
+
return va - s.VirtualAddress + s.PointerToRawData
|
681
|
+
end
|
682
|
+
end
|
683
|
+
# not found with regular search. assume any of VirtualSize was 0, and try with RawSize
|
684
|
+
sections.each do |s|
|
685
|
+
if (s.VirtualAddress...(s.VirtualAddress+s.SizeOfRawData)).include?(va)
|
686
|
+
return va - s.VirtualAddress + s.PointerToRawData
|
687
|
+
end
|
688
|
+
end
|
689
|
+
logger.error "[?] can't find file_offset of VA 0x#{va.to_i.to_s(16)}"
|
690
|
+
nil
|
691
|
+
end
|
692
|
+
|
693
|
+
def _read_resource_directory_tree f
|
694
|
+
return nil unless pe(f) && pe(f).ioh && f
|
695
|
+
res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
|
696
|
+
return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
|
697
|
+
res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
|
698
|
+
res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
|
699
|
+
unless res_section
|
700
|
+
logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
|
701
|
+
return []
|
702
|
+
end
|
703
|
+
f.seek res_section.PointerToRawData
|
704
|
+
IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
|
705
|
+
#@resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
|
706
|
+
IMAGE_RESOURCE_DIRECTORY.read(f)
|
707
|
+
end
|
708
|
+
|
709
|
+
class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data, :valid)
|
710
|
+
def bitmap_hdr
|
711
|
+
bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
|
712
|
+
raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
|
713
|
+
|
714
|
+
bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
|
715
|
+
|
716
|
+
colors_used = bmp_info_hdr.biClrUsed
|
717
|
+
colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
|
718
|
+
|
719
|
+
# XXX: one byte in each color is unused!
|
720
|
+
@palette_size = colors_used * 4 # each color takes 4 bytes
|
721
|
+
|
722
|
+
# scanlines are DWORD-aligned and padded to DWORD-align with zeroes
|
723
|
+
# XXX: some data may be hidden in padding bytes!
|
724
|
+
scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
|
725
|
+
scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
|
726
|
+
|
727
|
+
@imgdata_size = scanline_size * bmp_info_hdr.biHeight
|
728
|
+
"BM" + [
|
729
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
|
730
|
+
0,
|
731
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
732
|
+
].pack("V3") + bmp_info_hdr.pack
|
733
|
+
ensure
|
734
|
+
bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
|
735
|
+
end
|
736
|
+
|
737
|
+
# only valid for types BITMAP, ICON & CURSOR
|
738
|
+
def restore_bitmap src_fname
|
739
|
+
File.open(src_fname, "rb") do |f|
|
740
|
+
parse f
|
741
|
+
if data.first == "PNG"
|
742
|
+
"\x89PNG" +f.read(self.size-4)
|
743
|
+
else
|
744
|
+
bitmap_hdr + f.read(@palette_size + @imgdata_size)
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
def bitmap_mask src_fname
|
750
|
+
File.open(src_fname, "rb") do |f|
|
751
|
+
parse f
|
752
|
+
bmp_info_hdr = bitmap_hdr
|
753
|
+
bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
|
754
|
+
return nil if bitmap_size >= self.size
|
755
|
+
|
756
|
+
mask_size = self.size - bitmap_size
|
757
|
+
f.seek file_offset + bitmap_size
|
758
|
+
|
759
|
+
bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
|
760
|
+
bmp_info_hdr.biBitCount = 1
|
761
|
+
bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
|
762
|
+
bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
|
763
|
+
|
764
|
+
palette = [0,0xffffff].pack('V2')
|
765
|
+
@palette_size = palette.size
|
766
|
+
|
767
|
+
"BM" + [
|
768
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + mask_size,
|
769
|
+
0,
|
770
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
771
|
+
].pack("V3") + bmp_info_hdr.pack + palette + f.read(mask_size)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
# also sets the file position for restore_bitmap next call
|
776
|
+
def parse f
|
777
|
+
raise "called parse with type not set" unless self.type
|
778
|
+
#return if self.data
|
779
|
+
|
780
|
+
self.data = []
|
781
|
+
case type
|
782
|
+
when 'BITMAP','ICON'
|
783
|
+
f.seek file_offset
|
784
|
+
if f.read(4) == "\x89PNG"
|
785
|
+
data << 'PNG'
|
786
|
+
else
|
787
|
+
f.seek file_offset
|
788
|
+
data << BITMAPINFOHEADER.read(f)
|
789
|
+
end
|
790
|
+
when 'CURSOR'
|
791
|
+
f.seek file_offset
|
792
|
+
data << CURSOR_HOTSPOT.read(f)
|
793
|
+
data << BITMAPINFOHEADER.read(f)
|
794
|
+
when 'GROUP_CURSOR'
|
795
|
+
f.seek file_offset
|
796
|
+
data << CUR_ICO_HEADER.read(f)
|
797
|
+
nRead = CUR_ICO_HEADER::SIZE
|
798
|
+
data.last.wNumImages.times do
|
799
|
+
if nRead >= self.size
|
800
|
+
PEdump.logger.error "[!] refusing to read CURDIRENTRY beyond resource size"
|
801
|
+
break
|
802
|
+
end
|
803
|
+
data << CURDIRENTRY.read(f)
|
804
|
+
nRead += CURDIRENTRY::SIZE
|
805
|
+
end
|
806
|
+
when 'GROUP_ICON'
|
807
|
+
f.seek file_offset
|
808
|
+
data << CUR_ICO_HEADER.read(f)
|
809
|
+
nRead = CUR_ICO_HEADER::SIZE
|
810
|
+
data.last.wNumImages.times do
|
811
|
+
if nRead >= self.size
|
812
|
+
PEdump.logger.error "[!] refusing to read ICODIRENTRY beyond resource size"
|
813
|
+
break
|
814
|
+
end
|
815
|
+
data << ICODIRENTRY.read(f)
|
816
|
+
nRead += ICODIRENTRY::SIZE
|
817
|
+
end
|
818
|
+
when 'STRING'
|
819
|
+
f.seek file_offset
|
820
|
+
16.times do
|
821
|
+
break if f.tell >= file_offset+self.size
|
822
|
+
nChars = f.read(2).to_s.unpack('v').first.to_i
|
823
|
+
t =
|
824
|
+
if nChars*2 + 1 > self.size
|
825
|
+
# TODO: if it's not 1st string in table then truncated size must be less
|
826
|
+
PEdump.logger.error "[!] string size(#{nChars*2}) > stringtable size(#{self.size}). truncated to #{self.size-2}"
|
827
|
+
f.read(self.size-2)
|
828
|
+
else
|
829
|
+
f.read(nChars*2)
|
830
|
+
end
|
831
|
+
data <<
|
832
|
+
begin
|
833
|
+
t.force_encoding('UTF-16LE').encode!('UTF-8')
|
834
|
+
rescue
|
835
|
+
t.force_encoding('ASCII')
|
836
|
+
tt = t.size > 0x10 ? t[0,0x10].inspect+'...' : t.inspect
|
837
|
+
PEdump.logger.error "[!] cannot convert #{tt} to UTF-16"
|
838
|
+
[nChars,t].pack('va*')
|
839
|
+
end
|
840
|
+
end
|
841
|
+
# XXX: check if readed strings summary length is less than resource data length
|
842
|
+
end
|
843
|
+
|
844
|
+
data.delete_if do |x|
|
845
|
+
valid = !x.respond_to?(:valid?) || x.valid?
|
846
|
+
PEdump.logger.warn "[?] ignoring invalid #{x.class}" unless valid
|
847
|
+
!valid
|
848
|
+
end
|
849
|
+
ensure
|
850
|
+
validate
|
851
|
+
end
|
852
|
+
|
853
|
+
def validate
|
854
|
+
self.valid =
|
855
|
+
case type
|
856
|
+
when 'BITMAP','ICON','CURSOR'
|
857
|
+
data.any?{ |x| x.is_a?(BITMAPINFOHEADER) && x.valid? }
|
858
|
+
else
|
859
|
+
true
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
STRING = Struct.new(:id, :lang, :value)
|
865
|
+
|
866
|
+
def strings f=nil
|
867
|
+
r = []
|
868
|
+
Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
|
869
|
+
res.data.each_with_index do |string,idx|
|
870
|
+
r << STRING.new( ((res.id-1)<<4) + idx, res.lang, string ) unless string.empty?
|
871
|
+
end
|
872
|
+
end
|
873
|
+
r
|
874
|
+
end
|
875
|
+
|
876
|
+
# see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
|
877
|
+
|
878
|
+
class BITMAPINFOHEADER < create_struct 'V3v2V6',
|
879
|
+
:biSize, # BITMAPINFOHEADER::SIZE
|
880
|
+
:biWidth,
|
881
|
+
:biHeight,
|
882
|
+
:biPlanes,
|
883
|
+
:biBitCount,
|
884
|
+
:biCompression,
|
885
|
+
:biSizeImage,
|
886
|
+
:biXPelsPerMeter,
|
887
|
+
:biYPelsPerMeter,
|
888
|
+
:biClrUsed,
|
889
|
+
:biClrImportant
|
890
|
+
|
891
|
+
def valid?
|
892
|
+
self.biSize == 40
|
893
|
+
end
|
894
|
+
end
|
895
|
+
|
896
|
+
# http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
|
897
|
+
CUR_ICO_HEADER = create_struct('v3',
|
898
|
+
:wReserved, # always 0
|
899
|
+
:wResID, # always 2
|
900
|
+
:wNumImages # Number of cursor images/directory entries
|
901
|
+
)
|
902
|
+
|
903
|
+
CURDIRENTRY = create_struct 'v4Vv',
|
904
|
+
:wWidth,
|
905
|
+
:wHeight, # Divide by 2 to get the actual height.
|
906
|
+
:wPlanes,
|
907
|
+
:wBitCount,
|
908
|
+
:dwBytesInImage,
|
909
|
+
:wID
|
910
|
+
|
911
|
+
CURSOR_HOTSPOT = create_struct 'v2', :x, :y
|
912
|
+
|
913
|
+
ICODIRENTRY = create_struct 'C4v2Vv',
|
914
|
+
:bWidth,
|
915
|
+
:bHeight,
|
916
|
+
:bColors,
|
917
|
+
:bReserved,
|
918
|
+
:wPlanes,
|
919
|
+
:wBitCount,
|
920
|
+
:dwBytesInImage,
|
921
|
+
:wID
|
922
|
+
|
923
|
+
ROOT_RES_NAMES = [nil] + # numeration is started from 1
|
924
|
+
%w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
|
925
|
+
%w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
|
926
|
+
%w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
|
927
|
+
|
928
|
+
def resources f=nil
|
929
|
+
@resources ||= _scan_resources(f)
|
930
|
+
end
|
931
|
+
|
932
|
+
def _scan_resources f=nil, dir=nil
|
933
|
+
dir ||= resource_directory(f)
|
934
|
+
return nil unless dir
|
935
|
+
dir.entries.map do |entry|
|
936
|
+
case entry.data
|
937
|
+
when IMAGE_RESOURCE_DIRECTORY
|
938
|
+
if dir == @resource_directory # root resource directory
|
939
|
+
entry_type =
|
940
|
+
if entry.Name & 0x8000_0000 == 0
|
941
|
+
# root resource directory & entry name is a number
|
942
|
+
ROOT_RES_NAMES[entry.Name] || entry.name
|
943
|
+
else
|
944
|
+
entry.name
|
945
|
+
end
|
946
|
+
_scan_resources(f,entry.data).each do |res|
|
947
|
+
res.type = entry_type
|
948
|
+
res.parse f
|
949
|
+
end
|
950
|
+
else
|
951
|
+
_scan_resources(f,entry.data).each do |res|
|
952
|
+
res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
|
953
|
+
res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
|
954
|
+
end
|
955
|
+
end
|
956
|
+
when IMAGE_RESOURCE_DATA_ENTRY
|
957
|
+
Resource.new(
|
958
|
+
nil, # type
|
959
|
+
entry.name,
|
960
|
+
nil, # id
|
961
|
+
entry.Name, # lang
|
962
|
+
#entry.data.OffsetToData + @resource_data_base,
|
963
|
+
va2file(entry.data.OffsetToData),
|
964
|
+
entry.data.Size,
|
965
|
+
entry.data.CodePage,
|
966
|
+
entry.data.Reserved
|
967
|
+
)
|
968
|
+
else
|
969
|
+
logger.error "[!] invalid resource entry: #{entry.data.inspect}"
|
970
|
+
nil
|
971
|
+
end
|
972
|
+
end.flatten.compact
|
973
|
+
end
|
974
|
+
|
975
|
+
def packer f = nil
|
976
|
+
@packer ||= pe(f) && @pe.ioh &&
|
977
|
+
begin
|
978
|
+
if !(va=@pe.ioh.AddressOfEntryPoint)
|
979
|
+
logger.error "[?] can't find EntryPoint RVA"
|
980
|
+
nil
|
981
|
+
elsif !(ofs = va2file(va))
|
982
|
+
logger.error "[?] can't find EntryPoint RVA (0x#{va.to_s(16)}) file offset"
|
983
|
+
nil
|
984
|
+
else
|
985
|
+
require 'pedump/packer'
|
986
|
+
if PEdump::Packer.all.size == 0
|
987
|
+
logger.error "[?] no packer definitions found"
|
988
|
+
nil
|
989
|
+
else
|
990
|
+
Packer.of(f, ofs)
|
991
|
+
end
|
992
|
+
end
|
993
|
+
end
|
994
|
+
end
|
995
|
+
alias :packers :packer
|
996
|
+
end
|
997
|
+
|
998
|
+
####################################################################################
|
999
|
+
|
1000
|
+
if $0 == __FILE__
|
1001
|
+
require 'pp'
|
1002
|
+
dump = PEdump.new(ARGV.shift).dump
|
1003
|
+
if ARGV.any?
|
1004
|
+
ARGV.each do |arg|
|
1005
|
+
if dump.respond_to?(arg)
|
1006
|
+
pp dump.send(arg)
|
1007
|
+
elsif arg == 'restore_bitmaps'
|
1008
|
+
File.open(dump.fname,"rb") do |fi|
|
1009
|
+
r = dump.resources.
|
1010
|
+
find_all{ |r| %w'ICON BITMAP CURSOR'.include?(r.type) }.
|
1011
|
+
each do |r|
|
1012
|
+
fname = r.name.tr("/# ",'_')+".bmp"
|
1013
|
+
puts "[.] #{fname}"
|
1014
|
+
File.open(fname,"wb"){ |fo| fo << r.restore_bitmap(fi) }
|
1015
|
+
if mask = r.bitmap_mask(fi)
|
1016
|
+
fname.sub! '.bmp', '.mask.bmp'
|
1017
|
+
puts "[.] #{fname}"
|
1018
|
+
File.open(fname,"wb"){ |fo| fo << r.bitmap_mask(fi) }
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
exit
|
1023
|
+
else
|
1024
|
+
puts "[?] invalid arg #{arg.inspect}"
|
1025
|
+
end
|
1026
|
+
end
|
1027
|
+
exit
|
1028
|
+
end
|
1029
|
+
p dump.mz
|
1030
|
+
require './lib/hexdump_helper' if File.exist?("lib/hexdump_helper.rb")
|
1031
|
+
if defined?(HexdumpHelper)
|
1032
|
+
include HexdumpHelper
|
1033
|
+
puts hexdump(dump.dos_stub) if dump.dos_stub
|
1034
|
+
puts
|
1035
|
+
if dump.rich_hdr
|
1036
|
+
puts hexdump(dump.rich_hdr)
|
1037
|
+
puts
|
1038
|
+
p(dump.rich_hdr.decode)
|
1039
|
+
puts hexdump(dump.rich_hdr.dexor)
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
pp dump.pe
|
1043
|
+
pp dump.resources
|
1044
|
+
end
|