pedump 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/pedump +7 -0
- data/lib/pedump.rb +665 -0
- data/lib/pedump/cli.rb +359 -0
- data/pedump.gemspec +63 -0
- data/spec/pedump_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +109 -0
data/.document
ADDED
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
|
data/Gemfile.lock
ADDED
@@ -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)
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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
|
data/bin/pedump
ADDED
data/lib/pedump.rb
ADDED
@@ -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
|