pedump 0.4.5 → 0.4.6
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/Gemfile +3 -1
- data/Gemfile.lock +5 -1
- data/README.md +22 -20
- data/Rakefile +25 -0
- data/VERSION +1 -1
- data/lib/pedump.rb +92 -45
- data/lib/pedump/cli.rb +56 -16
- data/lib/pedump/comparer.rb +147 -0
- data/lib/pedump/core.rb +12 -18
- data/lib/pedump/loader.rb +131 -0
- data/lib/pedump/loader/section.rb +51 -0
- data/lib/pedump/logger.rb +67 -0
- data/lib/pedump/pe.rb +3 -0
- data/lib/pedump/resources.rb +3 -3
- data/lib/pedump/unpacker.rb +26 -0
- data/lib/pedump/unpacker/aspack.rb +853 -0
- data/lib/pedump/unpacker/upx.rb +13 -0
- data/lib/pedump/version.rb +1 -1
- data/lib/pedump/version_info.rb +8 -3
- data/misc/aspack/Makefile +3 -0
- data/misc/aspack/aspack_unlzx.c +92 -0
- data/misc/aspack/lzxdec.c +479 -0
- data/misc/aspack/lzxdec.h +56 -0
- data/pedump.gemspec +24 -5
- data/spec/pe_spec.rb +61 -0
- data/spec/unpackers/aspack_spec.rb +69 -0
- data/spec/unpackers/find_spec.rb +17 -0
- metadata +53 -18
data/lib/pedump/cli.rb
CHANGED
@@ -3,6 +3,32 @@ require 'pedump/packer'
|
|
3
3
|
require 'pedump/version_info'
|
4
4
|
require 'optparse'
|
5
5
|
|
6
|
+
begin
|
7
|
+
require 'shellwords' # from ruby 1.9.3
|
8
|
+
rescue LoadError
|
9
|
+
unless ''.respond_to?(:shellescape)
|
10
|
+
class String
|
11
|
+
# File shellwords.rb, line 72
|
12
|
+
def shellescape
|
13
|
+
# An empty argument will be skipped, so return empty quotes.
|
14
|
+
return "''" if self.empty?
|
15
|
+
|
16
|
+
str = self.dup
|
17
|
+
|
18
|
+
# Process as a single byte sequence because not all shell
|
19
|
+
# implementations are multibyte aware.
|
20
|
+
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")
|
21
|
+
|
22
|
+
# A LF cannot be escaped with a backslash because a backslash + LF
|
23
|
+
# combo is regarded as line continuation and simply ignored.
|
24
|
+
str.gsub!(/\n/, "'\n'")
|
25
|
+
|
26
|
+
str
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
6
32
|
unless Object.instance_methods.include?(:try)
|
7
33
|
class Object
|
8
34
|
def try(*x)
|
@@ -71,6 +97,10 @@ class PEdump::CLI
|
|
71
97
|
@actions << :packer_only
|
72
98
|
end
|
73
99
|
|
100
|
+
opts.on '-r', "--recursive", "recurse dirs in packer detect" do
|
101
|
+
@options[:recursive] = true
|
102
|
+
end
|
103
|
+
|
74
104
|
opts.on "--all", "Dump all but resource-directory (default)" do
|
75
105
|
@actions = DEFAULT_ALL_ACTIONS
|
76
106
|
end
|
@@ -146,12 +176,20 @@ class PEdump::CLI
|
|
146
176
|
def dump_packer_only fnames
|
147
177
|
max_fname_len = fnames.map(&:size).max
|
148
178
|
fnames.each do |fname|
|
149
|
-
File.
|
150
|
-
@
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
179
|
+
if File.directory?(fname)
|
180
|
+
if @options[:recursive]
|
181
|
+
dump_packer_only(Dir[File.join(fname.shellescape,"*")])
|
182
|
+
else
|
183
|
+
STDERR.puts "[?] #{fname} is a directory, and recursive flag is not set"
|
184
|
+
end
|
185
|
+
else
|
186
|
+
File.open(fname,'rb') do |f|
|
187
|
+
@pedump = create_pedump fname
|
188
|
+
packers = @pedump.packers(f)
|
189
|
+
pname = Array(packers).first.try(:packer).try(:name)
|
190
|
+
pname ||= "unknown" if @options[:verbose] > 0
|
191
|
+
printf("%-*s %s\n", max_fname_len+1, "#{fname}:", pname) if pname
|
192
|
+
end
|
155
193
|
end
|
156
194
|
end
|
157
195
|
end
|
@@ -267,9 +305,7 @@ class PEdump::CLI
|
|
267
305
|
dump_opts = {:name => action}
|
268
306
|
case action
|
269
307
|
when :pe
|
270
|
-
|
271
|
-
data = @pedump.pe.signature + (@pedump.pe.ifh.try(:pack)||'') + (@pedump.pe.ioh.try(:pack)||'')
|
272
|
-
@pedump.pe.ifh.TimeDateStamp = Time.at(@pedump.pe.ifh.TimeDateStamp).utc
|
308
|
+
data = @pedump.pe.pack
|
273
309
|
when :resources
|
274
310
|
return dump_resources(data)
|
275
311
|
when :strings
|
@@ -420,12 +456,12 @@ class PEdump::CLI
|
|
420
456
|
printf fmt.tr('x','s'), *%w'RAW_START RAW_END INDEX CALLBKS ZEROFILL FLAGS'
|
421
457
|
data.each do |tls|
|
422
458
|
printf fmt,
|
423
|
-
tls.StartAddressOfRawData,
|
424
|
-
tls.EndAddressOfRawData,
|
425
|
-
tls.AddressOfIndex,
|
426
|
-
tls.AddressOfCallBacks,
|
427
|
-
tls.SizeOfZeroFill,
|
428
|
-
tls.Characteristics
|
459
|
+
tls.StartAddressOfRawData.to_i,
|
460
|
+
tls.EndAddressOfRawData.to_i,
|
461
|
+
tls.AddressOfIndex.to_i,
|
462
|
+
tls.AddressOfCallBacks.to_i,
|
463
|
+
tls.SizeOfZeroFill.to_i,
|
464
|
+
tls.Characteristics.to_i
|
429
465
|
end
|
430
466
|
end
|
431
467
|
|
@@ -694,7 +730,11 @@ class PEdump::CLI
|
|
694
730
|
end
|
695
731
|
end
|
696
732
|
|
697
|
-
def hexdump
|
733
|
+
def hexdump *args
|
734
|
+
self.class.hexdump(*args)
|
735
|
+
end
|
736
|
+
|
737
|
+
def self.hexdump data, h = {}
|
698
738
|
offset = h[:offset] || 0
|
699
739
|
add = h[:add] || 0
|
700
740
|
size = h[:size] || (data.size-offset)
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'pedump'
|
2
|
+
require 'pedump/loader'
|
3
|
+
|
4
|
+
########################################################################
|
5
|
+
# comparing 2 binaries
|
6
|
+
########################################################################
|
7
|
+
|
8
|
+
class PEdump::Comparer
|
9
|
+
attr_accessor :verbose
|
10
|
+
attr_accessor :ignored_data_dirs, :ignored_sections
|
11
|
+
|
12
|
+
METHODS = [:sections, :data_dirs, :imports, :resources, :pe_hdr]
|
13
|
+
|
14
|
+
def initialize ldr1, ldr2
|
15
|
+
@ldr1,@ldr2 = ldr1,ldr2
|
16
|
+
@ignored_data_dirs = []
|
17
|
+
@ignored_sections = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def equal?
|
21
|
+
METHODS.map{ |m| send("cmp_#{m}") }.uniq == [true]
|
22
|
+
end
|
23
|
+
|
24
|
+
def diff
|
25
|
+
METHODS.map{ |m| send("cmp_#{m}") ? nil : m }.compact
|
26
|
+
end
|
27
|
+
|
28
|
+
def cmp_pe_hdr
|
29
|
+
@ldr1.pe.ioh.AddressOfEntryPoint == @ldr2.pe.ioh.AddressOfEntryPoint &&
|
30
|
+
@ldr1.pe.ioh.ImageBase == @ldr2.pe.ioh.ImageBase
|
31
|
+
end
|
32
|
+
|
33
|
+
def cmp_resources
|
34
|
+
PEdump.quiet do
|
35
|
+
#@ldr1.pedump.resources == @ldr2.pedump.resources
|
36
|
+
@ldr1.pedump.resources.each_with_index do |r1,idx|
|
37
|
+
r2 = @ldr2.pedump.resources[idx]
|
38
|
+
if (r1.to_a - [r1.file_offset]) != (r2.to_a - [r2.file_offset])
|
39
|
+
p r1
|
40
|
+
p r2
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def cmp_sections
|
49
|
+
r = true
|
50
|
+
@ldr1.sections.each_with_index do |s1,idx|
|
51
|
+
next if @ignored_sections.include?(s1.name)
|
52
|
+
s2 = @ldr2.sections[idx]
|
53
|
+
|
54
|
+
if !s2
|
55
|
+
r = false
|
56
|
+
printf "[!] extra section %-12s in %s\n".red, s1.name.inspect, f1
|
57
|
+
elsif s1.data == s2.data
|
58
|
+
printf "[.] section: %s == %s\n".green, s1.name, s2.name if @verbose
|
59
|
+
else
|
60
|
+
r = false
|
61
|
+
printf "[!] section: %s != %s\n".red, s1.name, s2.name
|
62
|
+
self.class.cmp_ios *[s1,s2].map{ |section| StringIO.new(section.data) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
r
|
66
|
+
end
|
67
|
+
|
68
|
+
def cmp_data_dirs
|
69
|
+
r = true
|
70
|
+
@ldr1.pe.ioh.DataDirectory.each_with_index do |d1,idx|
|
71
|
+
break if idx == 15
|
72
|
+
d2 = @ldr2.pe.ioh.DataDirectory[idx]
|
73
|
+
|
74
|
+
case idx
|
75
|
+
when PEdump::IMAGE_DATA_DIRECTORY::BASERELOC
|
76
|
+
# total 8-byte size relocs == no relocs at all
|
77
|
+
next if [d1.va, d2.va].min == 0 && [d1.size, d2.size].max == 8
|
78
|
+
end
|
79
|
+
|
80
|
+
next if @ignored_data_dirs.include?(idx)
|
81
|
+
|
82
|
+
if d1.va != d2.va && d1.size != d2.size
|
83
|
+
r = false
|
84
|
+
printf "[!] data_dir: %-12s: SIZE & VA: %6x %6x | %6x %6x\n".red, d1.type,
|
85
|
+
d1.va, d1.size, d2.va, d2.size
|
86
|
+
elsif d1.va != d2.va
|
87
|
+
r = false
|
88
|
+
printf "[!] data_dir: %-12s: VA : %x != %x\n".red, d1.type, d1.va, d2.va
|
89
|
+
elsif d1.size != d2.size
|
90
|
+
r = false
|
91
|
+
printf "[!] data_dir: %-12s: SIZE : %x != %x\n".red, d1.type, d1.size, d2.size
|
92
|
+
end
|
93
|
+
end
|
94
|
+
r
|
95
|
+
end
|
96
|
+
|
97
|
+
def cmp_imports
|
98
|
+
@ldr1.pedump.imports.each_with_index do |iid1,idx|
|
99
|
+
iid2 = @ldr2.pedump.imports[idx]
|
100
|
+
if iid1 != iid2
|
101
|
+
puts "[!] diff imports".red
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
class << self
|
109
|
+
# arguments can be:
|
110
|
+
# a) filenames
|
111
|
+
# b) IO instances
|
112
|
+
# c) PEdump::Loader instances
|
113
|
+
def cmp *args
|
114
|
+
handles = []
|
115
|
+
if args.all?{|x| x.is_a?(String)}
|
116
|
+
handles = args.map{|x| File.open(x,"rb")}
|
117
|
+
_cmp(*handles.map{|h| PEdump::Loader.new(h)})
|
118
|
+
else
|
119
|
+
_cmp(*args)
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
handles.each(&:close)
|
123
|
+
end
|
124
|
+
|
125
|
+
# each arg is a PEdump::Loader
|
126
|
+
def _cmp ldr1, ldr2
|
127
|
+
new(ldr1, ldr2).equal?
|
128
|
+
end
|
129
|
+
|
130
|
+
def cmp_ios *ios
|
131
|
+
ndiff = 0
|
132
|
+
while !ios.any?(&:eof)
|
133
|
+
bytes = ios.map(&:readbyte)
|
134
|
+
if bytes.uniq.size > 1
|
135
|
+
ndiff += 1
|
136
|
+
printf ("\t%08x:"+" %02x"*ios.size).yellow+"\n", ios[0].pos-1, *bytes
|
137
|
+
if ndiff >= 5
|
138
|
+
puts "\t...".yellow
|
139
|
+
break
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
puts if ndiff > 0
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
data/lib/pedump/core.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'logger'
|
2
2
|
require 'pedump/version'
|
3
|
+
require 'pedump/logger'
|
3
4
|
|
4
5
|
class String
|
5
6
|
def xor x
|
@@ -30,26 +31,19 @@ class File
|
|
30
31
|
end
|
31
32
|
|
32
33
|
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
34
|
|
49
35
|
module Readable
|
50
|
-
|
36
|
+
# src can be IO or String, or anything that responds to :read or :unpack
|
37
|
+
def read src, size = nil
|
51
38
|
size ||= const_get 'SIZE'
|
52
|
-
data =
|
39
|
+
data =
|
40
|
+
if src.respond_to?(:read)
|
41
|
+
src.read(size).to_s
|
42
|
+
elsif src.respond_to?(:unpack)
|
43
|
+
src
|
44
|
+
else
|
45
|
+
raise "[?] don't know how to read from #{src.inspect}"
|
46
|
+
end
|
53
47
|
if data.size < size && PEdump.logger
|
54
48
|
PEdump.logger.error "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
|
55
49
|
end
|
@@ -67,7 +61,7 @@ class PEdump
|
|
67
61
|
case f
|
68
62
|
when /[aAC]/ then 1
|
69
63
|
when 'v' then 2
|
70
|
-
when 'V' then 4
|
64
|
+
when 'V','l' then 4
|
71
65
|
when 'Q' then 8
|
72
66
|
else raise "unknown fmt #{f.inspect}"
|
73
67
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'pedump'
|
2
|
+
require 'stringio'
|
3
|
+
require 'pedump/loader/section'
|
4
|
+
|
5
|
+
class PEdump::Loader
|
6
|
+
attr_accessor :mz_hdr, :dos_stub, :pe_hdr, :sections, :pedump
|
7
|
+
|
8
|
+
# shortcuts
|
9
|
+
alias :pe :pe_hdr
|
10
|
+
def ep; @pe_hdr.ioh.AddressOfEntryPoint; end
|
11
|
+
def ep= v; @pe_hdr.ioh.AddressOfEntryPoint=v; end
|
12
|
+
|
13
|
+
########################################################################
|
14
|
+
# constructors
|
15
|
+
########################################################################
|
16
|
+
|
17
|
+
def initialize io = nil, pedump_params = {}
|
18
|
+
@pedump = PEdump.new(io, pedump_params)
|
19
|
+
if io
|
20
|
+
@mz_hdr = @pedump.mz
|
21
|
+
@dos_stub = @pedump.dos_stub
|
22
|
+
@pe_hdr = @pedump.pe
|
23
|
+
load_sections @pedump.sections, io
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_sections section_hdrs, f = nil
|
28
|
+
if section_hdrs.is_a?(Array) && section_hdrs.map(&:class).uniq == [PEdump::IMAGE_SECTION_HEADER]
|
29
|
+
@sections = section_hdrs.map{ |x| Section.new(x, :deferred_load_io => f) }
|
30
|
+
if f.respond_to?(:seek) && f.respond_to?(:read)
|
31
|
+
#
|
32
|
+
# converted to deferred loading
|
33
|
+
#
|
34
|
+
# section_hdrs.each_with_index do |sect_hdr, idx|
|
35
|
+
# f.seek sect_hdr.PointerToRawData
|
36
|
+
# @sections[idx].data = f.read(sect_hdr.SizeOfRawData)
|
37
|
+
# end
|
38
|
+
elsif f
|
39
|
+
raise "invalid 2nd arg: #{f.inspect}"
|
40
|
+
end
|
41
|
+
else
|
42
|
+
raise "invalid arg: #{section_hdrs.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
########################################################################
|
47
|
+
# VA conversion
|
48
|
+
########################################################################
|
49
|
+
|
50
|
+
def va2section va
|
51
|
+
@sections.find{ |x| x.range.include?(va) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def va2stream va
|
55
|
+
return nil unless section = va2section(va)
|
56
|
+
StringIO.new(section.data).tap do |io|
|
57
|
+
io.seek va-section.va
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
########################################################################
|
62
|
+
# virtual memory read/write
|
63
|
+
########################################################################
|
64
|
+
|
65
|
+
def [] va, size
|
66
|
+
section = va2section(va)
|
67
|
+
raise "no section for va=0x#{va.to_s 16}" unless section
|
68
|
+
offset = va - section.va
|
69
|
+
raise "negative offset #{offset}" if offset < 0
|
70
|
+
r = section.data[offset,size]
|
71
|
+
if r.size < size
|
72
|
+
# append some empty data
|
73
|
+
r << ("\x00".force_encoding('binary')) * (size - r.size)
|
74
|
+
end
|
75
|
+
r
|
76
|
+
end
|
77
|
+
|
78
|
+
def []= va, size, data
|
79
|
+
raise "data.size != size" if data.size != size
|
80
|
+
section = va2section(va)
|
81
|
+
raise "no section for va=0x#{va.to_s 16}" unless section
|
82
|
+
offset = va - section.va
|
83
|
+
raise "negative offset #{offset}" if offset < 0
|
84
|
+
if section.data.size < offset
|
85
|
+
# append some empty data
|
86
|
+
section.data << ("\x00".force_encoding('binary') * (offset-section.data.size))
|
87
|
+
end
|
88
|
+
section.data[offset, data.size] = data
|
89
|
+
end
|
90
|
+
|
91
|
+
########################################################################
|
92
|
+
# generating PE binary
|
93
|
+
########################################################################
|
94
|
+
|
95
|
+
def section_table
|
96
|
+
@sections.map do |section|
|
97
|
+
section.hdr.SizeOfRawData = section.data.size
|
98
|
+
section.hdr.pack
|
99
|
+
end.join
|
100
|
+
end
|
101
|
+
|
102
|
+
def dump f
|
103
|
+
align = @pe_hdr.ioh.FileAlignment
|
104
|
+
|
105
|
+
mz_size = @mz_hdr.pack.size
|
106
|
+
raise "odd mz_size #{mz_size}" if mz_size % 0x10 != 0
|
107
|
+
@mz_hdr.header_paragraphs = mz_size / 0x10 # offset of dos_stub
|
108
|
+
@mz_hdr.lfanew = mz_size + @dos_stub.size # offset of PE hdr
|
109
|
+
f.write @mz_hdr.pack
|
110
|
+
f.write @dos_stub
|
111
|
+
f.write @pe_hdr.pack
|
112
|
+
f.write @pe_hdr.ioh.DataDirectory.map(&:pack).join
|
113
|
+
|
114
|
+
section_tbl_offset = f.tell # store offset for 2nd write of section table
|
115
|
+
f.write section_table
|
116
|
+
|
117
|
+
@sections.each do |section|
|
118
|
+
f.seek(align - (f.tell % align), IO::SEEK_CUR) if f.tell % align != 0
|
119
|
+
section.hdr.PointerToRawData = f.tell # fix raw_ptr
|
120
|
+
f.write(section.data)
|
121
|
+
end
|
122
|
+
|
123
|
+
eof = f.tell
|
124
|
+
|
125
|
+
# 2nd write of section table with correct raw_ptr's
|
126
|
+
f.seek section_tbl_offset
|
127
|
+
f.write section_table
|
128
|
+
|
129
|
+
f.seek eof
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class PEdump::Loader
|
2
|
+
class Section
|
3
|
+
attr_accessor :hdr
|
4
|
+
attr_writer :data
|
5
|
+
|
6
|
+
EMPTY_DATA = ''.force_encoding('binary')
|
7
|
+
|
8
|
+
def initialize x = nil, args = {}
|
9
|
+
if x.is_a?(PEdump::IMAGE_SECTION_HEADER)
|
10
|
+
@hdr = x.dup
|
11
|
+
end
|
12
|
+
@data = EMPTY_DATA.dup
|
13
|
+
@deferred_load_io = args[:deferred_load_io]
|
14
|
+
@deferred_load_pos = args[:deferred_load_pos] || (@hdr && @hdr.PointerToRawData)
|
15
|
+
@deferred_load_size = args[:deferred_load_size] || (@hdr && @hdr.SizeOfRawData)
|
16
|
+
end
|
17
|
+
|
18
|
+
def name; @hdr.Name; end
|
19
|
+
def va ; @hdr.VirtualAddress; end
|
20
|
+
def vsize; @hdr.VirtualSize; end
|
21
|
+
def flags; @hdr.Characteristics; end
|
22
|
+
def flags= f; @hdr.Characteristics= f; end
|
23
|
+
|
24
|
+
def data
|
25
|
+
if @data.empty? && @deferred_load_io && @deferred_load_pos && @deferred_load_size.to_i > 0
|
26
|
+
begin
|
27
|
+
old_pos = @deferred_load_io.tell
|
28
|
+
@deferred_load_io.seek @deferred_load_pos
|
29
|
+
@data = @deferred_load_io.binmode.read(@deferred_load_size) || EMPTY_DATA.dup
|
30
|
+
ensure
|
31
|
+
if @deferred_load_io && old_pos
|
32
|
+
@deferred_load_io.seek old_pos
|
33
|
+
@deferred_load_io = nil # prevent read only on 1st access to data
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@data
|
38
|
+
end
|
39
|
+
|
40
|
+
def range
|
41
|
+
va...(va+vsize)
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"#<Section name=%-10s va=%8x vsize=%8x rawsize=%8s>" % [
|
46
|
+
name.inspect, va, vsize,
|
47
|
+
@data.size > 0 ? @data.size.to_s(16) : (@deferred_load_io ? "<defer>" : 0)
|
48
|
+
]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|