pedump 0.5.4 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pedump/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'pedump'
2
2
  require 'pedump/packer'
3
+ require 'pedump/rich'
3
4
  require 'pedump/version_info'
4
5
  require 'optparse'
5
6
 
@@ -33,13 +34,14 @@ class PEdump::CLI
33
34
  attr_accessor :data, :argv
34
35
 
35
36
  KNOWN_ACTIONS = (
36
- %w'mz dos_stub rich pe ne data_directory sections tls security' +
37
- %w'strings resources resource_directory imports exports version_info packer web console packer_only'
37
+ %w'mz dos_stub rich pe ne te data_directory sections tls security' +
38
+ %w'strings resources resource_directory imports exports version_info packer web console packer_only' +
39
+ %w'extract' # 'disasm'
38
40
  ).map(&:to_sym)
39
41
 
40
- DEFAULT_ALL_ACTIONS = KNOWN_ACTIONS - %w'resource_directory web packer_only console'.map(&:to_sym)
42
+ DEFAULT_ALL_ACTIONS = KNOWN_ACTIONS - %w'resource_directory web packer_only console extract disasm'.map(&:to_sym)
41
43
 
42
- URL_BASE = "http://pedump.me"
44
+ URL_BASE = "https://pedump.me"
43
45
 
44
46
  def initialize argv = ARGV
45
47
  @argv = argv
@@ -96,8 +98,24 @@ class PEdump::CLI
96
98
  opts.on "--all", "Dump all but resource-directory (default)" do
97
99
  @actions = DEFAULT_ALL_ACTIONS
98
100
  end
101
+
102
+ opts.separator ''
103
+
104
+ # opts.on "--disasm [X]", "Disassemble a symbol/VA" do |v|
105
+ # @actions << [:disasm, v]
106
+ # end
107
+ opts.on("--extract ID", "Extract a resource/section/data_dir",
108
+ "ID: datadir:EXPORT - datadir by type",
109
+ "ID: resource:0x98478 - resource by offset",
110
+ "ID: resource:ICON/#1 - resource by type & name",
111
+ "ID: section:.text - section by name",
112
+ "ID: section:rva/0x1000 - section by RVA",
113
+ "ID: section:raw/0x400 - section by RAW_PTR",
114
+ ) do |v|
115
+ @actions << [:extract, v]
116
+ end
99
117
  opts.on "--va2file VA", "Convert RVA to file offset" do |va|
100
- @actions << [:va2file,va]
118
+ @actions << [:va2file, va]
101
119
  end
102
120
 
103
121
  opts.separator ''
@@ -133,9 +151,9 @@ class PEdump::CLI
133
151
  @file_name = fname
134
152
 
135
153
  File.open(fname,'rb') do |f|
136
- @pedump = create_pedump fname
154
+ @pedump = create_pedump f
137
155
 
138
- next if !@options[:force] && !@pedump.mz(f)
156
+ next if !@options[:force] && !@pedump.supported_file?(f)
139
157
 
140
158
  @actions.each do |action|
141
159
  case action
@@ -152,8 +170,8 @@ class PEdump::CLI
152
170
  # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
153
171
  end
154
172
 
155
- def create_pedump fname
156
- PEdump.new(fname, :force => @options[:force]).tap do |x|
173
+ def create_pedump io
174
+ PEdump.new(io, :force => @options[:force]).tap do |x|
157
175
  x.logger.level =
158
176
  case @options[:verbose]
159
177
  when -100..-3
@@ -194,16 +212,14 @@ class PEdump::CLI
194
212
  end
195
213
 
196
214
  class ProgressProxy
197
- attr_reader :pbar
198
-
199
- def initialize file
200
- @file = file
201
- @pbar = ProgressBar.new("[.] uploading", file.size, STDOUT)
202
- @pbar.try(:file_transfer_mode)
203
- @pbar.bar_mark = '='
215
+ def initialize file, prefix = "[.] uploading: ", io = $stdout
216
+ @file = file
217
+ @io = io
218
+ @prefix = prefix
204
219
  end
205
220
  def read *args
206
- @pbar.inc args.first
221
+ @io.write("\r#{@prefix}#{@file.tell}/#{@file.size} ")
222
+ @io.flush
207
223
  @file.read *args
208
224
  end
209
225
  def method_missing *args
@@ -212,6 +228,10 @@ class PEdump::CLI
212
228
  def respond_to? *args
213
229
  @file.respond_to?(args.first) || super(*args)
214
230
  end
231
+
232
+ def finish!
233
+ @io.write("\r#{@prefix}#{@file.size}/#{@file.size} \n")
234
+ end
215
235
  end
216
236
 
217
237
  def upload f
@@ -224,17 +244,17 @@ class PEdump::CLI
224
244
  require 'open-uri'
225
245
  require 'net/http'
226
246
  require 'net/http/post/multipart'
227
- require 'progressbar'
228
247
 
229
- stdout_sync = STDOUT.sync
230
- STDOUT.sync = true
248
+ stdout_sync = $stdout.sync
249
+ $stdout.sync = true
231
250
 
232
251
  md5 = Digest::MD5.file(f.path).hexdigest
233
252
  @pedump.logger.info "[.] md5: #{md5}"
234
253
  file_url = "#{URL_BASE}/#{md5}/"
235
254
 
236
255
  @pedump.logger.warn "[.] checking if file already uploaded.."
237
- Net::HTTP.start('pedump.me') do |http|
256
+ uri = URI.parse URL_BASE
257
+ Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == 'https')) do |http|
238
258
  http.open_timeout = 10
239
259
  http.read_timeout = 10
240
260
  # doing HTTP HEAD is a lot faster than open-uri
@@ -250,24 +270,26 @@ class PEdump::CLI
250
270
 
251
271
  f.rewind
252
272
 
253
- # upload with progressbar
273
+ # upload with progress
254
274
  post_url = URI.parse(URL_BASE+'/')
275
+ # UploadIO is from multipart-post
255
276
  uio = UploadIO.new(f, "application/octet-stream", File.basename(f.path))
256
277
  ppx = ProgressProxy.new(uio)
257
278
  req = Net::HTTP::Post::Multipart.new post_url.path, "file" => ppx
258
- res = Net::HTTP.start(post_url.host, post_url.port){ |http| http.request(req) }
259
- ppx.pbar.finish
260
-
261
- puts
262
- puts "[.] analyzing..."
279
+ res = Net::HTTP.start(post_url.host, post_url.port, use_ssl: (post_url.scheme == 'https')) do |http|
280
+ http.request(req)
281
+ end
282
+ ppx.finish!
263
283
 
264
- if (r=open(File.join(URL_BASE,md5,'analyze')).read) != "OK"
265
- raise "invalid server response: #{r}"
284
+ unless [200, 302, 303].include?(res.code.to_i)
285
+ @pedump.logger.fatal "[!] invalid server response: #{res.code} #{res.msg}"
286
+ @pedump.logger.fatal res.body unless res.body.empty?
287
+ exit(1)
266
288
  end
267
289
 
268
290
  puts "[.] uploaded: #{file_url}"
269
291
  ensure
270
- STDOUT.sync = stdout_sync
292
+ $stdout.sync = stdout_sync
271
293
  end
272
294
 
273
295
  def console f
@@ -311,6 +333,10 @@ class PEdump::CLI
311
333
  def dump_action action, f
312
334
  if action.is_a?(Array)
313
335
  case action[0]
336
+ when :disasm
337
+ return
338
+ when :extract
339
+ return extract action[1]
314
340
  when :va2file
315
341
  @pedump.sections(f)
316
342
  va = action[1] =~ /(^0x)|(h$)/i ? action[1].to_i(16) : action[1].to_i
@@ -457,6 +483,8 @@ class PEdump::CLI
457
483
  case data.first
458
484
  when PEdump::IMAGE_DATA_DIRECTORY
459
485
  dump_data_dir data
486
+ when PEdump::EFI_IMAGE_DATA_DIRECTORY
487
+ dump_efi_data_dir data
460
488
  when PEdump::IMAGE_SECTION_HEADER
461
489
  dump_sections data
462
490
  when PEdump::Resource
@@ -781,19 +809,26 @@ class PEdump::CLI
781
809
  end
782
810
  end
783
811
 
784
-
785
812
  def dump_data_dir data
786
813
  data.each do |row|
787
814
  printf " %-12s rva:0x%8x size:0x %8x\n", row.type, row.va.to_i, row.size.to_i
788
815
  end
789
816
  end
790
817
 
818
+ def dump_efi_data_dir data
819
+ data.each_with_index do |row, idx|
820
+ printf " %-12s rva:0x%8x size:0x %8x\n", PEdump::EFI_IMAGE_DATA_DIRECTORY::TYPES[idx], row.va.to_i, row.size.to_i
821
+ end
822
+ end
823
+
791
824
  def dump_rich_hdr data
792
825
  if decoded = data.decode
793
- puts " LIB_ID VERSION TIMES_USED "
826
+ puts " ID VER COUNT DESCRIPTION"
794
827
  decoded.each do |row|
795
- printf " %5d %2x %7d %4x %7d %3x\n",
796
- row.id, row.id, row.version, row.version, row.times, row.times
828
+ # printf " %5d %4x %7d %4x %12d %8x\n",
829
+ # row.id, row.id, row.version, row.version, row.times, row.times
830
+ printf " %4x %4x %12d %s\n",
831
+ row.id, row.version, row.times, ::PEdump::RICH_IDS[(row.id<<16) + row.version]
797
832
  end
798
833
  else
799
834
  puts "# raw:"
@@ -804,4 +839,79 @@ class PEdump::CLI
804
839
  end
805
840
  end
806
841
 
842
+ def disasm x
843
+ puts "TODO"
844
+ end
845
+
846
+ def extract x
847
+ a = x.split(':',2)
848
+ case a[0]
849
+ when 'datadir'
850
+ extract_datadir a[1]
851
+ when 'resource'
852
+ extract_resource a[1]
853
+ when 'section'
854
+ extract_section a[1]
855
+ else
856
+ raise "invalid #{x.inspect}"
857
+ end
858
+ end
859
+
860
+ def extract_datadir id
861
+ entry = @pedump.data_directory.find{ |x| x.type == id }
862
+ unless entry
863
+ @pedump.logger.fatal "[!] entry #{id.inspect} not found"
864
+ exit(1)
865
+ end
866
+ if entry.size != 0
867
+ IO::copy_stream @pedump.io, $stdout, entry.size, @pedump.va2file(entry.va)
868
+ end
869
+ end
870
+
871
+ def extract_resource id
872
+ res = nil
873
+ if id =~ /\A0x\h+\Z/
874
+ needle = id.to_i(16)
875
+ res = @pedump.resources.find{ |r| r.file_offset == needle }
876
+ elsif id =~ /\A\d+\Z/
877
+ needle = id.to_i(10)
878
+ res = @pedump.resources.find{ |r| r.file_offset == needle }
879
+ elsif id['/']
880
+ type, name = id.split('/', 2)
881
+ res = @pedump.resources.find{ |r| r.type == type && r.name == name }
882
+ else
883
+ @pedump.logger.fatal "[!] invalid resource id #{id.inspect}"
884
+ exit(1)
885
+ end
886
+ unless res
887
+ @pedump.logger.fatal "[!] resource #{id.inspect} not found"
888
+ exit(1)
889
+ end
890
+ IO::copy_stream @pedump.io, $stdout, res.size, res.file_offset
891
+ end
892
+
893
+ def extract_section id
894
+ section = nil
895
+ if id['/']
896
+ a = id.split('/',2)
897
+ if a[0] == 'rva'
898
+ value = a[1].to_i(0) # auto hex/dec, but also can parse oct and bin
899
+ section = @pedump.sections.find{ |s| s.VirtualAddress == value }
900
+ elsif a[0] == 'raw'
901
+ value = a[1].to_i(0) # auto hex/dec, but also can parse oct and bin
902
+ section = @pedump.sections.find{ |s| s.PointerToRawData == value }
903
+ else
904
+ @pedump.logger.fatal "[!] invalid section id #{id.inspect}"
905
+ exit(1)
906
+ end
907
+ else
908
+ section = @pedump.sections.find{ |s| s.Name == id }
909
+ end
910
+ unless section
911
+ @pedump.logger.fatal "[!] section #{id.inspect} not found"
912
+ exit(1)
913
+ end
914
+ IO::copy_stream @pedump.io, $stdout, section.SizeOfRawData, section.PointerToRawData
915
+ end
916
+
807
917
  end # class PEdump::CLI
@@ -10,15 +10,16 @@ class PEdump::Loader
10
10
  @hdr = x.dup
11
11
  end
12
12
  @data = EMPTY_DATA.dup
13
+ @delta = args[:delta] || 0
13
14
  @deferred_load_io = args[:deferred_load_io]
14
- @deferred_load_pos = args[:deferred_load_pos] || (@hdr && @hdr.PointerToRawData)
15
+ @deferred_load_pos = args[:deferred_load_pos] || (@hdr && (@hdr.PointerToRawData - @delta))
15
16
  @deferred_load_size = args[:deferred_load_size] || (@hdr && @hdr.SizeOfRawData)
16
17
  @image_base = args[:image_base] || 0
17
18
  end
18
19
 
19
20
  def name; @hdr.Name; end
20
- def va ; @hdr.VirtualAddress + @image_base; end
21
- def rva ; @hdr.VirtualAddress; end
21
+ def va ; @hdr.VirtualAddress + @image_base - @delta; end
22
+ def rva ; @hdr.VirtualAddress - @delta; end
22
23
  def vsize; @hdr.VirtualSize; end
23
24
  def flags; @hdr.Characteristics; end
24
25
  def flags= f; @hdr.Characteristics= f; end
@@ -51,6 +52,7 @@ class PEdump::Loader
51
52
  @data.size > 0 ? @data.size.to_s(16) : (@deferred_load_io ? "<defer>" : 0)
52
53
  ]
53
54
  r << (" dlpos=%8x" % @deferred_load_pos) if @deferred_load_pos
55
+ r << (" delta=%3x" % @delta) if @delta != 0
54
56
  r << ">"
55
57
  end
56
58
  end
data/lib/pedump/loader.rb CHANGED
@@ -15,7 +15,15 @@ class PEdump::Loader
15
15
 
16
16
  # shortcuts
17
17
  alias :pe :pe_hdr
18
- def ep; @pe_hdr.ioh.AddressOfEntryPoint; end
18
+
19
+ def ep
20
+ if @pe_hdr
21
+ @pe_hdr.try(:ioh).try(:AddressOfEntryPoint)
22
+ elsif @te_hdr
23
+ @te_hdr.AddressOfEntryPoint - @delta
24
+ end
25
+ end
26
+
19
27
  def ep= v; @pe_hdr.ioh.AddressOfEntryPoint=v; end
20
28
 
21
29
  ########################################################################
@@ -23,12 +31,24 @@ class PEdump::Loader
23
31
  ########################################################################
24
32
 
25
33
  def initialize io = nil, params = {}
34
+ # @delta is for EFI TE loading, based on https://github.com/gdbinit/TELoader/blob/master/teloader.cpp
35
+ @delta = 0
26
36
  @pedump = PEdump.new(io, params)
27
37
  if io
28
38
  @mz_hdr = @pedump.mz
29
39
  @dos_stub = @pedump.dos_stub
30
40
  @pe_hdr = @pedump.pe
31
- @image_base = params[:image_base] || @pe_hdr.try(:ioh).try(:ImageBase) || 0
41
+ @te_hdr = @pedump.te
42
+
43
+ @image_base = params[:image_base]
44
+ if @pe_hdr
45
+ @image_base ||= @pe_hdr.try(:ioh).try(:ImageBase)
46
+ elsif @te_hdr
47
+ @image_base ||= @te_hdr.ImageBase
48
+ @delta = @pedump.te_shift
49
+ end
50
+ @image_base ||= 0
51
+
32
52
  load_sections @pedump.sections, io
33
53
  end
34
54
  @find_limit = params[:find_limit] || DEFAULT_FIND_LIMIT
@@ -38,7 +58,7 @@ class PEdump::Loader
38
58
  if section_hdrs.is_a?(Array)
39
59
  @sections = section_hdrs.map do |x|
40
60
  raise "unknown section hdr: #{x.inspect}" unless x.is_a?(PEdump::IMAGE_SECTION_HEADER)
41
- Section.new(x, :deferred_load_io => f, :image_base => @image_base )
61
+ Section.new(x, :deferred_load_io => f, :image_base => @image_base, :delta => @delta )
42
62
  end
43
63
  if f.respond_to?(:seek) && f.respond_to?(:read)
44
64
  #
@@ -249,10 +269,12 @@ class PEdump::Loader
249
269
  def names
250
270
  return @names if @names
251
271
  @names = {}
252
- if oep = @pe_hdr.try(:ioh).try(:AddressOfEntryPoint)
253
- oep += @image_base
254
- @names[oep] = 'start'
272
+
273
+ oep = ep()
274
+ if oep
275
+ @names[oep + @image_base] = 'start'
255
276
  end
277
+
256
278
  _parse_imports
257
279
  _parse_exports
258
280
  #TODO: debug info
data/lib/pedump/ne.rb CHANGED
@@ -405,7 +405,7 @@ class PEdump
405
405
  begin
406
406
  ne_offset = mz(f) && mz(f).lfanew
407
407
  if ne_offset.nil?
408
- logger.fatal "[!] NULL NE offset (e_lfanew)."
408
+ logger.debug "[!] NULL NE offset (e_lfanew)."
409
409
  nil
410
410
  elsif ne_offset > f.size
411
411
  logger.fatal "[!] NE offset beyond EOF."
data/lib/pedump/pe.rb CHANGED
@@ -24,78 +24,87 @@ class PEdump
24
24
  signature + ifh.pack + ioh.pack
25
25
  end
26
26
 
27
- def self.read f, args = {}
27
+ def self.read_sections f, nToRead, args = {}
28
28
  force = args[:force]
29
29
 
30
+ if nToRead > 0xffff
31
+ if force.is_a?(Numeric) && force > 1
32
+ PEdump.logger.warn "[!] too many sections (#{nToRead}). forced. reading all"
33
+ else
34
+ PEdump.logger.warn "[!] too many sections (#{nToRead}). not forced, reading first 65535"
35
+ nToRead = 65535
36
+ end
37
+ end
38
+
39
+ sections = []
40
+ nToRead.times do
41
+ break if f.eof?
42
+ sections << IMAGE_SECTION_HEADER.read(f)
43
+ end
44
+
45
+ if sections.any?
46
+ # zero all missing values of last section
47
+ sections.last.tap do |last_section|
48
+ last_section.each_pair do |k,v|
49
+ last_section[k] = 0 if v.nil?
50
+ end
51
+ end
52
+ end
53
+
54
+ sections
55
+ end
56
+
57
+ def self.read f, args = {}
30
58
  pe_offset = f.tell
31
59
  pe_sig = f.read 4
32
60
  #logger.error "[!] 'NE' format is not supported!" if pe_sig == "NE\x00\x00"
33
61
  if pe_sig != "PE\x00\x00"
34
- if force
62
+ if args[:force]
35
63
  logger.warn "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect})"
36
64
  else
37
65
  logger.debug "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect}). (not forced)"
38
66
  return nil
39
67
  end
40
68
  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
69
+ pe = PE.new(pe_sig)
70
+ pe.image_file_header = IMAGE_FILE_HEADER.read(f)
71
+ ioh_offset = f.tell # offset to IMAGE_OPTIONAL_HEADER
72
+ if pe.ifh.SizeOfOptionalHeader.to_i > 0
73
+ if pe.x64?
74
+ pe.image_optional_header = IMAGE_OPTIONAL_HEADER64.read(f, pe.ifh.SizeOfOptionalHeader)
75
+ else
76
+ pe.image_optional_header = IMAGE_OPTIONAL_HEADER32.read(f, pe.ifh.SizeOfOptionalHeader)
50
77
  end
78
+ end
51
79
 
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
80
+ nToRead=pe.ifh.NumberOfSections.to_i
60
81
 
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
82
+ # 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.
83
+ # // http://www.phreedom.org/research/tinype/
84
+ f.seek( ioh_offset + pe.ifh.SizeOfOptionalHeader.to_i )
85
+ pe.sections = read_sections(f, nToRead, args)
69
86
 
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
87
+ pe_end = f.tell
88
+ if s=pe.sections.find{ |s| (pe_offset...pe_end).include?(s.va) }
89
+ if args[:pass2]
90
+ # already called with CompositeIO ?
91
+ PEdump.logger.error "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! 2nd time?!"
78
92
 
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
93
+ elsif pe_end-pe_offset < 0x100_000
94
+ PEdump.logger.warn "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! trying to rebuild..."
95
+ f.seek pe_offset
96
+ data = f.read(s.va-pe_offset)
97
+ f.seek s.PointerToRawData
98
+ io = CompositeIO.new(StringIO.new(data), f)
99
+ args1 = args.dup
100
+ args1[:pass2] = true
101
+ return PE.read(io, args1)
102
+ else
103
+ PEdump.logger.error "[!] section with va=0x#{s.va.to_s(16)} overwrites PE header! too big to rebuild!"
97
104
  end
98
105
  end
106
+
107
+ pe
99
108
  end
100
109
 
101
110
  def self.logger; PEdump.logger; end
@@ -106,7 +115,7 @@ class PEdump
106
115
  begin
107
116
  pe_offset = mz(f) && mz(f).lfanew
108
117
  if pe_offset.nil?
109
- logger.fatal "[!] NULL PE offset (e_lfanew). cannot continue."
118
+ logger.debug "[!] NULL PE offset (e_lfanew). cannot continue."
110
119
  nil
111
120
  elsif pe_offset > f.size
112
121
  logger.fatal "[!] PE offset beyond EOF. cannot continue."