zsteg 0.0.0

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