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
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