zsteg 0.0.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.
Files changed (51) hide show
  1. data/Gemfile +15 -0
  2. data/Gemfile.lock +38 -0
  3. data/README.md +46 -0
  4. data/README.md.tpl +31 -0
  5. data/Rakefile +99 -0
  6. data/TODO +14 -0
  7. data/VERSION +1 -0
  8. data/bin/zsteg +7 -0
  9. data/cmp_bmp.rb +47 -0
  10. data/cmp_png.rb +42 -0
  11. data/lib/zsteg.rb +12 -0
  12. data/lib/zsteg/checker.rb +228 -0
  13. data/lib/zsteg/checker/wbstego.rb +98 -0
  14. data/lib/zsteg/cli.rb +132 -0
  15. data/lib/zsteg/extractor.rb +21 -0
  16. data/lib/zsteg/extractor/byte_extractor.rb +94 -0
  17. data/lib/zsteg/extractor/color_extractor.rb +95 -0
  18. data/lib/zsteg/file_cmd.rb +63 -0
  19. data/lib/zsteg/result.rb +90 -0
  20. data/pngsteg.gemspec +65 -0
  21. data/samples/06_enc.png +0 -0
  22. data/samples/Code.png +0 -0
  23. data/samples/README +4 -0
  24. data/samples/camouflage-password.png +0 -0
  25. data/samples/camouflage.png +0 -0
  26. data/samples/cats.png +0 -0
  27. data/samples/flower.png +0 -0
  28. data/samples/flower_rgb1.png +0 -0
  29. data/samples/flower_rgb2.png +0 -0
  30. data/samples/flower_rgb3.png +0 -0
  31. data/samples/flower_rgb4.png +0 -0
  32. data/samples/flower_rgb5.png +0 -0
  33. data/samples/flower_rgb6.png +0 -0
  34. data/samples/montenach-enc.png +0 -0
  35. data/samples/ndh2k12_sp113.bmp.7z +0 -0
  36. data/samples/openstego_q2.png +0 -0
  37. data/samples/openstego_send.png +0 -0
  38. data/samples/stg300.png +0 -0
  39. data/samples/wbsteg_noenc.bmp +0 -0
  40. data/samples/wbsteg_noenc_17.bmp +0 -0
  41. data/samples/wbsteg_noenc_even.bmp +0 -0
  42. data/samples/wbsteg_noenc_even_17.bmp +0 -0
  43. data/spec/camouflage_spec.rb +9 -0
  44. data/spec/cats_spec.rb +23 -0
  45. data/spec/flowers_spec.rb +11 -0
  46. data/spec/openstego_spec.rb +21 -0
  47. data/spec/simple_spec.rb +22 -0
  48. data/spec/spec_helper.rb +39 -0
  49. data/spec/wbstego_spec.rb +10 -0
  50. data/spec/zlib_spec.rb +6 -0
  51. metadata +198 -0
@@ -0,0 +1,98 @@
1
+ module ZSteg
2
+ class Checker
3
+ module WBStego
4
+
5
+ class Result < IOStruct.new "a3a3a*", :size, :ext, :data, :even
6
+ def initialize *args
7
+ super
8
+ if self.size.is_a?(String)
9
+ self.size = (self.size[0,3] + "\x00").unpack('V')[0]
10
+ end
11
+ self.even = false if self.even.nil?
12
+ end
13
+
14
+ def to_s
15
+ inspect.sub("#<struct #{self.class.to_s}", "<wbStego").red
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def used_colors
21
+ raise "TODO"
22
+ end
23
+
24
+ # from wbStego4open sources
25
+ def calc_avail_size image
26
+ space = 0
27
+ biHeader = image.hdr
28
+ if biHeader.biCompression == 0
29
+ case biHeader.biBitCount
30
+ when 4
31
+ space = 2*image.imagedata_size if used_colors < 9
32
+ when 8
33
+ space = image.imagedata_size if used_colors < 129
34
+ when 24
35
+ space = image.imagedata_size
36
+ end
37
+ else
38
+ raise "TODO"
39
+ # if biHeader.biBitCount=4 then begin
40
+ # if UsedColors<9 then space:=GetAvailSizeRLE else space:=0;
41
+ # end;
42
+ # if biHeader.biBitCount=8 then begin
43
+ # if UsedColors<129 then space:=GetAvailSizeRLE else space:=0;
44
+ # end;
45
+ end
46
+ space/8
47
+ end
48
+
49
+ def check data, params = {}
50
+ return if data.size < 4
51
+ return if params[:bit_order] != :lsb
52
+ if params[:image].format == :bmp
53
+ return if params[:order] !~ /b/i
54
+ end
55
+
56
+ size1 = (data[0,3] + "\x00").unpack('V')[0]
57
+ avail_size =
58
+ if params[:image].format == :bmp
59
+ calc_avail_size(params[:image])
60
+ else
61
+ params[:max_hidden_size]
62
+ end
63
+ return if size1 == 0 || size1 > avail_size
64
+ size2 = (data[3,3] + "\x00").unpack('V')[0]
65
+ # p [size1, size2, avail_size]
66
+ if size2 < avail_size
67
+ spacing = 1.0*avail_size/(size2+5) - 1
68
+ # puts "[d] spacing=#{spacing}"
69
+ if spacing > 0
70
+ error = 0
71
+ r = ''
72
+ 6.upto(data.size-1) do |idx|
73
+ if error < 1
74
+ r << data[idx]
75
+ error += spacing
76
+ else
77
+ error -= 1
78
+ end
79
+ end
80
+ # puts "[d] r=#{r.inspect} (#{r.size})"
81
+ ext = r[0,3]
82
+ return unless valid_ext?(ext)
83
+ return Result.new(size2, ext, r[3..-1], true)
84
+ end
85
+ end
86
+ # no even distribution
87
+ return unless valid_ext?(data[3,3])
88
+ return Result.read(data)
89
+ end
90
+
91
+ # XXX require that file extension be 7-bit ASCII
92
+ def valid_ext? ext
93
+ ext =~ /\A[\x20-\x7e]+\Z/
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,132 @@
1
+ require 'optparse'
2
+ require 'awesome_print'
3
+
4
+ module ZSteg
5
+ class CLI
6
+ DEFAULT_ACTIONS = %w'check'
7
+ DEFAULT_LIMIT = 256
8
+ DEFAULT_ORDER = 'auto'
9
+
10
+ def initialize argv = ARGV
11
+ @argv = argv
12
+ end
13
+
14
+ def run
15
+ @actions = []
16
+ @options = {
17
+ :verbose => 0,
18
+ :limit => DEFAULT_LIMIT,
19
+ :bits => [1,2,3,4],
20
+ :order => DEFAULT_ORDER
21
+ }
22
+ optparser = OptionParser.new do |opts|
23
+ opts.banner = "Usage: zsteg [options] filename.png"
24
+ opts.separator ""
25
+
26
+ opts.on("-c", "--channels X", /[rgba,]+/,
27
+ "channels (R/G/B/A) or any combination, comma separated",
28
+ "valid values: r,g,b,a,rg,rgb,bgr,rgba,..."
29
+ ){ |x| @options[:channels] = x.split(',') }
30
+
31
+ opts.on("-l", "--limit N", Integer,
32
+ "limit bytes checked, 0 = no limit (default: #{DEFAULT_LIMIT})"
33
+ ){ |n| @options[:limit] = n }
34
+
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)
41
+ end
42
+ end
43
+
44
+ opts.on "--lsb", "least significant BIT comes first" do
45
+ @options[:bit_order] = :lsb
46
+ end
47
+ opts.on "--msb", "most significant BIT comes first" do
48
+ @options[:bit_order] = :msb
49
+ end
50
+
51
+ opts.on("-o", "--order X", /all|auto|[bxy,]+/i,
52
+ "pixel iteration order (default: '#{DEFAULT_ORDER}')",
53
+ "valid values: ALL,xy,yx,XY,YX,xY,Xy,bY,...",
54
+ ){ |x| @options[:order] = x.split(',') }
55
+
56
+ opts.on "-E", "--extract NAME", "extract specified payload, NAME is like '1b,rgb,lsb'" do |x|
57
+ @actions << [:extract, x]
58
+ end
59
+
60
+ opts.separator ""
61
+ opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
62
+ @options[:verbose] += 1
63
+ end
64
+ opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
65
+ @options[:verbose] -= 1
66
+ end
67
+ end
68
+
69
+ if (argv = optparser.parse(@argv)).empty?
70
+ puts optparser.help
71
+ return
72
+ end
73
+
74
+ @actions = DEFAULT_ACTIONS if @actions.empty?
75
+
76
+ argv.each_with_index do |fname,idx|
77
+ if argv.size > 1 && @options[:verbose] >= 0
78
+ puts if idx > 0
79
+ puts "[.] #{fname}".green
80
+ end
81
+ @fname = fname
82
+
83
+ @actions.each do |action|
84
+ if action.is_a?(Array)
85
+ self.send(*action) if self.respond_to?(action.first)
86
+ else
87
+ self.send(action) if self.respond_to?(action)
88
+ end
89
+ end
90
+ end
91
+ rescue Errno::EPIPE
92
+ # output interrupt, f.ex. when piping output to a 'head' command
93
+ # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
94
+ end
95
+
96
+ ###########################################################################
97
+ # actions
98
+
99
+ def check
100
+ Checker.new(@fname, @options).check
101
+ end
102
+
103
+ def extract name
104
+ if ['extradata', 'data after IEND'].include?(name)
105
+ img = ZPNG::Image.load(@fname)
106
+ print img.extradata
107
+ return
108
+ end
109
+
110
+ h = {}
111
+ name.split(',').each do |x|
112
+ case x
113
+ when 'lsb'
114
+ h[:bit_order] = :lsb
115
+ when 'msb'
116
+ h[:bit_order] = :msb
117
+ when /(\d)b/
118
+ h[:bits] = $1.to_i
119
+ when /\A[rgba]+\Z/
120
+ h[:channels] = x.split('')
121
+ when /\Axy|yx\Z/i
122
+ h[:order] = x
123
+ else
124
+ raise "uknown param #{x.inspect}"
125
+ end
126
+ end
127
+ h[:limit] = @options[:limit] if @options[:limit] != DEFAULT_LIMIT
128
+ print Extractor.new(@fname, @options).extract(h)
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,21 @@
1
+ module ZSteg
2
+ class Extractor
3
+
4
+ include ByteExtractor
5
+ include ColorExtractor
6
+
7
+ # image can be either filename or ZPNG::Image
8
+ def initialize image, params = {}
9
+ @image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
10
+ @verbose = params[:verbose]
11
+ end
12
+
13
+ def extract params = {}
14
+ if params[:order] =~ /b/i
15
+ byte_extract params
16
+ else
17
+ color_extract params
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,94 @@
1
+ module ZSteg
2
+ class Extractor
3
+ # ByteExtractor extracts bits from each scanline bytes
4
+ # actual for BMP+wbStego combination
5
+ module ByteExtractor
6
+
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
14
+
15
+
16
+ data = ''.force_encoding('binary')
17
+ a = []
18
+ byte_iterator(params[:order]) do |x,y|
19
+ sl = @image.scanlines[y]
20
+
21
+ value = sl.decoded_bytes[x].ord
22
+ bits.times do |bidx|
23
+ a << ((value & (1<<(bits-bidx-1))) == 0 ? 0 : 1)
24
+ end
25
+
26
+ if a.size >= 8
27
+ byte = 0
28
+ if params[:bit_order] == :msb
29
+ 8.times{ |i| byte |= (a.shift<<i)}
30
+ else
31
+ 8.times{ |i| byte |= (a.shift<<(7-i))}
32
+ end
33
+ #printf "[d] %02x %08b\n", byte, byte
34
+ data << byte.chr
35
+ if data.size >= limit
36
+ print "[limit #{params[:limit]}]".gray if @verbose > 1
37
+ break
38
+ end
39
+ end
40
+ end
41
+ if params[:strip_tail_zeroes] != false && data[-1,1] == "\x00"
42
+ oldsz = data.size
43
+ data.sub!(/\x00+\Z/,'')
44
+ print "[zerotail #{oldsz-data.size}]".gray if @verbose > 1
45
+ end
46
+ data
47
+ end
48
+
49
+ # 'xy': b=0,y=0; b=1,y=0; b=2,y=0; ...
50
+ # 'yx': b=0,y=0; b=0,y=1; b=0,y=2; ...
51
+ # ...
52
+ # 'xY': b=0, y=MAX; b=1, y=MAX; b=2, y=MAX; ...
53
+ # 'XY': b=MAX,y=MAX; b=MAX-1,y=MAX; b=MAX-2,y=MAX; ...
54
+ def byte_iterator type = nil
55
+ if type.nil? || type == 'auto'
56
+ type = @image.format == :bmp ? 'bY' : 'by'
57
+ end
58
+ raise "invalid iterator type #{type}" unless type =~ /\A(by|yb)\Z/i
59
+
60
+ sl0 = @image.scanlines.first
61
+
62
+ x0,x1,xstep =
63
+ if type.index('b')
64
+ [0, sl0.decoded_bytes.size-1, 1]
65
+ else
66
+ [sl0.decoded_bytes.size-1, 0, -1]
67
+ end
68
+
69
+ y0,y1,ystep =
70
+ if type.index('y')
71
+ [0, @image.height-1, 1]
72
+ else
73
+ [@image.height-1, 0, -1]
74
+ end
75
+
76
+ if type[0,1].downcase == 'b'
77
+ # ROW iterator
78
+ y0.step(y1,ystep) do |y|
79
+ x0.step(x1,xstep) do |x|
80
+ yield x,y
81
+ end
82
+ end
83
+ else
84
+ # COLUMN iterator
85
+ x0.step(x1,xstep) do |x|
86
+ y0.step(y1,ystep) do |y|
87
+ yield x,y
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,95 @@
1
+ module ZSteg
2
+ class Extractor
3
+ # ColorExtractor extracts bits from each pixel's color
4
+ module ColorExtractor
5
+
6
+ def color_extract params = {}
7
+ channels = (Array(params[:channels]) + Array(params[:channel])).compact
8
+
9
+ limit = params[:limit].to_i
10
+ limit = 2**32 if limit <= 0
11
+
12
+ bits = params[:bits]
13
+ raise "invalid bits value #{bits.inspect}" unless (1..8).include?(bits)
14
+ mask = 2**bits - 1
15
+
16
+
17
+ data = ''.force_encoding('binary')
18
+ a = []
19
+ coord_iterator(params[:order]) do |x,y|
20
+ color = @image[x,y]
21
+
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)
26
+ end
27
+ end
28
+
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
41
+ end
42
+ end
43
+ end
44
+ if params[:strip_tail_zeroes] != false && data[-1,1] == "\x00"
45
+ oldsz = data.size
46
+ data.sub!(/\x00+\Z/,'')
47
+ print "[zerotail #{oldsz-data.size}]".gray if @verbose > 1
48
+ end
49
+ data
50
+ end
51
+
52
+ # 'xy': x=0,y=0; x=1,y=0; x=2,y=0; ...
53
+ # 'yx': x=0,y=0; x=0,y=1; x=0,y=2; ...
54
+ # ...
55
+ # 'xY': x=0, y=MAX; x=1, y=MAX; x=2, y=MAX; ...
56
+ # 'XY': x=MAX,y=MAX; x=MAX-1,y=MAX; x=MAX-2,y=MAX; ...
57
+ def coord_iterator type = nil
58
+ if type.nil? || type == 'auto'
59
+ type = @image.format == :bmp ? 'xY' : 'xy'
60
+ end
61
+ raise "invalid iterator type #{type}" unless type =~ /\A(xy|yx)\Z/i
62
+
63
+ x0,x1,xstep =
64
+ if type.index('x')
65
+ [0, @image.width-1, 1]
66
+ else
67
+ [@image.width-1, 0, -1]
68
+ end
69
+
70
+ y0,y1,ystep =
71
+ if type.index('y')
72
+ [0, @image.height-1, 1]
73
+ else
74
+ [@image.height-1, 0, -1]
75
+ end
76
+
77
+ if type[0,1].downcase == 'x'
78
+ # ROW iterator
79
+ y0.step(y1,ystep) do |y|
80
+ x0.step(x1,xstep) do |x|
81
+ yield x,y
82
+ end
83
+ end
84
+ else
85
+ # COLUMN iterator
86
+ x0.step(x1,xstep) do |x|
87
+ y0.step(y1,ystep) do |y|
88
+ yield x,y
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,63 @@
1
+ require 'open3'
2
+ require 'tempfile'
3
+
4
+ module ZSteg
5
+ class FileCmd
6
+
7
+ IGNORES = [
8
+ 'data',
9
+ 'empty',
10
+ 'Sendmail frozen configuration',
11
+ 'DOS executable',
12
+ 'Dyalog APL',
13
+ '8086 relocatable',
14
+ 'SysEx File',
15
+ 'COM executable',
16
+ 'Non-ISO extended-ASCII text',
17
+ 'ISO-8859 text',
18
+ 'very short file',
19
+ 'International EBCDIC text',
20
+ 'lif file',
21
+ 'AmigaOS bitmap font'
22
+ ]
23
+
24
+ def start!
25
+ @stdin, @stdout, @stderr, @wait_thr = Open3.popen3("file -n -b -f -")
26
+ end
27
+
28
+ def check_file fname
29
+ @stdin.puts fname
30
+ r = @stdout.gets.force_encoding('binary').strip
31
+ IGNORES.any?{ |x| r.index(x) == 0 } ? nil : r
32
+ end
33
+
34
+ def check_data data
35
+ @tempfile ||= Tempfile.new('zsteg', :encoding => 'binary')
36
+ @tempfile.rewind
37
+ @tempfile.write data
38
+ @tempfile.flush
39
+ check_file @tempfile.path
40
+ end
41
+
42
+ def stop!
43
+ @stdin.close
44
+ @stdout.close
45
+ @stderr.close
46
+ ensure
47
+ if @tempfile
48
+ @tempfile.close
49
+ @tempfile.unlink
50
+ @tempfile = nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ if __FILE__ == $0
57
+ filecmd = ZSteg::FileCmd.new
58
+ ARGV.each do |fname|
59
+ p filecmd.check_file fname
60
+ p filecmd.check_data File.binread(fname)
61
+ end
62
+ filecmd.stop!
63
+ end