zsteg 0.0.0 → 0.0.1

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.
Files changed (78) hide show
  1. data/Gemfile +2 -7
  2. data/Gemfile.lock +2 -4
  3. data/README.md +72 -1
  4. data/README.md.tpl +23 -0
  5. data/Rakefile +5 -3
  6. data/TODO +5 -2
  7. data/VERSION +1 -1
  8. data/bin/zsteg-mask +7 -0
  9. data/lib/zsteg/checker/wbstego.rb +69 -14
  10. data/lib/zsteg/checker.rb +137 -34
  11. data/lib/zsteg/cli.rb +92 -35
  12. data/lib/zsteg/extractor/byte_extractor.rb +36 -21
  13. data/lib/zsteg/extractor/color_extractor.rb +68 -34
  14. data/lib/zsteg/extractor.rb +36 -1
  15. data/lib/zsteg/file_cmd.rb +64 -1
  16. data/lib/zsteg/mask_cli.rb +268 -0
  17. data/lib/zsteg/masker.rb +52 -0
  18. data/lib/zsteg/result.rb +27 -32
  19. data/lib/zsteg.rb +2 -0
  20. data/samples/hackquest/crypt.bmp +0 -0
  21. data/samples/hackquest/square.bmp +0 -0
  22. data/samples/wbstego/wbsteg_blowfish_pass_1.bmp +0 -0
  23. data/samples/wbstego/wbsteg_cast128_pass_1.bmp +0 -0
  24. data/samples/wbstego/wbsteg_enc_pass_pass.bmp +0 -0
  25. data/samples/wbstego/wbsteg_enc_pass_pass_even.bmp +0 -0
  26. data/samples/wbstego/wbsteg_mix_pass_1.bmp +0 -0
  27. data/samples/wbstego/wbsteg_mix_pass_1_even.bmp +0 -0
  28. data/samples/wbstego/wbsteg_mix_pass_foobar.bmp +0 -0
  29. data/samples/wbstego/wbsteg_mix_pass_pass.bmp +0 -0
  30. data/samples/wbstego/wbsteg_mixenc_pass_pass_even.bmp +0 -0
  31. data/samples/{wbsteg_noenc.bmp → wbstego/wbsteg_noenc.bmp} +0 -0
  32. data/samples/wbstego/wbsteg_noenc.png +0 -0
  33. data/samples/wbstego/wbsteg_noenc_.bmp +0 -0
  34. data/samples/{wbsteg_noenc_17.bmp → wbstego/wbsteg_noenc_17.bmp} +0 -0
  35. data/samples/wbstego/wbsteg_noenc__.bmp +0 -0
  36. data/samples/{wbsteg_noenc_even.bmp → wbstego/wbsteg_noenc_even.bmp} +0 -0
  37. data/samples/wbstego/wbsteg_noenc_even2.bmp +0 -0
  38. data/samples/{wbsteg_noenc_even_17.bmp → wbstego/wbsteg_noenc_even_17.bmp} +0 -0
  39. data/samples/wbstego/wbsteg_noenc_even_17_.bmp +0 -0
  40. data/samples/wbstego/wbsteg_noenc_ext_ABC.bmp +0 -0
  41. data/samples/wbstego/wbsteg_rijndael_pass_1.bmp +0 -0
  42. data/samples/wbstego/wbsteg_rijndael_pass_pass.bmp +0 -0
  43. data/samples/wbstego/wbsteg_rijndael_pass_pass_even.bmp +0 -0
  44. data/samples/wbstego/wbsteg_twofish_pass_1.bmp +0 -0
  45. data/samples/wechall/5ZMGcCLxpcpsru03.g00000010.png +0 -0
  46. data/samples/wechall/5ZMGcCLxpcpsru03.png +0 -0
  47. data/samples/wechall/stegano1.bmp +0 -0
  48. data/spec/checker_spec.rb +47 -0
  49. data/spec/easybmp_spec.rb +9 -0
  50. data/spec/hackquest_spec.rb +18 -0
  51. data/spec/mask_spec.rb +14 -0
  52. data/spec/polictf2012_spec.rb +48 -0
  53. data/spec/prime_spec.rb +9 -0
  54. data/spec/r3g2b3_spec.rb +9 -0
  55. data/spec/spec_helper.rb +21 -4
  56. data/spec/wbstego_spec.rb +21 -3
  57. data/spec/wechall_spec.rb +26 -0
  58. data/tmp/.keep +0 -0
  59. data/zsteg.gemspec +121 -0
  60. metadata +47 -43
  61. data/samples/06_enc.png +0 -0
  62. data/samples/Code.png +0 -0
  63. data/samples/README +0 -4
  64. data/samples/camouflage-password.png +0 -0
  65. data/samples/camouflage.png +0 -0
  66. data/samples/cats.png +0 -0
  67. data/samples/flower.png +0 -0
  68. data/samples/flower_rgb1.png +0 -0
  69. data/samples/flower_rgb2.png +0 -0
  70. data/samples/flower_rgb3.png +0 -0
  71. data/samples/flower_rgb4.png +0 -0
  72. data/samples/flower_rgb5.png +0 -0
  73. data/samples/flower_rgb6.png +0 -0
  74. data/samples/montenach-enc.png +0 -0
  75. data/samples/ndh2k12_sp113.bmp.7z +0 -0
  76. data/samples/openstego_q2.png +0 -0
  77. data/samples/openstego_send.png +0 -0
  78. data/samples/stg300.png +0 -0
data/lib/zsteg/cli.rb CHANGED
@@ -1,11 +1,8 @@
1
1
  require 'optparse'
2
- require 'awesome_print'
3
2
 
4
3
  module ZSteg
5
4
  class CLI
6
5
  DEFAULT_ACTIONS = %w'check'
7
- DEFAULT_LIMIT = 256
8
- DEFAULT_ORDER = 'auto'
9
6
 
10
7
  def initialize argv = ARGV
11
8
  @argv = argv
@@ -15,30 +12,35 @@ module ZSteg
15
12
  @actions = []
16
13
  @options = {
17
14
  :verbose => 0,
18
- :limit => DEFAULT_LIMIT,
19
- :bits => [1,2,3,4],
20
- :order => DEFAULT_ORDER
15
+ :limit => Checker::DEFAULT_LIMIT,
16
+ :order => Checker::DEFAULT_ORDER
21
17
  }
22
18
  optparser = OptionParser.new do |opts|
23
- opts.banner = "Usage: zsteg [options] filename.png"
19
+ opts.banner = "Usage: zsteg [options] filename.png [param_string]"
24
20
  opts.separator ""
25
21
 
26
- opts.on("-c", "--channels X", /[rgba,]+/,
22
+ opts.on("-c", "--channels X", /[rgba,1-8]+/,
27
23
  "channels (R/G/B/A) or any combination, comma separated",
28
- "valid values: r,g,b,a,rg,rgb,bgr,rgba,..."
24
+ "valid values: r,g,b,a,rg,bgr,rgba,r3g2b3,..."
29
25
  ){ |x| @options[:channels] = x.split(',') }
30
26
 
31
27
  opts.on("-l", "--limit N", Integer,
32
- "limit bytes checked, 0 = no limit (default: #{DEFAULT_LIMIT})"
28
+ "limit bytes checked, 0 = no limit (default: #{@options[:limit]})"
33
29
  ){ |n| @options[:limit] = n }
34
30
 
35
- opts.on("-b", "--bits N", /[\d,-]+/,
36
- "number of bits (1..8), single value or '1,3,5' or '1-8'") do |n|
37
- if n['-']
38
- @options[:bits] = Range.new(*n.split('-').map(&:to_i)).to_a
39
- else
40
- @options[:bits] = n.split(',').map(&:to_i)
31
+ opts.on("-b", "--bits N", "number of bits, single int value or '1,3,5' or range '1-8'",
32
+ "advanced: specify individual bits like '00001110' or '0x88'"
33
+ ) do |x|
34
+ a = []
35
+ x.split(',').each do |x1|
36
+ if x1['-']
37
+ t = x1.split('-')
38
+ a << Range.new(parse_bits(t[0]), parse_bits(t[1])).to_a
39
+ else
40
+ a << parse_bits(x1)
41
+ end
41
42
  end
43
+ @options[:bits] = a.flatten.uniq
42
44
  end
43
45
 
44
46
  opts.on "--lsb", "least significant BIT comes first" do
@@ -48,8 +50,20 @@ module ZSteg
48
50
  @options[:bit_order] = :msb
49
51
  end
50
52
 
53
+ opts.on "-P", "--prime", "analyze/extract only prime bytes/pixels" do
54
+ @options[:prime] = true
55
+ end
56
+ # opts.on "--pixel-align", "pixel-align hidden data (EasyBMP)" do
57
+ # @options[:pixel_align] = true
58
+ # end
59
+
60
+ opts.on "-a", "--all", "try all known methods" do
61
+ @options[:prime] = :all
62
+ @options[:order] = :all
63
+ end
64
+
51
65
  opts.on("-o", "--order X", /all|auto|[bxy,]+/i,
52
- "pixel iteration order (default: '#{DEFAULT_ORDER}')",
66
+ "pixel iteration order (default: '#{@options[:order]}')",
53
67
  "valid values: ALL,xy,yx,XY,YX,xY,Xy,bY,...",
54
68
  ){ |x| @options[:order] = x.split(',') }
55
69
 
@@ -64,6 +78,11 @@ module ZSteg
64
78
  opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
65
79
  @options[:verbose] -= 1
66
80
  end
81
+ opts.on "-C", "--[no-]color", "Force (or disable) color output (default: auto)" do |x|
82
+ Sickill::Rainbow.enabled = x
83
+ end
84
+ opts.separator "\nPARAMS SHORTCUT\n"+
85
+ "\tzsteg fname.png 2b,b,lsb,xy ==> --bits 2 --channel b --lsb --order xy"
67
86
  end
68
87
 
69
88
  if (argv = optparser.parse(@argv)).empty?
@@ -73,12 +92,19 @@ module ZSteg
73
92
 
74
93
  @actions = DEFAULT_ACTIONS if @actions.empty?
75
94
 
95
+ argv.each do |arg|
96
+ if arg[','] && !File.exist?(arg)
97
+ @options.merge!(decode_param_string(arg))
98
+ argv.delete arg
99
+ end
100
+ end
101
+
76
102
  argv.each_with_index do |fname,idx|
77
103
  if argv.size > 1 && @options[:verbose] >= 0
78
104
  puts if idx > 0
79
105
  puts "[.] #{fname}".green
80
106
  end
81
- @fname = fname
107
+ next unless @img=load_image(@fname=fname)
82
108
 
83
109
  @actions.each do |action|
84
110
  if action.is_a?(Array)
@@ -93,39 +119,70 @@ module ZSteg
93
119
  # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
94
120
  end
95
121
 
96
- ###########################################################################
97
- # actions
98
-
99
- def check
100
- Checker.new(@fname, @options).check
122
+ def load_image fname
123
+ if File.directory?(fname)
124
+ puts "[?] #{fname} is a directory".yellow
125
+ else
126
+ ZPNG::Image.load(fname)
127
+ end
128
+ rescue ZPNG::Exception, Errno::ENOENT
129
+ puts "[!] #{$!.inspect}".red
101
130
  end
102
131
 
103
- def extract name
104
- if ['extradata', 'data after IEND'].include?(name)
105
- img = ZPNG::Image.load(@fname)
106
- print img.extradata
107
- return
132
+ def parse_bits x
133
+ case x
134
+ when '1', 1 # catch NOT A BINARY MASK early
135
+ 1
136
+ when /^0x[0-9a-f]+$/i # hex, mask
137
+ 0x100 + x.to_i(16)
138
+ when /^(?:0b)?[01]+$/i # binary, mask
139
+ 0x100 + x.to_i(2)
140
+ when /^\d+$/ # decimal, number of bits
141
+ x.to_i
142
+ else
143
+ raise "invalid bits value: #{x.inspect}"
108
144
  end
145
+ end
109
146
 
147
+ def decode_param_string s
110
148
  h = {}
111
- name.split(',').each do |x|
149
+ s.split(',').each do |x|
112
150
  case x
113
151
  when 'lsb'
114
152
  h[:bit_order] = :lsb
115
153
  when 'msb'
116
154
  h[:bit_order] = :msb
117
- when /(\d)b/
118
- h[:bits] = $1.to_i
155
+ when /^(\d)b$/, /^b(\d)$/
156
+ h[:bits] = parse_bits($1)
119
157
  when /\A[rgba]+\Z/
120
- h[:channels] = x.split('')
121
- when /\Axy|yx\Z/i
158
+ h[:channels] = [x]
159
+ when /\Axy|yx|yb|by\Z/i
122
160
  h[:order] = x
161
+ when 'prime'
162
+ h[:prime] = true
123
163
  else
124
164
  raise "uknown param #{x.inspect}"
125
165
  end
126
166
  end
127
- h[:limit] = @options[:limit] if @options[:limit] != DEFAULT_LIMIT
128
- print Extractor.new(@fname, @options).extract(h)
167
+ h
168
+ end
169
+
170
+ ###########################################################################
171
+ # actions
172
+
173
+ def check
174
+ Checker.new(@img, @options).check
175
+ end
176
+
177
+ def extract name
178
+ if ['extradata', 'data after IEND'].include?(name)
179
+ print @img.extradata
180
+ return
181
+ end
182
+
183
+ h = decode_param_string name
184
+ h[:limit] = @options[:limit] if @options[:limit] != Checker::DEFAULT_LIMIT
185
+ print Extractor.new(@img, @options).extract(h)
129
186
  end
130
187
 
131
188
  end
@@ -5,22 +5,23 @@ module ZSteg
5
5
  module ByteExtractor
6
6
 
7
7
  def byte_extract params = {}
8
- limit = params[:limit].to_i
9
- limit = 2**32 if limit <= 0
10
-
11
- bits = params[:bits]
12
- raise "invalid bits value #{bits.inspect}" unless (1..8).include?(bits)
13
- mask = 2**bits - 1
8
+ masks = bits2masks params[:bits]
14
9
 
10
+ if params[:prime]
11
+ pregenerate_primes(
12
+ :max => @image.scanlines[0].size * @image.height,
13
+ :count => (@limit*8.0/masks.size).ceil
14
+ )
15
+ end
15
16
 
16
17
  data = ''.force_encoding('binary')
17
18
  a = []
18
- byte_iterator(params[:order]) do |x,y|
19
+ byte_iterator(params) do |x,y|
19
20
  sl = @image.scanlines[y]
20
21
 
21
- value = sl.decoded_bytes[x].ord
22
- bits.times do |bidx|
23
- a << ((value & (1<<(bits-bidx-1))) == 0 ? 0 : 1)
22
+ value = sl.decoded_bytes.getbyte(x)
23
+ masks.each do |mask|
24
+ a << ((value & mask) == 0 ? 0 : 1)
24
25
  end
25
26
 
26
27
  if a.size >= 8
@@ -32,8 +33,8 @@ module ZSteg
32
33
  end
33
34
  #printf "[d] %02x %08b\n", byte, byte
34
35
  data << byte.chr
35
- if data.size >= limit
36
- print "[limit #{params[:limit]}]".gray if @verbose > 1
36
+ if data.size >= @limit
37
+ print "[limit #@limit]".gray if @verbose > 1
37
38
  break
38
39
  end
39
40
  end
@@ -51,7 +52,8 @@ module ZSteg
51
52
  # ...
52
53
  # 'xY': b=0, y=MAX; b=1, y=MAX; b=2, y=MAX; ...
53
54
  # 'XY': b=MAX,y=MAX; b=MAX-1,y=MAX; b=MAX-2,y=MAX; ...
54
- def byte_iterator type = nil
55
+ def byte_iterator params
56
+ type = params[:order]
55
57
  if type.nil? || type == 'auto'
56
58
  type = @image.format == :bmp ? 'bY' : 'by'
57
59
  end
@@ -59,6 +61,7 @@ module ZSteg
59
61
 
60
62
  sl0 = @image.scanlines.first
61
63
 
64
+ # XXX don't try to run it on interlaced PNGs!
62
65
  x0,x1,xstep =
63
66
  if type.index('b')
64
67
  [0, sl0.decoded_bytes.size-1, 1]
@@ -73,19 +76,31 @@ module ZSteg
73
76
  [@image.height-1, 0, -1]
74
77
  end
75
78
 
79
+ # cannot join these lines from ByteExtractor and ColorExtractor into
80
+ # one method for performance reason:
81
+ # it will require additional yield() for EACH BYTE iterated
82
+
76
83
  if type[0,1].downcase == 'b'
77
84
  # ROW iterator
78
- y0.step(y1,ystep) do |y|
79
- x0.step(x1,xstep) do |x|
80
- yield x,y
81
- end
85
+ if params[:prime]
86
+ idx = 0
87
+ y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x|
88
+ yield(x,y) if @primes.include?(idx)
89
+ idx += 1
90
+ }}
91
+ else
92
+ y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x| yield(x,y) }}
82
93
  end
83
94
  else
84
95
  # COLUMN iterator
85
- x0.step(x1,xstep) do |x|
86
- y0.step(y1,ystep) do |y|
87
- yield x,y
88
- end
96
+ if params[:prime]
97
+ idx = 0
98
+ x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y|
99
+ yield(x,y) if @primes.include?(idx)
100
+ idx += 1
101
+ }}
102
+ else
103
+ x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y| yield(x,y) }}
89
104
  end
90
105
  end
91
106
  end
@@ -4,40 +4,61 @@ module ZSteg
4
4
  module ColorExtractor
5
5
 
6
6
  def color_extract params = {}
7
- channels = (Array(params[:channels]) + Array(params[:channel])).compact
7
+ channels = Array(params[:channels])
8
+ #pixel_align = params[:pixel_align]
8
9
 
9
- limit = params[:limit].to_i
10
- limit = 2**32 if limit <= 0
10
+ ch_masks = []
11
+ case channels.first.size
12
+ when 1
13
+ # ['r', 'g', 'b']
14
+ channels.each{ |c| ch_masks << [c[0], bits2masks(params[:bits])] }
15
+ when 2
16
+ # ['r3', 'g2', 'b3']
17
+ channels.each{ |c| ch_masks << [c[0], bits2masks(c[1].to_i)] }
18
+ else
19
+ raise "invalid channels: #{channels.inspect}"
20
+ end
11
21
 
12
- bits = params[:bits]
13
- raise "invalid bits value #{bits.inspect}" unless (1..8).include?(bits)
14
- mask = 2**bits - 1
22
+ # total number of bits = sum of all channels bits
23
+ nbits = ch_masks.map{ |x| x[1].size }.inject(&:+)
15
24
 
25
+ if params[:prime]
26
+ pregenerate_primes(
27
+ :max => @image.width * @image.height,
28
+ :count => (@limit*8.0/nbits/channels.size).ceil
29
+ )
30
+ end
16
31
 
17
32
  data = ''.force_encoding('binary')
18
33
  a = []
19
- coord_iterator(params[:order]) do |x,y|
20
- color = @image[x,y]
34
+ #puts
35
+ catch :limit do
36
+ coord_iterator(params) do |x,y|
37
+ color = @image[x,y]
21
38
 
22
- channels.each do |c|
23
- value = color.send(c)
24
- bits.times do |bidx|
25
- a << ((value & (1<<(bits-bidx-1))) == 0 ? 0 : 1)
39
+ ch_masks.each do |c,masks|
40
+ value = color.send(c)
41
+ masks.each do |mask|
42
+ a << ((value & mask) == 0 ? 0 : 1)
43
+ end
26
44
  end
27
- end
45
+ #p [x,y,a.size,a]
28
46
 
29
- if a.size >= 8
30
- byte = 0
31
- if params[:bit_order] == :msb
32
- 8.times{ |i| byte |= (a.shift<<i)}
33
- else
34
- 8.times{ |i| byte |= (a.shift<<(7-i))}
35
- end
36
- #printf "[d] %02x %08b\n", byte, byte
37
- data << byte.chr
38
- if data.size >= limit
39
- print "[limit #{params[:limit]}]".gray if @verbose > 1
40
- break
47
+ while a.size >= 8
48
+ byte = 0
49
+ #puts a.join
50
+ if params[:bit_order] == :msb
51
+ 8.times{ |i| byte |= (a.shift<<i)}
52
+ else
53
+ 8.times{ |i| byte |= (a.shift<<(7-i))}
54
+ end
55
+ #printf "[d] %02x %08b\n", byte, byte
56
+ data << byte.chr
57
+ if data.size >= @limit
58
+ print "[limit #@limit]".gray if @verbose > 1
59
+ throw :limit
60
+ end
61
+ #a.clear if pixel_align
41
62
  end
42
63
  end
43
64
  end
@@ -54,7 +75,8 @@ module ZSteg
54
75
  # ...
55
76
  # 'xY': x=0, y=MAX; x=1, y=MAX; x=2, y=MAX; ...
56
77
  # 'XY': x=MAX,y=MAX; x=MAX-1,y=MAX; x=MAX-2,y=MAX; ...
57
- def coord_iterator type = nil
78
+ def coord_iterator params
79
+ type = params[:order]
58
80
  if type.nil? || type == 'auto'
59
81
  type = @image.format == :bmp ? 'xY' : 'xy'
60
82
  end
@@ -74,19 +96,31 @@ module ZSteg
74
96
  [@image.height-1, 0, -1]
75
97
  end
76
98
 
99
+ # cannot join these lines from ByteExtractor and ColorExtractor into
100
+ # one method for performance reason:
101
+ # it will require additional yield() for EACH BYTE iterated
102
+
77
103
  if type[0,1].downcase == 'x'
78
104
  # ROW iterator
79
- y0.step(y1,ystep) do |y|
80
- x0.step(x1,xstep) do |x|
81
- yield x,y
82
- end
105
+ if params[:prime]
106
+ idx = 0
107
+ y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x|
108
+ yield(x,y) if @primes.include?(idx)
109
+ idx += 1
110
+ }}
111
+ else
112
+ y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x| yield(x,y) }}
83
113
  end
84
114
  else
85
115
  # COLUMN iterator
86
- x0.step(x1,xstep) do |x|
87
- y0.step(y1,ystep) do |y|
88
- yield x,y
89
- end
116
+ if params[:prime]
117
+ idx = 0
118
+ x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y|
119
+ yield(x,y) if @primes.include?(idx)
120
+ idx += 1
121
+ }}
122
+ else
123
+ x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y| yield(x,y) }}
90
124
  end
91
125
  end
92
126
  end
@@ -1,3 +1,6 @@
1
+ require 'prime'
2
+ require 'set'
3
+
1
4
  module ZSteg
2
5
  class Extractor
3
6
 
@@ -7,15 +10,47 @@ module ZSteg
7
10
  # image can be either filename or ZPNG::Image
8
11
  def initialize image, params = {}
9
12
  @image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
10
- @verbose = params[:verbose]
13
+ @verbose = params[:verbose] || 0
11
14
  end
12
15
 
13
16
  def extract params = {}
17
+ @limit = params[:limit].to_i
18
+ @limit = 2**32 if @limit <= 0
19
+
14
20
  if params[:order] =~ /b/i
15
21
  byte_extract params
16
22
  else
17
23
  color_extract params
18
24
  end
19
25
  end
26
+
27
+ def pregenerate_primes h
28
+ @primes ||= Set.new
29
+ return if @primes.size >= h[:count]
30
+
31
+ count = h[:count]
32
+ Prime.each(h[:max]) do |prime|
33
+ @primes << prime
34
+ break if @primes.size >= count
35
+ end
36
+ end
37
+
38
+ def bits2masks bits
39
+ masks = []
40
+ if (1..8).include?(bits)
41
+ # number of bits
42
+ bits.times do |i|
43
+ masks << (1<<(bits-i-1))
44
+ end
45
+ else
46
+ # mask
47
+ bits &= 0xff
48
+ 8.times do |i|
49
+ mask = (1<<(bits-i-1))
50
+ masks << mask if (bits & mask) != 0
51
+ end
52
+ end
53
+ masks
54
+ end
20
55
  end
21
56
  end