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
@@ -8,6 +8,7 @@ module ZSteg
8
8
  'data',
9
9
  'empty',
10
10
  'Sendmail frozen configuration',
11
+ 'DBase 3 data file',
11
12
  'DOS executable',
12
13
  'Dyalog APL',
13
14
  '8086 relocatable',
@@ -18,9 +19,50 @@ module ZSteg
18
19
  'very short file',
19
20
  'International EBCDIC text',
20
21
  'lif file',
21
- 'AmigaOS bitmap font'
22
+ 'AmigaOS bitmap font',
23
+ 'a python script text executable' # common false positive
22
24
  ]
23
25
 
26
+ MIN_DATA_SIZE = 5
27
+
28
+ class Result < Struct.new(:title, :data)
29
+ COLORMAP_TEXT = {
30
+ /DBase 3 data/i => :gray
31
+ }
32
+ COLORMAP_WORD = {
33
+ /bitmap|jpeg|pdf|zip|rar|7-?z/i => :bright_red,
34
+ }
35
+
36
+ def to_s
37
+ if title[/UTF-8 Unicode text/i]
38
+ begin
39
+ t = data.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")
40
+ rescue
41
+ t = data.force_encoding('binary')
42
+ end
43
+ return "utf8: " + t
44
+ end
45
+ COLORMAP_TEXT.each do |re,color|
46
+ return colorize(color) if title[re]
47
+ end
48
+ title.downcase.split.each do |word|
49
+ COLORMAP_WORD.each do |re,color|
50
+ return colorize(color) if title.index(re) == 0
51
+ end
52
+ end
53
+ colorize(:yellow)
54
+ end
55
+
56
+ def colorize color
57
+ if color == :gray
58
+ # gray whole string
59
+ "file: #{title}".send(color)
60
+ else
61
+ "file: " + title.send(color)
62
+ end
63
+ end
64
+ end
65
+
24
66
  def start!
25
67
  @stdin, @stdout, @stderr, @wait_thr = Open3.popen3("file -n -b -f -")
26
68
  end
@@ -39,6 +81,27 @@ module ZSteg
39
81
  check_file @tempfile.path
40
82
  end
41
83
 
84
+ # checks data and resurns Result, if any
85
+ def data2result data
86
+ return if data.size < MIN_DATA_SIZE
87
+
88
+ title = check_data data
89
+ return unless title
90
+
91
+ if title[/UTF-8 Unicode text/i]
92
+ begin
93
+ t = data.force_encoding("UTF-8")
94
+ rescue
95
+ t = data.force_encoding('binary')
96
+ end
97
+ if t.size >= Checker::MIN_TEXT_LENGTH
98
+ ZSteg::Result::UnicodeText.new(t,0)
99
+ end
100
+ else
101
+ Result.new(title,data)
102
+ end
103
+ end
104
+
42
105
  def stop!
43
106
  @stdin.close
44
107
  @stdout.close
@@ -0,0 +1,268 @@
1
+ require 'optparse'
2
+ require 'set'
3
+ require 'digest/md5'
4
+
5
+ module ZSteg
6
+ class MaskCLI
7
+ DEFAULT_ACTIONS = %w'mask'
8
+
9
+ COMMON_MASKS = [
10
+ 0b0000_0001, 0b0000_0011, 0b0000_0111, 0b0000_1111,
11
+ 0b0000_0010, 0b0000_0100, 0b0000_1000,
12
+ 0b0001_0000, 0b0010_0000, 0b0100_0000, 0b1000_0000,
13
+ ]
14
+
15
+ CHANNELS = [:r, :g, :b, :a]
16
+
17
+ def initialize argv = ARGV
18
+ @argv = argv
19
+ @wasfiles = Set.new
20
+ @cache = {}
21
+ end
22
+
23
+ def run
24
+ @actions = []
25
+ @options = {
26
+ :verbose => 0,
27
+ :masks => Hash.new{|k,v| k[v] = [] },
28
+ :normalize => true
29
+ }
30
+ optparser = OptionParser.new do |opts|
31
+ opts.banner = "Usage: zsteg-mask [options] filename.png [param_string]"
32
+ opts.separator ""
33
+
34
+ opts.on("-m", "--mask M", "apply mask to all channels",
35
+ "mask: 0-255 OR 0x00-0xff OR 00000000-11111111",
36
+ "OR 'all' for all common masks"
37
+ ){ |x| @options[:masks][:all] << parse_mask(x) }
38
+
39
+ opts.on("-R", "--red M", "red channel mask"){ |x|
40
+ @options[:masks][:r] << parse_mask(x) }
41
+
42
+ opts.on("-G", "--green M", "green channel mask"){ |x|
43
+ @options[:masks][:g] << parse_mask(x) }
44
+
45
+ opts.on("-B", "--blue M", "blue channel mask"){ |x|
46
+ @options[:masks][:b] << parse_mask(x) }
47
+
48
+ opts.on("-A", "--alpha M", "alpha channel mask"){ |x|
49
+ @options[:masks][:a] << parse_mask(x) }
50
+
51
+ opts.separator ""
52
+
53
+ opts.on "-a", "--all", "try all common masks (default)" do
54
+ @options[:try_all] = true
55
+ end
56
+
57
+ opts.separator ""
58
+
59
+ opts.on "-N", "--[no-]normalize", "normalize color value after applying mask",
60
+ "(default: normalize)" do |x|
61
+ @options[:normalize] = x
62
+ end
63
+
64
+ opts.on "-O", "--outfile FILENAME", "output single result to specified file" do |x|
65
+ @options[:outfile] = x
66
+ end
67
+
68
+ opts.on "-D", "--dir DIRNAME", "output multiple results to specified dir" do |x|
69
+ @options[:dir] = x
70
+ end
71
+
72
+ opts.separator ""
73
+ opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
74
+ @options[:verbose] += 1
75
+ end
76
+ opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
77
+ @options[:verbose] -= 1
78
+ end
79
+ opts.on "-C", "--[no-]color", "Force (or disable) color output (default: auto)" do |x|
80
+ Sickill::Rainbow.enabled = x
81
+ end
82
+ end
83
+
84
+ if (argv = optparser.parse(@argv)).empty?
85
+ puts optparser.help
86
+ return
87
+ end
88
+
89
+ # default :all mask if none specified
90
+ if @options[:masks].empty?
91
+ @options[:try_all] = true
92
+ end
93
+
94
+ @actions = DEFAULT_ACTIONS if @actions.empty?
95
+
96
+ argv.each do |arg|
97
+ if arg[','] && !File.exist?(arg)
98
+ @options.merge!(decode_param_string(arg))
99
+ argv.delete arg
100
+ end
101
+ end
102
+
103
+ argv.each_with_index do |fname,idx|
104
+ if argv.size > 1 && @options[:verbose] >= 0
105
+ puts if idx > 0
106
+ puts "[.] #{fname}".green
107
+ end
108
+ next unless @image=load_image(@fname=fname)
109
+
110
+ @actions.each do |action|
111
+ if action.is_a?(Array)
112
+ self.send(*action) if self.respond_to?(action.first)
113
+ else
114
+ self.send(action) if self.respond_to?(action)
115
+ end
116
+ end
117
+ end
118
+ rescue Errno::EPIPE
119
+ # output interrupt, f.ex. when piping output to a 'head' command
120
+ # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
121
+ end
122
+
123
+ def parse_mask x
124
+ case x
125
+ when /0x/i
126
+ x.to_i(16)
127
+ when /^[01]{8}$/
128
+ x.to_i(2)
129
+ when /^\d{1,3}$/
130
+ x.to_i
131
+ when /^all$/
132
+ COMMON_MASKS
133
+ else raise "invalid mask #{x.inspect}"
134
+ end
135
+ end
136
+
137
+ def load_image fname
138
+ if File.directory?(fname)
139
+ puts "[?] #{fname} is a directory".yellow
140
+ else
141
+ ZPNG::Image.load(fname)
142
+ end
143
+ rescue ZPNG::Exception, Errno::ENOENT
144
+ puts "[!] #{$!.inspect}".red
145
+ end
146
+
147
+ ###########################################################################
148
+ # actions
149
+
150
+ def mask
151
+ masks = @options[:masks]
152
+ masks.each{ |k,v| v.flatten!; v.uniq! }
153
+
154
+ if @options[:try_all]
155
+ # try all common masks
156
+ masks = masks[:all] || []
157
+ masks = COMMON_MASKS if masks.empty?
158
+ masks.each{ |x| run_masker x,x,x,x }
159
+ masks.each{ |x| run_masker x,0,0,0xff }
160
+ masks.each{ |x| run_masker 0,x,0,0xff }
161
+ masks.each{ |x| run_masker 0,0,x,0xff }
162
+ if @image.alpha_used?
163
+ masks.each{ |x| run_masker 0,0,0,x }
164
+ end
165
+
166
+ elsif CHANNELS.all?{ |c| !masks[c] || masks[c].empty? }
167
+ # no specific channels
168
+ masks[:all].each do |x|
169
+ run_masker x,x,x,x
170
+ end
171
+
172
+ else
173
+ # specific channels
174
+ CHANNELS.each{ |x| masks[x] = [x==:a ? 0xff : 0] if !masks[x] || masks[x].empty? }
175
+ masks[:r].each do |r|
176
+ masks[:g].each do |g|
177
+ masks[:b].each do |b|
178
+ if @image.alpha_used?
179
+ masks[:a].each do |a|
180
+ run_masker r,g,b,a
181
+ end
182
+ else
183
+ run_masker r,g,b,0xff
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def _all_pixels_same img
194
+ sl0 = img.scanlines.first
195
+ return false if sl0.pixels.to_a.uniq.size != 1
196
+
197
+ db0 = sl0.decoded_bytes
198
+ img.scanlines[1..-1].each do |sl|
199
+ return false if sl.decoded_bytes != db0
200
+ end
201
+ true
202
+ end
203
+
204
+ def run_masker r,g,b,a
205
+ params = @options.dup
206
+ params[:masks] = params[:masks].merge( :r => r, :g => g, :b => b, :a => a)
207
+ fname,color = @options[:outfile],nil
208
+ fname,color = masks2fname(params[:masks]) unless fname
209
+
210
+ print "[.] #{fname.send(color||:to_s)} .. "
211
+
212
+ raise "already written to #{fname}" if @wasfiles.include?(fname)
213
+ @wasfiles << fname
214
+
215
+ dst = Masker.new(@image, params).mask
216
+
217
+ if _all_pixels_same(dst)
218
+ puts "all pixels = #{dst[0,0].inspect}".gray
219
+ return
220
+ end
221
+
222
+ data = dst.export
223
+
224
+ md5 = Digest::MD5.hexdigest(data)
225
+ if @cache[md5]
226
+ puts "same as #{File.basename(@cache[md5])}".gray
227
+ return
228
+ end
229
+ @cache[md5] = fname
230
+
231
+ File.open(fname, "wb"){ |f| f<<data }
232
+ printf "%6d bytes\n".green, File.size(fname)
233
+ end
234
+
235
+ def masks2fname masks
236
+ masks = masks.dup.delete_if{ |k,v| !CHANNELS.include?(k) }
237
+ bname = @fname.sub(/#{Regexp.escape(File.extname(@fname))}$/,'')
238
+ color = nil
239
+ raise "TODO" if masks.values.all?(&:nil?)
240
+ if masks.values.uniq.size == 1
241
+ tail = "%08b" % masks.values.first
242
+ else
243
+ a = []
244
+ masks.each do |c,mask|
245
+ a << "%s%08b" % [c,mask] if mask && mask != 0
246
+ end
247
+ raise "TODO" if a.empty?
248
+ a -= ['a11111111'] if a.size > 1 # fully opaque alpha is OK
249
+ if a.size == 1
250
+ color =
251
+ case a[0][0,1]
252
+ when 'r'; :red
253
+ when 'g'; :green
254
+ when 'b'; :blue
255
+ when 'a'; :gray
256
+ else nil
257
+ end
258
+ end
259
+ tail = a.join(".")
260
+ end
261
+
262
+ fname = [bname, tail, "png"].join('.')
263
+ fname = File.join(@options[:dir], File.basename(fname)) if @options[:dir]
264
+ [fname, color]
265
+ end
266
+
267
+ end
268
+ end
@@ -0,0 +1,52 @@
1
+ require 'set'
2
+
3
+ module ZSteg
4
+ class Masker
5
+ def initialize image, params = {}
6
+ @src = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
7
+ @masks = params[:masks] || {}
8
+ @mask = params[:mask] || @masks[:all]
9
+ @normalize = params[:normalize]
10
+ [:r, :g, :b, :a].each{ |x| @masks[x] ||= @mask }
11
+ end
12
+
13
+ def mask params = {}
14
+ dst = ZPNG::Image.new :width => @src.width, :height => @src.height
15
+ rm, gm, bm, am = @masks[:r], @masks[:g], @masks[:b], @masks[:a]
16
+ rd, gd, bd, ad = rm==0?1:rm, gm==0?1:gm, bm==0?1:bm, am==0?1:am
17
+ # duplicate loops for performance reason
18
+ if @normalize
19
+ if rm == 0 && gm == 0 && bm == 0 && am != 0
20
+ # alpha2grayscale
21
+ @src.each_pixel do |c,x,y|
22
+ c.r = c.g = c.b = (c.a & am) * 255 / ad
23
+ c.a = 0xff
24
+ dst[x,y] = c
25
+ end
26
+ else
27
+ # normal operation
28
+ @src.each_pixel do |c,x,y|
29
+ #TODO: c.to_depth(8)
30
+ # further possible optimizations:
31
+ # a) precalculate (255 / Xm)
32
+ c.r = (c.r & rm) * 255 / rd
33
+ c.g = (c.g & gm) * 255 / gd
34
+ c.b = (c.b & bm) * 255 / bd
35
+ c.a = (c.a & am) * 255 / ad
36
+ dst[x,y] = c
37
+ end
38
+ end
39
+ else
40
+ @src.each_pixel do |c,x,y|
41
+ #TODO: c.to_depth(8)
42
+ c.r &= rm
43
+ c.g &= gm
44
+ c.b &= bm
45
+ c.a &= am
46
+ dst[x,y] = c
47
+ end
48
+ end
49
+ dst
50
+ end
51
+ end
52
+ end
data/lib/zsteg/result.rb CHANGED
@@ -17,13 +17,30 @@ module ZSteg
17
17
  end
18
18
 
19
19
  def to_s
20
- super.sub(/^<Result::/,'').sub(/>$/,'').red
20
+ super.sub(/^<Result::/,'').sub(/>$/,'').bright_red
21
21
  end
22
22
  end
23
23
 
24
24
  class Text < Struct.new(:text, :offset)
25
+ def one_char?
26
+ (text =~ /\A(.)\1+\Z/m) == 0
27
+ rescue # invalid byte sequence in UTF-8
28
+ text.chars.to_a.uniq.size == 1 # ~10x slower than regexp
29
+ end
30
+
25
31
  def to_s
26
- "text: ".gray + (offset == 0 ? text.inspect.red : text.inspect)
32
+ "text: ".gray +
33
+ if one_char?
34
+ "[#{text[0].inspect} repeated #{text.size} times]".gray
35
+ elsif offset == 0
36
+ # first byte of data is also first char of text
37
+ text.inspect.bright_red
38
+ elsif text.size > 10 && text[' '] && text =~ /\A[a-z0-9 .,:!_-]+\Z/i
39
+ # text is ASCII with spaces
40
+ text.inspect.bright_red
41
+ else
42
+ text.inspect
43
+ end
27
44
  end
28
45
  end
29
46
 
@@ -33,9 +50,15 @@ module ZSteg
33
50
  # part of data is text
34
51
  class PartialText < Text; end
35
52
 
53
+ # unicode text
54
+ class UnicodeText < Text; end
55
+
36
56
  class Zlib < Struct.new(:data, :offset)
57
+ MAX_SHOW_SIZE = 100
37
58
  def to_s
38
- "zlib: data=#{data.inspect.red}, offset=#{offset}"
59
+ x = data
60
+ x=x[0,MAX_SHOW_SIZE] + "..." if x.size > MAX_SHOW_SIZE
61
+ "zlib: data=#{x.inspect.bright_red}, offset=#{offset}, size=#{data.size}"
39
62
  end
40
63
  end
41
64
 
@@ -45,34 +68,6 @@ module ZSteg
45
68
  end
46
69
  end
47
70
 
48
- class FileCmd < Struct.new(:title, :data)
49
- COLORMAP = {
50
- /bitmap|jpeg|pdf|zip|rar|7z/i => :red,
51
- /DBase 3 data/i => :gray
52
- }
53
-
54
- def to_s
55
- if title[/UTF-8 Unicode text/i]
56
- begin
57
- t = data.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")
58
- rescue
59
- t = data.force_encoding('binary')
60
- end
61
- return "utf8: " + t
62
- end
63
- COLORMAP.each do |re,color|
64
- if title[re]
65
- if color == :gray
66
- return "file: #{title}".send(color)
67
- else
68
- return "file: " + title.send(color)
69
- end
70
- end
71
- end
72
- "file: " + title.yellow
73
- end
74
- end
75
-
76
71
  class Camouflage < Struct.new(:hidden_data_len, :host_orig_len)
77
72
  def initialize(data)
78
73
  self.hidden_data_len = (data[0x1a,4] || '').unpack('V').first
@@ -83,7 +78,7 @@ module ZSteg
83
78
  end
84
79
 
85
80
  def to_s
86
- super.red
81
+ super.bright_red
87
82
  end
88
83
  end
89
84
  end