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 +1 -1
- data/Gemfile.lock +2 -2
- data/TODO +6 -0
- data/VERSION +1 -1
- data/bin/zsteg +5 -4
- data/bin/zsteg-mask +5 -4
- data/bin/zsteg-reflow +8 -0
- data/lib/zsteg.rb +25 -0
- data/lib/zsteg/analyzer.rb +63 -0
- data/lib/zsteg/checker.rb +108 -38
- data/lib/zsteg/checker/scanline_checker.rb +82 -0
- data/lib/zsteg/checker/wbstego.rb +3 -0
- data/lib/zsteg/checker/zlib.rb +41 -0
- data/lib/zsteg/{cli.rb → cli/cli.rb} +63 -11
- data/lib/zsteg/{mask_cli.rb → cli/mask.rb} +6 -4
- data/lib/zsteg/cli/reflow.rb +241 -0
- data/lib/zsteg/extractor.rb +9 -9
- data/lib/zsteg/extractor/byte_extractor.rb +4 -4
- data/lib/zsteg/extractor/color_extractor.rb +10 -5
- data/lib/zsteg/result.rb +4 -9
- data/samples/newbiecontest/alph1-surprise.bmp.7z +0 -0
- data/spec/bin_spec.rb +10 -0
- data/spec/checker_spec.rb +3 -3
- data/spec/extradata_spec.rb +28 -0
- data/spec/mask_spec.rb +1 -1
- data/spec/newbiecontest_spec.rb +18 -0
- data/spec/spec_helper.rb +10 -5
- data/spec/wechall_spec.rb +1 -1
- data/writers/chunk_append.rb +48 -0
- data/writers/zlib_append.rb +64 -0
- data/zsteg.gemspec +19 -8
- metadata +41 -29
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -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.
|
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.
|
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
|
+
0.1.0
|
data/bin/zsteg
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require 'zsteg/cli'
|
3
|
+
base = File.expand_path('../lib', File.dirname(__FILE__))
|
4
|
+
$:.unshift(base)
|
6
5
|
|
7
|
-
|
6
|
+
require File.join(base, 'zsteg')
|
7
|
+
|
8
|
+
ZSteg::CLI.run
|
data/bin/zsteg-mask
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require 'zsteg/mask_cli'
|
3
|
+
base = File.expand_path('../lib', File.dirname(__FILE__))
|
4
|
+
$:.unshift(base)
|
6
5
|
|
7
|
-
|
6
|
+
require File.join(base, 'zsteg')
|
7
|
+
|
8
|
+
ZSteg::CLI.run
|
data/bin/zsteg-reflow
ADDED
data/lib/zsteg.rb
CHANGED
@@ -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
|
data/lib/zsteg/checker.rb
CHANGED
@@ -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
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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 = "
|
149
|
+
title = "scanline extradata"
|
113
150
|
show_title title, :bright_red
|
114
|
-
process_result
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
273
|
-
|
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 >=
|
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
|
-
|
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
|
-
|
297
|
-
|
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
|
-
|
318
|
-
|
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
|