zsteg 0.0.0 → 0.0.1

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 (78) hide show
  1. data/Gemfile +2 -7
  2. data/Gemfile.lock +2 -4
  3. data/README.md +72 -1
  4. data/README.md.tpl +23 -0
  5. data/Rakefile +5 -3
  6. data/TODO +5 -2
  7. data/VERSION +1 -1
  8. data/bin/zsteg-mask +7 -0
  9. data/lib/zsteg/checker/wbstego.rb +69 -14
  10. data/lib/zsteg/checker.rb +137 -34
  11. data/lib/zsteg/cli.rb +92 -35
  12. data/lib/zsteg/extractor/byte_extractor.rb +36 -21
  13. data/lib/zsteg/extractor/color_extractor.rb +68 -34
  14. data/lib/zsteg/extractor.rb +36 -1
  15. data/lib/zsteg/file_cmd.rb +64 -1
  16. data/lib/zsteg/mask_cli.rb +268 -0
  17. data/lib/zsteg/masker.rb +52 -0
  18. data/lib/zsteg/result.rb +27 -32
  19. data/lib/zsteg.rb +2 -0
  20. data/samples/hackquest/crypt.bmp +0 -0
  21. data/samples/hackquest/square.bmp +0 -0
  22. data/samples/wbstego/wbsteg_blowfish_pass_1.bmp +0 -0
  23. data/samples/wbstego/wbsteg_cast128_pass_1.bmp +0 -0
  24. data/samples/wbstego/wbsteg_enc_pass_pass.bmp +0 -0
  25. data/samples/wbstego/wbsteg_enc_pass_pass_even.bmp +0 -0
  26. data/samples/wbstego/wbsteg_mix_pass_1.bmp +0 -0
  27. data/samples/wbstego/wbsteg_mix_pass_1_even.bmp +0 -0
  28. data/samples/wbstego/wbsteg_mix_pass_foobar.bmp +0 -0
  29. data/samples/wbstego/wbsteg_mix_pass_pass.bmp +0 -0
  30. data/samples/wbstego/wbsteg_mixenc_pass_pass_even.bmp +0 -0
  31. data/samples/{wbsteg_noenc.bmp → wbstego/wbsteg_noenc.bmp} +0 -0
  32. data/samples/wbstego/wbsteg_noenc.png +0 -0
  33. data/samples/wbstego/wbsteg_noenc_.bmp +0 -0
  34. data/samples/{wbsteg_noenc_17.bmp → wbstego/wbsteg_noenc_17.bmp} +0 -0
  35. data/samples/wbstego/wbsteg_noenc__.bmp +0 -0
  36. data/samples/{wbsteg_noenc_even.bmp → wbstego/wbsteg_noenc_even.bmp} +0 -0
  37. data/samples/wbstego/wbsteg_noenc_even2.bmp +0 -0
  38. data/samples/{wbsteg_noenc_even_17.bmp → wbstego/wbsteg_noenc_even_17.bmp} +0 -0
  39. data/samples/wbstego/wbsteg_noenc_even_17_.bmp +0 -0
  40. data/samples/wbstego/wbsteg_noenc_ext_ABC.bmp +0 -0
  41. data/samples/wbstego/wbsteg_rijndael_pass_1.bmp +0 -0
  42. data/samples/wbstego/wbsteg_rijndael_pass_pass.bmp +0 -0
  43. data/samples/wbstego/wbsteg_rijndael_pass_pass_even.bmp +0 -0
  44. data/samples/wbstego/wbsteg_twofish_pass_1.bmp +0 -0
  45. data/samples/wechall/5ZMGcCLxpcpsru03.g00000010.png +0 -0
  46. data/samples/wechall/5ZMGcCLxpcpsru03.png +0 -0
  47. data/samples/wechall/stegano1.bmp +0 -0
  48. data/spec/checker_spec.rb +47 -0
  49. data/spec/easybmp_spec.rb +9 -0
  50. data/spec/hackquest_spec.rb +18 -0
  51. data/spec/mask_spec.rb +14 -0
  52. data/spec/polictf2012_spec.rb +48 -0
  53. data/spec/prime_spec.rb +9 -0
  54. data/spec/r3g2b3_spec.rb +9 -0
  55. data/spec/spec_helper.rb +21 -4
  56. data/spec/wbstego_spec.rb +21 -3
  57. data/spec/wechall_spec.rb +26 -0
  58. data/tmp/.keep +0 -0
  59. data/zsteg.gemspec +121 -0
  60. metadata +47 -43
  61. data/samples/06_enc.png +0 -0
  62. data/samples/Code.png +0 -0
  63. data/samples/README +0 -4
  64. data/samples/camouflage-password.png +0 -0
  65. data/samples/camouflage.png +0 -0
  66. data/samples/cats.png +0 -0
  67. data/samples/flower.png +0 -0
  68. data/samples/flower_rgb1.png +0 -0
  69. data/samples/flower_rgb2.png +0 -0
  70. data/samples/flower_rgb3.png +0 -0
  71. data/samples/flower_rgb4.png +0 -0
  72. data/samples/flower_rgb5.png +0 -0
  73. data/samples/flower_rgb6.png +0 -0
  74. data/samples/montenach-enc.png +0 -0
  75. data/samples/ndh2k12_sp113.bmp.7z +0 -0
  76. data/samples/openstego_q2.png +0 -0
  77. data/samples/openstego_send.png +0 -0
  78. data/samples/stg300.png +0 -0
data/Gemfile CHANGED
@@ -1,13 +1,8 @@
1
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"
2
+
3
+ gem 'zpng', ">= 0.2.2"
7
4
  gem "iostruct"
8
5
 
9
- # Add dependencies to develop your gem here.
10
- # Include everything needed to run rake, tests, features, etc.
11
6
  group :development do
12
7
  gem "rspec", ">= 2.8.0"
13
8
  gem "bundler", ">= 1.0.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,6 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- awesome_print (1.1.0)
5
4
  diff-lcs (1.1.3)
6
5
  git (1.2.5)
7
6
  iostruct (0.0.1)
@@ -23,16 +22,15 @@ GEM
23
22
  rspec-expectations (2.12.1)
24
23
  diff-lcs (~> 1.1.3)
25
24
  rspec-mocks (2.12.1)
26
- zpng (0.2.1)
25
+ zpng (0.2.2)
27
26
  rainbow
28
27
 
29
28
  PLATFORMS
30
29
  ruby
31
30
 
32
31
  DEPENDENCIES
33
- awesome_print
34
32
  bundler (>= 1.0.0)
35
33
  iostruct
36
34
  jeweler (~> 1.8.4)
37
35
  rspec (>= 2.8.0)
38
- zpng (>= 0.2.1)
36
+ zpng (>= 0.2.2)
data/README.md CHANGED
@@ -18,6 +18,7 @@ Detects:
18
18
  * zlib-compressed data
19
19
  * [OpenStego](http://openstego.sourceforge.net/)
20
20
  * [Camouflage 1.2.1](http://camouflage.unfiction.com/)
21
+ * [LSB with The Eratosthenes set](http://wiki.cedricbonhomme.org/security:steganography)
21
22
 
22
23
 
23
24
  Usage
@@ -25,7 +26,7 @@ Usage
25
26
 
26
27
  # zsteg -h
27
28
 
28
- Usage: zsteg [options] filename.png
29
+ Usage: zsteg [options] filename.png [param_string]
29
30
 
30
31
  -c, --channels X channels (R/G/B/A) or any combination, comma separated
31
32
  valid values: r,g,b,a,rg,rgb,bgr,rgba,...
@@ -33,13 +34,83 @@ Usage
33
34
  -b, --bits N number of bits (1..8), single value or '1,3,5' or '1-8'
34
35
  --lsb least significant BIT comes first
35
36
  --msb most significant BIT comes first
37
+ -P, --prime analyze/extract only prime bytes/pixels
38
+ -a, --all try all known methods
36
39
  -o, --order X pixel iteration order (default: 'auto')
37
40
  valid values: ALL,xy,yx,XY,YX,xY,Xy,bY,...
38
41
  -E, --extract NAME extract specified payload, NAME is like '1b,rgb,lsb'
39
42
 
40
43
  -v, --verbose Run verbosely (can be used multiple times)
41
44
  -q, --quiet Silent any warnings (can be used multiple times)
45
+ -C, --[no-]color Force (or disable) color output (default: auto)
46
+
47
+ PARAMS SHORTCUT
48
+ zsteg fname.png 2b,b,lsb,xy ==> --bits 2 --channel b --lsb --order xy
49
+
50
+ Examples
51
+ --------
52
+
53
+ ### Simple LSB
54
+
55
+ # zsteg flower_rgb3.png
56
+
57
+ 3b,rgb,lsb,xy .. text: "SuperSecretMessage"
58
+
59
+ ### Multi-result file
60
+
61
+ # zsteg cats.png
62
+
63
+ meta F .. ["Z" repeated 14999985 times]
64
+ meta C .. text: "Fourth and last cat is Luke"
65
+ meta A .. [same as "meta F"]
66
+ meta date:create .. text: "2012-03-15T23:32:46+07:00"
67
+ meta date:modify .. text: "2012-03-15T23:32:14+07:00"
68
+ 1b,r,lsb,xy .. text: "Second cat is Marussia"
69
+ 1b,g,lsb,xy .. text: "Good, but look a bit deeper..."
70
+ 1b,bgr,lsb,xy .. text: "MF_WIhf>"
71
+ 2b,g,lsb,xy .. text: "VHello, third kitten is Bessy"
72
+
73
+ ### wbStego even distributed
74
+
75
+ # zsteg wbstego/wbsteg_noenc_even.bmp 1b,lsb,bY -v
76
+
77
+ 1b,lsb,bY .. <wbStego size=22, data="xtSuperSecretMessage\n", even=true, mix=true, controlbyte="t">
78
+ 00000000: 51 00 00 16 00 00 74 0d b5 78 1e a1 39 74 e8 38 |Q.....t..x..9t.8|
79
+ 00000010: 53 c6 56 94 75 d1 a5 70 84 c8 27 65 fe 08 72 35 |S.V.u..p..'e..r5|
80
+ 00000020: 1f 3e 53 5d a7 65 8b 6e 3b 63 6b 1d bf 72 ee 27 |.>S].e.n;ck..r.'|
81
+ 00000030: 65 8d ee 82 74 da 8d 4d b3 8a 06 65 7e f8 73 9c |e...t..M...e~.s.|
82
+ 00000040: 36 0c 73 aa bd 61 67 29 37 67 5f 0b 06 65 1f a4 |6.s..ag)7g_..e..|
83
+ 00000050: 0a a1 f8 35 |...5 |
84
+
85
+ ### wbStego encrypted
86
+
87
+ # zsteg wbstego/wbsteg_blowfish_pass_1.bmp 1b,lsb,bY -v
88
+
89
+ 1b,lsb,bY .. <wbStego size=26, data="\rC\xF5\xBF#\xFF[6\e\xB3"..., even=false, hdr="\x01", enc="Blowfish">
90
+ 00000000: 1a 00 00 00 ff 01 01 0d 43 f5 bf 23 ff 5b 36 1b |........C..#.[6.|
91
+ 00000010: b3 17 42 4a 3f ba eb c7 ee 9c d7 7a 2b |..BJ?......z+ |
92
+
93
+ ### zlib
94
+
95
+ # zsteg ndh2k12_sp113.bmp -b 1 -o yx -v
42
96
 
97
+ 1b,rgb,lsb,yx .. zlib: data="%PDF-1.4\n%\xC3\xA4\xC3\xBC\xC3\xB6\xC3\x9F\n2 0 obj\n<</Length 3 0 R/Filter/FlateDecode>>\nstream\nx\x9C\x8DT\xC9n\xDB@\f\xBD\xCFW\xF0\x1C \x13\x92\xB3\x03\x86\x80\xC8K\xD1\xDE\\\b\xE8\xA1\xE8)K\x8B\xA2n\x91\\\xF2\xFB!5Zl\xD5v\v\x01\xD4\x90C\xBE\xF7\x86\x1A\n-\xC1\x9By\x01\x94'\x94`=d\xCF\xF0\xFA\x04_n\xE0\xF7\x10Gx\xFDn\xDA\xCE\xB0\x8F6\x80s$Y\xDD#\xDC\xED\b\x1CC\xF7\xBCBBF\x87^\xDE\xA1\xE9~\x9Amg\xF6\x8BZ\xCAYj", offset=4
98
+ 00000000: 00 02 eb 9b 78 9c d4 b9 65 54 24 cc 92 36 58 b8 |....x...eT$..6X.|
99
+ 00000010: d3 68 e3 ee ee 4e e3 ee ee 0e 85 bb 3b dd 68 23 |.h...N......;.h#|
100
+ 00000020: 8d bb bb bb 3b 8d bb bb 3b 34 ee 6e 1f ef 7b ef |....;...;4.n..{.|
101
+ 00000030: 9d 3b b3 e7 cc 9e d9 3d df 9e dd cd 8a 1f 99 19 |.;.....=........|
102
+ 00000040: 99 55 11 99 4f 58 25 99 82 88 18 1d 13 3d 2b 2c |.U..OX%......=+,|
103
+ 00000050: 59 6f 7e 6f 7b 6f 63 6f 16 2c 33 21 23 a1 9d 91 |Yo~o{oco.,3!#...|
104
+ 00000060: 25 2c 2f 2f 83 0c d0 d6 cc d9 9c 90 e5 73 46 89 |%,//.........sF.|
105
+ 00000070: 41 cc c2 da 19 e8 c8 20 66 6d e8 0c 14 01 1a db |A...... fm......|
106
+ 00000080: 99 00 f9 f8 60 9d 9c 1d 81 86 36 b0 ee e9 bf 54 |....`.....6....T|
107
+ 00000090: 86 6d 57 05 e0 3b 26 d5 2f 71 09 51 63 eb c0 82 |.mW..;&./q.Qc...|
108
+ 000000a0: bf 0f 49 4f 6f e8 40 ff c9 f9 43 25 1d 9e 6b 1b |..IOo.@...C%..k.|
109
+ 000000b0: a3 73 fd 42 c4 a6 65 3d ef 0a 07 32 17 2d dc f9 |.s.B..e=...2.-..|
110
+ 000000c0: 10 8c 0d 4b d7 9d e6 01 12 4f 11 6f f0 cd 64 f2 |...K.....O.o..d.|
111
+ 000000d0: f2 19 5c df 76 eb 01 49 dc fd cd 76 65 a2 3a 8a |..\.v..I...ve.:.|
112
+ 000000e0: fd bb 13 a9 e6 3a c9 da 19 34 ae f0 43 bb 90 90 |.....:...4..C...|
113
+ 000000f0: 58 88 de 46 ce 91 6f aa 8d d9 7d b8 d6 88 a6 65 |X..F..o...}....e|
43
114
 
44
115
  License
45
116
  -------
data/README.md.tpl CHANGED
@@ -18,6 +18,7 @@ Detects:
18
18
  * zlib-compressed data
19
19
  * [OpenStego](http://openstego.sourceforge.net/)
20
20
  * [Camouflage 1.2.1](http://camouflage.unfiction.com/)
21
+ * [LSB with The Eratosthenes set](http://wiki.cedricbonhomme.org/security:steganography)
21
22
 
22
23
 
23
24
  Usage
@@ -25,6 +26,28 @@ Usage
25
26
 
26
27
  % zsteg -h
27
28
 
29
+ Examples
30
+ --------
31
+
32
+ ### Simple LSB
33
+
34
+ % zsteg flower_rgb3.png
35
+
36
+ ### Multi-result file
37
+
38
+ % zsteg cats.png
39
+
40
+ ### wbStego even distributed
41
+
42
+ % zsteg wbstego/wbsteg_noenc_even.bmp 1b,lsb,bY -v
43
+
44
+ ### wbStego encrypted
45
+
46
+ % zsteg wbstego/wbsteg_blowfish_pass_1.bmp 1b,lsb,bY -v
47
+
48
+ ### zlib
49
+
50
+ % zsteg ndh2k12_sp113.bmp -b 1 -o yx -v
28
51
 
29
52
  License
30
53
  -------
data/Rakefile CHANGED
@@ -23,7 +23,8 @@ Jeweler::Tasks.new do |gem|
23
23
  gem.authors = ["Andrey \"Zed\" Zaikin"]
24
24
  #gem.executables = %w'zsteg'
25
25
  gem.files.include "lib/**/*.rb"
26
- gem.files.include "bin/zsteg"
26
+ gem.files.include "bin/*"
27
+ gem.files.exclude "samples/*"
27
28
  # dependencies defined in Gemfile
28
29
  end
29
30
  Jeweler::RubygemsDotOrgTasks.new
@@ -53,9 +54,10 @@ task :readme do
53
54
  puts "[.] #{cmd} ..."
54
55
  r = " # #{cmd}\n\n"
55
56
  cmd.sub! /^zsteg/,"../bin/zsteg"
56
- lines = `#{cmd}`.sub(/\A\n+/m,'').sub(/\s+\Z/,'').split("\n")
57
+ lines = `#{cmd}`.sub(/\A\n+/m,'').split("\n")
58
+ lines.map!{ |l| l.split("\r").last } # emulate CR's
57
59
  lines = lines[0,25] + ['...'] if lines.size > 50
58
- r << lines.map{|x| " #{x}"}.join("\n")
60
+ r << lines.map{|x| " #{x}"}.join("\n").rstrip
59
61
  r << "\n"
60
62
  end
61
63
  Dir.chdir 'samples'
data/TODO CHANGED
@@ -1,9 +1,12 @@
1
1
  [ ] make 'extract' cmd stream its data directly to stdout
2
2
  [ ] gzip
3
- [*] wbStego
3
+ [ ] wbStego: 4bpp/8bpp BMP
4
4
  [ ] openstego
5
5
  [ ] tmp/steg*/*.bmp
6
6
  [ ] 4bpp/8bpp BMP
7
+ [ ] http://search.cpan.org/~nwclark/Acme-Steganography-Image-Png-0.06/Png.pm
8
+ [ ] http://registry.gimp.org/node/25988 - GIMP stego plugin
9
+ [ ] zsteg-mask: normalize all to white
7
10
 
8
11
  [+] auto pixel order for BMP
9
12
  [+] BMP
@@ -11,4 +14,4 @@
11
14
 
12
15
  [ ] detect AES from http://punkroy.drque.net/PNG_Steganography/Steganography5.php
13
16
  [?] http://tobyinkster.co.uk/article/steg-encode/
14
- [ ] Sieve of Eratosthenes: http://wiki.cedricbonhomme.org/security:steganography
17
+ [+] Sieve of Eratosthenes: http://wiki.cedricbonhomme.org/security:steganography
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.0.1
data/bin/zsteg-mask ADDED
@@ -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/mask_cli'
6
+
7
+ ZSteg::MaskCLI.new.run
@@ -2,17 +2,60 @@ module ZSteg
2
2
  class Checker
3
3
  module WBStego
4
4
 
5
- class Result < IOStruct.new "a3a3a*", :size, :ext, :data, :even
5
+ ENCRYPTIONS = [
6
+ nil, # 0
7
+ "Blowfish", # 1
8
+ "Twofish", # 2
9
+ "CAST128", # 3
10
+ "Rijndael", # 4
11
+ ]
12
+
13
+ class Result < IOStruct.new "a3a3a*", :size, :ext, :data, :even, :hdr, :enc, :mix, :controlbyte
14
+ attr_accessor :color
15
+
6
16
  def initialize *args
7
17
  super
8
18
  if self.size.is_a?(String)
9
19
  self.size = (self.size[0,3] + "\x00").unpack('V')[0]
10
20
  end
11
- self.even = false if self.even.nil?
21
+ self.even ||= false
22
+ #self.encrypted ||= false
23
+ if ext[0,2] == "\x00\xff"
24
+ # wbStego 4.x header
25
+ self.hdr = data[0,ext[2].ord] # 3rd ext byte is hdr len
26
+ self.data = data[hdr.size..-1]
27
+ self.ext = nil # encrypted files have no ext
28
+ self.enc = ENCRYPTIONS[hdr[0].ord] || "unknown ##{hdr[0].ord}"
29
+ elsif (cb=ext[0].ord) & 0xc0 != 0
30
+ # wbStego 2.x/3.x controlbyte
31
+ self.controlbyte = ext[0]
32
+ self.data = ext[1..-1] + data
33
+ self.ext = nil # have ext but its encrypted/mixed with data
34
+ self.mix = true if cb & 0x40 != 0
35
+ self.enc = "wbStego 2.x/3.x" if cb & 0x80 != 0
36
+ end
12
37
  end
13
38
 
14
39
  def to_s
15
- inspect.sub("#<struct #{self.class.to_s}", "<wbStego").red
40
+ s = inspect.
41
+ sub("#<struct #{self.class.to_s}", "<wbStego").
42
+ gsub(/, \w+=nil/,'')
43
+
44
+ color = @color
45
+
46
+ if ext && !valid_ext?
47
+ s.sub!(data.inspect, data[0,10].inspect+"...") if data && data.size>13
48
+ color ||= :gray
49
+ else
50
+ s.sub!(data.inspect, data[0,10].inspect+"...") if data && data.size>13 && enc
51
+ color ||= :bright_red
52
+ end
53
+ s.send(color)
54
+ end
55
+
56
+ # XXX require that file extension be 7-bit ASCII
57
+ def valid_ext?
58
+ ext =~ /\A[\x20-\x7e]+\Z/ && !ext['*'] && !ext['?']
16
59
  end
17
60
  end
18
61
 
@@ -49,8 +92,15 @@ module ZSteg
49
92
  def check data, params = {}
50
93
  return if data.size < 4
51
94
  return if params[:bit_order] != :lsb
95
+
96
+ force_color = nil
97
+
52
98
  if params[:image].format == :bmp
53
99
  return if params[:order] !~ /b/i
100
+ else
101
+ # PNG
102
+ return if Array(params[:channels]).join != 'bgr'
103
+ force_color = :gray if params[:order] != 'xY'
54
104
  end
55
105
 
56
106
  size1 = (data[0,3] + "\x00").unpack('V')[0]
@@ -61,9 +111,16 @@ module ZSteg
61
111
  params[:max_hidden_size]
62
112
  end
63
113
  return if size1 == 0 || size1 > avail_size
114
+
115
+ # check if too many zeroes, prevent false positive
116
+ nzeroes = data[3..-1].count("\x00")
117
+ return if nzeroes > 10 && data.size-3-nzeroes < 4
118
+
119
+ result = nil
120
+
64
121
  size2 = (data[3,3] + "\x00").unpack('V')[0]
65
122
  # p [size1, size2, avail_size]
66
- if size2 < avail_size
123
+ if size2 < avail_size && size2 > 0
67
124
  spacing = 1.0*avail_size/(size2+5) - 1
68
125
  # puts "[d] spacing=#{spacing}"
69
126
  if spacing > 0
@@ -77,21 +134,19 @@ module ZSteg
77
134
  error -= 1
78
135
  end
79
136
  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)
137
+ if r.size > 4
138
+ ext = r[0,3]
139
+ result = Result.new(size2, ext, r[3..-1], true)
140
+ end
84
141
  end
85
142
  end
86
143
  # no even distribution
87
- return unless valid_ext?(data[3,3])
88
- return Result.read(data)
144
+ #return unless valid_ext?(data[3,3])
145
+ result ||= Result.read(data)
146
+ result.color = force_color if result && force_color
147
+ result
89
148
  end
90
149
 
91
- # XXX require that file extension be 7-bit ASCII
92
- def valid_ext? ext
93
- ext =~ /\A[\x20-\x7e]+\Z/
94
- end
95
150
  end
96
151
  end
97
152
  end
data/lib/zsteg/checker.rb CHANGED
@@ -1,16 +1,21 @@
1
1
  require 'stringio'
2
2
  require 'zlib'
3
+ require 'set'
3
4
 
4
5
  module ZSteg
5
6
  class Checker
6
- attr_accessor :params, :channels, :verbose
7
+ attr_accessor :params, :channels, :verbose, :results
7
8
 
8
- MIN_TEXT_LENGTH = 8
9
+ MIN_TEXT_LENGTH = 8
10
+ MIN_WHOLETEXT_LENGTH = 6 # when entire data is a text
11
+ DEFAULT_BITS = [1,2,3,4]
12
+ DEFAULT_ORDER = 'auto'
13
+ DEFAULT_LIMIT = 256 # number of checked bytes, 0 = no limit
9
14
 
10
15
  # image can be either filename or ZPNG::Image
11
16
  def initialize image, params = {}
12
17
  @params = params
13
- @cache = {}
18
+ @cache = {}; @wastitles = Set.new
14
19
  @image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
15
20
  @extractor = Extractor.new(@image, params)
16
21
  @channels = params[:channels] ||
@@ -19,32 +24,67 @@ module ZSteg
19
24
  else
20
25
  %w'r g b rgb bgr'
21
26
  end
22
- @verbose = params[:verbose] || 0
27
+ @verbose = params[:verbose] || -2
23
28
  @file_cmd = FileCmd.new
29
+ @results = []
30
+
31
+ @params[:bits] ||= DEFAULT_BITS
32
+ @params[:order] ||= DEFAULT_ORDER
33
+ @params[:limit] ||= DEFAULT_LIMIT
34
+ end
35
+
36
+ private
37
+
38
+ # catch Kernel#print for easier verbosity handling
39
+ def print *args
40
+ Kernel.print(*args) if @verbose >= 0
41
+ end
42
+
43
+ # catch Kernel#printf for easier verbosity handling
44
+ def printf *args
45
+ Kernel.printf(*args) if @verbose >= 0
46
+ end
47
+
48
+ # catch Kernel#puts for easier verbosity handling
49
+ def puts *args
50
+ Kernel.puts(*args) if @verbose >= 0
24
51
  end
25
52
 
53
+ public
54
+
26
55
  def check
27
56
  @found_anything = false
28
57
  @file_cmd.start!
29
58
 
30
59
  check_extradata
31
60
  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'
61
+ check_imagedata
62
+
63
+ if @image.format == :bmp
64
+ case params[:order].to_s.downcase
65
+ when /all/
66
+ params[:order] = %w'bY xY xy yx XY YX Xy yX Yx'
67
+ when /auto/
68
+ params[:order] = %w'bY xY'
69
+ end
70
+ else
71
+ case params[:order].to_s.downcase
72
+ when /all/
73
+ params[:order] = %w'xy yx XY YX Xy yX xY Yx'
74
+ when /auto/
75
+ params[:order] = 'xy'
76
+ end
38
77
  end
39
78
 
40
79
  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 )
80
+ (params[:prime] == :all ? [false,true] : [params[:prime]]).each do |prime|
81
+ Array(params[:bits]).uniq.each do |bits|
82
+ p1 = @params.merge :bits => bits, :order => order, :prime => prime
83
+ if order[/b/i]
84
+ # byte iterator does not need channels
85
+ check_channels nil, p1
86
+ else
87
+ channels.each{ |c| check_channels c, p1 }
48
88
  end
49
89
  end
50
90
  end
@@ -55,15 +95,22 @@ module ZSteg
55
95
  else
56
96
  puts "\r[=] nothing :(" + " "*20 # line cleanup
57
97
  end
98
+
99
+ @results
58
100
  ensure
59
101
  @file_cmd.stop!
60
102
  end
61
103
 
104
+ def check_imagedata
105
+ h = { :title => "imagedata", :show_title => true }
106
+ process_result @image.imagedata, h
107
+ end
108
+
62
109
  def check_extradata
63
110
  if @image.extradata
64
111
  @found_anything = true
65
112
  title = "data after IEND"
66
- show_title title, :red
113
+ show_title title, :bright_red
67
114
  process_result @image.extradata, :special => true, :title => title
68
115
  end
69
116
  end
@@ -83,24 +130,73 @@ module ZSteg
83
130
  return
84
131
  end
85
132
 
86
- title = ["#{params[:bits]}b",channels,params[:bit_order],params[:order]].compact.join(',')
87
- show_title title
88
-
89
133
  p1 = params.clone
90
- p1.delete :channel
91
- p1[:title] = title
92
134
 
135
+ # number of bits
136
+ # equals to params[:bits] if in range 1..8
137
+ # otherwise equals to number of 1's, like 0b1000_0001
138
+ nbits = p1[:bits] <= 8 ? p1[:bits] : (p1[:bits]&0xff).to_s(2).count("1")
139
+
140
+ show_bits = true
141
+ # channels is a String
93
142
  if channels
94
- p1[:channels] = channels.split('')
95
- @max_hidden_size = p1[:channels].size*@image.width
143
+ p1[:channels] =
144
+ if channels[1] && channels[1] =~ /\A\d\Z/
145
+ # 'r3g2b3'
146
+ a=[]
147
+ cbits = 0
148
+ (channels.size/2).times do |i|
149
+ a << (t=channels[i*2,2])
150
+ cbits += t[1].to_i
151
+ end
152
+ show_bits = false
153
+ @max_hidden_size = cbits * @image.width
154
+ a
155
+ else
156
+ # 'rgb'
157
+ a = channels.chars.to_a
158
+ @max_hidden_size = a.size * @image.width * nbits
159
+ a
160
+ end
161
+ # p1[:channels] is an Array
96
162
  elsif params[:order] =~ /b/i
97
163
  # byte extractor
98
- @max_hidden_size = @image.scanlines[0].decoded_bytes.size
164
+ @max_hidden_size = @image.scanlines[0].decoded_bytes.size * nbits
99
165
  else
100
166
  raise "invalid params #{params.inspect}"
101
167
  end
102
- @max_hidden_size *= p1[:bits]*@image.height/8
168
+ @max_hidden_size *= @image.height/8
169
+
170
+ bits_tag =
171
+ if show_bits
172
+ if params[:bits] > 0x100
173
+ if params[:bits].to_s(2) =~ /(1{1,8})$/
174
+ # mask => number of bits
175
+ "b#{$1.size}"
176
+ else
177
+ # mask
178
+ "b#{(params[:bits]&0xff).to_s(2)}"
179
+ end
180
+ else
181
+ # number of bits
182
+ "b#{params[:bits]}"
183
+ end
184
+ end
185
+
186
+ title = [
187
+ bits_tag,
188
+ channels,
189
+ params[:bit_order],
190
+ params[:order],
191
+ params[:prime] ? 'prime' : nil
192
+ ].compact.join(',')
103
193
 
194
+ return if @wastitles.include?(title)
195
+ @wastitles << title
196
+
197
+ show_title title
198
+
199
+ p1[:title] = title
104
200
  data = @extractor.extract p1
105
201
 
106
202
  @need_cr = !process_result(data, p1) # carriage return needed?
@@ -108,7 +204,7 @@ module ZSteg
108
204
  end
109
205
 
110
206
  def show_title title, color = :gray
111
- printf "\r[.] %-14s.. ".send(color), title
207
+ printf "\r%-20s.. ".send(color), title
112
208
  $stdout.flush
113
209
  end
114
210
 
@@ -129,12 +225,15 @@ module ZSteg
129
225
  # TODO: store hash of data for large datas
130
226
  @cache[data] = params[:title]
131
227
 
132
- result = data2result data, params
228
+ if result = data2result(data, params)
229
+ @results << result
230
+ end
133
231
 
134
232
  case verbose
135
233
  when -999..0
136
234
  # verbosity=0: only show result if anything interesting found
137
235
  if result && !result.is_a?(Result::OneChar)
236
+ show_title params[:title] if params[:show_title]
138
237
  puts result
139
238
  return true
140
239
  else
@@ -146,6 +245,7 @@ module ZSteg
146
245
  end
147
246
 
148
247
  # verbosity>1: always show hexdump
248
+ show_title params[:title] if params[:show_title]
149
249
 
150
250
  if params[:special]
151
251
  puts result.is_a?(Result::PartialText) ? nil : result
@@ -183,24 +283,27 @@ module ZSteg
183
283
  end
184
284
  end
185
285
 
186
- if data =~ /\A[\x20-\x7e\r\n\t]+\Z/
286
+ if data.size >= MIN_WHOLETEXT_LENGTH && data =~ /\A[\x20-\x7e\r\n\t]+\Z/
187
287
  # whole ASCII
188
288
  return Result::WholeText.new(data, 0)
189
289
  end
190
290
 
191
- if r = @file_cmd.check_data(data)
192
- return Result::FileCmd.new(r, data)
291
+ # XXX TODO refactor params hack
292
+ if !params.key?(:no_check_file) && (r = @file_cmd.data2result(data))
293
+ return r
193
294
  end
194
295
 
296
+ # try to find zlib
195
297
  # http://blog.w3challs.com/index.php?post/2012/03/25/NDH2k12-Prequals-We-are-looking-for-a-real-hacker-Wallpaper-image
196
298
  # http://blog.w3challs.com/public/ndh2k12_prequalls/sp113.bmp
197
- if idx = data.index(/\x78[\x9c\xda\x01]/)
299
+ # XXX TODO refactor params hack
300
+ if !params.key?(:no_check_zlib) && (idx = data.index(/\x78[\x9c\xda\x01]/))
198
301
  begin
199
302
  # x = Zlib::Inflate.inflate(data[idx,4096])
200
303
  zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
201
304
  x = zi.inflate data[idx..-1]
202
305
  # decompress OK
203
- return Result::Zlib.new x, idx
306
+ return Result::Zlib.new x, idx if x.size > 2
204
307
  rescue Zlib::BufError
205
308
  # tried to decompress, but got EOF - need more data
206
309
  return Result::Zlib.new x, idx