pedump 0.4.3 → 0.4.4
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/VERSION +1 -1
- data/lib/pedump.rb +83 -476
- data/lib/pedump/cli.rb +18 -1
- data/lib/pedump/core.rb +91 -0
- data/lib/pedump/pe.rb +2 -0
- data/lib/pedump/resources.rb +364 -0
- data/lib/pedump/tls.rb +17 -0
- data/lib/pedump/version.rb +1 -1
- data/pedump.gemspec +5 -1
- data/spec/manyimportsW7_spec.rb +22 -0
- metadata +20 -16
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.4.
|
1
|
+
0.4.4
|
data/lib/pedump.rb
CHANGED
@@ -1,43 +1,16 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require 'logger'
|
3
2
|
require 'stringio'
|
4
|
-
require 'pedump/
|
3
|
+
require 'pedump/core'
|
5
4
|
require 'pedump/pe'
|
6
|
-
require 'pedump/
|
5
|
+
require 'pedump/resources'
|
6
|
+
require 'pedump/version_info'
|
7
|
+
require 'pedump/tls'
|
7
8
|
|
8
9
|
# pedump.rb by zed_0xff
|
9
10
|
#
|
10
11
|
# http://zed.0xff.me
|
11
12
|
# http://github.com/zed-0xff
|
12
13
|
|
13
|
-
class String
|
14
|
-
def xor x
|
15
|
-
if x.is_a?(String)
|
16
|
-
r = ''
|
17
|
-
j = 0
|
18
|
-
0.upto(self.size-1) do |i|
|
19
|
-
r << (self[i].ord^x[j].ord).chr
|
20
|
-
j+=1
|
21
|
-
j=0 if j>= x.size
|
22
|
-
end
|
23
|
-
r
|
24
|
-
else
|
25
|
-
r = ''
|
26
|
-
0.upto(self.size-1) do |i|
|
27
|
-
r << (self[i].ord^x).chr
|
28
|
-
end
|
29
|
-
r
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class File
|
35
|
-
def checked_seek newpos
|
36
|
-
@file_range ||= (0..size)
|
37
|
-
@file_range.include?(newpos) && (seek(newpos) || true)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
14
|
class PEdump
|
42
15
|
attr_accessor :fname, :logger, :force
|
43
16
|
|
@@ -51,66 +24,6 @@ class PEdump
|
|
51
24
|
@logger = @@logger = params[:logger] || PEdump::Logger.new(STDERR)
|
52
25
|
end
|
53
26
|
|
54
|
-
class Logger < ::Logger
|
55
|
-
def initialize *args
|
56
|
-
super
|
57
|
-
@formatter = proc do |severity,_,_,msg|
|
58
|
-
# quick and dirty way to remove duplicate messages
|
59
|
-
if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
|
60
|
-
''
|
61
|
-
else
|
62
|
-
@prevmsg = msg
|
63
|
-
"#{msg}\n"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
@level = Logger::WARN
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
module Readable
|
71
|
-
def read file, size = nil
|
72
|
-
size ||= const_get 'SIZE'
|
73
|
-
data = file.read(size).to_s
|
74
|
-
if data.size < size && PEdump.logger
|
75
|
-
PEdump.logger.error "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
|
76
|
-
end
|
77
|
-
new(*data.unpack(const_get('FORMAT')))
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
class << self
|
82
|
-
def logger; @@logger; end
|
83
|
-
def logger= l; @@logger=l; end
|
84
|
-
|
85
|
-
def create_struct fmt, *args
|
86
|
-
size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
|
87
|
-
[len.to_i, 1].max *
|
88
|
-
case f
|
89
|
-
when /[aAC]/ then 1
|
90
|
-
when 'v' then 2
|
91
|
-
when 'V' then 4
|
92
|
-
when 'Q' then 8
|
93
|
-
else raise "unknown fmt #{f.inspect}"
|
94
|
-
end
|
95
|
-
end.inject(&:+)
|
96
|
-
|
97
|
-
Struct.new( *args ).tap do |x|
|
98
|
-
x.const_set 'FORMAT', fmt
|
99
|
-
x.const_set 'SIZE', size
|
100
|
-
x.class_eval do
|
101
|
-
def pack
|
102
|
-
to_a.pack self.class.const_get('FORMAT')
|
103
|
-
end
|
104
|
-
def empty?
|
105
|
-
to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
|
106
|
-
end
|
107
|
-
end
|
108
|
-
x.extend Readable
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
|
114
27
|
# http://www.delorie.com/djgpp/doc/exe/
|
115
28
|
MZ = create_struct( "a2v13Qv2V6",
|
116
29
|
:signature,
|
@@ -448,8 +361,37 @@ class PEdump
|
|
448
361
|
end
|
449
362
|
end
|
450
363
|
|
451
|
-
def
|
452
|
-
|
364
|
+
def va2file va
|
365
|
+
return nil if va.nil?
|
366
|
+
|
367
|
+
sections.each do |s|
|
368
|
+
if (s.VirtualAddress...(s.VirtualAddress+s.VirtualSize)).include?(va)
|
369
|
+
return va - s.VirtualAddress + s.PointerToRawData
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# not found with regular search. assume any of VirtualSize was 0, and try with RawSize
|
374
|
+
sections.each do |s|
|
375
|
+
if (s.VirtualAddress...(s.VirtualAddress+s.SizeOfRawData)).include?(va)
|
376
|
+
return va - s.VirtualAddress + s.PointerToRawData
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
# still not found, bad/zero VirtualSizes & RawSizes ?
|
381
|
+
|
382
|
+
# a special case - PE without sections
|
383
|
+
return va if sections.empty?
|
384
|
+
|
385
|
+
# check if only one section
|
386
|
+
if sections.size == 1 || sections.all?{ |s| s.VirtualAddress.to_i == 0 }
|
387
|
+
s = sections.first
|
388
|
+
return va - s.VirtualAddress + s.PointerToRawData
|
389
|
+
end
|
390
|
+
|
391
|
+
# TODO: not all VirtualAdresses == 0 case
|
392
|
+
|
393
|
+
logger.error "[?] can't find file_offset of VA 0x#{va.to_i.to_s(16)}"
|
394
|
+
nil
|
453
395
|
end
|
454
396
|
|
455
397
|
# OPTIONAL: assigns @mz, @rich_hdr, @pe, etc
|
@@ -499,14 +441,34 @@ class PEdump
|
|
499
441
|
return nil unless pe(f) && pe(f).ioh && f
|
500
442
|
dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::IMPORT]
|
501
443
|
return [] if !dir || (dir.va == 0 && dir.size == 0)
|
502
|
-
|
503
|
-
file_offset = va2file(va)
|
444
|
+
file_offset = va2file(dir.va)
|
504
445
|
return nil unless file_offset
|
446
|
+
|
447
|
+
# scan TLS first, to catch many fake imports trick from
|
448
|
+
# http://code.google.com/p/corkami/source/browse/trunk/asm/PE/manyimportsW7.asm
|
449
|
+
tls_aoi = nil
|
450
|
+
if (tls = tls(f)) && tls.any?
|
451
|
+
tls_aoi = tls.first.AddressOfIndex.to_i - @pe.ioh.ImageBase.to_i
|
452
|
+
tls_aoi = tls_aoi > 0 ? va2file(tls_aoi) : nil
|
453
|
+
end
|
454
|
+
|
505
455
|
f.seek file_offset
|
506
456
|
r = []
|
507
|
-
|
457
|
+
while true
|
458
|
+
if tls_aoi && tls_aoi == file_offset+16
|
459
|
+
# catched the neat trick! :)
|
460
|
+
# f.tell + 12 = offset of 'FirstThunk' field from start of IMAGE_IMPORT_DESCRIPTOR structure
|
461
|
+
logger.warn "[!] catched the 'imports terminator in TLS trick'"
|
462
|
+
# http://code.google.com/p/corkami/source/browse/trunk/asm/PE/manyimportsW7.asm
|
463
|
+
break
|
464
|
+
end
|
465
|
+
t=IMAGE_IMPORT_DESCRIPTOR.read(f)
|
466
|
+
break if t.Name.to_i == 0 # also catches EOF
|
508
467
|
r << t
|
468
|
+
file_offset += IMAGE_IMPORT_DESCRIPTOR::SIZE
|
469
|
+
break if r.size == 3
|
509
470
|
end
|
471
|
+
|
510
472
|
logger.warn "[?] non-empty last IMAGE_IMPORT_DESCRIPTOR: #{t.inspect}" unless t.empty?
|
511
473
|
@imports = r.each do |x|
|
512
474
|
if x.Name.to_i != 0 && (va = va2file(x.Name))
|
@@ -643,356 +605,40 @@ class PEdump
|
|
643
605
|
end
|
644
606
|
|
645
607
|
##############################################################################
|
646
|
-
#
|
608
|
+
# TLS
|
647
609
|
##############################################################################
|
648
610
|
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
if root
|
659
|
-
@@loopchk1 = Hash.new(0)
|
660
|
-
@@loopchk2 = Hash.new(0)
|
661
|
-
@@loopchk3 = Hash.new(0)
|
662
|
-
elsif (@@loopchk1[f.tell] += 1) > 1
|
663
|
-
PEdump.logger.error "[!] #{self}: loop1 detected at file pos #{f.tell}" if @@loopchk1[f.tell] < 2
|
611
|
+
def tls f=nil
|
612
|
+
@tls ||= pe(f) && pe(f).ioh && f &&
|
613
|
+
begin
|
614
|
+
dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::TLS]
|
615
|
+
return nil if !dir || dir.va == 0
|
616
|
+
return nil unless file_offset = va2file(dir.va)
|
617
|
+
f.seek file_offset
|
618
|
+
if f.eof?
|
619
|
+
logger.info "[?] TLS info beyond EOF"
|
664
620
|
return nil
|
665
621
|
end
|
666
|
-
read_without_children(f).tap do |r|
|
667
|
-
nToRead = r.NumberOfNamedEntries.to_i + r.NumberOfIdEntries.to_i
|
668
|
-
r.entries = []
|
669
|
-
nToRead.times do |i|
|
670
|
-
if f.eof?
|
671
|
-
PEdump.logger.error "[!] #{self}: #{nToRead} entries in directory, but got EOF on #{i}-th."
|
672
|
-
break
|
673
|
-
end
|
674
|
-
if (@@loopchk2[f.tell] += 1) > 1
|
675
|
-
PEdump.logger.error "[!] #{self}: loop2 detected at file pos #{f.tell}" if @@loopchk2[f.tell] < 2
|
676
|
-
next
|
677
|
-
end
|
678
|
-
r.entries << IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
|
679
|
-
end
|
680
|
-
#r.entries.uniq!
|
681
|
-
r.entries.each do |entry|
|
682
|
-
entry.name =
|
683
|
-
if entry.Name.to_i & 0x8000_0000 > 0
|
684
|
-
# Name is an address of unicode string
|
685
|
-
f.seek base + entry.Name & 0x7fff_ffff
|
686
|
-
nChars = f.read(2).to_s.unpack("v").first.to_i
|
687
|
-
begin
|
688
|
-
f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
|
689
|
-
rescue
|
690
|
-
PEdump.logger.error "[!] #{self} failed to read entry name: #{$!}"
|
691
|
-
"???"
|
692
|
-
end
|
693
|
-
else
|
694
|
-
# Name is a numeric id
|
695
|
-
"##{entry.Name}"
|
696
|
-
end
|
697
|
-
if entry.OffsetToData && f.checked_seek(base + entry.OffsetToData & 0x7fff_ffff)
|
698
|
-
if (@@loopchk3[f.tell] += 1) > 1
|
699
|
-
PEdump.logger.error "[!] #{self}: loop3 detected at file pos #{f.tell}" if @@loopchk3[f.tell] < 2
|
700
|
-
next
|
701
|
-
end
|
702
|
-
entry.data =
|
703
|
-
if entry.OffsetToData & 0x8000_0000 > 0
|
704
|
-
# child is a directory
|
705
|
-
IMAGE_RESOURCE_DIRECTORY.read(f,false)
|
706
|
-
else
|
707
|
-
# child is a resource
|
708
|
-
IMAGE_RESOURCE_DATA_ENTRY.read(f)
|
709
|
-
end
|
710
|
-
end
|
711
|
-
end
|
712
|
-
@@loopchk1 = @@loopchk2 = @@loopchk3 = nil if root # save some memory
|
713
|
-
end
|
714
|
-
end
|
715
|
-
end
|
716
|
-
end
|
717
|
-
|
718
|
-
IMAGE_RESOURCE_DIRECTORY_ENTRY = create_struct 'V2',
|
719
|
-
:Name, :OffsetToData,
|
720
|
-
:name, :data
|
721
622
|
|
722
|
-
|
723
|
-
:OffsetToData, :Size, :CodePage, :Reserved
|
623
|
+
start_offset = f.tell
|
724
624
|
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
return va - s.VirtualAddress + s.PointerToRawData
|
731
|
-
end
|
732
|
-
end
|
733
|
-
|
734
|
-
# not found with regular search. assume any of VirtualSize was 0, and try with RawSize
|
735
|
-
sections.each do |s|
|
736
|
-
if (s.VirtualAddress...(s.VirtualAddress+s.SizeOfRawData)).include?(va)
|
737
|
-
return va - s.VirtualAddress + s.PointerToRawData
|
738
|
-
end
|
739
|
-
end
|
740
|
-
|
741
|
-
# still not found, bad/zero VirtualSizes & RawSizes ?
|
742
|
-
|
743
|
-
# a special case - PE without sections
|
744
|
-
return va if sections.empty?
|
745
|
-
|
746
|
-
# check if only one section
|
747
|
-
if sections.size == 1 || sections.all?{ |s| s.VirtualAddress.to_i == 0 }
|
748
|
-
s = sections.first
|
749
|
-
return va - s.VirtualAddress + s.PointerToRawData
|
750
|
-
end
|
751
|
-
|
752
|
-
# TODO: not all VirtualAdresses == 0 case
|
753
|
-
|
754
|
-
logger.error "[?] can't find file_offset of VA 0x#{va.to_i.to_s(16)}"
|
755
|
-
nil
|
756
|
-
end
|
757
|
-
|
758
|
-
def _read_resource_directory_tree f
|
759
|
-
return nil unless pe(f) && pe(f).ioh && f
|
760
|
-
res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
|
761
|
-
return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
|
762
|
-
res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
|
763
|
-
res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
|
764
|
-
unless res_section
|
765
|
-
logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
|
766
|
-
return []
|
767
|
-
end
|
768
|
-
f.seek res_section.PointerToRawData
|
769
|
-
IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
|
770
|
-
#@resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
|
771
|
-
IMAGE_RESOURCE_DIRECTORY.read(f)
|
772
|
-
end
|
773
|
-
|
774
|
-
class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data, :valid)
|
775
|
-
def bitmap_hdr
|
776
|
-
bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
|
777
|
-
raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
|
778
|
-
|
779
|
-
bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
|
780
|
-
|
781
|
-
colors_used = bmp_info_hdr.biClrUsed
|
782
|
-
colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
|
783
|
-
|
784
|
-
# XXX: one byte in each color is unused!
|
785
|
-
@palette_size = colors_used * 4 # each color takes 4 bytes
|
786
|
-
|
787
|
-
# scanlines are DWORD-aligned and padded to DWORD-align with zeroes
|
788
|
-
# XXX: some data may be hidden in padding bytes!
|
789
|
-
scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
|
790
|
-
scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
|
791
|
-
|
792
|
-
@imgdata_size = scanline_size * bmp_info_hdr.biHeight
|
793
|
-
"BM" + [
|
794
|
-
BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
|
795
|
-
0,
|
796
|
-
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
797
|
-
].pack("V3") + bmp_info_hdr.pack
|
798
|
-
ensure
|
799
|
-
bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
|
800
|
-
end
|
801
|
-
|
802
|
-
# only valid for types BITMAP, ICON & CURSOR
|
803
|
-
def restore_bitmap src_fname
|
804
|
-
File.open(src_fname, "rb") do |f|
|
805
|
-
parse f
|
806
|
-
if data.first == "PNG"
|
807
|
-
"\x89PNG" +f.read(self.size-4)
|
808
|
-
else
|
809
|
-
bitmap_hdr + f.read(@palette_size + @imgdata_size)
|
810
|
-
end
|
811
|
-
end
|
812
|
-
end
|
813
|
-
|
814
|
-
def bitmap_mask src_fname
|
815
|
-
File.open(src_fname, "rb") do |f|
|
816
|
-
parse f
|
817
|
-
bmp_info_hdr = bitmap_hdr
|
818
|
-
bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
|
819
|
-
return nil if bitmap_size >= self.size
|
820
|
-
|
821
|
-
mask_size = self.size - bitmap_size
|
822
|
-
f.seek file_offset + bitmap_size
|
823
|
-
|
824
|
-
bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
|
825
|
-
bmp_info_hdr.biBitCount = 1
|
826
|
-
bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
|
827
|
-
bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
|
828
|
-
|
829
|
-
palette = [0,0xffffff].pack('V2')
|
830
|
-
@palette_size = palette.size
|
831
|
-
|
832
|
-
"BM" + [
|
833
|
-
BITMAPINFOHEADER::SIZE + 14 + @palette_size + mask_size,
|
834
|
-
0,
|
835
|
-
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
836
|
-
].pack("V3") + bmp_info_hdr.pack + palette + f.read(mask_size)
|
837
|
-
end
|
838
|
-
end
|
839
|
-
|
840
|
-
# also sets the file position for restore_bitmap next call
|
841
|
-
def parse f
|
842
|
-
raise "called parse with type not set" unless self.type
|
843
|
-
#return if self.data
|
844
|
-
|
845
|
-
self.data = []
|
846
|
-
case type
|
847
|
-
when 'BITMAP','ICON'
|
848
|
-
f.seek file_offset
|
849
|
-
if f.read(4) == "\x89PNG"
|
850
|
-
data << 'PNG'
|
851
|
-
else
|
852
|
-
f.seek file_offset
|
853
|
-
data << BITMAPINFOHEADER.read(f)
|
854
|
-
end
|
855
|
-
when 'CURSOR'
|
856
|
-
f.seek file_offset
|
857
|
-
data << CURSOR_HOTSPOT.read(f)
|
858
|
-
data << BITMAPINFOHEADER.read(f)
|
859
|
-
when 'GROUP_CURSOR'
|
860
|
-
f.seek file_offset
|
861
|
-
data << CUR_ICO_HEADER.read(f)
|
862
|
-
nRead = CUR_ICO_HEADER::SIZE
|
863
|
-
data.last.wNumImages.to_i.times do
|
864
|
-
if nRead >= self.size
|
865
|
-
PEdump.logger.error "[!] refusing to read CURDIRENTRY beyond resource size"
|
866
|
-
break
|
867
|
-
end
|
868
|
-
data << CURDIRENTRY.read(f)
|
869
|
-
nRead += CURDIRENTRY::SIZE
|
870
|
-
end
|
871
|
-
when 'GROUP_ICON'
|
872
|
-
f.seek file_offset
|
873
|
-
data << CUR_ICO_HEADER.read(f)
|
874
|
-
nRead = CUR_ICO_HEADER::SIZE
|
875
|
-
data.last.wNumImages.to_i.times do
|
876
|
-
if nRead >= self.size
|
877
|
-
PEdump.logger.error "[!] refusing to read ICODIRENTRY beyond resource size"
|
625
|
+
klass = @pe.x64? ? IMAGE_TLS_DIRECTORY64 : IMAGE_TLS_DIRECTORY32
|
626
|
+
r = []
|
627
|
+
while !f.eof? && (entry = klass.read(f))
|
628
|
+
if entry.AddressOfCallBacks.to_i == 0
|
629
|
+
logger.warn "[?] non-empty TLS terminator: #{entry.inspect}" unless entry.empty?
|
878
630
|
break
|
879
631
|
end
|
880
|
-
|
881
|
-
|
882
|
-
end
|
883
|
-
when 'STRING'
|
884
|
-
f.seek file_offset
|
885
|
-
16.times do
|
886
|
-
break if f.tell >= file_offset+self.size
|
887
|
-
nChars = f.read(2).to_s.unpack('v').first.to_i
|
888
|
-
t =
|
889
|
-
if nChars*2 + 1 > self.size
|
890
|
-
# TODO: if it's not 1st string in table then truncated size must be less
|
891
|
-
PEdump.logger.error "[!] string size(#{nChars*2}) > stringtable size(#{self.size}). truncated to #{self.size-2}"
|
892
|
-
f.read(self.size-2)
|
893
|
-
else
|
894
|
-
f.read(nChars*2)
|
895
|
-
end
|
896
|
-
data <<
|
897
|
-
begin
|
898
|
-
t.force_encoding('UTF-16LE').encode!('UTF-8')
|
899
|
-
rescue
|
900
|
-
t.force_encoding('ASCII')
|
901
|
-
tt = t.size > 0x10 ? t[0,0x10].inspect+'...' : t.inspect
|
902
|
-
PEdump.logger.error "[!] cannot convert #{tt} to UTF-16"
|
903
|
-
[nChars,t].pack('va*')
|
904
|
-
end
|
632
|
+
r << entry
|
633
|
+
break if dir.size > 0 && f.tell >= (start_offset+dir.size)
|
905
634
|
end
|
906
|
-
|
907
|
-
when 'VERSION'
|
908
|
-
require 'pedump/version_info'
|
909
|
-
f.seek file_offset
|
910
|
-
data << PEdump::VS_VERSIONINFO.read(f)
|
911
|
-
end
|
912
|
-
|
913
|
-
data.delete_if do |x|
|
914
|
-
valid = !x.respond_to?(:valid?) || x.valid?
|
915
|
-
PEdump.logger.warn "[?] ignoring invalid #{x.class}" unless valid
|
916
|
-
!valid
|
635
|
+
r
|
917
636
|
end
|
918
|
-
ensure
|
919
|
-
validate
|
920
|
-
end
|
921
|
-
|
922
|
-
def validate
|
923
|
-
self.valid =
|
924
|
-
case type
|
925
|
-
when 'BITMAP','ICON','CURSOR'
|
926
|
-
data.any?{ |x| x.is_a?(BITMAPINFOHEADER) && x.valid? } || data.first == 'PNG'
|
927
|
-
else
|
928
|
-
true
|
929
|
-
end
|
930
|
-
end
|
931
637
|
end
|
932
638
|
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
r = []
|
937
|
-
Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
|
938
|
-
res.data.each_with_index do |string,idx|
|
939
|
-
r << STRING.new( ((res.id-1)<<4) + idx, res.lang, string ) unless string.empty?
|
940
|
-
end
|
941
|
-
end
|
942
|
-
r
|
943
|
-
end
|
944
|
-
|
945
|
-
# see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
|
946
|
-
|
947
|
-
class BITMAPINFOHEADER < create_struct 'V3v2V6',
|
948
|
-
:biSize, # BITMAPINFOHEADER::SIZE
|
949
|
-
:biWidth,
|
950
|
-
:biHeight,
|
951
|
-
:biPlanes,
|
952
|
-
:biBitCount,
|
953
|
-
:biCompression,
|
954
|
-
:biSizeImage,
|
955
|
-
:biXPelsPerMeter,
|
956
|
-
:biYPelsPerMeter,
|
957
|
-
:biClrUsed,
|
958
|
-
:biClrImportant
|
959
|
-
|
960
|
-
def valid?
|
961
|
-
self.biSize == 40
|
962
|
-
end
|
963
|
-
end
|
964
|
-
|
965
|
-
# http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
|
966
|
-
CUR_ICO_HEADER = create_struct('v3',
|
967
|
-
:wReserved, # always 0
|
968
|
-
:wResID, # always 2
|
969
|
-
:wNumImages # Number of cursor images/directory entries
|
970
|
-
)
|
971
|
-
|
972
|
-
CURDIRENTRY = create_struct 'v4Vv',
|
973
|
-
:wWidth,
|
974
|
-
:wHeight, # Divide by 2 to get the actual height.
|
975
|
-
:wPlanes,
|
976
|
-
:wBitCount,
|
977
|
-
:dwBytesInImage,
|
978
|
-
:wID
|
979
|
-
|
980
|
-
CURSOR_HOTSPOT = create_struct 'v2', :x, :y
|
981
|
-
|
982
|
-
ICODIRENTRY = create_struct 'C4v2Vv',
|
983
|
-
:bWidth,
|
984
|
-
:bHeight,
|
985
|
-
:bColors,
|
986
|
-
:bReserved,
|
987
|
-
:wPlanes,
|
988
|
-
:wBitCount,
|
989
|
-
:dwBytesInImage,
|
990
|
-
:wID
|
991
|
-
|
992
|
-
ROOT_RES_NAMES = [nil] + # numeration is started from 1
|
993
|
-
%w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
|
994
|
-
%w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
|
995
|
-
%w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
|
639
|
+
##############################################################################
|
640
|
+
# resources
|
641
|
+
##############################################################################
|
996
642
|
|
997
643
|
def resources f=nil
|
998
644
|
@resources ||= _scan_resources(f)
|
@@ -1002,48 +648,9 @@ class PEdump
|
|
1002
648
|
resources(f) && resources(f).find_all{ |res| res.type == 'VERSION' }.map(&:data).flatten
|
1003
649
|
end
|
1004
650
|
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
dir.entries.map do |entry|
|
1009
|
-
case entry.data
|
1010
|
-
when IMAGE_RESOURCE_DIRECTORY
|
1011
|
-
if dir == @resource_directory # root resource directory
|
1012
|
-
entry_type =
|
1013
|
-
if entry.Name & 0x8000_0000 == 0
|
1014
|
-
# root resource directory & entry name is a number
|
1015
|
-
ROOT_RES_NAMES[entry.Name] || entry.name
|
1016
|
-
else
|
1017
|
-
entry.name
|
1018
|
-
end
|
1019
|
-
_scan_resources(f,entry.data).each do |res|
|
1020
|
-
res.type = entry_type
|
1021
|
-
res.parse f
|
1022
|
-
end
|
1023
|
-
else
|
1024
|
-
_scan_resources(f,entry.data).each do |res|
|
1025
|
-
res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
|
1026
|
-
res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
|
1027
|
-
end
|
1028
|
-
end
|
1029
|
-
when IMAGE_RESOURCE_DATA_ENTRY
|
1030
|
-
Resource.new(
|
1031
|
-
nil, # type
|
1032
|
-
entry.name,
|
1033
|
-
nil, # id
|
1034
|
-
entry.Name, # lang
|
1035
|
-
#entry.data.OffsetToData + @resource_data_base,
|
1036
|
-
va2file(entry.data.OffsetToData),
|
1037
|
-
entry.data.Size,
|
1038
|
-
entry.data.CodePage,
|
1039
|
-
entry.data.Reserved
|
1040
|
-
)
|
1041
|
-
else
|
1042
|
-
logger.error "[!] invalid resource entry: #{entry.data.inspect}"
|
1043
|
-
nil
|
1044
|
-
end
|
1045
|
-
end.flatten.compact
|
1046
|
-
end
|
651
|
+
##############################################################################
|
652
|
+
# packer / compiler detection
|
653
|
+
##############################################################################
|
1047
654
|
|
1048
655
|
def packer f = nil
|
1049
656
|
@packer ||= pe(f) && @pe.ioh &&
|
data/lib/pedump/cli.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'pedump'
|
2
2
|
require 'pedump/packer'
|
3
|
+
require 'pedump/version_info'
|
3
4
|
require 'optparse'
|
4
5
|
|
5
6
|
unless Object.instance_methods.include?(:try)
|
@@ -14,7 +15,7 @@ class PEdump::CLI
|
|
14
15
|
attr_accessor :data, :argv
|
15
16
|
|
16
17
|
KNOWN_ACTIONS = (
|
17
|
-
%w'mz dos_stub rich pe data_directory sections' +
|
18
|
+
%w'mz dos_stub rich pe data_directory sections tls' +
|
18
19
|
%w'strings resources resource_directory imports exports version_info packer web packer_only'
|
19
20
|
).map(&:to_sym)
|
20
21
|
|
@@ -400,6 +401,8 @@ class PEdump::CLI
|
|
400
401
|
dump_packers data
|
401
402
|
when PEdump::VS_VERSIONINFO
|
402
403
|
dump_version_info data
|
404
|
+
when PEdump::IMAGE_TLS_DIRECTORY32, PEdump::IMAGE_TLS_DIRECTORY64
|
405
|
+
dump_tls data
|
403
406
|
else
|
404
407
|
puts "[?] don't know how to dump: #{data.inspect[0,50]}" unless data.empty?
|
405
408
|
end
|
@@ -412,6 +415,20 @@ class PEdump::CLI
|
|
412
415
|
end
|
413
416
|
end
|
414
417
|
|
418
|
+
def dump_tls data
|
419
|
+
fmt = "%10x %10x %8x %8x %8x %8x\n"
|
420
|
+
printf fmt.tr('x','s'), *%w'RAW_START RAW_END INDEX CALLBKS ZEROFILL FLAGS'
|
421
|
+
data.each do |tls|
|
422
|
+
printf fmt,
|
423
|
+
tls.StartAddressOfRawData,
|
424
|
+
tls.EndAddressOfRawData,
|
425
|
+
tls.AddressOfIndex,
|
426
|
+
tls.AddressOfCallBacks,
|
427
|
+
tls.SizeOfZeroFill,
|
428
|
+
tls.Characteristics
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
415
432
|
def dump_version_info data
|
416
433
|
if @options[:format] != :table
|
417
434
|
File.open(@file_name,'rb') do |f|
|
data/lib/pedump/core.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'pedump/version'
|
3
|
+
|
4
|
+
class String
|
5
|
+
def xor x
|
6
|
+
if x.is_a?(String)
|
7
|
+
r = ''
|
8
|
+
j = 0
|
9
|
+
0.upto(self.size-1) do |i|
|
10
|
+
r << (self[i].ord^x[j].ord).chr
|
11
|
+
j+=1
|
12
|
+
j=0 if j>= x.size
|
13
|
+
end
|
14
|
+
r
|
15
|
+
else
|
16
|
+
r = ''
|
17
|
+
0.upto(self.size-1) do |i|
|
18
|
+
r << (self[i].ord^x).chr
|
19
|
+
end
|
20
|
+
r
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class File
|
26
|
+
def checked_seek newpos
|
27
|
+
@file_range ||= (0..size)
|
28
|
+
@file_range.include?(newpos) && (seek(newpos) || true)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class PEdump
|
33
|
+
class Logger < ::Logger
|
34
|
+
def initialize *args
|
35
|
+
super
|
36
|
+
@formatter = proc do |severity,_,_,msg|
|
37
|
+
# quick and dirty way to remove duplicate messages
|
38
|
+
if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
|
39
|
+
''
|
40
|
+
else
|
41
|
+
@prevmsg = msg
|
42
|
+
"#{msg}\n"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@level = Logger::WARN
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module Readable
|
50
|
+
def read file, size = nil
|
51
|
+
size ||= const_get 'SIZE'
|
52
|
+
data = file.read(size).to_s
|
53
|
+
if data.size < size && PEdump.logger
|
54
|
+
PEdump.logger.error "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
|
55
|
+
end
|
56
|
+
new(*data.unpack(const_get('FORMAT')))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def logger; @@logger; end
|
62
|
+
def logger= l; @@logger=l; end
|
63
|
+
|
64
|
+
def create_struct fmt, *args
|
65
|
+
size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
|
66
|
+
[len.to_i, 1].max *
|
67
|
+
case f
|
68
|
+
when /[aAC]/ then 1
|
69
|
+
when 'v' then 2
|
70
|
+
when 'V' then 4
|
71
|
+
when 'Q' then 8
|
72
|
+
else raise "unknown fmt #{f.inspect}"
|
73
|
+
end
|
74
|
+
end.inject(&:+)
|
75
|
+
|
76
|
+
Struct.new( *args ).tap do |x|
|
77
|
+
x.const_set 'FORMAT', fmt
|
78
|
+
x.const_set 'SIZE', size
|
79
|
+
x.class_eval do
|
80
|
+
def pack
|
81
|
+
to_a.pack self.class.const_get('FORMAT')
|
82
|
+
end
|
83
|
+
def empty?
|
84
|
+
to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
x.extend Readable
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/pedump/pe.rb
CHANGED
@@ -0,0 +1,364 @@
|
|
1
|
+
class PEdump
|
2
|
+
|
3
|
+
def resource_directory f=nil
|
4
|
+
@resource_directory ||= _read_resource_directory_tree(f)
|
5
|
+
end
|
6
|
+
|
7
|
+
def _read_resource_directory_tree f
|
8
|
+
return nil unless pe(f) && pe(f).ioh && f
|
9
|
+
res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
|
10
|
+
return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
|
11
|
+
res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
|
12
|
+
res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
|
13
|
+
unless res_section
|
14
|
+
logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
f.seek res_section.PointerToRawData
|
18
|
+
IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
|
19
|
+
#@resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
|
20
|
+
IMAGE_RESOURCE_DIRECTORY.read(f)
|
21
|
+
end
|
22
|
+
|
23
|
+
class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data, :valid)
|
24
|
+
def bitmap_hdr
|
25
|
+
bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
|
26
|
+
raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
|
27
|
+
|
28
|
+
bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
|
29
|
+
|
30
|
+
colors_used = bmp_info_hdr.biClrUsed
|
31
|
+
colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
|
32
|
+
|
33
|
+
# XXX: one byte in each color is unused!
|
34
|
+
@palette_size = colors_used * 4 # each color takes 4 bytes
|
35
|
+
|
36
|
+
# scanlines are DWORD-aligned and padded to DWORD-align with zeroes
|
37
|
+
# XXX: some data may be hidden in padding bytes!
|
38
|
+
scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
|
39
|
+
scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
|
40
|
+
|
41
|
+
@imgdata_size = scanline_size * bmp_info_hdr.biHeight
|
42
|
+
"BM" + [
|
43
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
|
44
|
+
0,
|
45
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
46
|
+
].pack("V3") + bmp_info_hdr.pack
|
47
|
+
ensure
|
48
|
+
bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
|
49
|
+
end
|
50
|
+
|
51
|
+
# only valid for types BITMAP, ICON & CURSOR
|
52
|
+
def restore_bitmap src_fname
|
53
|
+
File.open(src_fname, "rb") do |f|
|
54
|
+
parse f
|
55
|
+
if data.first == "PNG"
|
56
|
+
"\x89PNG" +f.read(self.size-4)
|
57
|
+
else
|
58
|
+
bitmap_hdr + f.read(@palette_size + @imgdata_size)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def bitmap_mask src_fname
|
64
|
+
File.open(src_fname, "rb") do |f|
|
65
|
+
parse f
|
66
|
+
bmp_info_hdr = bitmap_hdr
|
67
|
+
bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
|
68
|
+
return nil if bitmap_size >= self.size
|
69
|
+
|
70
|
+
mask_size = self.size - bitmap_size
|
71
|
+
f.seek file_offset + bitmap_size
|
72
|
+
|
73
|
+
bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
|
74
|
+
bmp_info_hdr.biBitCount = 1
|
75
|
+
bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
|
76
|
+
bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
|
77
|
+
|
78
|
+
palette = [0,0xffffff].pack('V2')
|
79
|
+
@palette_size = palette.size
|
80
|
+
|
81
|
+
"BM" + [
|
82
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size + mask_size,
|
83
|
+
0,
|
84
|
+
BITMAPINFOHEADER::SIZE + 14 + @palette_size
|
85
|
+
].pack("V3") + bmp_info_hdr.pack + palette + f.read(mask_size)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# also sets the file position for restore_bitmap next call
|
90
|
+
def parse f
|
91
|
+
raise "called parse with type not set" unless self.type
|
92
|
+
#return if self.data
|
93
|
+
|
94
|
+
self.data = []
|
95
|
+
case type
|
96
|
+
when 'BITMAP','ICON'
|
97
|
+
f.seek file_offset
|
98
|
+
if f.read(4) == "\x89PNG"
|
99
|
+
data << 'PNG'
|
100
|
+
else
|
101
|
+
f.seek file_offset
|
102
|
+
data << BITMAPINFOHEADER.read(f)
|
103
|
+
end
|
104
|
+
when 'CURSOR'
|
105
|
+
f.seek file_offset
|
106
|
+
data << CURSOR_HOTSPOT.read(f)
|
107
|
+
data << BITMAPINFOHEADER.read(f)
|
108
|
+
when 'GROUP_CURSOR'
|
109
|
+
f.seek file_offset
|
110
|
+
data << CUR_ICO_HEADER.read(f)
|
111
|
+
nRead = CUR_ICO_HEADER::SIZE
|
112
|
+
data.last.wNumImages.to_i.times do
|
113
|
+
if nRead >= self.size
|
114
|
+
PEdump.logger.error "[!] refusing to read CURDIRENTRY beyond resource size"
|
115
|
+
break
|
116
|
+
end
|
117
|
+
data << CURDIRENTRY.read(f)
|
118
|
+
nRead += CURDIRENTRY::SIZE
|
119
|
+
end
|
120
|
+
when 'GROUP_ICON'
|
121
|
+
f.seek file_offset
|
122
|
+
data << CUR_ICO_HEADER.read(f)
|
123
|
+
nRead = CUR_ICO_HEADER::SIZE
|
124
|
+
data.last.wNumImages.to_i.times do
|
125
|
+
if nRead >= self.size
|
126
|
+
PEdump.logger.error "[!] refusing to read ICODIRENTRY beyond resource size"
|
127
|
+
break
|
128
|
+
end
|
129
|
+
data << ICODIRENTRY.read(f)
|
130
|
+
nRead += ICODIRENTRY::SIZE
|
131
|
+
end
|
132
|
+
when 'STRING'
|
133
|
+
f.seek file_offset
|
134
|
+
16.times do
|
135
|
+
break if f.tell >= file_offset+self.size
|
136
|
+
nChars = f.read(2).to_s.unpack('v').first.to_i
|
137
|
+
t =
|
138
|
+
if nChars*2 + 1 > self.size
|
139
|
+
# TODO: if it's not 1st string in table then truncated size must be less
|
140
|
+
PEdump.logger.error "[!] string size(#{nChars*2}) > stringtable size(#{self.size}). truncated to #{self.size-2}"
|
141
|
+
f.read(self.size-2)
|
142
|
+
else
|
143
|
+
f.read(nChars*2)
|
144
|
+
end
|
145
|
+
data <<
|
146
|
+
begin
|
147
|
+
t.force_encoding('UTF-16LE').encode!('UTF-8')
|
148
|
+
rescue
|
149
|
+
t.force_encoding('ASCII')
|
150
|
+
tt = t.size > 0x10 ? t[0,0x10].inspect+'...' : t.inspect
|
151
|
+
PEdump.logger.error "[!] cannot convert #{tt} to UTF-16"
|
152
|
+
[nChars,t].pack('va*')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
# XXX: check if readed strings summary length is less than resource data length
|
156
|
+
when 'VERSION'
|
157
|
+
require 'pedump/version_info'
|
158
|
+
f.seek file_offset
|
159
|
+
data << PEdump::VS_VERSIONINFO.read(f)
|
160
|
+
end
|
161
|
+
|
162
|
+
data.delete_if do |x|
|
163
|
+
valid = !x.respond_to?(:valid?) || x.valid?
|
164
|
+
PEdump.logger.warn "[?] ignoring invalid #{x.class}" unless valid
|
165
|
+
!valid
|
166
|
+
end
|
167
|
+
ensure
|
168
|
+
validate
|
169
|
+
end
|
170
|
+
|
171
|
+
def validate
|
172
|
+
self.valid =
|
173
|
+
case type
|
174
|
+
when 'BITMAP','ICON','CURSOR'
|
175
|
+
data.any?{ |x| x.is_a?(BITMAPINFOHEADER) && x.valid? } || data.first == 'PNG'
|
176
|
+
else
|
177
|
+
true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
STRING = Struct.new(:id, :lang, :value)
|
183
|
+
|
184
|
+
def strings f=nil
|
185
|
+
r = []
|
186
|
+
Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
|
187
|
+
res.data.each_with_index do |string,idx|
|
188
|
+
r << STRING.new( ((res.id-1)<<4) + idx, res.lang, string ) unless string.empty?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
r
|
192
|
+
end
|
193
|
+
|
194
|
+
# see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
|
195
|
+
|
196
|
+
class BITMAPINFOHEADER < create_struct 'V3v2V6',
|
197
|
+
:biSize, # BITMAPINFOHEADER::SIZE
|
198
|
+
:biWidth,
|
199
|
+
:biHeight,
|
200
|
+
:biPlanes,
|
201
|
+
:biBitCount,
|
202
|
+
:biCompression,
|
203
|
+
:biSizeImage,
|
204
|
+
:biXPelsPerMeter,
|
205
|
+
:biYPelsPerMeter,
|
206
|
+
:biClrUsed,
|
207
|
+
:biClrImportant
|
208
|
+
|
209
|
+
def valid?
|
210
|
+
self.biSize == 40
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
|
215
|
+
CUR_ICO_HEADER = create_struct('v3',
|
216
|
+
:wReserved, # always 0
|
217
|
+
:wResID, # always 2
|
218
|
+
:wNumImages # Number of cursor images/directory entries
|
219
|
+
)
|
220
|
+
|
221
|
+
CURDIRENTRY = create_struct 'v4Vv',
|
222
|
+
:wWidth,
|
223
|
+
:wHeight, # Divide by 2 to get the actual height.
|
224
|
+
:wPlanes,
|
225
|
+
:wBitCount,
|
226
|
+
:dwBytesInImage,
|
227
|
+
:wID
|
228
|
+
|
229
|
+
CURSOR_HOTSPOT = create_struct 'v2', :x, :y
|
230
|
+
|
231
|
+
ICODIRENTRY = create_struct 'C4v2Vv',
|
232
|
+
:bWidth,
|
233
|
+
:bHeight,
|
234
|
+
:bColors,
|
235
|
+
:bReserved,
|
236
|
+
:wPlanes,
|
237
|
+
:wBitCount,
|
238
|
+
:dwBytesInImage,
|
239
|
+
:wID
|
240
|
+
|
241
|
+
ROOT_RES_NAMES = [nil] + # numeration is started from 1
|
242
|
+
%w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
|
243
|
+
%w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
|
244
|
+
%w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
|
245
|
+
|
246
|
+
IMAGE_RESOURCE_DIRECTORY_ENTRY = create_struct 'V2',
|
247
|
+
:Name, :OffsetToData,
|
248
|
+
:name, :data
|
249
|
+
|
250
|
+
IMAGE_RESOURCE_DATA_ENTRY = create_struct 'V4',
|
251
|
+
:OffsetToData, :Size, :CodePage, :Reserved
|
252
|
+
|
253
|
+
IMAGE_RESOURCE_DIRECTORY = create_struct 'V2v4',
|
254
|
+
:Characteristics, :TimeDateStamp, # 2dw
|
255
|
+
:MajorVersion, :MinorVersion, :NumberOfNamedEntries, :NumberOfIdEntries, # 4w
|
256
|
+
:entries # manual
|
257
|
+
class IMAGE_RESOURCE_DIRECTORY
|
258
|
+
class << self
|
259
|
+
attr_accessor :base
|
260
|
+
alias :read_without_children :read
|
261
|
+
def read f, root=true
|
262
|
+
if root
|
263
|
+
@@loopchk1 = Hash.new(0)
|
264
|
+
@@loopchk2 = Hash.new(0)
|
265
|
+
@@loopchk3 = Hash.new(0)
|
266
|
+
elsif (@@loopchk1[f.tell] += 1) > 1
|
267
|
+
PEdump.logger.error "[!] #{self}: loop1 detected at file pos #{f.tell}" if @@loopchk1[f.tell] < 2
|
268
|
+
return nil
|
269
|
+
end
|
270
|
+
read_without_children(f).tap do |r|
|
271
|
+
nToRead = r.NumberOfNamedEntries.to_i + r.NumberOfIdEntries.to_i
|
272
|
+
r.entries = []
|
273
|
+
nToRead.times do |i|
|
274
|
+
if f.eof?
|
275
|
+
PEdump.logger.error "[!] #{self}: #{nToRead} entries in directory, but got EOF on #{i}-th."
|
276
|
+
break
|
277
|
+
end
|
278
|
+
if (@@loopchk2[f.tell] += 1) > 1
|
279
|
+
PEdump.logger.error "[!] #{self}: loop2 detected at file pos #{f.tell}" if @@loopchk2[f.tell] < 2
|
280
|
+
next
|
281
|
+
end
|
282
|
+
r.entries << IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
|
283
|
+
end
|
284
|
+
#r.entries.uniq!
|
285
|
+
r.entries.each do |entry|
|
286
|
+
entry.name =
|
287
|
+
if entry.Name.to_i & 0x8000_0000 > 0
|
288
|
+
# Name is an address of unicode string
|
289
|
+
f.seek base + entry.Name & 0x7fff_ffff
|
290
|
+
nChars = f.read(2).to_s.unpack("v").first.to_i
|
291
|
+
begin
|
292
|
+
f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
|
293
|
+
rescue
|
294
|
+
PEdump.logger.error "[!] #{self} failed to read entry name: #{$!}"
|
295
|
+
"???"
|
296
|
+
end
|
297
|
+
else
|
298
|
+
# Name is a numeric id
|
299
|
+
"##{entry.Name}"
|
300
|
+
end
|
301
|
+
if entry.OffsetToData && f.checked_seek(base + entry.OffsetToData & 0x7fff_ffff)
|
302
|
+
if (@@loopchk3[f.tell] += 1) > 1
|
303
|
+
PEdump.logger.error "[!] #{self}: loop3 detected at file pos #{f.tell}" if @@loopchk3[f.tell] < 2
|
304
|
+
next
|
305
|
+
end
|
306
|
+
entry.data =
|
307
|
+
if entry.OffsetToData & 0x8000_0000 > 0
|
308
|
+
# child is a directory
|
309
|
+
IMAGE_RESOURCE_DIRECTORY.read(f,false)
|
310
|
+
else
|
311
|
+
# child is a resource
|
312
|
+
IMAGE_RESOURCE_DATA_ENTRY.read(f)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
@@loopchk1 = @@loopchk2 = @@loopchk3 = nil if root # save some memory
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def _scan_resources f=nil, dir=nil
|
323
|
+
dir ||= resource_directory(f)
|
324
|
+
return nil unless dir
|
325
|
+
dir.entries.map do |entry|
|
326
|
+
case entry.data
|
327
|
+
when IMAGE_RESOURCE_DIRECTORY
|
328
|
+
if dir == @resource_directory # root resource directory
|
329
|
+
entry_type =
|
330
|
+
if entry.Name & 0x8000_0000 == 0
|
331
|
+
# root resource directory & entry name is a number
|
332
|
+
ROOT_RES_NAMES[entry.Name] || entry.name
|
333
|
+
else
|
334
|
+
entry.name
|
335
|
+
end
|
336
|
+
_scan_resources(f,entry.data).each do |res|
|
337
|
+
res.type = entry_type
|
338
|
+
res.parse f
|
339
|
+
end
|
340
|
+
else
|
341
|
+
_scan_resources(f,entry.data).each do |res|
|
342
|
+
res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
|
343
|
+
res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
|
344
|
+
end
|
345
|
+
end
|
346
|
+
when IMAGE_RESOURCE_DATA_ENTRY
|
347
|
+
Resource.new(
|
348
|
+
nil, # type
|
349
|
+
entry.name,
|
350
|
+
nil, # id
|
351
|
+
entry.Name, # lang
|
352
|
+
#entry.data.OffsetToData + @resource_data_base,
|
353
|
+
va2file(entry.data.OffsetToData),
|
354
|
+
entry.data.Size,
|
355
|
+
entry.data.CodePage,
|
356
|
+
entry.data.Reserved
|
357
|
+
)
|
358
|
+
else
|
359
|
+
logger.error "[!] invalid resource entry: #{entry.data.inspect}"
|
360
|
+
nil
|
361
|
+
end
|
362
|
+
end.flatten.compact
|
363
|
+
end
|
364
|
+
end
|
data/lib/pedump/tls.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class PEdump
|
2
|
+
IMAGE_TLS_DIRECTORY32 = create_struct 'V6',
|
3
|
+
:StartAddressOfRawData,
|
4
|
+
:EndAddressOfRawData,
|
5
|
+
:AddressOfIndex,
|
6
|
+
:AddressOfCallBacks,
|
7
|
+
:SizeOfZeroFill,
|
8
|
+
:Characteristics
|
9
|
+
|
10
|
+
IMAGE_TLS_DIRECTORY64 = create_struct 'Q4V2',
|
11
|
+
:StartAddressOfRawData,
|
12
|
+
:EndAddressOfRawData,
|
13
|
+
:AddressOfIndex,
|
14
|
+
:AddressOfCallBacks,
|
15
|
+
:SizeOfZeroFill,
|
16
|
+
:Characteristics
|
17
|
+
end
|
data/lib/pedump/version.rb
CHANGED
data/pedump.gemspec
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "pedump"
|
8
|
-
s.version = "0.4.
|
8
|
+
s.version = "0.4.4"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Andrey \"Zed\" Zaikin"]
|
@@ -36,9 +36,12 @@ Gem::Specification.new do |s|
|
|
36
36
|
"lib/pedump.rb",
|
37
37
|
"lib/pedump/cli.rb",
|
38
38
|
"lib/pedump/composite_io.rb",
|
39
|
+
"lib/pedump/core.rb",
|
39
40
|
"lib/pedump/packer.rb",
|
40
41
|
"lib/pedump/pe.rb",
|
42
|
+
"lib/pedump/resources.rb",
|
41
43
|
"lib/pedump/sig_parser.rb",
|
44
|
+
"lib/pedump/tls.rb",
|
42
45
|
"lib/pedump/version.rb",
|
43
46
|
"lib/pedump/version_info.rb",
|
44
47
|
"pedump.gemspec",
|
@@ -51,6 +54,7 @@ Gem::Specification.new do |s|
|
|
51
54
|
"spec/foldedhdr_spec.rb",
|
52
55
|
"spec/imports_badterm_spec.rb",
|
53
56
|
"spec/imports_vterm_spec.rb",
|
57
|
+
"spec/manyimportsW7_spec.rb",
|
54
58
|
"spec/pe_spec.rb",
|
55
59
|
"spec/pedump_spec.rb",
|
56
60
|
"spec/resource_spec.rb",
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/pedump')
|
3
|
+
|
4
|
+
describe "corkami/manyimportsW7.exe" do
|
5
|
+
before :all do
|
6
|
+
@sample = sample
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should have 2 imports" do
|
10
|
+
@sample.imports.size.should == 2
|
11
|
+
@sample.imports.map(&:module_name).should == %w'kernel32.dll msvcrt.dll'
|
12
|
+
@sample.imports.map do |iid|
|
13
|
+
(iid.original_first_thunk + iid.first_thunk).uniq.map(&:name)
|
14
|
+
end.flatten.should == ["ExitProcess", "printf"]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have 1 TLS" do
|
18
|
+
@sample.tls.size.should == 1
|
19
|
+
@sample.tls.first.AddressOfIndex.should == 0x401148
|
20
|
+
@sample.tls.first.AddressOfCallBacks.should == 0x401100
|
21
|
+
end
|
22
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pedump
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2011-12-20 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multipart-post
|
16
|
-
requirement: &
|
16
|
+
requirement: &70245146676060 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.1.4
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70245146676060
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: progressbar
|
27
|
-
requirement: &
|
27
|
+
requirement: &70245146675500 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.9.2
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70245146675500
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &70245146674820 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 2.3.0
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70245146674820
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70245146674020 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 1.0.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70245146674020
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: jeweler
|
60
|
-
requirement: &
|
60
|
+
requirement: &70245146673180 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.6.4
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70245146673180
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rcov
|
71
|
-
requirement: &
|
71
|
+
requirement: &70245146672260 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70245146672260
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: awesome_print
|
82
|
-
requirement: &
|
82
|
+
requirement: &70245146671740 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70245146671740
|
91
91
|
description: dump headers, sections, extract resources of win32 PE exe,dll,etc
|
92
92
|
email: zed.0xff@gmail.com
|
93
93
|
executables:
|
@@ -115,9 +115,12 @@ files:
|
|
115
115
|
- lib/pedump.rb
|
116
116
|
- lib/pedump/cli.rb
|
117
117
|
- lib/pedump/composite_io.rb
|
118
|
+
- lib/pedump/core.rb
|
118
119
|
- lib/pedump/packer.rb
|
119
120
|
- lib/pedump/pe.rb
|
121
|
+
- lib/pedump/resources.rb
|
120
122
|
- lib/pedump/sig_parser.rb
|
123
|
+
- lib/pedump/tls.rb
|
121
124
|
- lib/pedump/version.rb
|
122
125
|
- lib/pedump/version_info.rb
|
123
126
|
- pedump.gemspec
|
@@ -130,6 +133,7 @@ files:
|
|
130
133
|
- spec/foldedhdr_spec.rb
|
131
134
|
- spec/imports_badterm_spec.rb
|
132
135
|
- spec/imports_vterm_spec.rb
|
136
|
+
- spec/manyimportsW7_spec.rb
|
133
137
|
- spec/pe_spec.rb
|
134
138
|
- spec/pedump_spec.rb
|
135
139
|
- spec/resource_spec.rb
|
@@ -152,7 +156,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
156
|
version: '0'
|
153
157
|
segments:
|
154
158
|
- 0
|
155
|
-
hash:
|
159
|
+
hash: -576155968655045053
|
156
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
161
|
none: false
|
158
162
|
requirements:
|