pedump 0.3.3

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.
@@ -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