zsteg 0.0.1 → 0.1.0

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