zsteg 0.0.1 → 0.1.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.
- 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
|