pedump 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.6.4"
12
+ gem "rcov", ">= 0"
13
+ end
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ git (1.2.5)
6
+ jeweler (1.6.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rake (0.9.2.2)
11
+ rcov (0.9.11)
12
+ rspec (2.3.0)
13
+ rspec-core (~> 2.3.0)
14
+ rspec-expectations (~> 2.3.0)
15
+ rspec-mocks (~> 2.3.0)
16
+ rspec-core (2.3.1)
17
+ rspec-expectations (2.3.0)
18
+ diff-lcs (~> 1.1.2)
19
+ rspec-mocks (2.3.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bundler (~> 1.0.0)
26
+ jeweler (~> 1.6.4)
27
+ rcov
28
+ rspec (~> 2.3.0)
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Andrey "Zed" Zaikin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ = pedump
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to pedump
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Andrey "Zed" Zaikin. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "pedump"
18
+ gem.homepage = "http://github.com/zed-0xff/pedump"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{dump win32 PE executable files with a pure ruby}
21
+ gem.description = %Q{dump headers, sections, extract resources}
22
+ gem.email = "zed.0xff@gmail.com"
23
+ gem.authors = ["Andrey \"Zed\" Zaikin"]
24
+ gem.executables = %w'pedump'
25
+ gem.files.include "lib/**/*.rb"
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rspec/core'
31
+ require 'rspec/core/rake_task'
32
+ RSpec::Core::RakeTask.new(:spec) do |spec|
33
+ spec.pattern = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov = true
39
+ end
40
+
41
+ task :default => :spec
42
+
43
+ #require 'rake/rdoctask'
44
+ #Rake::RDocTask.new do |rdoc|
45
+ # version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+ #
47
+ # rdoc.rdoc_dir = 'rdoc'
48
+ # rdoc.title = "pedump #{version}"
49
+ # rdoc.rdoc_files.include('README*')
50
+ # rdoc.rdoc_files.include('lib/**/*.rb')
51
+ #end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
+ require 'pedump'
5
+ require 'pedump/cli'
6
+
7
+ PEdump::CLI.new.run
@@ -0,0 +1,665 @@
1
+ #!/usr/bin/env ruby
2
+ require 'logger'
3
+
4
+ # pedump.rb by zed_0xff
5
+ #
6
+ # http://zed.0xff.me
7
+ # http://github.com/zed-0xff
8
+
9
+ class String
10
+ def xor x
11
+ if x.is_a?(String)
12
+ r = ''
13
+ j = 0
14
+ 0.upto(self.size-1) do |i|
15
+ r << (self[i].ord^x[j].ord).chr
16
+ j+=1
17
+ j=0 if j>= x.size
18
+ end
19
+ r
20
+ else
21
+ r = ''
22
+ 0.upto(self.size-1) do |i|
23
+ r << (self[i].ord^x).chr
24
+ end
25
+ r
26
+ end
27
+ end
28
+ end
29
+
30
+ class PEdump
31
+ attr_accessor :fname, :logger
32
+ attr_reader :mz, :dos_stub, :rich_hdr, :pe, :resources, :resource_directory
33
+
34
+ alias :rich_header :rich_hdr
35
+ alias :rich :rich_hdr
36
+
37
+ class << self
38
+ def logger; @@logger; end
39
+ def logger= l; @@logger=l; end
40
+
41
+ def create_struct fmt, *args
42
+ size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
43
+ [len.to_i, 1].max *
44
+ case f
45
+ when /[aAC]/ then 1
46
+ when 'v' then 2
47
+ when 'V' then 4
48
+ when 'Q' then 8
49
+ else raise "unknown fmt #{f.inspect}"
50
+ end
51
+ end.inject(&:+)
52
+
53
+ Struct.new( *args ).tap do |x|
54
+ x.const_set 'FORMAT', fmt
55
+ x.const_set 'SIZE', size
56
+ x.class_eval do
57
+ def pack
58
+ to_a.pack self.class.const_get('FORMAT')
59
+ end
60
+ def empty?
61
+ to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
62
+ end
63
+ end
64
+ def x.read file, size = nil
65
+ size ||= const_get 'SIZE'
66
+ new(*file.read(size).to_s.unpack(const_get('FORMAT')))
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ # http://www.delorie.com/djgpp/doc/exe/
73
+ MZ = create_struct( "a2v13",
74
+ :signature,
75
+ :bytes_in_last_block,
76
+ :blocks_in_file,
77
+ :num_relocs,
78
+ :header_paragraphs,
79
+ :min_extra_paragraphs,
80
+ :max_extra_paragraphs,
81
+ :ss,
82
+ :sp,
83
+ :checksum,
84
+ :ip,
85
+ :cs,
86
+ :reloc_table_offset,
87
+ :overlay_number
88
+ )
89
+
90
+ PE = Struct.new(
91
+ :signature, # "PE\x00\x00"
92
+ :image_file_header,
93
+ :image_optional_header,
94
+ :section_table
95
+ )
96
+ class PE; alias :ifh :image_file_header; end
97
+ class PE; alias :ioh :image_optional_header; end
98
+
99
+ # http://msdn.microsoft.com/en-us/library/ms809762.aspx
100
+ IMAGE_FILE_HEADER = create_struct( 'v2V3v2',
101
+ :Machine, # w
102
+ :NumberOfSections, # w
103
+ :TimeDateStamp, # dw
104
+ :PointerToSymbolTable, # dw
105
+ :NumberOfSymbols, # dw
106
+ :SizeOfOptionalHeader, # w
107
+ :Characteristics # w
108
+ )
109
+ class IMAGE_FILE_HEADER
110
+ def initialize *args
111
+ super
112
+ self.TimeDateStamp = Time.at(self.TimeDateStamp)
113
+ end
114
+ def method_missing mname
115
+ mname = mname.to_s.capitalize
116
+ self.send(mname) if self.respond_to?(mname)
117
+ end
118
+ end
119
+
120
+ # http://msdn.microsoft.com/en-us/library/ms809762.aspx
121
+ IMAGE_OPTIONAL_HEADER = create_struct( 'vC2V9v6V4v2V6',
122
+ :Magic, # w
123
+ :MajorLinkerVersion, :MinorLinkerVersion, # 2b
124
+ :SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, # 9dw
125
+ :BaseOfCode, :BaseOfData, :ImageBase, :SectionAlignment, :FileAlignment,
126
+ :MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
127
+ :MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
128
+ :Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
129
+ :Subsystem, :DllCharacteristics, # 2w
130
+ :SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 6dw
131
+ :LoaderFlags, :NumberOfRvaAndSizes,
132
+ :DataDirectory # readed manually
133
+ )
134
+
135
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx)
136
+ IMAGE_OPTIONAL_HEADER64 = create_struct( 'vC2V5QV2v6V4v2Q4V2',
137
+ :Magic, # w
138
+ :MajorLinkerVersion, :MinorLinkerVersion, # 2b
139
+ :SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, :BaseOfCode, # 5dw
140
+ :ImageBase, # qw
141
+ :SectionAlignment, :FileAlignment, # 2dw
142
+ :MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
143
+ :MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
144
+ :Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
145
+ :Subsystem, :DllCharacteristics, # 2w
146
+ :SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 4qw
147
+ :LoaderFlags, :NumberOfRvaAndSizes, #2dw
148
+ :DataDirectory # readed manually
149
+ )
150
+
151
+ class IMAGE_OPTIONAL_HEADER
152
+ def self.read file, size = SIZE
153
+ usual_size = 224
154
+ PEdump.logger.warn "[?] unusual size of IMAGE_OPTIONAL_HEADER = #{size} (must be #{usual_size})" if size != usual_size
155
+ new(*file.read([size,SIZE].min).unpack(FORMAT)).tap do |ioh|
156
+ ioh.DataDirectory = []
157
+
158
+ # check if "...this address is outside the memory mapped file and is zeroed by the OS"
159
+ # see http://www.phreedom.org/solar/code/tinype/, section "Removing the data directories"
160
+ ioh.each_pair{ |k,v| ioh[k] = 0 if v.nil? }
161
+
162
+ # http://opcode0x90.wordpress.com/2007/04/22/windows-loader-does-it-differently/
163
+ # maximum of 0x10 entries, even if bigger
164
+ [0x10,ioh.NumberOfRvaAndSizes].min.times do |idx|
165
+ ioh.DataDirectory << IMAGE_DATA_DIRECTORY.read(file)
166
+ ioh.DataDirectory.last.type = IMAGE_DATA_DIRECTORY::TYPES[idx]
167
+ end
168
+ #ioh.DataDirectory.pop while ioh.DataDirectory.last.empty?
169
+ end
170
+ end
171
+ end
172
+
173
+ class IMAGE_OPTIONAL_HEADER64
174
+ def self.read file, size = SIZE
175
+ usual_size = 240
176
+ PEdump.logger.warn "[?] unusual size of IMAGE_OPTIONAL_HEADER = #{size} (must be #{usual_size})" if size != usual_size
177
+ new(*file.read([size,SIZE].min).unpack(FORMAT)).tap do |ioh|
178
+ ioh.DataDirectory = []
179
+
180
+ # check if "...this address is outside the memory mapped file and is zeroed by the OS"
181
+ # see http://www.phreedom.org/solar/code/tinype/, section "Removing the data directories"
182
+ ioh.each_pair{ |k,v| ioh[k] = 0 if v.nil? }
183
+
184
+ # http://opcode0x90.wordpress.com/2007/04/22/windows-loader-does-it-differently/
185
+ # maximum of 0x10 entries, even if bigger
186
+ [0x10,ioh.NumberOfRvaAndSizes].min.times do |idx|
187
+ ioh.DataDirectory << IMAGE_DATA_DIRECTORY.read(file)
188
+ ioh.DataDirectory.last.type = IMAGE_DATA_DIRECTORY::TYPES[idx]
189
+ end
190
+ #ioh.DataDirectory.pop while ioh.DataDirectory.last.empty?
191
+ end
192
+ end
193
+ end
194
+
195
+ IMAGE_DATA_DIRECTORY = create_struct( "VV", :va, :size, :type )
196
+ IMAGE_DATA_DIRECTORY::TYPES =
197
+ %w'EXPORT IMPORT RESOURCE EXCEPTION SECURITY BASERELOC DEBUG ARCHITECTURE GLOBALPTR TLS LOAD_CONFIG
198
+ Bound_IAT IAT Delay_IAT CLR_Header'
199
+ IMAGE_DATA_DIRECTORY::TYPES.each_with_index do |type,idx|
200
+ IMAGE_DATA_DIRECTORY.const_set(type,idx)
201
+ end
202
+
203
+ IMAGE_SECTION_HEADER = create_struct( 'A8V6v2V',
204
+ :Name, # A8 6dw
205
+ :VirtualSize, :VirtualAddress, :SizeOfRawData, :PointerToRawData, :PointerToRelocations, :PointerToLinenumbers,
206
+ :NumberOfRelocations, :NumberOfLinenumbers, # 2w
207
+ :Characteristics # dw
208
+ )
209
+ class IMAGE_SECTION_HEADER
210
+ alias :flags :Characteristics
211
+ def flags_desc
212
+ r = ''
213
+ f = self.flags.to_i
214
+ r << (f & 0x4000_0000 > 0 ? 'R' : '-')
215
+ r << (f & 0x8000_0000 > 0 ? 'W' : '-')
216
+ r << (f & 0x2000_0000 > 0 ? 'X' : '-')
217
+ r << ' CODE' if f & 0x20 > 0
218
+
219
+ # section contains initialized data. Almost all sections except executable and the .bss section have this flag set
220
+ r << ' IDATA' if f & 0x40 > 0
221
+
222
+ # section contains uninitialized data (for example, the .bss section)
223
+ r << ' UDATA' if f & 0x80 > 0
224
+
225
+ r << ' DISCARDABLE' if f & 0x02000000 > 0
226
+ r << ' SHARED' if f & 0x10000000 > 0
227
+ r
228
+ end
229
+ end
230
+
231
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx
232
+ IMAGE_SUBSYSTEMS = %w'UNKNOWN NATIVE WINDOWS_GUI WINDOWS_CUI' + [nil,'OS2_CUI',nil,'POSIX_CUI',nil] +
233
+ %w'WINDOWS_CE_GUI EFI_APPLICATION EFI_BOOT_SERVICE_DRIVER EFI_RUNTIME_DRIVER EFI_ROM XBOX' +
234
+ [nil, 'WINDOWS_BOOT_APPLICATION']
235
+
236
+ # http://ntcore.com/files/richsign.htm
237
+ class RichHdr < String
238
+ attr_accessor :offset, :key # xor key
239
+
240
+ Entry = Struct.new(:version,:id,:times)
241
+ class Entry
242
+ def inspect
243
+ "<id=#{id}, version=#{version}, times=#{times}>"
244
+ end
245
+ end
246
+
247
+ def self.from_dos_stub stub
248
+ key = stub[stub.index('Rich')+4,4]
249
+ start_idx = stub.index(key.xor('DanS'))
250
+ end_idx = stub.index('Rich')+8
251
+ if stub[end_idx..-1].tr("\x00",'') != ''
252
+ raise "[!] non-zero dos stub after rich_hdr: #{stub[end_idx..-1].inspect}"
253
+ end
254
+ RichHdr.new(stub[start_idx, end_idx-start_idx]).tap do |x|
255
+ x.key = key
256
+ x.offset = stub.offset + start_idx
257
+ end
258
+ end
259
+
260
+ def dexor
261
+ self[4..-9].sub(/\A(#{Regexp::escape(key)})+/,'').xor(key)
262
+ end
263
+
264
+ def decode
265
+ x = dexor
266
+ raise "dexored size must be a multiple of 8" unless x.size%8 == 0
267
+ x.unpack('vvV'*(x.size/8)).each_slice(3).map{ |slice| Entry.new(*slice)}
268
+ end
269
+ end
270
+
271
+ class DOSStub < String
272
+ attr_accessor :offset
273
+ end
274
+
275
+ def initialize fname, params = {}
276
+ @fname = fname
277
+ @logger = params[:logger] ||
278
+ begin
279
+ Logger.new(STDERR).tap do |l|
280
+ l.formatter = proc{ |_,_,_,msg| "#{msg}\n" }
281
+ l.level = Logger::WARN
282
+ end
283
+ end
284
+ @@logger = @logger
285
+ end
286
+
287
+ def logger= l
288
+ @logger = @@logger = l
289
+ end
290
+
291
+ def self.dump fname
292
+ new(fname).dump
293
+ end
294
+
295
+ def dump
296
+ File.open(@fname) do |f|
297
+ mz_hdr = f.read 0x40
298
+ @mz = MZ.new(*mz_hdr.unpack(MZ::FORMAT))
299
+ if @mz.signature != 'MZ' && @mz.signature != 'ZM'
300
+ logger.warn "[?] no MZ header (want: 'MZ' or 'ZM', got: #{@mz.signature.inspect}"
301
+ end
302
+ @pe_offset = mz_hdr.unpack("@60V").first
303
+ logger.info "[.] PE offset = 0x%x" % @pe_offset
304
+
305
+ if @pe_offset > 0x1000
306
+ logger.error "[!] PE offset is too big!"
307
+ else
308
+ dos_stub_offset = @mz.header_paragraphs * 0x10
309
+ dos_stub_size = @pe_offset - dos_stub_offset
310
+ if dos_stub_size > 0
311
+ f.seek dos_stub_offset
312
+ @dos_stub = DOSStub.new f.read(dos_stub_size)
313
+ @dos_stub.offset = dos_stub_offset
314
+ if @dos_stub['Rich']
315
+ @rich_hdr = RichHdr.from_dos_stub(@dos_stub)
316
+ @dos_stub[@dos_stub.index(@rich_hdr)..-1] = ''
317
+ end
318
+ else
319
+ logger.info "[.] uncommon DOS stub size = #{dos_stub_size}"
320
+ end
321
+ end
322
+
323
+ f.seek @pe_offset
324
+ @pe = PE.new(*f.read(4).unpack("a4"))
325
+ logger.warn "[?] no PE header (want: 'PE\\x00\\x00', got: #{@pe.signature.inspect})" if @pe.signature != "PE\x00\x00"
326
+ logger.error "[!] 'NE' format is not supported!" if @pe.signature == "NE\x00\x00"
327
+ @pe.image_file_header = IMAGE_FILE_HEADER.read(f)
328
+ if @pe.ifh.SizeOfOptionalHeader > 0
329
+ if @pe.ifh.Machine == 0x8664
330
+ @pe.image_optional_header = IMAGE_OPTIONAL_HEADER64.read(f, @pe.ifh.SizeOfOptionalHeader)
331
+ else
332
+ @pe.image_optional_header = IMAGE_OPTIONAL_HEADER.read(f, @pe.ifh.SizeOfOptionalHeader)
333
+ end
334
+ end
335
+ @pe.section_table = @pe.ifh.NumberOfSections.times.map do
336
+ IMAGE_SECTION_HEADER.read(f)
337
+ end
338
+
339
+ @resource_directory = _read_resource_directory_tree(f)
340
+ end
341
+ self
342
+ end
343
+
344
+ def data_directory; @pe.ioh && @pe.ioh.DataDirectory; end
345
+ def sections; @pe.section_table; end
346
+ alias :section_table :sections
347
+
348
+ IMAGE_RESOURCE_DIRECTORY = create_struct 'V2v4',
349
+ :Characteristics, :TimeDateStamp, # 2dw
350
+ :MajorVersion, :MinorVersion, :NumberOfNamedEntries, :NumberOfIdEntries, # 4w
351
+ :entries # manual
352
+ class IMAGE_RESOURCE_DIRECTORY
353
+ class << self
354
+ attr_accessor :base
355
+ alias :read_without_children :read
356
+ def read f
357
+ read_without_children(f).tap do |r|
358
+ r.entries = (r.NumberOfNamedEntries + r.NumberOfIdEntries).times.map do
359
+ IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
360
+ end.each do |entry|
361
+ entry.name =
362
+ if entry.Name & 0x8000_0000 > 0
363
+ # Name is an address of unicode string
364
+ f.seek base + entry.Name & 0x7fff_ffff
365
+ nChars = f.read(2).unpack("v").first
366
+ f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
367
+ else
368
+ # Name is a numeric id
369
+ "##{entry.Name}"
370
+ end
371
+ f.seek base + entry.OffsetToData & 0x7fff_ffff
372
+ entry.data =
373
+ if entry.OffsetToData & 0x8000_0000 > 0
374
+ # child is a directory
375
+ IMAGE_RESOURCE_DIRECTORY.read(f)
376
+ else
377
+ # child is a resource
378
+ IMAGE_RESOURCE_DATA_ENTRY.read(f)
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ IMAGE_RESOURCE_DIRECTORY_ENTRY = create_struct 'V2',
387
+ :Name, :OffsetToData,
388
+ :name, :data
389
+
390
+ IMAGE_RESOURCE_DATA_ENTRY = create_struct 'V4',
391
+ :OffsetToData, :Size, :CodePage, :Reserved
392
+
393
+ def va2file va
394
+ sections.each do |s|
395
+ if (s.VirtualAddress..(s.VirtualAddress+s.VirtualSize)).include?(va)
396
+ return va - s.VirtualAddress + s.PointerToRawData
397
+ end
398
+ end
399
+ nil
400
+ end
401
+
402
+ def _read_resource_directory_tree f
403
+ return nil unless @pe.ioh
404
+ res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
405
+ return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
406
+ res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
407
+ res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
408
+ unless res_section
409
+ logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
410
+ return []
411
+ end
412
+ f.seek res_section.PointerToRawData
413
+ IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
414
+ @resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
415
+ IMAGE_RESOURCE_DIRECTORY.read(f)
416
+ end
417
+
418
+ class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data)
419
+ def bitmap_hdr
420
+ bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
421
+ raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
422
+
423
+ bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
424
+
425
+ colors_used = bmp_info_hdr.biClrUsed
426
+ colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
427
+
428
+ # XXX: one byte in each color is unused!
429
+ @palette_size = colors_used * 4 # each color takes 4 bytes
430
+
431
+ # scanlines are DWORD-aligned and padded to DWORD-align with zeroes
432
+ # XXX: some data may be hidden in padding bytes!
433
+ scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
434
+ scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
435
+
436
+ @imgdata_size = scanline_size * bmp_info_hdr.biHeight
437
+ "BM" + [
438
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
439
+ 0,
440
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size
441
+ ].pack("V3") + bmp_info_hdr.pack
442
+ ensure
443
+ bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
444
+ end
445
+
446
+ # only valid for types BITMAP, ICON & CURSOR
447
+ def restore_bitmap src_fname
448
+ File.open(src_fname, "rb") do |f|
449
+ parse f
450
+ bitmap_hdr + f.read(@palette_size + @imgdata_size)
451
+ end
452
+ end
453
+
454
+ def bitmap_and_mask src_fname
455
+ File.open(src_fname, "rb") do |f|
456
+ parse f
457
+ bmp_info_hdr = bitmap_hdr
458
+ bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
459
+ return nil if bitmap_size >= self.size
460
+
461
+ and_mask_size = self.size - bitmap_size
462
+ f.seek file_offset + bitmap_size
463
+
464
+ bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
465
+ bmp_info_hdr.biBitCount = 1
466
+ bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
467
+ bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
468
+
469
+ palette = [0,0xffffff].pack('V2')
470
+ @palette_size = palette.size
471
+
472
+ "BM" + [
473
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size + and_mask_size,
474
+ 0,
475
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size
476
+ ].pack("V3") + bmp_info_hdr.pack + palette + f.read(and_mask_size)
477
+ end
478
+ end
479
+
480
+ # also sets the file position for restore_bitmap next call
481
+ def parse f
482
+ raise "called parse with type not set" unless self.type
483
+ #return if self.data
484
+
485
+ self.data = []
486
+ case type
487
+ when 'BITMAP','ICON'
488
+ f.seek file_offset
489
+ data << BITMAPINFOHEADER.read(f)
490
+ when 'CURSOR'
491
+ f.seek file_offset
492
+ data << CURSOR_HOTSPOT.read(f)
493
+ data << BITMAPINFOHEADER.read(f)
494
+ when 'GROUP_CURSOR'
495
+ f.seek file_offset
496
+ data << CUR_ICO_HEADER.read(f)
497
+ data.last.wNumImages.times do
498
+ data << CURDIRENTRY.read(f)
499
+ end
500
+ when 'GROUP_ICON'
501
+ f.seek file_offset
502
+ data << CUR_ICO_HEADER.read(f)
503
+ data.last.wNumImages.times do
504
+ data << ICODIRENTRY.read(f)
505
+ end
506
+ when 'STRING'
507
+ f.seek file_offset
508
+ 16.times do
509
+ nChars = f.read(2).unpack('v').first
510
+ data << f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
511
+ end
512
+ # XXX: check if readed strings summary length is less than resource data length
513
+ end
514
+ end
515
+ end
516
+
517
+ STRING = Struct.new(:id, :lang, :value)
518
+
519
+ def strings
520
+ r = []
521
+ Array(resources).find_all{ |x| x.type == 'STRING'}.each do |res|
522
+ res.data.each_with_index do |string,idx|
523
+ r << STRING.new( ((res.id-1)<<4) + idx, res.lang, string ) unless string.empty?
524
+ end
525
+ end
526
+ r
527
+ end
528
+
529
+ # see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
530
+
531
+ BITMAPINFOHEADER = create_struct 'V3v2V6',
532
+ :biSize, # BITMAPINFOHEADER::SIZE
533
+ :biWidth,
534
+ :biHeight,
535
+ :biPlanes,
536
+ :biBitCount,
537
+ :biCompression,
538
+ :biSizeImage,
539
+ :biXPelsPerMeter,
540
+ :biYPelsPerMeter,
541
+ :biClrUsed,
542
+ :biClrImportant
543
+
544
+ # http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
545
+ CUR_ICO_HEADER = create_struct('v3',
546
+ :wReserved, # always 0
547
+ :wResID, # always 2
548
+ :wNumImages # Number of cursor images/directory entries
549
+ )
550
+
551
+ CURDIRENTRY = create_struct 'v4Vv',
552
+ :wWidth,
553
+ :wHeight, # Divide by 2 to get the actual height.
554
+ :wPlanes,
555
+ :wBitCount,
556
+ :dwBytesInImage,
557
+ :wID
558
+
559
+ CURSOR_HOTSPOT = create_struct 'v2', :x, :y
560
+
561
+ ICODIRENTRY = create_struct 'C4v2Vv',
562
+ :bWidth,
563
+ :bHeight,
564
+ :bColors,
565
+ :bReserved,
566
+ :wPlanes,
567
+ :wBitCount,
568
+ :dwBytesInImage,
569
+ :wID
570
+
571
+ ROOT_RES_NAMES = [nil] + # numeration is started from 1
572
+ %w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
573
+ %w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
574
+ %w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
575
+
576
+ def resources dir = @resource_directory
577
+ return nil unless dir
578
+ dir.entries.map do |entry|
579
+ case entry.data
580
+ when IMAGE_RESOURCE_DIRECTORY
581
+ if dir == @resource_directory # root resource directory
582
+ entry_type =
583
+ if entry.Name & 0x8000_0000 == 0
584
+ # root resource directory & entry name is a number
585
+ ROOT_RES_NAMES[entry.Name] || entry.name
586
+ else
587
+ entry.name
588
+ end
589
+ File.open(@fname,"rb") do |f|
590
+ resources(entry.data).each do |res|
591
+ res.type = entry_type
592
+ res.parse f
593
+ end
594
+ end
595
+ else
596
+ resources(entry.data).each do |res|
597
+ res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
598
+ res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
599
+ end
600
+ end
601
+ when IMAGE_RESOURCE_DATA_ENTRY
602
+ Resource.new(
603
+ nil, # type
604
+ entry.name,
605
+ nil, # id
606
+ entry.Name, # lang
607
+ entry.data.OffsetToData + @resource_data_base,
608
+ entry.data.Size,
609
+ entry.data.CodePage,
610
+ entry.data.Reserved
611
+ )
612
+ else
613
+ raise "[!] invalid resource entry: #{entry.data.inspect}"
614
+ end
615
+ end.flatten
616
+ end
617
+ end
618
+
619
+ ####################################################################################
620
+
621
+ if $0 == __FILE__
622
+ require 'pp'
623
+ dump = PEdump.new(ARGV.shift).dump
624
+ if ARGV.any?
625
+ ARGV.each do |arg|
626
+ if dump.respond_to?(arg)
627
+ pp dump.send(arg)
628
+ elsif arg == 'restore_bitmaps'
629
+ File.open(dump.fname,"rb") do |fi|
630
+ r = dump.resources.
631
+ find_all{ |r| %w'ICON BITMAP CURSOR'.include?(r.type) }.
632
+ each do |r|
633
+ fname = r.name.tr("/# ",'_')+".bmp"
634
+ puts "[.] #{fname}"
635
+ File.open(fname,"wb"){ |fo| fo << r.restore_bitmap(fi) }
636
+ if and_mask = r.bitmap_and_mask(fi)
637
+ fname.sub! '.bmp', '.and_mask.bmp'
638
+ puts "[.] #{fname}"
639
+ File.open(fname,"wb"){ |fo| fo << r.bitmap_and_mask(fi) }
640
+ end
641
+ end
642
+ end
643
+ exit
644
+ else
645
+ puts "[?] invalid arg #{arg.inspect}"
646
+ end
647
+ end
648
+ exit
649
+ end
650
+ p dump.mz
651
+ require './lib/hexdump_helper' if File.exist?("lib/hexdump_helper.rb")
652
+ if defined?(HexdumpHelper)
653
+ include HexdumpHelper
654
+ puts hexdump(dump.dos_stub) if dump.dos_stub
655
+ puts
656
+ if dump.rich_hdr
657
+ puts hexdump(dump.rich_hdr)
658
+ puts
659
+ p(dump.rich_hdr.decode)
660
+ puts hexdump(dump.rich_hdr.dexor)
661
+ end
662
+ end
663
+ pp dump.pe
664
+ pp dump.resources
665
+ end