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
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem 'zpng', ">= 0.2.1"
6
+ gem "awesome_print"
7
+ gem "iostruct"
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "rspec", ">= 2.8.0"
13
+ gem "bundler", ">= 1.0.0"
14
+ gem "jeweler", "~> 1.8.4"
15
+ end
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ awesome_print (1.1.0)
5
+ diff-lcs (1.1.3)
6
+ git (1.2.5)
7
+ iostruct (0.0.1)
8
+ jeweler (1.8.4)
9
+ bundler (~> 1.0)
10
+ git (>= 1.2.5)
11
+ rake
12
+ rdoc
13
+ json (1.7.5)
14
+ rainbow (1.1.4)
15
+ rake (10.0.3)
16
+ rdoc (3.12)
17
+ json (~> 1.4)
18
+ rspec (2.12.0)
19
+ rspec-core (~> 2.12.0)
20
+ rspec-expectations (~> 2.12.0)
21
+ rspec-mocks (~> 2.12.0)
22
+ rspec-core (2.12.2)
23
+ rspec-expectations (2.12.1)
24
+ diff-lcs (~> 1.1.3)
25
+ rspec-mocks (2.12.1)
26
+ zpng (0.2.1)
27
+ rainbow
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ awesome_print
34
+ bundler (>= 1.0.0)
35
+ iostruct
36
+ jeweler (~> 1.8.4)
37
+ rspec (>= 2.8.0)
38
+ zpng (>= 0.2.1)
@@ -0,0 +1,46 @@
1
+ zsteg
2
+ ======
3
+
4
+
5
+ Description
6
+ -----------
7
+ detect stegano-hidden data in PNG & BMP
8
+
9
+
10
+ Installation
11
+ ------------
12
+ gem install zsteg
13
+
14
+
15
+ Detects:
16
+ --------
17
+ * LSB steganography in PNG & BMP
18
+ * zlib-compressed data
19
+ * [OpenStego](http://openstego.sourceforge.net/)
20
+ * [Camouflage 1.2.1](http://camouflage.unfiction.com/)
21
+
22
+
23
+ Usage
24
+ -----
25
+
26
+ # zsteg -h
27
+
28
+ Usage: zsteg [options] filename.png
29
+
30
+ -c, --channels X channels (R/G/B/A) or any combination, comma separated
31
+ valid values: r,g,b,a,rg,rgb,bgr,rgba,...
32
+ -l, --limit N limit bytes checked, 0 = no limit (default: 256)
33
+ -b, --bits N number of bits (1..8), single value or '1,3,5' or '1-8'
34
+ --lsb least significant BIT comes first
35
+ --msb most significant BIT comes first
36
+ -o, --order X pixel iteration order (default: 'auto')
37
+ valid values: ALL,xy,yx,XY,YX,xY,Xy,bY,...
38
+ -E, --extract NAME extract specified payload, NAME is like '1b,rgb,lsb'
39
+
40
+ -v, --verbose Run verbosely (can be used multiple times)
41
+ -q, --quiet Silent any warnings (can be used multiple times)
42
+
43
+
44
+ License
45
+ -------
46
+ Released under the MIT License. See the [LICENSE](https://github.com/zed-0xff/zsteg/blob/master/LICENSE.txt) file for further details.
@@ -0,0 +1,31 @@
1
+ zsteg
2
+ ======
3
+
4
+
5
+ Description
6
+ -----------
7
+ detect stegano-hidden data in PNG & BMP
8
+
9
+
10
+ Installation
11
+ ------------
12
+ gem install zsteg
13
+
14
+
15
+ Detects:
16
+ --------
17
+ * LSB steganography in PNG & BMP
18
+ * zlib-compressed data
19
+ * [OpenStego](http://openstego.sourceforge.net/)
20
+ * [Camouflage 1.2.1](http://camouflage.unfiction.com/)
21
+
22
+
23
+ Usage
24
+ -----
25
+
26
+ % zsteg -h
27
+
28
+
29
+ License
30
+ -------
31
+ Released under the MIT License. See the [LICENSE](https://github.com/zed-0xff/zsteg/blob/master/LICENSE.txt) file for further details.
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "zsteg"
18
+ gem.homepage = "http://github.com/zed-0xff/zsteg"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Detect stegano-hidden data in PNG & BMP files.}
21
+ #gem.description = %Q{TODO: longer description of your gem}
22
+ gem.email = "zed.0xff@gmail.com"
23
+ gem.authors = ["Andrey \"Zed\" Zaikin"]
24
+ #gem.executables = %w'zsteg'
25
+ gem.files.include "lib/**/*.rb"
26
+ gem.files.include "bin/zsteg"
27
+ # dependencies defined in Gemfile
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
30
+
31
+ require 'rspec/core'
32
+ require 'rspec/core/rake_task'
33
+ RSpec::Core::RakeTask.new(:spec) do |spec|
34
+ spec.pattern = FileList['spec/**/*_spec.rb']
35
+ end
36
+
37
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
38
+ spec.pattern = 'spec/**/*_spec.rb'
39
+ spec.rcov = true
40
+ end
41
+
42
+ task :default => :spec
43
+
44
+ desc "build readme"
45
+ task :readme do
46
+ require 'erb'
47
+ tpl = File.read('README.md.tpl').gsub(/^%\s+(.+)/) do |x|
48
+ x.sub! /^%/,''
49
+ "<%= run(\"#{x}\") %>"
50
+ end
51
+ def run cmd
52
+ cmd.strip!
53
+ puts "[.] #{cmd} ..."
54
+ r = " # #{cmd}\n\n"
55
+ cmd.sub! /^zsteg/,"../bin/zsteg"
56
+ lines = `#{cmd}`.sub(/\A\n+/m,'').sub(/\s+\Z/,'').split("\n")
57
+ lines = lines[0,25] + ['...'] if lines.size > 50
58
+ r << lines.map{|x| " #{x}"}.join("\n")
59
+ r << "\n"
60
+ end
61
+ Dir.chdir 'samples'
62
+ result = ERB.new(tpl,nil,'%>').result
63
+ Dir.chdir '..'
64
+ File.open('README.md','w'){ |f| f << result }
65
+ end
66
+
67
+ Rake::Task[:console].clear
68
+
69
+ # from /usr/local/lib64/ruby/gems/1.9.1/gems/jeweler-1.8.4/lib/jeweler/tasks.rb
70
+ desc "Start IRB with all runtime dependencies loaded"
71
+ task :console, [:script] do |t,args|
72
+ dirs = ['./ext', './lib'].select { |dir| File.directory?(dir) }
73
+
74
+ original_load_path = $LOAD_PATH
75
+
76
+ cmd = if File.exist?('Gemfile')
77
+ require 'bundler'
78
+ Bundler.setup(:default)
79
+ end
80
+
81
+ # add the project code directories
82
+ $LOAD_PATH.unshift(*dirs)
83
+
84
+ # clear ARGV so IRB is not confused
85
+ ARGV.clear
86
+
87
+ require 'irb'
88
+
89
+ # ZZZ actually added only these 2 lines
90
+ require 'zsteg'
91
+ include ZSteg
92
+
93
+ # set the optional script to run
94
+ IRB.conf[:SCRIPT] = args.script
95
+ IRB.start
96
+
97
+ # return the $LOAD_PATH to it's original state
98
+ $LOAD_PATH.reject! { |path| !(original_load_path.include?(path)) }
99
+ end
data/TODO ADDED
@@ -0,0 +1,14 @@
1
+ [ ] make 'extract' cmd stream its data directly to stdout
2
+ [ ] gzip
3
+ [*] wbStego
4
+ [ ] openstego
5
+ [ ] tmp/steg*/*.bmp
6
+ [ ] 4bpp/8bpp BMP
7
+
8
+ [+] auto pixel order for BMP
9
+ [+] BMP
10
+ [+] zlib
11
+
12
+ [ ] detect AES from http://punkroy.drque.net/PNG_Steganography/Steganography5.php
13
+ [?] http://tobyinkster.co.uk/article/steg-encode/
14
+ [ ] Sieve of Eratosthenes: http://wiki.cedricbonhomme.org/security:steganography
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
+ require 'zsteg'
5
+ require 'zsteg/cli'
6
+
7
+ ZSteg::CLI.new.run
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ require 'zpng'
3
+ require 'awesome_print'
4
+
5
+ @show_all = true
6
+
7
+ images = ARGV.map{ |fname| ZPNG::Image.load(fname) }
8
+ raise "need at least 2 images" if images.size < 2
9
+
10
+ limit = 25
11
+ alpha_used = images.any?(&:alpha_used?)
12
+ channels = alpha_used ? %w'r g b a' : %w'r g b'
13
+ channels.reverse!
14
+
15
+ printf "%6s %4s %4s : %s ...\n".magenta, "#", "X", "Y", (alpha_used ? "RRGGBBAA":"RRGGBB").reverse
16
+
17
+ idx = ndiff = 0
18
+ (images[0].height-1).downto(0) do |y|
19
+ 0.upto(images[0].width-1) do |x|
20
+ colors = images.map{ |img| img[x,y] }
21
+ if colors.uniq.size > 1 || @show_all
22
+ ndiff += 1
23
+ printf "%6d %4d %4d : ", idx, x, y
24
+ t = Array.new(images.size){ '' }
25
+ channels.each do |channel|
26
+ values = colors.map{ |color| color.send(channel) }
27
+ if values.uniq.size == 1
28
+ # all equal
29
+ values.each_with_index do |value,idx|
30
+ t[idx] << "%02x".gray % value
31
+ end
32
+ else
33
+ # got diff
34
+ values.each_with_index do |value,idx|
35
+ t[idx] << "%02x".red % value
36
+ end
37
+ end
38
+ end
39
+ puts t.join(' ')
40
+ end
41
+ idx += 1
42
+ if limit && ndiff >= limit
43
+ puts "[.] diff limit #{limit} reached"
44
+ exit
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ require 'zpng'
3
+ require 'awesome_print'
4
+
5
+ images = ARGV.map{ |fname| ZPNG::Image.load(fname) }
6
+ raise "need at least 2 images" if images.size < 2
7
+
8
+ limit = 20
9
+ alpha_used = images.any?(&:alpha_used?)
10
+ channels = alpha_used ? %w'r g b a' : %w'r g b'
11
+
12
+ printf "%6s %4s %4s : %s ...\n".magenta, "#", "X", "Y", (alpha_used ? "RRGGBBAA":"RRGGBB")
13
+
14
+ idx = ndiff = 0
15
+ images[0].each_pixel do |c,x,y|
16
+ colors = images.map{ |img| img[x,y] }
17
+ if colors.uniq.size > 1
18
+ ndiff += 1
19
+ printf "%6d %4d %4d : ", idx, x, y
20
+ t = Array.new(images.size){ '' }
21
+ channels.each do |channel|
22
+ values = colors.map{ |color| color.send(channel) }
23
+ if values.uniq.size == 1
24
+ # all equal
25
+ values.each_with_index do |value,idx|
26
+ t[idx] << "%02x".gray % value
27
+ end
28
+ else
29
+ # got diff
30
+ values.each_with_index do |value,idx|
31
+ t[idx] << "%02x".red % value
32
+ end
33
+ end
34
+ end
35
+ puts t.join(' ')
36
+ end
37
+ idx += 1
38
+ if limit && ndiff >= limit
39
+ puts "[.] diff limit #{limit} reached"
40
+ break
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ require 'zpng'
2
+ require 'iostruct'
3
+
4
+ require 'zsteg/extractor/byte_extractor'
5
+ require 'zsteg/extractor/color_extractor'
6
+ require 'zsteg/extractor'
7
+
8
+ require 'zsteg/checker'
9
+ require 'zsteg/result'
10
+ require 'zsteg/file_cmd'
11
+
12
+ require 'zsteg/checker/wbstego'
@@ -0,0 +1,228 @@
1
+ require 'stringio'
2
+ require 'zlib'
3
+
4
+ module ZSteg
5
+ class Checker
6
+ attr_accessor :params, :channels, :verbose
7
+
8
+ MIN_TEXT_LENGTH = 8
9
+
10
+ # image can be either filename or ZPNG::Image
11
+ def initialize image, params = {}
12
+ @params = params
13
+ @cache = {}
14
+ @image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
15
+ @extractor = Extractor.new(@image, params)
16
+ @channels = params[:channels] ||
17
+ if @image.alpha_used?
18
+ %w'r g b a rgb bgr rgba abgr'
19
+ else
20
+ %w'r g b rgb bgr'
21
+ end
22
+ @verbose = params[:verbose] || 0
23
+ @file_cmd = FileCmd.new
24
+ end
25
+
26
+ def check
27
+ @found_anything = false
28
+ @file_cmd.start!
29
+
30
+ check_extradata
31
+ check_metadata
32
+
33
+ case params[:order].to_s.downcase
34
+ when /all/
35
+ params[:order] = %w'xy yx XY YX Xy yX xY Yx'
36
+ when /auto/
37
+ params[:order] = @image.format == :bmp ? %w'bY xY' : 'xy'
38
+ end
39
+
40
+ Array(params[:order]).uniq.each do |order|
41
+ Array(params[:bits]).uniq.each do |bits|
42
+ if order[/b/i]
43
+ # byte iterator does not need channels
44
+ check_channels nil, @params.merge( :bits => bits, :order => order )
45
+ else
46
+ channels.each do |c|
47
+ check_channels c, @params.merge( :bits => bits, :order => order )
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ if @found_anything
54
+ print "\r" + " "*20 + "\r" if @need_cr
55
+ else
56
+ puts "\r[=] nothing :(" + " "*20 # line cleanup
57
+ end
58
+ ensure
59
+ @file_cmd.stop!
60
+ end
61
+
62
+ def check_extradata
63
+ if @image.extradata
64
+ @found_anything = true
65
+ title = "data after IEND"
66
+ show_title title, :red
67
+ process_result @image.extradata, :special => true, :title => title
68
+ end
69
+ end
70
+
71
+ def check_metadata
72
+ @image.metadata.each do |k,v|
73
+ @found_anything = true
74
+ show_title(title = "meta #{k}")
75
+ process_result v, :special => true, :title => title
76
+ end
77
+ end
78
+
79
+ def check_channels channels, params
80
+ unless params[:bit_order]
81
+ check_channels(channels, params.merge(:bit_order => :lsb))
82
+ check_channels(channels, params.merge(:bit_order => :msb))
83
+ return
84
+ end
85
+
86
+ title = ["#{params[:bits]}b",channels,params[:bit_order],params[:order]].compact.join(',')
87
+ show_title title
88
+
89
+ p1 = params.clone
90
+ p1.delete :channel
91
+ p1[:title] = title
92
+
93
+ if channels
94
+ p1[:channels] = channels.split('')
95
+ @max_hidden_size = p1[:channels].size*@image.width
96
+ elsif params[:order] =~ /b/i
97
+ # byte extractor
98
+ @max_hidden_size = @image.scanlines[0].decoded_bytes.size
99
+ else
100
+ raise "invalid params #{params.inspect}"
101
+ end
102
+ @max_hidden_size *= p1[:bits]*@image.height/8
103
+
104
+ data = @extractor.extract p1
105
+
106
+ @need_cr = !process_result(data, p1) # carriage return needed?
107
+ @found_anything ||= !@need_cr
108
+ end
109
+
110
+ def show_title title, color = :gray
111
+ printf "\r[.] %-14s.. ".send(color), title
112
+ $stdout.flush
113
+ end
114
+
115
+ # returns true if was any output
116
+ def process_result data, params
117
+ verbose = params[:special] ? [@verbose,1.5].max : @verbose
118
+
119
+ if @cache[data]
120
+ if verbose > 1
121
+ puts "[same as #{@cache[data].inspect}]".gray
122
+ return true
123
+ else
124
+ # silent return
125
+ return false
126
+ end
127
+ end
128
+
129
+ # TODO: store hash of data for large datas
130
+ @cache[data] = params[:title]
131
+
132
+ result = data2result data, params
133
+
134
+ case verbose
135
+ when -999..0
136
+ # verbosity=0: only show result if anything interesting found
137
+ if result && !result.is_a?(Result::OneChar)
138
+ puts result
139
+ return true
140
+ else
141
+ return false
142
+ end
143
+ when 1
144
+ # verbosity=1: if anything interesting found show result & hexdump
145
+ return false unless result
146
+ end
147
+
148
+ # verbosity>1: always show hexdump
149
+
150
+ if params[:special]
151
+ puts result.is_a?(Result::PartialText) ? nil : result
152
+ else
153
+ puts result
154
+ end
155
+ if data.size > 0 && !result.is_a?(Result::OneChar) && !result.is_a?(Result::WholeText)
156
+ print ZPNG::Hexdump.dump(data){ |x| x.prepend(" "*4) }
157
+ end
158
+ true
159
+ end
160
+
161
+ def data2result data, params
162
+ if one_char?(data)
163
+ return Result::OneChar.new(data[0,1], data.size)
164
+ end
165
+
166
+ if idx = data.index('OPENSTEGO')
167
+ io = StringIO.new(data)
168
+ io.seek(idx+9)
169
+ return Result::OpenStego.read(io)
170
+ end
171
+
172
+ if data[0,2] == "\x00\x00" && data[3,3] == "\xed\xcd\x01"
173
+ return Result::Camouflage.new(data)
174
+ end
175
+
176
+ # only BMP & 1-bit-per-channel
177
+ if params[:bits] == 1 && params[:bit_order] == :lsb
178
+ if x = WBStego.check(data, params.merge(
179
+ :image => @image,
180
+ :max_hidden_size => @max_hidden_size
181
+ ))
182
+ return x
183
+ end
184
+ end
185
+
186
+ if data =~ /\A[\x20-\x7e\r\n\t]+\Z/
187
+ # whole ASCII
188
+ return Result::WholeText.new(data, 0)
189
+ end
190
+
191
+ if r = @file_cmd.check_data(data)
192
+ return Result::FileCmd.new(r, data)
193
+ end
194
+
195
+ # http://blog.w3challs.com/index.php?post/2012/03/25/NDH2k12-Prequals-We-are-looking-for-a-real-hacker-Wallpaper-image
196
+ # http://blog.w3challs.com/public/ndh2k12_prequalls/sp113.bmp
197
+ if idx = data.index(/\x78[\x9c\xda\x01]/)
198
+ begin
199
+ # x = Zlib::Inflate.inflate(data[idx,4096])
200
+ zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
201
+ x = zi.inflate data[idx..-1]
202
+ # decompress OK
203
+ return Result::Zlib.new x, idx
204
+ rescue Zlib::BufError
205
+ # tried to decompress, but got EOF - need more data
206
+ return Result::Zlib.new x, idx
207
+ rescue Zlib::DataError, Zlib::NeedDict
208
+ # not a zlib
209
+ ensure
210
+ zi.close if zi && !zi.closed?
211
+ end
212
+ end
213
+
214
+ if (r=data[/[\x20-\x7e\r\n\t]{#{MIN_TEXT_LENGTH},}/])
215
+ return Result::PartialText.new(r, data.index(r))
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ # returns true if String s consists of one repeating character
222
+ # performance-optimized
223
+ # 16Mb string = 0.7s on Core i5 1.7GHz
224
+ def one_char? s
225
+ (s =~ /\A(.)\1+\Z/m) == 0
226
+ end
227
+ end
228
+ end