pedump 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +20 -0
- data/README.md +410 -0
- data/Rakefile +179 -0
- data/VERSION +1 -0
- data/bin/pedump +7 -0
- data/data/fs.txt +224 -0
- data/data/jc-userdb.txt +14371 -0
- data/data/sig.bin +0 -0
- data/data/signatures.txt +678 -0
- data/data/userdb.txt +14083 -0
- data/lib/pedump.rb +868 -0
- data/lib/pedump/cli.rb +804 -0
- data/lib/pedump/comparer.rb +147 -0
- data/lib/pedump/composite_io.rb +56 -0
- data/lib/pedump/core.rb +38 -0
- data/lib/pedump/core_ext/try.rb +57 -0
- data/lib/pedump/loader.rb +393 -0
- data/lib/pedump/loader/minidump.rb +351 -0
- data/lib/pedump/loader/section.rb +57 -0
- data/lib/pedump/logger.rb +67 -0
- data/lib/pedump/ne.rb +425 -0
- data/lib/pedump/ne/version_info.rb +171 -0
- data/lib/pedump/packer.rb +173 -0
- data/lib/pedump/pe.rb +121 -0
- data/lib/pedump/resources.rb +436 -0
- data/lib/pedump/security.rb +58 -0
- data/lib/pedump/sig_parser.rb +507 -0
- data/lib/pedump/tls.rb +17 -0
- data/lib/pedump/unpacker.rb +26 -0
- data/lib/pedump/unpacker/aspack.rb +858 -0
- data/lib/pedump/unpacker/upx.rb +13 -0
- data/lib/pedump/version.rb +10 -0
- data/lib/pedump/version_info.rb +171 -0
- data/misc/aspack/Makefile +3 -0
- data/misc/aspack/aspack_unlzx.c +92 -0
- data/misc/aspack/lzxdec.c +479 -0
- data/misc/aspack/lzxdec.h +56 -0
- data/misc/nedump.c +751 -0
- data/pedump.gemspec +109 -0
- metadata +227 -0
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'pedump/sig_parser'
|
2
|
+
|
3
|
+
class PEdump
|
4
|
+
class Packer < Struct.new(:name, :re, :ep_only, :size)
|
5
|
+
|
6
|
+
DATA_ROOT = File.dirname(File.dirname(File.dirname(__FILE__)))
|
7
|
+
BIN_SIGS_FILE = File.join(DATA_ROOT, "data", "sig.bin")
|
8
|
+
|
9
|
+
class Match < Struct.new(:offset, :packer)
|
10
|
+
def name
|
11
|
+
packer.name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Guess < Match
|
16
|
+
def initialize name
|
17
|
+
self.offset = 0
|
18
|
+
self.packer = Packer.new(name, nil, nil, 0)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def all
|
24
|
+
@@all ||=
|
25
|
+
begin
|
26
|
+
r = unmarshal
|
27
|
+
unless r
|
28
|
+
msg = "[?] #{self}: unmarshal failed, using slow text parsing instead"
|
29
|
+
if PEdump.respond_to?(:logger) && PEdump.logger
|
30
|
+
PEdump.logger.warn msg
|
31
|
+
else
|
32
|
+
STDERR.puts msg
|
33
|
+
end
|
34
|
+
r = SigParser.parse
|
35
|
+
end
|
36
|
+
r
|
37
|
+
end
|
38
|
+
end
|
39
|
+
alias :load :all
|
40
|
+
|
41
|
+
# default deep-scan flag
|
42
|
+
@@deep = false
|
43
|
+
|
44
|
+
def default_deep
|
45
|
+
@@deep
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_deep= value
|
49
|
+
@@deep = value
|
50
|
+
end
|
51
|
+
|
52
|
+
def max_size
|
53
|
+
@@max_size ||= all.map(&:size).max
|
54
|
+
end
|
55
|
+
|
56
|
+
def of data, h = {}
|
57
|
+
if data.respond_to?(:read) && data.respond_to?(:seek) && h[:pedump]
|
58
|
+
of_pedump data, h
|
59
|
+
elsif data.respond_to?(:read) && data.respond_to?(:seek) && h[:ep_offset]
|
60
|
+
of_pe_file data, h
|
61
|
+
else
|
62
|
+
of_data data
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# try to determine packer of FILE f, h[:pedump] is a PEdump instance
|
67
|
+
def of_pedump f, h
|
68
|
+
pedump = h[:pedump]
|
69
|
+
pe = pedump.pe
|
70
|
+
if !(va=pe.ioh.AddressOfEntryPoint)
|
71
|
+
pedump.logger.error "[?] can't find EntryPoint RVA"
|
72
|
+
nil
|
73
|
+
elsif va == 0 && pe.dll?
|
74
|
+
pedump.logger.debug "[.] it's a DLL with no EntryPoint"
|
75
|
+
nil
|
76
|
+
elsif !(ofs = pedump.va2file(va))
|
77
|
+
pedump.logger.error "[?] can't find EntryPoint RVA (0x#{va.to_s(16)}) file offset"
|
78
|
+
nil
|
79
|
+
else
|
80
|
+
r = of_pe_file(f, h.merge({:ep_offset => ofs}))
|
81
|
+
return r if r && r.any?
|
82
|
+
|
83
|
+
# nothing found, try to guess by pe section names
|
84
|
+
if pedump.sections
|
85
|
+
if pedump.sections.any?{ |s| s.Name.to_s =~ /upx/i }
|
86
|
+
return [Guess.new('UPX?')]
|
87
|
+
end
|
88
|
+
if pedump.sections.any?{ |s| s.Name.to_s =~ /aspack/i }
|
89
|
+
return [Guess.new('ASPack?')]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# try to determine packer of FILE f, ep_offset - offset to entrypoint from start of file
|
97
|
+
def of_pe_file f, h
|
98
|
+
h[:deep] = @@deep unless h.key?(:deep)
|
99
|
+
h[:deep] = 1 if h[:deep] == true
|
100
|
+
h[:deep] = 0 if h[:deep] == false
|
101
|
+
|
102
|
+
begin
|
103
|
+
f.seek(h[:ep_offset]) # offset of PE EntryPoint from start of file
|
104
|
+
rescue
|
105
|
+
if h[:pedump] && h[:pedump].logger
|
106
|
+
h[:pedump].logger.warn "[?] failed to seek to EP at #{h[:ep_offset]}, skipping packer detect"
|
107
|
+
end
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
r = Array(of_data(f.read(max_size)))
|
112
|
+
return r if r && r.any? && h[:deep] < 2
|
113
|
+
r += scan_whole_file(f,
|
114
|
+
:limit => (h[:deep] > 0 ? nil : 1048576),
|
115
|
+
:deep => h[:deep]
|
116
|
+
) # scan only 1st mb unless :deep
|
117
|
+
end
|
118
|
+
|
119
|
+
BLOCK_SIZE = 0x10000
|
120
|
+
|
121
|
+
def scan_whole_file f, h = {}
|
122
|
+
h[:limit] ||= f.size
|
123
|
+
f.seek( pos = 0 )
|
124
|
+
buf = ''.force_encoding('binary')
|
125
|
+
sigs =
|
126
|
+
if h[:deep].is_a?(Numeric) && h[:deep] > 1
|
127
|
+
self.all
|
128
|
+
else
|
129
|
+
self.find_all{ |sig| !sig.ep_only }
|
130
|
+
end
|
131
|
+
r = []
|
132
|
+
while true
|
133
|
+
f.read BLOCK_SIZE, buf
|
134
|
+
pos += buf.size
|
135
|
+
sigs.each do |sig|
|
136
|
+
if idx = buf.index(sig.re)
|
137
|
+
r << Match.new(f.tell-buf.size+idx, sig)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
break if f.eof? || pos >= h[:limit]
|
141
|
+
# overlap the read for the case when read buffer boundary breaks signature
|
142
|
+
f.seek -max_size-2, IO::SEEK_CUR
|
143
|
+
pos -= (max_size+2)
|
144
|
+
end
|
145
|
+
r
|
146
|
+
end
|
147
|
+
|
148
|
+
def of_data data
|
149
|
+
r = []
|
150
|
+
return r unless data
|
151
|
+
each do |packer|
|
152
|
+
if (idx=data.index(packer.re)) == 0
|
153
|
+
r << Match.new(idx, packer)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
r.any? ? r.sort_by{ |x| -x.packer.size } : nil
|
157
|
+
end
|
158
|
+
|
159
|
+
def method_missing *args, &block
|
160
|
+
all.respond_to?(args.first) ? all.send(*args,&block) : super
|
161
|
+
end
|
162
|
+
|
163
|
+
def unmarshal
|
164
|
+
File.open(BIN_SIGS_FILE,"rb") do |f|
|
165
|
+
Marshal.load(f)
|
166
|
+
end
|
167
|
+
rescue
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/pedump/pe.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'pedump/composite_io'
|
2
|
+
|
3
|
+
class PEdump
|
4
|
+
class PE < Struct.new(
|
5
|
+
:signature, # "PE\x00\x00"
|
6
|
+
:image_file_header,
|
7
|
+
:image_optional_header, # includes data directory
|
8
|
+
:section_table
|
9
|
+
)
|
10
|
+
alias :ifh :image_file_header
|
11
|
+
alias :ifh= :image_file_header=
|
12
|
+
alias :ioh :image_optional_header
|
13
|
+
alias :ioh= :image_optional_header=
|
14
|
+
alias :sections :section_table
|
15
|
+
alias :sections= :section_table=
|
16
|
+
def x64?
|
17
|
+
ifh && ifh.Machine == 0x8664
|
18
|
+
end
|
19
|
+
def dll?
|
20
|
+
ifh && ifh.flags.include?('DLL')
|
21
|
+
end
|
22
|
+
|
23
|
+
def pack
|
24
|
+
signature + ifh.pack + ioh.pack
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read f, args = {}
|
28
|
+
force = args[:force]
|
29
|
+
|
30
|
+
pe_offset = f.tell
|
31
|
+
pe_sig = f.read 4
|
32
|
+
#logger.error "[!] 'NE' format is not supported!" if pe_sig == "NE\x00\x00"
|
33
|
+
if pe_sig != "PE\x00\x00"
|
34
|
+
if force
|
35
|
+
logger.warn "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect})"
|
36
|
+
else
|
37
|
+
logger.debug "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect}). (not forced)"
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
PE.new(pe_sig).tap do |pe|
|
42
|
+
pe.image_file_header = IMAGE_FILE_HEADER.read(f)
|
43
|
+
ioh_offset = f.tell # offset to IMAGE_OPTIONAL_HEADER
|
44
|
+
if pe.ifh.SizeOfOptionalHeader.to_i > 0
|
45
|
+
if pe.x64?
|
46
|
+
pe.image_optional_header = IMAGE_OPTIONAL_HEADER64.read(f, pe.ifh.SizeOfOptionalHeader)
|
47
|
+
else
|
48
|
+
pe.image_optional_header = IMAGE_OPTIONAL_HEADER32.read(f, pe.ifh.SizeOfOptionalHeader)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if (nToRead=pe.ifh.NumberOfSections.to_i) > 0xffff
|
53
|
+
if force.is_a?(Numeric) && force > 1
|
54
|
+
logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). forced. reading all"
|
55
|
+
else
|
56
|
+
logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). not forced, reading first 65535"
|
57
|
+
nToRead = 65535
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# The Windows loader expects to find the PE section headers after the optional header. It calculates the address of the first section header by adding SizeOfOptionalHeader to the beginning of the optional header.
|
62
|
+
# // http://www.phreedom.org/research/tinype/
|
63
|
+
f.seek( ioh_offset + pe.ifh.SizeOfOptionalHeader.to_i )
|
64
|
+
pe.sections = []
|
65
|
+
nToRead.times do
|
66
|
+
break if f.eof?
|
67
|
+
pe.sections << IMAGE_SECTION_HEADER.read(f)
|
68
|
+
end
|
69
|
+
|
70
|
+
if pe.sections.any?
|
71
|
+
# zero all missing values of last section
|
72
|
+
pe.sections.last.tap do |last_section|
|
73
|
+
last_section.each_pair do |k,v|
|
74
|
+
last_section[k] = 0 if v.nil?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
pe_end = f.tell
|
80
|
+
if s=pe.sections.find{ |s| (pe_offset...pe_end).include?(s.va) }
|
81
|
+
if args[:pass2]
|
82
|
+
# already called with CompositeIO ?
|
83
|
+
logger.error "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! 2nd time?!"
|
84
|
+
|
85
|
+
elsif pe_end-pe_offset < 0x100_000
|
86
|
+
logger.warn "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! trying to rebuild..."
|
87
|
+
f.seek pe_offset
|
88
|
+
data = f.read(s.va-pe_offset)
|
89
|
+
f.seek s.PointerToRawData
|
90
|
+
io = CompositeIO.new(StringIO.new(data), f)
|
91
|
+
args1 = args.dup
|
92
|
+
args1[:pass2] = true
|
93
|
+
return PE.read(io, args1)
|
94
|
+
else
|
95
|
+
logger.error "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! too big to rebuild!"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.logger; PEdump.logger; end
|
102
|
+
end
|
103
|
+
|
104
|
+
def pe f=@io
|
105
|
+
@pe ||=
|
106
|
+
begin
|
107
|
+
pe_offset = mz(f) && mz(f).lfanew
|
108
|
+
if pe_offset.nil?
|
109
|
+
logger.fatal "[!] NULL PE offset (e_lfanew). cannot continue."
|
110
|
+
nil
|
111
|
+
elsif pe_offset > f.size
|
112
|
+
logger.fatal "[!] PE offset beyond EOF. cannot continue."
|
113
|
+
nil
|
114
|
+
else
|
115
|
+
f.seek pe_offset
|
116
|
+
PE.read f, :force => @force
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,436 @@
|
|
1
|
+
class PEdump
|
2
|
+
|
3
|
+
def resource_directory f=@io
|
4
|
+
@resource_directory ||=
|
5
|
+
if pe(f)
|
6
|
+
_read_resource_directory_tree(f)
|
7
|
+
elsif ne(f)
|
8
|
+
ne(f).resource_directory(f)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def _read_resource_directory_tree f
|
13
|
+
return nil unless pe(f) && pe(f).ioh && f
|
14
|
+
res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
|
15
|
+
return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
|
16
|
+
res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
|
17
|
+
res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
|
18
|
+
unless res_section
|
19
|
+
logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
|
20
|
+
return []
|
21
|
+
end
|
22
|
+
f.seek res_section.PointerToRawData
|
23
|
+
IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
|
24
|
+
#@resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
|
25
|
+
IMAGE_RESOURCE_DIRECTORY.read(f)
|
26
|
+
end
|
27
|
+
|
28
|
+
class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data, :valid)
|
29
|
+
def bitmap_hdr
|
30
|
+
bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
|
31
|
+
raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
|
32
|
+
|
33
|
+
bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
|
34
|
+
|
35
|
+
colors_used = bmp_info_hdr.biClrUsed
|
36
|
+
colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
|
37
|
+
|
38
|
+
# XXX: one byte in each color is unused!
|
39
|
+
@palette_size = colors_used * 4 # each color takes 4 bytes
|
40
|
+
|
41
|
+
# scanlines are DWORD-aligned and padded to DWORD-align with zeroes
|
42
|
+
# XXX: some data may be hidden in padding bytes!
|
43
|
+
scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
|
44
|
+
scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
|
45
|
+
|
46
|
+
@imgdata_size = scanline_size * bmp_info_hdr.biHeight
|
47
|
+
"BM" + [
|
48
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
|
49
|
+
0,
|
50
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
51
|
+
].pack("V3") + bmp_info_hdr.pack
|
52
|
+
ensure
|
53
|
+
bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
|
54
|
+
end
|
55
|
+
|
56
|
+
# only valid for types BITMAP, ICON & CURSOR
|
57
|
+
def restore_bitmap src_fname
|
58
|
+
File.open(src_fname, "rb") do |f|
|
59
|
+
parse f
|
60
|
+
if data.first == "PNG"
|
61
|
+
"\x89PNG" +f.read(self.size-4)
|
62
|
+
else
|
63
|
+
bitmap_hdr + f.read(@palette_size + @imgdata_size)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# only valid for types BITMAP, ICON & CURSOR
|
69
|
+
def restore_icon src_fname
|
70
|
+
File.open(src_fname, "rb") do |f|
|
71
|
+
parse f
|
72
|
+
if data.first == "PNG"
|
73
|
+
"\x89PNG" +f.read(self.size-4)
|
74
|
+
else
|
75
|
+
icondir = [
|
76
|
+
0, # Reserved. Must always be 0.
|
77
|
+
1, # image type: 1 for icon (.ICO), 2 for cursor (.CUR). Other values are invalid
|
78
|
+
1, # number of images in the file
|
79
|
+
].pack("v3")
|
80
|
+
bitmap_hdr = data.first # BITMAPINFOHEADER
|
81
|
+
icondirentry = ICODIRENTRY.new(
|
82
|
+
bitmap_hdr.biWidth,
|
83
|
+
bitmap_hdr.biHeight / (%w'ICON CURSOR'.include?(type) ? 2 : 1),
|
84
|
+
0, # XXX: bColors: may be wrong here
|
85
|
+
0,
|
86
|
+
1,
|
87
|
+
bitmap_hdr.biBitCount,
|
88
|
+
bitmap_hdr.biSizeImage,
|
89
|
+
icondir.size + 2 + ICODIRENTRY::SIZE # offset of BMP data from the beginning of ICO file
|
90
|
+
)
|
91
|
+
# ICONDIRENTRY is 2 bytes larger than ICODIRENTRY
|
92
|
+
icondir + icondirentry.pack + "\x00\x00" + bitmap_hdr.pack + f.read(self.size)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# only valid for types BITMAP, ICON & CURSOR
|
98
|
+
def bitmap_mask src_fname
|
99
|
+
File.open(src_fname, "rb") do |f|
|
100
|
+
parse f
|
101
|
+
bmp_info_hdr = bitmap_hdr
|
102
|
+
bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
|
103
|
+
return nil if bitmap_size >= self.size
|
104
|
+
|
105
|
+
mask_size = self.size - bitmap_size
|
106
|
+
f.seek file_offset + bitmap_size
|
107
|
+
|
108
|
+
bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
|
109
|
+
bmp_info_hdr.biBitCount = 1
|
110
|
+
bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
|
111
|
+
bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
|
112
|
+
|
113
|
+
palette = [0,0xffffff].pack('V2')
|
114
|
+
@palette_size = palette.size
|
115
|
+
|
116
|
+
"BM" + [
|
117
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + mask_size,
|
118
|
+
0,
|
119
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
120
|
+
].pack("V3") + bmp_info_hdr.pack + palette + f.read(mask_size)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# also sets the file position for restore_bitmap next call
|
125
|
+
def parse f
|
126
|
+
raise "called parse with type not set" unless self.type
|
127
|
+
#return if self.data
|
128
|
+
|
129
|
+
self.data = []
|
130
|
+
return nil unless file_offset
|
131
|
+
|
132
|
+
case type
|
133
|
+
when 'BITMAP','ICON'
|
134
|
+
f.seek file_offset
|
135
|
+
if f.read(4) == "\x89PNG"
|
136
|
+
data << 'PNG'
|
137
|
+
else
|
138
|
+
f.seek file_offset
|
139
|
+
data << BITMAPINFOHEADER.read(f)
|
140
|
+
end
|
141
|
+
when 'CURSOR'
|
142
|
+
f.seek file_offset
|
143
|
+
data << CURSOR_HOTSPOT.read(f)
|
144
|
+
data << BITMAPINFOHEADER.read(f)
|
145
|
+
when 'GROUP_CURSOR'
|
146
|
+
f.seek file_offset
|
147
|
+
data << CUR_ICO_HEADER.read(f)
|
148
|
+
nRead = CUR_ICO_HEADER::SIZE
|
149
|
+
data.last.wNumImages.to_i.times do
|
150
|
+
if nRead >= self.size
|
151
|
+
PEdump.logger.error "[!] refusing to read CURDIRENTRY beyond resource size"
|
152
|
+
break
|
153
|
+
end
|
154
|
+
data << CURDIRENTRY.read(f)
|
155
|
+
nRead += CURDIRENTRY::SIZE
|
156
|
+
end
|
157
|
+
when 'GROUP_ICON'
|
158
|
+
f.seek file_offset
|
159
|
+
data << CUR_ICO_HEADER.read(f)
|
160
|
+
nRead = CUR_ICO_HEADER::SIZE
|
161
|
+
data.last.wNumImages.to_i.times do
|
162
|
+
if nRead >= self.size
|
163
|
+
PEdump.logger.error "[!] refusing to read ICODIRENTRY beyond resource size"
|
164
|
+
break
|
165
|
+
end
|
166
|
+
data << ICODIRENTRY.read(f)
|
167
|
+
nRead += ICODIRENTRY::SIZE
|
168
|
+
end
|
169
|
+
when 'STRING'
|
170
|
+
f.seek file_offset
|
171
|
+
16.times do
|
172
|
+
break if f.tell >= file_offset+self.size
|
173
|
+
nChars = f.read(2).to_s.unpack('v').first.to_i
|
174
|
+
t =
|
175
|
+
if nChars*2 + 1 > self.size
|
176
|
+
# TODO: if it's not 1st string in table then truncated size must be less
|
177
|
+
PEdump.logger.error "[!] string size(#{nChars*2}) > stringtable size(#{self.size}). truncated to #{self.size-2}"
|
178
|
+
f.read(self.size-2)
|
179
|
+
else
|
180
|
+
f.read(nChars*2)
|
181
|
+
end
|
182
|
+
data <<
|
183
|
+
begin
|
184
|
+
t.force_encoding('UTF-16LE').encode!('UTF-8')
|
185
|
+
rescue
|
186
|
+
t.force_encoding('ASCII')
|
187
|
+
tt = t.size > 0x10 ? t[0,0x10].inspect+'...' : t.inspect
|
188
|
+
PEdump.logger.error "[!] cannot convert #{tt} to UTF-16"
|
189
|
+
[nChars,t].pack('va*')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
# XXX: check if readed strings summary length is less than resource data length
|
193
|
+
when 'VERSION'
|
194
|
+
f.seek file_offset
|
195
|
+
data << PEdump::VS_VERSIONINFO.read(f)
|
196
|
+
end
|
197
|
+
|
198
|
+
data.delete_if do |x|
|
199
|
+
valid = !x.respond_to?(:valid?) || x.valid?
|
200
|
+
PEdump.logger.warn "[?] ignoring invalid #{x.class}" unless valid
|
201
|
+
!valid
|
202
|
+
end
|
203
|
+
ensure
|
204
|
+
validate
|
205
|
+
end
|
206
|
+
|
207
|
+
def validate
|
208
|
+
self.valid = self.file_offset &&
|
209
|
+
case type
|
210
|
+
when 'BITMAP','ICON','CURSOR'
|
211
|
+
data.any?{ |x| x.is_a?(BITMAPINFOHEADER) && x.valid? } || data.first == 'PNG'
|
212
|
+
when 'GROUP_ICON'
|
213
|
+
# rough validation
|
214
|
+
data.first.is_a?(CUR_ICO_HEADER) && data.size == data.first.wNumImages.to_i+1
|
215
|
+
else
|
216
|
+
true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def valid?
|
221
|
+
valid
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
STRING = Struct.new(:id, :lang, :value)
|
226
|
+
|
227
|
+
def strings f=@io
|
228
|
+
r = []
|
229
|
+
Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
|
230
|
+
res.data.each_with_index do |string,idx|
|
231
|
+
r << STRING.new( ((res.id.to_i-1)<<4) + idx, res.lang, string ) unless string.empty?
|
232
|
+
end
|
233
|
+
end
|
234
|
+
r
|
235
|
+
end
|
236
|
+
|
237
|
+
# see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
|
238
|
+
|
239
|
+
class BITMAPINFOHEADER < IOStruct.new 'V3v2V6',
|
240
|
+
:biSize, # BITMAPINFOHEADER::SIZE
|
241
|
+
:biWidth,
|
242
|
+
:biHeight,
|
243
|
+
:biPlanes,
|
244
|
+
:biBitCount,
|
245
|
+
:biCompression,
|
246
|
+
:biSizeImage,
|
247
|
+
:biXPelsPerMeter,
|
248
|
+
:biYPelsPerMeter,
|
249
|
+
:biClrUsed,
|
250
|
+
:biClrImportant
|
251
|
+
|
252
|
+
def valid?
|
253
|
+
self.biSize == 40
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
|
258
|
+
CUR_ICO_HEADER = IOStruct.new('v3',
|
259
|
+
:wReserved, # always 0
|
260
|
+
:wResID, # always 2
|
261
|
+
:wNumImages # Number of cursor images/directory entries
|
262
|
+
)
|
263
|
+
|
264
|
+
CURDIRENTRY = IOStruct.new 'v4Vv',
|
265
|
+
:wWidth,
|
266
|
+
:wHeight, # Divide by 2 to get the actual height.
|
267
|
+
:wPlanes,
|
268
|
+
:wBitCount,
|
269
|
+
:dwBytesInImage,
|
270
|
+
:wID
|
271
|
+
|
272
|
+
CURSOR_HOTSPOT = IOStruct.new 'v2', :x, :y
|
273
|
+
|
274
|
+
ICODIRENTRY = IOStruct.new 'C4v2Vv',
|
275
|
+
:bWidth,
|
276
|
+
:bHeight,
|
277
|
+
:bColors,
|
278
|
+
:bReserved,
|
279
|
+
:wPlanes,
|
280
|
+
:wBitCount,
|
281
|
+
:dwBytesInImage,
|
282
|
+
:wID
|
283
|
+
|
284
|
+
ROOT_RES_NAMES = [nil] + # numeration is started from 1
|
285
|
+
%w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
|
286
|
+
%w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
|
287
|
+
%w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
|
288
|
+
|
289
|
+
IMAGE_RESOURCE_DIRECTORY_ENTRY = IOStruct.new 'V2',
|
290
|
+
:Name, :OffsetToData,
|
291
|
+
:name, :data
|
292
|
+
|
293
|
+
IMAGE_RESOURCE_DATA_ENTRY = IOStruct.new 'V4',
|
294
|
+
:OffsetToData, :Size, :CodePage, :Reserved
|
295
|
+
|
296
|
+
IMAGE_RESOURCE_DIRECTORY = IOStruct.new 'V2v4',
|
297
|
+
:Characteristics, :TimeDateStamp, # 2dw
|
298
|
+
:MajorVersion, :MinorVersion, :NumberOfNamedEntries, :NumberOfIdEntries, # 4w
|
299
|
+
:entries # manual
|
300
|
+
class IMAGE_RESOURCE_DIRECTORY
|
301
|
+
class << self
|
302
|
+
attr_accessor :base
|
303
|
+
alias :read_without_children :read
|
304
|
+
def read f, root=true
|
305
|
+
if root
|
306
|
+
@@loopchk1 = Hash.new(0)
|
307
|
+
@@loopchk2 = Hash.new(0)
|
308
|
+
@@loopchk3 = Hash.new(0)
|
309
|
+
@@nErrors1 = 0
|
310
|
+
@@nErrors2 = 0
|
311
|
+
elsif (@@loopchk1[f.tell] += 1) > 1
|
312
|
+
PEdump.logger.error "[!] #{self}: loop1 detected at file pos #{f.tell}" if @@loopchk1[f.tell] < 2
|
313
|
+
return nil
|
314
|
+
end
|
315
|
+
read_without_children(f).tap do |r|
|
316
|
+
nToRead = r.NumberOfNamedEntries.to_i + r.NumberOfIdEntries.to_i
|
317
|
+
r.entries = []
|
318
|
+
nToRead.times do |i|
|
319
|
+
if f.eof?
|
320
|
+
PEdump.logger.error "[!] #{self}: #{nToRead} entries in directory, but got EOF on #{i}-th."
|
321
|
+
break
|
322
|
+
end
|
323
|
+
if (@@loopchk2[f.tell] += 1) > 1
|
324
|
+
PEdump.logger.error "[!] #{self}: loop2 detected at file pos #{f.tell}" if @@loopchk2[f.tell] < 2
|
325
|
+
next
|
326
|
+
end
|
327
|
+
r.entries << IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
|
328
|
+
end
|
329
|
+
#r.entries.uniq!
|
330
|
+
r.entries.each_with_index do |entry,idx|
|
331
|
+
entry.name =
|
332
|
+
if (entry.Name.to_i & 0x8000_0000 > 0) && f.checked_seek(base + entry.Name & 0x7fff_ffff)
|
333
|
+
# Name is an address of unicode string
|
334
|
+
nChars = f.read(2).to_s.unpack("v").first.to_i
|
335
|
+
begin
|
336
|
+
f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
|
337
|
+
rescue
|
338
|
+
PEdump.logger.error "[!] #{self} failed to read entry name: #{$!}"
|
339
|
+
if (@@nErrors1+=1) > MAX_ERRORS
|
340
|
+
PEdump.logger.warn "[?] too many errors getting resource names, stopped on #{idx} of #{r.entries.size}"
|
341
|
+
r.entries = r.entries[0,idx]
|
342
|
+
break
|
343
|
+
|
344
|
+
end
|
345
|
+
"???"
|
346
|
+
end
|
347
|
+
else
|
348
|
+
# Name is a numeric id
|
349
|
+
"##{entry.Name}"
|
350
|
+
end
|
351
|
+
if entry.OffsetToData
|
352
|
+
if (@@loopchk3[entry.OffsetToData] += 1) > 1
|
353
|
+
PEdump.logger.error "[!] #{self}: loop3 detected at file pos #{f.tell}" if @@loopchk3[f.tell] < 2
|
354
|
+
if (@@nErrors2+=1) > MAX_ERRORS
|
355
|
+
PEdump.logger.warn "[?] too many errors getting resource data, stopped on #{idx} of #{r.entries.size}"
|
356
|
+
r.entries = r.entries[0,idx]
|
357
|
+
break
|
358
|
+
|
359
|
+
end
|
360
|
+
next
|
361
|
+
end
|
362
|
+
next unless f.checked_seek(base + entry.OffsetToData & 0x7fff_ffff)
|
363
|
+
entry.data =
|
364
|
+
if entry.OffsetToData & 0x8000_0000 > 0
|
365
|
+
# child is a directory
|
366
|
+
IMAGE_RESOURCE_DIRECTORY.read(f,false)
|
367
|
+
else
|
368
|
+
# child is a resource
|
369
|
+
IMAGE_RESOURCE_DATA_ENTRY.read(f)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
@@loopchk1 = @@loopchk2 = @@loopchk3 = nil if root # save some memory
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def _scan_pe_resources f=@io, dir=nil
|
380
|
+
dir ||= resource_directory(f)
|
381
|
+
return nil unless dir
|
382
|
+
@pe_res_errors ||= 0
|
383
|
+
r = []
|
384
|
+
dir.entries.each_with_index do |entry,idx|
|
385
|
+
case entry.data
|
386
|
+
when IMAGE_RESOURCE_DIRECTORY
|
387
|
+
if dir == @resource_directory # root resource directory
|
388
|
+
entry_type =
|
389
|
+
if entry.Name & 0x8000_0000 == 0
|
390
|
+
# root resource directory & entry name is a number
|
391
|
+
ROOT_RES_NAMES[entry.Name] || entry.name
|
392
|
+
else
|
393
|
+
entry.name
|
394
|
+
end
|
395
|
+
r += _scan_pe_resources(f,entry.data).each do |res|
|
396
|
+
res.type = entry_type
|
397
|
+
res.parse f
|
398
|
+
end
|
399
|
+
else
|
400
|
+
r += _scan_pe_resources(f,entry.data).each do |res|
|
401
|
+
res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
|
402
|
+
res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
|
403
|
+
end
|
404
|
+
end
|
405
|
+
when IMAGE_RESOURCE_DATA_ENTRY
|
406
|
+
file_offset = va2file(entry.data.OffsetToData, :quiet => (@pe_res_errors > MAX_ERRORS))
|
407
|
+
unless file_offset
|
408
|
+
@pe_res_errors += 1
|
409
|
+
if @pe_res_errors > MAX_ERRORS
|
410
|
+
PEdump.logger.warn "[?] too many errors getting resource data, stopped on #{idx} of #{dir.entries.size}"
|
411
|
+
break
|
412
|
+
end
|
413
|
+
end
|
414
|
+
r << Resource.new(
|
415
|
+
nil, # type
|
416
|
+
entry.name,
|
417
|
+
nil, # id
|
418
|
+
entry.Name, # lang
|
419
|
+
#entry.data.OffsetToData + @resource_data_base,
|
420
|
+
file_offset,
|
421
|
+
entry.data.Size,
|
422
|
+
entry.data.CodePage,
|
423
|
+
entry.data.Reserved
|
424
|
+
)
|
425
|
+
else
|
426
|
+
if entry.data
|
427
|
+
logger.error "[!] invalid resource entry: #{entry.data.inspect}"
|
428
|
+
else
|
429
|
+
# show NULL entries only in verbose mode
|
430
|
+
logger.info "[!] invalid resource entry: #{entry.data.inspect}"
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
r.flatten.compact
|
435
|
+
end
|
436
|
+
end
|