zsteg 0.0.1 → 0.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem 'zpng', ">= 0.2.2"
3
+ gem 'zpng', ">= 0.2.3"
4
4
  gem "iostruct"
5
5
 
6
6
  group :development do
@@ -22,7 +22,7 @@ GEM
22
22
  rspec-expectations (2.12.1)
23
23
  diff-lcs (~> 1.1.3)
24
24
  rspec-mocks (2.12.1)
25
- zpng (0.2.2)
25
+ zpng (0.2.3)
26
26
  rainbow
27
27
 
28
28
  PLATFORMS
@@ -33,4 +33,4 @@ DEPENDENCIES
33
33
  iostruct
34
34
  jeweler (~> 1.8.4)
35
35
  rspec (>= 2.8.0)
36
- zpng (>= 0.2.2)
36
+ zpng (>= 0.2.3)
data/TODO CHANGED
@@ -7,6 +7,12 @@
7
7
  [ ] http://search.cpan.org/~nwclark/Acme-Steganography-Image-Png-0.06/Png.pm
8
8
  [ ] http://registry.gimp.org/node/25988 - GIMP stego plugin
9
9
  [ ] zsteg-mask: normalize all to white
10
+ [ ] stego by pixels of single color (hackquest on wiki)
11
+ [ ] advices on what tool to use
12
+ [ ] SilentEye
13
+ [ ] chunks length check, as in pngcheck
14
+
15
+ [ ] CLI: self-describe
10
16
 
11
17
  [+] auto pixel order for BMP
12
18
  [+] BMP
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.1.0
data/bin/zsteg CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
- require 'zsteg'
5
- require 'zsteg/cli'
3
+ base = File.expand_path('../lib', File.dirname(__FILE__))
4
+ $:.unshift(base)
6
5
 
7
- ZSteg::CLI.new.run
6
+ require File.join(base, 'zsteg')
7
+
8
+ ZSteg::CLI.run
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
- require 'zsteg'
5
- require 'zsteg/mask_cli'
3
+ base = File.expand_path('../lib', File.dirname(__FILE__))
4
+ $:.unshift(base)
6
5
 
7
- ZSteg::MaskCLI.new.run
6
+ require File.join(base, 'zsteg')
7
+
8
+ ZSteg::CLI.run
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ base = File.expand_path('../lib', File.dirname(__FILE__))
4
+ $:.unshift(base)
5
+
6
+ require File.join(base, 'zsteg')
7
+
8
+ ZSteg::CLI.run
@@ -10,5 +10,30 @@ require 'zsteg/result'
10
10
  require 'zsteg/file_cmd'
11
11
 
12
12
  require 'zsteg/checker/wbstego'
13
+ require 'zsteg/checker/scanline_checker'
14
+ require 'zsteg/checker/zlib'
13
15
 
14
16
  require 'zsteg/masker'
17
+
18
+ require 'zsteg/analyzer'
19
+
20
+ module ZSteg::CLI
21
+ class << self
22
+ def run
23
+ a = File.basename($0).downcase.scan(/\w+/) - %w'zsteg rb'
24
+ a = %w'cli' if a.empty?
25
+
26
+ klass = a.map(&:capitalize).join
27
+ req = a.join('_')
28
+ require File.expand_path( File.join('zsteg', 'cli', req), File.dirname(__FILE__))
29
+
30
+ const_get(klass).new.run
31
+ end
32
+
33
+ # shortcut for ZSteg::CLI::Cli.new, mostly for RSpec
34
+ def new *args
35
+ require 'zsteg/cli/cli'
36
+ Cli.new(*args)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,63 @@
1
+ require 'zpng'
2
+
3
+ module ZSteg
4
+ class Analyzer
5
+ def initialize image, params = {}
6
+ @params = params
7
+ @image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
8
+ end
9
+
10
+ def analyze!
11
+ if bs = detect_block_size
12
+ puts "[!] possible image block size is #{bs.join('x')}, downscaling may be necessary".yellow
13
+ end
14
+ end
15
+
16
+ def check_block_size dx, dy, x0, y0
17
+ c0 = @image[x0,y0]
18
+ y0.upto(y0+dy-1) do |y|
19
+ x0.upto(x0+dx-1) do |x|
20
+ return if @image[x,y] != c0
21
+ end
22
+ end
23
+ true
24
+ end
25
+
26
+ def detect_block_size
27
+ x=y=0
28
+ c0 = @image[x,y]
29
+ dx = dy = 1
30
+
31
+ while (x+dx) < @image.width && @image[x+dx,y] == c0
32
+ dx+=1
33
+ end
34
+ while (y+dy) < @image.height && @image[x,y+dy] == c0
35
+ dy+=1
36
+ end
37
+
38
+ return if dx<2 && dy<2
39
+ return if [1, @image.width].include?(dx) && [1, @image.height].include?(dy)
40
+
41
+ # check 3x3 block
42
+ 0.step([dy*3, @image.height-1].min, dy) do |y|
43
+ 0.step([dx*3, @image.width-1].min, dx) do |x|
44
+ return unless check_block_size dx, dy, x, y
45
+ end
46
+ end
47
+
48
+ [dx,dy]
49
+ end
50
+ end
51
+ end
52
+
53
+ if __FILE__ == $0
54
+ ARGV.each do |fname|
55
+ printf "\r[.] %-40s .. ", fname
56
+ begin
57
+ ZSteg::Analyzer.new(fname).analyze!
58
+ rescue
59
+ p $!
60
+ end
61
+ end
62
+ puts
63
+ end
@@ -1,3 +1,4 @@
1
+ #coding: utf-8
1
2
  require 'stringio'
2
3
  require 'zlib'
3
4
  require 'set'
@@ -6,11 +7,11 @@ module ZSteg
6
7
  class Checker
7
8
  attr_accessor :params, :channels, :verbose, :results
8
9
 
9
- MIN_TEXT_LENGTH = 8
10
- MIN_WHOLETEXT_LENGTH = 6 # when entire data is a text
11
10
  DEFAULT_BITS = [1,2,3,4]
12
11
  DEFAULT_ORDER = 'auto'
13
12
  DEFAULT_LIMIT = 256 # number of checked bytes, 0 = no limit
13
+ DEFAULT_EXTRA_CHECKS = true
14
+ DEFAULT_MIN_STR_LEN = 8
14
15
 
15
16
  # image can be either filename or ZPNG::Image
16
17
  def initialize image, params = {}
@@ -31,6 +32,16 @@ module ZSteg
31
32
  @params[:bits] ||= DEFAULT_BITS
32
33
  @params[:order] ||= DEFAULT_ORDER
33
34
  @params[:limit] ||= DEFAULT_LIMIT
35
+
36
+ if @params[:min_str_len]
37
+ @min_str_len = @min_wholetext_len = @params[:min_str_len]
38
+ else
39
+ @min_str_len = DEFAULT_MIN_STR_LEN
40
+ @min_wholetext_len = @min_str_len - 2
41
+ end
42
+ @strings_re = /[\x20-\x7e\r\n\t]{#@min_str_len,}/
43
+
44
+ @extra_checks = params.fetch(:extra_checks, DEFAULT_EXTRA_CHECKS)
34
45
  end
35
46
 
36
47
  private
@@ -56,9 +67,11 @@ module ZSteg
56
67
  @found_anything = false
57
68
  @file_cmd.start!
58
69
 
59
- check_extradata
60
- check_metadata
61
- check_imagedata
70
+ if @extra_checks
71
+ check_extradata
72
+ check_metadata
73
+ check_imagedata
74
+ end
62
75
 
63
76
  if @image.format == :bmp
64
77
  case params[:order].to_s.downcase
@@ -96,6 +109,11 @@ module ZSteg
96
109
  puts "\r[=] nothing :(" + " "*20 # line cleanup
97
110
  end
98
111
 
112
+ if @extra_checks
113
+ Analyzer.new(@image).analyze!
114
+ end
115
+
116
+ # return everything found if this method was called from some code
99
117
  @results
100
118
  ensure
101
119
  @file_cmd.stop!
@@ -107,11 +125,30 @@ module ZSteg
107
125
  end
108
126
 
109
127
  def check_extradata
110
- if @image.extradata
128
+ # accessing imagedata implicitly unpacks zlib stream
129
+ # zlib stream may contain extradata
130
+ if @image.imagedata.size > (t=@image.scanlines.map(&:size).inject(&:+))
131
+ @found_anything = true
132
+ data = @image.imagedata[t..-1]
133
+ title = "extradata:imagedata"
134
+ show_title title, :bright_red
135
+ process_result data, :special => true, :title => title
136
+ end
137
+
138
+ if @image.extradata.any?
139
+ @found_anything = true
140
+ @image.extradata.each_with_index do |data,idx|
141
+ title = "extradata:#{idx}"
142
+ show_title title, :bright_red
143
+ process_result data, :special => true, :title => title
144
+ end
145
+ end
146
+
147
+ if data = ScanlineChecker.check_image(@image, @params)
111
148
  @found_anything = true
112
- title = "data after IEND"
149
+ title = "scanline extradata"
113
150
  show_title title, :bright_red
114
- process_result @image.extradata, :special => true, :title => title
151
+ process_result data, :special => true, :title => title
115
152
  end
116
153
  end
117
154
 
@@ -199,6 +236,10 @@ module ZSteg
199
236
  p1[:title] = title
200
237
  data = @extractor.extract p1
201
238
 
239
+ if p1[:invert]
240
+ data.size.times{ |i| data.setbyte(i, data.getbyte(i)^0xff) }
241
+ end
242
+
202
243
  @need_cr = !process_result(data, p1) # carriage return needed?
203
244
  @found_anything ||= !@need_cr
204
245
  end
@@ -208,6 +249,21 @@ module ZSteg
208
249
  $stdout.flush
209
250
  end
210
251
 
252
+ def show_result result, params
253
+ case result
254
+ when Array
255
+ result.each_with_index do |r,idx|
256
+ # empty title for multiple results from same title
257
+ show_title(" ") if idx > 0
258
+ puts r
259
+ end
260
+ when nil, false
261
+ # do nothing?
262
+ else
263
+ puts result
264
+ end
265
+ end
266
+
211
267
  # returns true if was any output
212
268
  def process_result data, params
213
269
  verbose = params[:special] ? [@verbose,1.5].max : @verbose
@@ -234,7 +290,7 @@ module ZSteg
234
290
  # verbosity=0: only show result if anything interesting found
235
291
  if result && !result.is_a?(Result::OneChar)
236
292
  show_title params[:title] if params[:show_title]
237
- puts result
293
+ show_result result, params
238
294
  return true
239
295
  else
240
296
  return false
@@ -242,22 +298,28 @@ module ZSteg
242
298
  when 1
243
299
  # verbosity=1: if anything interesting found show result & hexdump
244
300
  return false unless result
301
+ else
302
+ # verbosity>1: always show hexdump
245
303
  end
246
304
 
247
- # verbosity>1: always show hexdump
248
305
  show_title params[:title] if params[:show_title]
249
306
 
250
307
  if params[:special]
251
308
  puts result.is_a?(Result::PartialText) ? nil : result
252
309
  else
253
- puts result
310
+ show_result result, params
254
311
  end
255
312
  if data.size > 0 && !result.is_a?(Result::OneChar) && !result.is_a?(Result::WholeText)
256
- print ZPNG::Hexdump.dump(data){ |x| x.prepend(" "*4) }
313
+ limit = (params[:limit] || @params[:limit]).to_i
314
+ t = limit > 0 ? data[0,limit] : data
315
+ print ZPNG::Hexdump.dump(t){ |x| x.prepend(" "*4) }
257
316
  end
258
317
  true
259
318
  end
260
319
 
320
+ CAMOUFLAGE_SIG1 = "\x00\x00".force_encoding('binary')
321
+ CAMOUFLAGE_SIG2 = "\xed\xcd\x01".force_encoding('binary')
322
+
261
323
  def data2result data, params
262
324
  if one_char?(data)
263
325
  return Result::OneChar.new(data[0,1], data.size)
@@ -269,8 +331,11 @@ module ZSteg
269
331
  return Result::OpenStego.read(io)
270
332
  end
271
333
 
272
- if data[0,2] == "\x00\x00" && data[3,3] == "\xed\xcd\x01"
273
- return Result::Camouflage.new(data)
334
+ # only in extradata
335
+ if params[:title]['extradata']
336
+ if data[0,2] == CAMOUFLAGE_SIG1 && data[3,3] == CAMOUFLAGE_SIG2
337
+ return Result::Camouflage.new(data)
338
+ end
274
339
  end
275
340
 
276
341
  # only BMP & 1-bit-per-channel
@@ -283,40 +348,45 @@ module ZSteg
283
348
  end
284
349
  end
285
350
 
286
- if data.size >= MIN_WHOLETEXT_LENGTH && data =~ /\A[\x20-\x7e\r\n\t]+\Z/
351
+ if data.size >= @min_wholetext_len && data =~ /\A[\x20-\x7e\r\n\t]+\Z/
287
352
  # whole ASCII
288
353
  return Result::WholeText.new(data, 0)
289
354
  end
290
355
 
291
- # XXX TODO refactor params hack
292
- if !params.key?(:no_check_file) && (r = @file_cmd.data2result(data))
356
+ if params.fetch(:file, true) && (r = @file_cmd.data2result(data))
293
357
  return r
294
358
  end
295
359
 
296
- # try to find zlib
297
- # http://blog.w3challs.com/index.php?post/2012/03/25/NDH2k12-Prequals-We-are-looking-for-a-real-hacker-Wallpaper-image
298
- # http://blog.w3challs.com/public/ndh2k12_prequalls/sp113.bmp
299
- # XXX TODO refactor params hack
300
- if !params.key?(:no_check_zlib) && (idx = data.index(/\x78[\x9c\xda\x01]/))
301
- begin
302
- # x = Zlib::Inflate.inflate(data[idx,4096])
303
- zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
304
- x = zi.inflate data[idx..-1]
305
- # decompress OK
306
- return Result::Zlib.new x, idx if x.size > 2
307
- rescue Zlib::BufError
308
- # tried to decompress, but got EOF - need more data
309
- return Result::Zlib.new x, idx
310
- rescue Zlib::DataError, Zlib::NeedDict
311
- # not a zlib
312
- ensure
313
- zi.close if zi && !zi.closed?
314
- end
360
+ if r = Checker::Zlib.check_data(data)
361
+ return r
315
362
  end
316
363
 
317
- if (r=data[/[\x20-\x7e\r\n\t]{#{MIN_TEXT_LENGTH},}/])
318
- return Result::PartialText.new(r, data.index(r))
364
+ case params.fetch(:strings, :first)
365
+ when :all
366
+ r=[]
367
+ data.scan(@strings_re) do
368
+ r << Result::PartialText.from_matchdata($~)
369
+ end
370
+ return r if r.any?
371
+ when :first
372
+ if data[@strings_re]
373
+ return Result::PartialText.from_matchdata($~)
374
+ end
375
+ when :longest
376
+ r=[]
377
+ data.scan(@strings_re){ r << $~ }
378
+ return Result::PartialText.from_matchdata(r.sort_by(&:size).last) if r.any?
319
379
  end
380
+
381
+ # utf-8 string matching, may be slow, may throw exceptions
382
+ # begin
383
+ # t = data.
384
+ # encode('UTF-16', 'UTF-8', :invalid => :replace, :replace => '').
385
+ # encode!('UTF-8', 'UTF-16')
386
+ # r = t.scan(/\p{Word}{#{MIN_TEXT_LENGTH},}/)
387
+ # r if r.any?
388
+ # rescue
389
+ # end
320
390
  end
321
391
 
322
392
  private
@@ -0,0 +1,82 @@
1
+ module ZSteg
2
+ class Checker
3
+ module ScanlineChecker
4
+ class << self
5
+ def check_image image, params={}
6
+ # TODO: interlaced images
7
+ sl = image.scanlines.first
8
+ significant_bits = sl.width*sl.bpp
9
+ total_bits = sl.size*8
10
+ # 1 byte for PNG scanline filter mode
11
+ # XXX maybe move this into ZPNG::ScanLine#data_size ?
12
+ total_bits -= 8 if image.format == :png
13
+ return if total_bits == significant_bits
14
+
15
+ #puts "[nbits] tb=#{total_bits}, sb=#{significant_bits}, nbits=#{total_bits-significant_bits}"
16
+ nbits = total_bits-significant_bits
17
+ raise "WTF" if nbits<0 # significant size greatar than total size?!
18
+
19
+ data = ''
20
+ scanlines = image.scanlines
21
+ # DO NOT use 'reverse!' here - it will affect original image too
22
+ scanlines = scanlines.reverse if image.format == :bmp
23
+ if nbits%8 == 0
24
+ # whole bytes
25
+ nbytes = nbits/8
26
+ scanlines.each do |sl|
27
+ data << sl.decoded_bytes[-nbytes,nbytes]
28
+ end
29
+ else
30
+ # extract a number of bits from each scanline
31
+ nbytes = (nbits/8.0).ceil # number of whole bytes, rounded up
32
+ mask = 2**nbits-1
33
+ a = []
34
+ scanlines.each do |sl|
35
+ bytes = sl.decoded_bytes[-nbytes,nbytes]
36
+ value = 0
37
+ # convert 1+ bytes into one big integer
38
+ bytes.each_byte{ |b| value = (value<<8) + b }
39
+
40
+ # remove unwanted bits
41
+ value &= mask
42
+
43
+ # fix[n] -> 0, 1
44
+ # Bit Reference - Returns the nth bit in the binary representation of fix
45
+ # http://www.ruby-doc.org/core-1.9.3/Fixnum.html#method-i-5B-5D
46
+ #
47
+ # also "<<" + "reverse!" is 30% faster than "unshift"
48
+ nbits.times{ |i| a << value[i] }
49
+ a.reverse!
50
+
51
+ while a.size >= 8
52
+ byte = 0
53
+ if params[:bit_order] == :msb
54
+ 8.times{ |i| byte |= (a.shift<<i)}
55
+ else
56
+ 8.times{ |i| byte |= (a.shift<<(7-i))}
57
+ end
58
+ data << byte.chr
59
+ # if data.size >= @limit
60
+ # print "[limit #@limit]".gray if @verbose > 1
61
+ # break
62
+ # end
63
+ end
64
+ end
65
+ end
66
+ return if data =~ /\A\x00+\Z/ # nothing special, only zero bytes
67
+
68
+ # something found
69
+ data
70
+ end
71
+ end # class << self
72
+ end # Scanline
73
+ end # Checker
74
+ end # ZSteg
75
+
76
+ if $0 == __FILE__
77
+ require 'zpng'
78
+ ARGV.each do |fname|
79
+ img = ZPNG::Image.load fname
80
+ ZSteg::Checker::ScanlineChecker.check_image img
81
+ end
82
+ end