pedump 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,593 @@
1
+ require 'pedump'
2
+ require 'optparse'
3
+
4
+ unless Object.instance_methods.include?(:try)
5
+ class Object
6
+ def try(*x)
7
+ send(*x) if respond_to?(x.first)
8
+ end
9
+ end
10
+ end
11
+
12
+ class PEdump::CLI
13
+ attr_accessor :data, :argv
14
+
15
+ KNOWN_ACTIONS = (
16
+ %w'mz dos_stub rich pe data_directory sections' +
17
+ %w'strings resources resource_directory imports exports packer web packer_only'
18
+ ).map(&:to_sym)
19
+
20
+ DEFAULT_ALL_ACTIONS = KNOWN_ACTIONS - %w'resource_directory web packer_only'.map(&:to_sym)
21
+
22
+ URL_BASE = "http://pedump.me"
23
+
24
+ def initialize argv = ARGV
25
+ @argv = argv
26
+ end
27
+
28
+ def run
29
+ @actions = []
30
+ @options = { :format => :table }
31
+ optparser = OptionParser.new do |opts|
32
+ opts.banner = "Usage: pedump [options]"
33
+
34
+ opts.on "-V", "--version", "Print version information and exit" do
35
+ puts PEdump::VERSION
36
+ exit
37
+ end
38
+ opts.on "-v", "--[no-]verbose", "Run verbosely" do |v|
39
+ @options[:verbose] ||= 0
40
+ @options[:verbose] += 1
41
+ end
42
+ opts.on "-F", "--force", "Try to dump by all means","(can cause exceptions & heavy wounds)" do |v|
43
+ @options[:force] ||= 0
44
+ @options[:force] += 1
45
+ end
46
+ opts.on "-f", "--format FORMAT", [:binary, :c, :dump, :hex, :inspect, :table],
47
+ "Output format: bin,c,dump,hex,inspect,table","(default: table)" do |v|
48
+ @options[:format] = v
49
+ end
50
+ KNOWN_ACTIONS.each do |t|
51
+ opts.on "--#{t.to_s.tr('_','-')}", eval("lambda{ |_| @actions << :#{t.to_s.tr('-','_')} }")
52
+ end
53
+ opts.on '-P', "--packer-only", "packer/compiler detect only,","mimics 'file' command output" do
54
+ @actions << :packer_only
55
+ end
56
+ opts.on "--all", "Dump all but resource-directory (default)" do
57
+ @actions = DEFAULT_ALL_ACTIONS
58
+ end
59
+ opts.on "--va2file VA", "Convert RVA to file offset" do |va|
60
+ @actions << [:va2file,va]
61
+ end
62
+ opts.on "-W", "--web", "Uploads files to a #{URL_BASE}","for a nice HTML tables with image previews,","candies & stuff" do
63
+ @actions << :web
64
+ end
65
+ end
66
+
67
+ if (@argv = optparser.parse(@argv)).empty?
68
+ puts optparser.help
69
+ return
70
+ end
71
+
72
+ if (@actions-KNOWN_ACTIONS).any?{ |x| !x.is_a?(Array) }
73
+ puts "[?] unknown actions: #{@actions-KNOWN_ACTIONS}"
74
+ @actions.delete_if{ |x| !KNOWN_ACTIONS.include?(x) }
75
+ end
76
+ @actions = DEFAULT_ALL_ACTIONS if @actions.empty?
77
+
78
+ if @actions.include?(:packer_only)
79
+ raise "[!] can't mix --packer-only with other actions" if @actions.size > 1
80
+ dump_packer_only(argv)
81
+ return
82
+ end
83
+
84
+ argv.each_with_index do |fname,idx|
85
+ @need_fname_header = (argv.size > 1)
86
+ @file_idx = idx
87
+ @file_name = fname
88
+
89
+ File.open(fname,'rb') do |f|
90
+ @pedump = PEdump.new(fname, :force => @options[:force]).tap do |x|
91
+ if @options[:verbose]
92
+ x.logger.level = @options[:verbose] > 1 ? Logger::INFO : Logger::DEBUG
93
+ end
94
+ end
95
+
96
+ next if !@options[:force] && !@pedump.mz(f)
97
+
98
+ @actions.each do |action|
99
+ if action == :web
100
+ upload f
101
+ else
102
+ dump_action action,f
103
+ end
104
+ end
105
+ end
106
+ end
107
+ rescue Errno::EPIPE
108
+ # output interrupt, f.ex. when piping output to a 'head' command
109
+ # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
110
+ end
111
+
112
+ def dump_packer_only fnames
113
+ max_fname_len = fnames.map(&:size).max
114
+ fnames.each do |fname|
115
+ File.open(fname,'rb') do |f|
116
+ @pedump = PEdump.new(fname, :force => @options[:force]).tap do |x|
117
+ if @options[:verbose]
118
+ x.logger.level = @options[:verbose] > 1 ? Logger::INFO : Logger::DEBUG
119
+ end
120
+ end
121
+ packers = @pedump.packers(f)
122
+ pname = Array(packers).first.try(:packer).try(:name)
123
+ pname ||= "unknown" if @options[:verbose]
124
+ printf("%-*s %s\n", max_fname_len+1, "#{fname}:", pname) if pname
125
+ end
126
+ end
127
+ end
128
+
129
+ class ProgressProxy
130
+ attr_reader :pbar
131
+
132
+ def initialize file
133
+ @file = file
134
+ @pbar = ProgressBar.new("[.] uploading", file.size, STDOUT)
135
+ @pbar.try(:file_transfer_mode)
136
+ @pbar.bar_mark = '='
137
+ end
138
+ def read *args
139
+ @pbar.inc args.first
140
+ @file.read *args
141
+ end
142
+ def method_missing *args
143
+ @file.send *args
144
+ end
145
+ def respond_to? *args
146
+ @file.respond_to?(*args) || super(*args)
147
+ end
148
+ end
149
+
150
+ def upload f
151
+ if @pedump.mz(f).signature != 'MZ'
152
+ @pedump.logger.error "[!] refusing to upload a non-MZ file"
153
+ return
154
+ end
155
+
156
+ require 'digest/md5'
157
+ require 'open-uri'
158
+ require 'net/http/post/multipart'
159
+ require 'progressbar'
160
+
161
+ stdout_sync = STDOUT.sync
162
+ STDOUT.sync = true
163
+
164
+ md5 = Digest::MD5.file(f.path).hexdigest
165
+ @pedump.logger.info "[.] md5: #{md5}"
166
+ file_url = "#{URL_BASE}/#{md5}/"
167
+
168
+ @pedump.logger.info "[.] checking if file already uploaded.."
169
+ begin
170
+ if (r=open(file_url).read) == "OK"
171
+ @pedump.logger.warn "[.] file already uploaded: #{file_url}"
172
+ return
173
+ else
174
+ raise "invalid server response: #{r}"
175
+ end
176
+ rescue OpenURI::HTTPError
177
+ raise unless $!.to_s == "404 Not Found"
178
+ end
179
+
180
+ f.rewind
181
+
182
+ # upload with progressbar
183
+ post_url = URI.parse(URL_BASE+'/')
184
+ uio = UploadIO.new(f, "application/octet-stream", File.basename(f.path))
185
+ ppx = ProgressProxy.new(uio)
186
+ req = Net::HTTP::Post::Multipart.new post_url.path, "file" => ppx
187
+ res = Net::HTTP.start(post_url.host, post_url.port){ |http| http.request(req) }
188
+ ppx.pbar.finish
189
+
190
+ puts
191
+ puts "[.] analyzing..."
192
+
193
+ if (r=open(File.join(URL_BASE,md5,'analyze')).read) != "OK"
194
+ raise "invalid server response: #{r}"
195
+ end
196
+
197
+ puts "[.] uploaded: #{file_url}"
198
+ ensure
199
+ STDOUT.sync = stdout_sync
200
+ end
201
+
202
+ def action_title action
203
+ if @need_fname_header
204
+ @need_fname_header = false
205
+ puts if @file_idx > 0
206
+ puts "# -----------------------------------------------"
207
+ puts "# #@file_name"
208
+ puts "# -----------------------------------------------"
209
+ end
210
+
211
+ s = action.to_s.upcase.tr('_',' ')
212
+ s += " Header" if [:mz, :pe, :rich].include?(action)
213
+ s = "Packer / Compiler" if action == :packer
214
+ "\n=== %s ===\n\n" % s
215
+ end
216
+
217
+ def dump_action action, f
218
+ if action.is_a?(Array)
219
+ case action[0]
220
+ when :va2file
221
+ @pedump.sections(f)
222
+ va = action[1] =~ /(^0x)|(h$)/i ? action[1].to_i(16) : action[1].to_i
223
+ file_offset = @pedump.va2file(va)
224
+ printf "va2file(0x%x) = 0x%x (%d)\n", va, file_offset, file_offset
225
+ return
226
+ else raise "unknown action #{action.inspect}"
227
+ end
228
+ end
229
+
230
+ data = @pedump.send(action, f)
231
+ return if !data || (data.respond_to?(:empty?) && data.empty?)
232
+
233
+ puts action_title(action)
234
+
235
+ return dump(data) if [:inspect, :table].include?(@options[:format])
236
+
237
+ dump_opts = {:name => action}
238
+ case action
239
+ when :pe
240
+ @pedump.pe.ifh.TimeDateStamp = @pedump.pe.ifh.TimeDateStamp.to_i
241
+ data = @pedump.pe.signature + (@pedump.pe.ifh.try(:pack)||'') + (@pedump.pe.ioh.try(:pack)||'')
242
+ @pedump.pe.ifh.TimeDateStamp = Time.at(@pedump.pe.ifh.TimeDateStamp)
243
+ when :resources
244
+ return dump_resources(data)
245
+ when :strings
246
+ return dump_strings(data)
247
+ when :imports
248
+ return dump_imports(data)
249
+ when :exports
250
+ return dump_exports(data)
251
+ else
252
+ if data.is_a?(Struct) && data.respond_to?(:pack)
253
+ data = data.pack
254
+ elsif data.is_a?(Array) && data.all?{ |x| x.is_a?(Struct) && x.respond_to?(:pack)}
255
+ data = data.map(&:pack).join
256
+ end
257
+ end
258
+ dump data, dump_opts
259
+ end
260
+
261
+ def dump data, opts = {}
262
+ case opts[:format] || @options[:format] || :dump
263
+ when :dump, :hexdump
264
+ puts hexdump(data)
265
+ when :hex
266
+ puts data.each_byte.map{ |x| "%02x" % x }.join(' ')
267
+ when :binary
268
+ print data
269
+ when :c
270
+ name = opts[:name] || "foo"
271
+ puts "// #{data.size} bytes total"
272
+ puts "unsigned char #{name}[] = {"
273
+ data.unpack('C*').each_slice(12) do |row|
274
+ puts " " + row.map{ |c| "0x%02x," % c}.join(" ")
275
+ end
276
+ puts "};"
277
+ when :inspect
278
+ require 'pp'
279
+ pp data
280
+ when :table
281
+ dump_table data
282
+ end
283
+ end
284
+
285
+ COMMENTS = {
286
+ :Machine => {
287
+ 0x014c => 'x86',
288
+ 0x0200 => 'Intel Itanium',
289
+ 0x8664 => 'x64',
290
+ 'default' => '???'
291
+ },
292
+ :Magic => {
293
+ 0x010b => '32-bit executable',
294
+ 0x020b => '64-bit executable',
295
+ 0x0107 => 'ROM image',
296
+ 'default' => '???'
297
+ },
298
+ :Subsystem => PEdump::IMAGE_SUBSYSTEMS
299
+ }
300
+
301
+ def dump_generic_table data
302
+ data.each_pair do |k,v|
303
+ case v
304
+ when Numeric
305
+ case k
306
+ when /\AMajor.*Version\Z/
307
+ printf "%30s: %24s\n", k.to_s.sub('Major',''), "#{v}.#{data[k.to_s.sub('Major','Minor')]}"
308
+ when /\AMinor.*Version\Z/
309
+ when /TimeDateStamp/
310
+ printf "%30s: %24s\n", k, Time.at(v).strftime('"%Y-%m-%d %H:%M:%S"')
311
+ else
312
+ if COMMENTS[k]
313
+ printf "%30s: %10d %12s %s\n", k, v, v<10 ? v : ("0x"+v.to_s(16)),
314
+ COMMENTS[k][v] || (COMMENTS[k].is_a?(Hash) ? COMMENTS[k]['default'] : '') || ''
315
+ else
316
+ printf "%30s: %10d %12s\n", k, v, v<10 ? v : ("0x"+v.to_s(16))
317
+ end
318
+ end
319
+ when Struct
320
+ printf "\n# %s:\n", v.class.to_s.split('::').last
321
+ dump_table v
322
+ when Time
323
+ printf "%30s: %24s\n", k, v.strftime('"%Y-%m-%d %H:%M:%S"')
324
+ when Array
325
+ next if %w'DataDirectory section_table'.include?(k)
326
+ else
327
+ printf "%30s: %24s\n", k, v.to_s.inspect
328
+ end
329
+ end
330
+ end
331
+
332
+ def dump_table data
333
+ if data.is_a?(Struct)
334
+ return dump_res_dir(data) if data.is_a?(PEdump::IMAGE_RESOURCE_DIRECTORY)
335
+ return dump_exports(data) if data.is_a?(PEdump::IMAGE_EXPORT_DIRECTORY)
336
+ dump_generic_table data
337
+ elsif data.is_a?(Enumerable) && data.map(&:class).uniq.size == 1
338
+ case data.first
339
+ when PEdump::IMAGE_DATA_DIRECTORY
340
+ dump_data_dir data
341
+ when PEdump::IMAGE_SECTION_HEADER
342
+ dump_sections data
343
+ when PEdump::Resource
344
+ dump_resources data
345
+ when PEdump::STRING
346
+ dump_strings data
347
+ when PEdump::IMAGE_IMPORT_DESCRIPTOR
348
+ dump_imports data
349
+ when PEdump::Packer::Match
350
+ dump_packers data
351
+ else
352
+ puts "[?] don't know how to dump: #{data.inspect[0,50]}" unless data.empty?
353
+ end
354
+ elsif data.is_a?(PEdump::DOSStub)
355
+ puts hexdump(data)
356
+ elsif data.is_a?(PEdump::RichHdr)
357
+ dump_rich_hdr data
358
+ else
359
+ puts "[?] Don't know how to display #{data.inspect[0,50]}... as a table"
360
+ end
361
+ end
362
+
363
+ def dump_packers data
364
+ if @options[:verbose]
365
+ data.each do |p|
366
+ printf "%8x %4d %s\n", p.offset, p.packer.size, p.packer.name
367
+ end
368
+ else
369
+ # show only largest detected unless verbose output requested
370
+ puts " #{data.first.packer.name}"
371
+ end
372
+ end
373
+
374
+ def dump_exports data
375
+ printf "# module %s\n# flags=0x%x ts=%s version=%d.%d ord_base=%d\n",
376
+ data.name.inspect,
377
+ data.Characteristics.to_i,
378
+ Time.at(data.TimeDateStamp.to_i).strftime('"%Y-%m-%d %H:%M:%S"'),
379
+ data.MajorVersion, data.MinorVersion,
380
+ data.Base
381
+
382
+ if @options[:verbose]
383
+ [%w'Names', %w'EntryPoints Functions', %w'Ordinals NameOrdinals'].each do |x|
384
+ va = data["AddressOf"+x.last]
385
+ ofs = @pedump.va2file(va) || '?'
386
+ printf "# %-12s rva=0x%08x file_offset=%8s\n", x.first, va, ofs
387
+ end
388
+ end
389
+
390
+ printf "# nFuncs=%d nNames=%d\n",
391
+ data.NumberOfFunctions,
392
+ data.NumberOfNames
393
+
394
+ return unless data.name_ordinals.any? || data.entry_points.any? || data.names.any?
395
+
396
+ puts
397
+
398
+ ord2name = {}
399
+ data.NumberOfNames.times do |i|
400
+ ord2name[data.name_ordinals[i]] ||= []
401
+ ord2name[data.name_ordinals[i]] << data.names[i]
402
+ end
403
+
404
+ printf "%5s %8s %s\n", "ORD", "ENTRY_VA", "NAME"
405
+ data.NumberOfFunctions.times do |i|
406
+ ep = data.entry_points[i]
407
+ names = ord2name[i+data.Base].try(:join,', ')
408
+ next if ep.to_i == 0 && names.nil?
409
+ printf "%5d %8x %s\n", i + data.Base, ep, names
410
+ end
411
+ end
412
+
413
+ def dump_imports data
414
+ fmt = "%-15s %5s %5s %s\n"
415
+ printf fmt, "MODULE_NAME", "HINT", "ORD", "FUNCTION_NAME"
416
+ data.each do |iid|
417
+ # image import descriptor
418
+ (Array(iid.original_first_thunk) + Array(iid.first_thunk)).uniq.each do |f|
419
+ next unless f
420
+ # imported function
421
+ printf fmt,
422
+ iid.module_name,
423
+ f.hint ? f.hint.to_s(16) : '',
424
+ f.ordinal ? f.ordinal.to_s(16) : '',
425
+ f.name
426
+ end
427
+ end
428
+ end
429
+
430
+ def dump_strings data
431
+ printf "%5s %5s %4s %s\n", "ID", "ID", "LANG", "STRING"
432
+ prev_lang = nil
433
+ data.sort_by{|s| [s.lang, s.id] }.each do |s|
434
+ #puts if prev_lang && prev_lang != s.lang
435
+ printf "%5d %5x %4x %s\n", s.id, s.id, s.lang, s.value.inspect
436
+ prev_lang = s.lang
437
+ end
438
+ end
439
+
440
+ def dump_res_dir entry, level = 0
441
+ if entry.is_a?(PEdump::IMAGE_RESOURCE_DIRECTORY)
442
+ # root entry
443
+ printf "dir? %8s %8s %5s %5s", "FLAGS", "TIMESTMP", "VERS", 'nEnt'
444
+ printf " | %-15s %8s | ", "NAME", "OFFSET"
445
+ printf "data? %8s %8s %5s %8s\n", 'DATA_OFS', 'DATA_SZ', 'CP', 'RESERVED'
446
+ end
447
+
448
+ dir =
449
+ case entry
450
+ when PEdump::IMAGE_RESOURCE_DIRECTORY
451
+ entry
452
+ when PEdump::IMAGE_RESOURCE_DIRECTORY_ENTRY
453
+ entry.data
454
+ end
455
+
456
+ fmt1 = "DIR: %8x %8x %5s %5d"
457
+ fmt1s = fmt1.tr("xd\nDIR:","ss ") % ['','','','']
458
+
459
+ if dir.is_a?(PEdump::IMAGE_RESOURCE_DIRECTORY)
460
+ printf fmt1,
461
+ dir.Characteristics, dir.TimeDateStamp,
462
+ [dir.MajorVersion,dir.MinorVersion].join('.'),
463
+ dir.NumberOfNamedEntries + dir.NumberOfIdEntries
464
+ else
465
+ print fmt1s
466
+ end
467
+
468
+ name =
469
+ case level
470
+ when 0 then "ROOT"
471
+ when 1 then PEdump::ROOT_RES_NAMES[entry.Name] || entry.name
472
+ else entry.name
473
+ end
474
+
475
+ printf " | %-15s", name
476
+ printf("\n%s %15s",fmt1s,'') if name.size > 15
477
+ printf " %8x | ", entry.respond_to?(:OffsetToData) ? entry.OffsetToData : 0
478
+
479
+ if dir.is_a?(PEdump::IMAGE_RESOURCE_DIRECTORY)
480
+ puts
481
+ dir.entries.each do |child|
482
+ dump_res_dir child, level+1
483
+ end
484
+ elsif dir
485
+ printf "DATA: %8x %8x %5s %8x\n", dir.OffsetToData, dir.Size, dir.CodePage, dir.Reserved
486
+ else
487
+ puts # null dir
488
+ end
489
+ end
490
+
491
+ # def dump_res_dir0 dir, level=0, dir_entry = nil
492
+ # dir_entry ||= PEdump::IMAGE_RESOURCE_DIRECTORY_ENTRY.new
493
+ # printf "%-10s %8x %8x %8x %5s %5d\n", dir_entry.name || "ROOT", dir_entry.OffsetToData.to_i,
494
+ # dir.Characteristics, dir.TimeDateStamp,
495
+ # [dir.MajorVersion,dir.MinorVersion].join('.'),
496
+ # dir.NumberOfNamedEntries + dir.NumberOfIdEntries
497
+ # dir.entries.each do |child|
498
+ # if child.data.is_a?(PEdump::IMAGE_RESOURCE_DIRECTORY)
499
+ # dump_res_dir child.data, level+1, child
500
+ # else
501
+ # print " "*(level+1) + "CHILD"
502
+ # child.data.each_pair do |k,v|
503
+ # print " #{k[0,2]}=#{v}"
504
+ # end
505
+ # puts
506
+ # #p child
507
+ # end
508
+ # end
509
+ # end
510
+
511
+ def dump_resources data
512
+ keys = []; fmt = []
513
+ fmt << "%11x " ; keys << :file_offset
514
+ fmt << "%5d " ; keys << :cp
515
+ fmt << "%5x " ; keys << :lang
516
+ fmt << "%8d " ; keys << :size
517
+ fmt << "%-13s "; keys << :type
518
+ fmt << "%s\n" ; keys << :name
519
+ printf fmt.join.tr('dx','s'), *keys.map(&:to_s).map(&:upcase)
520
+ data.each do |res|
521
+ fmt.each_with_index do |f,i|
522
+ v = res.send(keys[i])
523
+ if f['x']
524
+ printf f.tr('x','s'), v.to_i < 10 ? v.to_s : "0x#{v.to_s(16)}"
525
+ else
526
+ printf f, v
527
+ end
528
+ end
529
+ end
530
+ end
531
+
532
+ def dump_sections data
533
+ printf " %-8s %8s %8s %8s %8s %5s %8s %5s %8s %8s\n",
534
+ 'NAME', 'RVA', 'VSZ','RAW_SZ','RAW_PTR','nREL','REL_PTR','nLINE','LINE_PTR','FLAGS'
535
+ data.each do |s|
536
+ name = s.Name[/[^a-z0-9_.]/i] ? s.Name.inspect : s.Name
537
+ name = "#{name}\n " if name.size > 8
538
+ printf " %-8s %8x %8x %8x %8x %5x %8x %5x %8x %8x %s\n", name.to_s,
539
+ s.VirtualAddress.to_i, s.VirtualSize.to_i,
540
+ s.SizeOfRawData.to_i, s.PointerToRawData.to_i,
541
+ s.NumberOfRelocations.to_i, s.PointerToRelocations.to_i,
542
+ s.NumberOfLinenumbers.to_i, s.PointerToLinenumbers.to_i,
543
+ s.flags.to_i, s.flags_desc
544
+ end
545
+ end
546
+
547
+ def dump_data_dir data
548
+ data.each do |row|
549
+ printf " %-12s rva:0x%8x size:0x %8x\n", row.type, row.va.to_i, row.size.to_i
550
+ end
551
+ end
552
+
553
+ def dump_rich_hdr data
554
+ if decoded = data.decode
555
+ puts " LIB_ID VERSION TIMES_USED "
556
+ decoded.each do |row|
557
+ printf " %5d %2x %7d %4x %7d %3x\n",
558
+ row.id, row.id, row.version, row.version, row.times, row.times
559
+ end
560
+ else
561
+ puts "# raw:"
562
+ puts hexdump(data)
563
+ puts
564
+ puts "# dexored:"
565
+ puts hexdump(data.dexor)
566
+ end
567
+ end
568
+
569
+ def hexdump data, h = {}
570
+ offset = h[:offset] || 0
571
+ add = h[:add] || 0
572
+ size = h[:size] || (data.size-offset)
573
+ tail = h[:tail] || "\n"
574
+ width = h[:width] || 0x10 # row width, in bytes
575
+
576
+ size = data.size-offset if size+offset > data.size
577
+
578
+ r = ''; s = ''
579
+ r << "%08x: " % (offset + add)
580
+ ascii = ''
581
+ size.times do |i|
582
+ if i%width==0 && i>0
583
+ r << "%s |%s|\n%08x: " % [s, ascii, offset + add + i]
584
+ ascii = ''; s = ''
585
+ end
586
+ s << " " if i%width%8==0
587
+ c = data[offset+i].ord
588
+ s << "%02x " % c
589
+ ascii << ((32..126).include?(c) ? c.chr : '.')
590
+ end
591
+ r << "%-*s |%-*s|%s" % [width*3+width/8+(width%8==0?0:1), s, width, ascii, tail]
592
+ end
593
+ end