zsteg 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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