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