zsteg 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -7
- data/Gemfile.lock +2 -4
- data/README.md +72 -1
- data/README.md.tpl +23 -0
- data/Rakefile +5 -3
- data/TODO +5 -2
- data/VERSION +1 -1
- data/bin/zsteg-mask +7 -0
- data/lib/zsteg/checker/wbstego.rb +69 -14
- data/lib/zsteg/checker.rb +137 -34
- data/lib/zsteg/cli.rb +92 -35
- data/lib/zsteg/extractor/byte_extractor.rb +36 -21
- data/lib/zsteg/extractor/color_extractor.rb +68 -34
- data/lib/zsteg/extractor.rb +36 -1
- data/lib/zsteg/file_cmd.rb +64 -1
- data/lib/zsteg/mask_cli.rb +268 -0
- data/lib/zsteg/masker.rb +52 -0
- data/lib/zsteg/result.rb +27 -32
- data/lib/zsteg.rb +2 -0
- data/samples/hackquest/crypt.bmp +0 -0
- data/samples/hackquest/square.bmp +0 -0
- data/samples/wbstego/wbsteg_blowfish_pass_1.bmp +0 -0
- data/samples/wbstego/wbsteg_cast128_pass_1.bmp +0 -0
- data/samples/wbstego/wbsteg_enc_pass_pass.bmp +0 -0
- data/samples/wbstego/wbsteg_enc_pass_pass_even.bmp +0 -0
- data/samples/wbstego/wbsteg_mix_pass_1.bmp +0 -0
- data/samples/wbstego/wbsteg_mix_pass_1_even.bmp +0 -0
- data/samples/wbstego/wbsteg_mix_pass_foobar.bmp +0 -0
- data/samples/wbstego/wbsteg_mix_pass_pass.bmp +0 -0
- data/samples/wbstego/wbsteg_mixenc_pass_pass_even.bmp +0 -0
- data/samples/{wbsteg_noenc.bmp → wbstego/wbsteg_noenc.bmp} +0 -0
- data/samples/wbstego/wbsteg_noenc.png +0 -0
- data/samples/wbstego/wbsteg_noenc_.bmp +0 -0
- data/samples/{wbsteg_noenc_17.bmp → wbstego/wbsteg_noenc_17.bmp} +0 -0
- data/samples/wbstego/wbsteg_noenc__.bmp +0 -0
- data/samples/{wbsteg_noenc_even.bmp → wbstego/wbsteg_noenc_even.bmp} +0 -0
- data/samples/wbstego/wbsteg_noenc_even2.bmp +0 -0
- data/samples/{wbsteg_noenc_even_17.bmp → wbstego/wbsteg_noenc_even_17.bmp} +0 -0
- data/samples/wbstego/wbsteg_noenc_even_17_.bmp +0 -0
- data/samples/wbstego/wbsteg_noenc_ext_ABC.bmp +0 -0
- data/samples/wbstego/wbsteg_rijndael_pass_1.bmp +0 -0
- data/samples/wbstego/wbsteg_rijndael_pass_pass.bmp +0 -0
- data/samples/wbstego/wbsteg_rijndael_pass_pass_even.bmp +0 -0
- data/samples/wbstego/wbsteg_twofish_pass_1.bmp +0 -0
- data/samples/wechall/5ZMGcCLxpcpsru03.g00000010.png +0 -0
- data/samples/wechall/5ZMGcCLxpcpsru03.png +0 -0
- data/samples/wechall/stegano1.bmp +0 -0
- data/spec/checker_spec.rb +47 -0
- data/spec/easybmp_spec.rb +9 -0
- data/spec/hackquest_spec.rb +18 -0
- data/spec/mask_spec.rb +14 -0
- data/spec/polictf2012_spec.rb +48 -0
- data/spec/prime_spec.rb +9 -0
- data/spec/r3g2b3_spec.rb +9 -0
- data/spec/spec_helper.rb +21 -4
- data/spec/wbstego_spec.rb +21 -3
- data/spec/wechall_spec.rb +26 -0
- data/tmp/.keep +0 -0
- data/zsteg.gemspec +121 -0
- metadata +47 -43
- data/samples/06_enc.png +0 -0
- data/samples/Code.png +0 -0
- data/samples/README +0 -4
- data/samples/camouflage-password.png +0 -0
- data/samples/camouflage.png +0 -0
- data/samples/cats.png +0 -0
- data/samples/flower.png +0 -0
- data/samples/flower_rgb1.png +0 -0
- data/samples/flower_rgb2.png +0 -0
- data/samples/flower_rgb3.png +0 -0
- data/samples/flower_rgb4.png +0 -0
- data/samples/flower_rgb5.png +0 -0
- data/samples/flower_rgb6.png +0 -0
- data/samples/montenach-enc.png +0 -0
- data/samples/ndh2k12_sp113.bmp.7z +0 -0
- data/samples/openstego_q2.png +0 -0
- data/samples/openstego_send.png +0 -0
- data/samples/stg300.png +0 -0
data/lib/zsteg/file_cmd.rb
CHANGED
@@ -8,6 +8,7 @@ module ZSteg
|
|
8
8
|
'data',
|
9
9
|
'empty',
|
10
10
|
'Sendmail frozen configuration',
|
11
|
+
'DBase 3 data file',
|
11
12
|
'DOS executable',
|
12
13
|
'Dyalog APL',
|
13
14
|
'8086 relocatable',
|
@@ -18,9 +19,50 @@ module ZSteg
|
|
18
19
|
'very short file',
|
19
20
|
'International EBCDIC text',
|
20
21
|
'lif file',
|
21
|
-
'AmigaOS bitmap font'
|
22
|
+
'AmigaOS bitmap font',
|
23
|
+
'a python script text executable' # common false positive
|
22
24
|
]
|
23
25
|
|
26
|
+
MIN_DATA_SIZE = 5
|
27
|
+
|
28
|
+
class Result < Struct.new(:title, :data)
|
29
|
+
COLORMAP_TEXT = {
|
30
|
+
/DBase 3 data/i => :gray
|
31
|
+
}
|
32
|
+
COLORMAP_WORD = {
|
33
|
+
/bitmap|jpeg|pdf|zip|rar|7-?z/i => :bright_red,
|
34
|
+
}
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
if title[/UTF-8 Unicode text/i]
|
38
|
+
begin
|
39
|
+
t = data.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")
|
40
|
+
rescue
|
41
|
+
t = data.force_encoding('binary')
|
42
|
+
end
|
43
|
+
return "utf8: " + t
|
44
|
+
end
|
45
|
+
COLORMAP_TEXT.each do |re,color|
|
46
|
+
return colorize(color) if title[re]
|
47
|
+
end
|
48
|
+
title.downcase.split.each do |word|
|
49
|
+
COLORMAP_WORD.each do |re,color|
|
50
|
+
return colorize(color) if title.index(re) == 0
|
51
|
+
end
|
52
|
+
end
|
53
|
+
colorize(:yellow)
|
54
|
+
end
|
55
|
+
|
56
|
+
def colorize color
|
57
|
+
if color == :gray
|
58
|
+
# gray whole string
|
59
|
+
"file: #{title}".send(color)
|
60
|
+
else
|
61
|
+
"file: " + title.send(color)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
24
66
|
def start!
|
25
67
|
@stdin, @stdout, @stderr, @wait_thr = Open3.popen3("file -n -b -f -")
|
26
68
|
end
|
@@ -39,6 +81,27 @@ module ZSteg
|
|
39
81
|
check_file @tempfile.path
|
40
82
|
end
|
41
83
|
|
84
|
+
# checks data and resurns Result, if any
|
85
|
+
def data2result data
|
86
|
+
return if data.size < MIN_DATA_SIZE
|
87
|
+
|
88
|
+
title = check_data data
|
89
|
+
return unless title
|
90
|
+
|
91
|
+
if title[/UTF-8 Unicode text/i]
|
92
|
+
begin
|
93
|
+
t = data.force_encoding("UTF-8")
|
94
|
+
rescue
|
95
|
+
t = data.force_encoding('binary')
|
96
|
+
end
|
97
|
+
if t.size >= Checker::MIN_TEXT_LENGTH
|
98
|
+
ZSteg::Result::UnicodeText.new(t,0)
|
99
|
+
end
|
100
|
+
else
|
101
|
+
Result.new(title,data)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
42
105
|
def stop!
|
43
106
|
@stdin.close
|
44
107
|
@stdout.close
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'set'
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module ZSteg
|
6
|
+
class MaskCLI
|
7
|
+
DEFAULT_ACTIONS = %w'mask'
|
8
|
+
|
9
|
+
COMMON_MASKS = [
|
10
|
+
0b0000_0001, 0b0000_0011, 0b0000_0111, 0b0000_1111,
|
11
|
+
0b0000_0010, 0b0000_0100, 0b0000_1000,
|
12
|
+
0b0001_0000, 0b0010_0000, 0b0100_0000, 0b1000_0000,
|
13
|
+
]
|
14
|
+
|
15
|
+
CHANNELS = [:r, :g, :b, :a]
|
16
|
+
|
17
|
+
def initialize argv = ARGV
|
18
|
+
@argv = argv
|
19
|
+
@wasfiles = Set.new
|
20
|
+
@cache = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
@actions = []
|
25
|
+
@options = {
|
26
|
+
:verbose => 0,
|
27
|
+
:masks => Hash.new{|k,v| k[v] = [] },
|
28
|
+
:normalize => true
|
29
|
+
}
|
30
|
+
optparser = OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: zsteg-mask [options] filename.png [param_string]"
|
32
|
+
opts.separator ""
|
33
|
+
|
34
|
+
opts.on("-m", "--mask M", "apply mask to all channels",
|
35
|
+
"mask: 0-255 OR 0x00-0xff OR 00000000-11111111",
|
36
|
+
"OR 'all' for all common masks"
|
37
|
+
){ |x| @options[:masks][:all] << parse_mask(x) }
|
38
|
+
|
39
|
+
opts.on("-R", "--red M", "red channel mask"){ |x|
|
40
|
+
@options[:masks][:r] << parse_mask(x) }
|
41
|
+
|
42
|
+
opts.on("-G", "--green M", "green channel mask"){ |x|
|
43
|
+
@options[:masks][:g] << parse_mask(x) }
|
44
|
+
|
45
|
+
opts.on("-B", "--blue M", "blue channel mask"){ |x|
|
46
|
+
@options[:masks][:b] << parse_mask(x) }
|
47
|
+
|
48
|
+
opts.on("-A", "--alpha M", "alpha channel mask"){ |x|
|
49
|
+
@options[:masks][:a] << parse_mask(x) }
|
50
|
+
|
51
|
+
opts.separator ""
|
52
|
+
|
53
|
+
opts.on "-a", "--all", "try all common masks (default)" do
|
54
|
+
@options[:try_all] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
opts.separator ""
|
58
|
+
|
59
|
+
opts.on "-N", "--[no-]normalize", "normalize color value after applying mask",
|
60
|
+
"(default: normalize)" do |x|
|
61
|
+
@options[:normalize] = x
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on "-O", "--outfile FILENAME", "output single result to specified file" do |x|
|
65
|
+
@options[:outfile] = x
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on "-D", "--dir DIRNAME", "output multiple results to specified dir" do |x|
|
69
|
+
@options[:dir] = x
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.separator ""
|
73
|
+
opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
|
74
|
+
@options[:verbose] += 1
|
75
|
+
end
|
76
|
+
opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
|
77
|
+
@options[:verbose] -= 1
|
78
|
+
end
|
79
|
+
opts.on "-C", "--[no-]color", "Force (or disable) color output (default: auto)" do |x|
|
80
|
+
Sickill::Rainbow.enabled = x
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if (argv = optparser.parse(@argv)).empty?
|
85
|
+
puts optparser.help
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
# default :all mask if none specified
|
90
|
+
if @options[:masks].empty?
|
91
|
+
@options[:try_all] = true
|
92
|
+
end
|
93
|
+
|
94
|
+
@actions = DEFAULT_ACTIONS if @actions.empty?
|
95
|
+
|
96
|
+
argv.each do |arg|
|
97
|
+
if arg[','] && !File.exist?(arg)
|
98
|
+
@options.merge!(decode_param_string(arg))
|
99
|
+
argv.delete arg
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
argv.each_with_index do |fname,idx|
|
104
|
+
if argv.size > 1 && @options[:verbose] >= 0
|
105
|
+
puts if idx > 0
|
106
|
+
puts "[.] #{fname}".green
|
107
|
+
end
|
108
|
+
next unless @image=load_image(@fname=fname)
|
109
|
+
|
110
|
+
@actions.each do |action|
|
111
|
+
if action.is_a?(Array)
|
112
|
+
self.send(*action) if self.respond_to?(action.first)
|
113
|
+
else
|
114
|
+
self.send(action) if self.respond_to?(action)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
rescue Errno::EPIPE
|
119
|
+
# output interrupt, f.ex. when piping output to a 'head' command
|
120
|
+
# prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_mask x
|
124
|
+
case x
|
125
|
+
when /0x/i
|
126
|
+
x.to_i(16)
|
127
|
+
when /^[01]{8}$/
|
128
|
+
x.to_i(2)
|
129
|
+
when /^\d{1,3}$/
|
130
|
+
x.to_i
|
131
|
+
when /^all$/
|
132
|
+
COMMON_MASKS
|
133
|
+
else raise "invalid mask #{x.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def load_image fname
|
138
|
+
if File.directory?(fname)
|
139
|
+
puts "[?] #{fname} is a directory".yellow
|
140
|
+
else
|
141
|
+
ZPNG::Image.load(fname)
|
142
|
+
end
|
143
|
+
rescue ZPNG::Exception, Errno::ENOENT
|
144
|
+
puts "[!] #{$!.inspect}".red
|
145
|
+
end
|
146
|
+
|
147
|
+
###########################################################################
|
148
|
+
# actions
|
149
|
+
|
150
|
+
def mask
|
151
|
+
masks = @options[:masks]
|
152
|
+
masks.each{ |k,v| v.flatten!; v.uniq! }
|
153
|
+
|
154
|
+
if @options[:try_all]
|
155
|
+
# try all common masks
|
156
|
+
masks = masks[:all] || []
|
157
|
+
masks = COMMON_MASKS if masks.empty?
|
158
|
+
masks.each{ |x| run_masker x,x,x,x }
|
159
|
+
masks.each{ |x| run_masker x,0,0,0xff }
|
160
|
+
masks.each{ |x| run_masker 0,x,0,0xff }
|
161
|
+
masks.each{ |x| run_masker 0,0,x,0xff }
|
162
|
+
if @image.alpha_used?
|
163
|
+
masks.each{ |x| run_masker 0,0,0,x }
|
164
|
+
end
|
165
|
+
|
166
|
+
elsif CHANNELS.all?{ |c| !masks[c] || masks[c].empty? }
|
167
|
+
# no specific channels
|
168
|
+
masks[:all].each do |x|
|
169
|
+
run_masker x,x,x,x
|
170
|
+
end
|
171
|
+
|
172
|
+
else
|
173
|
+
# specific channels
|
174
|
+
CHANNELS.each{ |x| masks[x] = [x==:a ? 0xff : 0] if !masks[x] || masks[x].empty? }
|
175
|
+
masks[:r].each do |r|
|
176
|
+
masks[:g].each do |g|
|
177
|
+
masks[:b].each do |b|
|
178
|
+
if @image.alpha_used?
|
179
|
+
masks[:a].each do |a|
|
180
|
+
run_masker r,g,b,a
|
181
|
+
end
|
182
|
+
else
|
183
|
+
run_masker r,g,b,0xff
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def _all_pixels_same img
|
194
|
+
sl0 = img.scanlines.first
|
195
|
+
return false if sl0.pixels.to_a.uniq.size != 1
|
196
|
+
|
197
|
+
db0 = sl0.decoded_bytes
|
198
|
+
img.scanlines[1..-1].each do |sl|
|
199
|
+
return false if sl.decoded_bytes != db0
|
200
|
+
end
|
201
|
+
true
|
202
|
+
end
|
203
|
+
|
204
|
+
def run_masker r,g,b,a
|
205
|
+
params = @options.dup
|
206
|
+
params[:masks] = params[:masks].merge( :r => r, :g => g, :b => b, :a => a)
|
207
|
+
fname,color = @options[:outfile],nil
|
208
|
+
fname,color = masks2fname(params[:masks]) unless fname
|
209
|
+
|
210
|
+
print "[.] #{fname.send(color||:to_s)} .. "
|
211
|
+
|
212
|
+
raise "already written to #{fname}" if @wasfiles.include?(fname)
|
213
|
+
@wasfiles << fname
|
214
|
+
|
215
|
+
dst = Masker.new(@image, params).mask
|
216
|
+
|
217
|
+
if _all_pixels_same(dst)
|
218
|
+
puts "all pixels = #{dst[0,0].inspect}".gray
|
219
|
+
return
|
220
|
+
end
|
221
|
+
|
222
|
+
data = dst.export
|
223
|
+
|
224
|
+
md5 = Digest::MD5.hexdigest(data)
|
225
|
+
if @cache[md5]
|
226
|
+
puts "same as #{File.basename(@cache[md5])}".gray
|
227
|
+
return
|
228
|
+
end
|
229
|
+
@cache[md5] = fname
|
230
|
+
|
231
|
+
File.open(fname, "wb"){ |f| f<<data }
|
232
|
+
printf "%6d bytes\n".green, File.size(fname)
|
233
|
+
end
|
234
|
+
|
235
|
+
def masks2fname masks
|
236
|
+
masks = masks.dup.delete_if{ |k,v| !CHANNELS.include?(k) }
|
237
|
+
bname = @fname.sub(/#{Regexp.escape(File.extname(@fname))}$/,'')
|
238
|
+
color = nil
|
239
|
+
raise "TODO" if masks.values.all?(&:nil?)
|
240
|
+
if masks.values.uniq.size == 1
|
241
|
+
tail = "%08b" % masks.values.first
|
242
|
+
else
|
243
|
+
a = []
|
244
|
+
masks.each do |c,mask|
|
245
|
+
a << "%s%08b" % [c,mask] if mask && mask != 0
|
246
|
+
end
|
247
|
+
raise "TODO" if a.empty?
|
248
|
+
a -= ['a11111111'] if a.size > 1 # fully opaque alpha is OK
|
249
|
+
if a.size == 1
|
250
|
+
color =
|
251
|
+
case a[0][0,1]
|
252
|
+
when 'r'; :red
|
253
|
+
when 'g'; :green
|
254
|
+
when 'b'; :blue
|
255
|
+
when 'a'; :gray
|
256
|
+
else nil
|
257
|
+
end
|
258
|
+
end
|
259
|
+
tail = a.join(".")
|
260
|
+
end
|
261
|
+
|
262
|
+
fname = [bname, tail, "png"].join('.')
|
263
|
+
fname = File.join(@options[:dir], File.basename(fname)) if @options[:dir]
|
264
|
+
[fname, color]
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
data/lib/zsteg/masker.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ZSteg
|
4
|
+
class Masker
|
5
|
+
def initialize image, params = {}
|
6
|
+
@src = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
|
7
|
+
@masks = params[:masks] || {}
|
8
|
+
@mask = params[:mask] || @masks[:all]
|
9
|
+
@normalize = params[:normalize]
|
10
|
+
[:r, :g, :b, :a].each{ |x| @masks[x] ||= @mask }
|
11
|
+
end
|
12
|
+
|
13
|
+
def mask params = {}
|
14
|
+
dst = ZPNG::Image.new :width => @src.width, :height => @src.height
|
15
|
+
rm, gm, bm, am = @masks[:r], @masks[:g], @masks[:b], @masks[:a]
|
16
|
+
rd, gd, bd, ad = rm==0?1:rm, gm==0?1:gm, bm==0?1:bm, am==0?1:am
|
17
|
+
# duplicate loops for performance reason
|
18
|
+
if @normalize
|
19
|
+
if rm == 0 && gm == 0 && bm == 0 && am != 0
|
20
|
+
# alpha2grayscale
|
21
|
+
@src.each_pixel do |c,x,y|
|
22
|
+
c.r = c.g = c.b = (c.a & am) * 255 / ad
|
23
|
+
c.a = 0xff
|
24
|
+
dst[x,y] = c
|
25
|
+
end
|
26
|
+
else
|
27
|
+
# normal operation
|
28
|
+
@src.each_pixel do |c,x,y|
|
29
|
+
#TODO: c.to_depth(8)
|
30
|
+
# further possible optimizations:
|
31
|
+
# a) precalculate (255 / Xm)
|
32
|
+
c.r = (c.r & rm) * 255 / rd
|
33
|
+
c.g = (c.g & gm) * 255 / gd
|
34
|
+
c.b = (c.b & bm) * 255 / bd
|
35
|
+
c.a = (c.a & am) * 255 / ad
|
36
|
+
dst[x,y] = c
|
37
|
+
end
|
38
|
+
end
|
39
|
+
else
|
40
|
+
@src.each_pixel do |c,x,y|
|
41
|
+
#TODO: c.to_depth(8)
|
42
|
+
c.r &= rm
|
43
|
+
c.g &= gm
|
44
|
+
c.b &= bm
|
45
|
+
c.a &= am
|
46
|
+
dst[x,y] = c
|
47
|
+
end
|
48
|
+
end
|
49
|
+
dst
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/zsteg/result.rb
CHANGED
@@ -17,13 +17,30 @@ module ZSteg
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def to_s
|
20
|
-
super.sub(/^<Result::/,'').sub(/>$/,'').
|
20
|
+
super.sub(/^<Result::/,'').sub(/>$/,'').bright_red
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
24
|
class Text < Struct.new(:text, :offset)
|
25
|
+
def one_char?
|
26
|
+
(text =~ /\A(.)\1+\Z/m) == 0
|
27
|
+
rescue # invalid byte sequence in UTF-8
|
28
|
+
text.chars.to_a.uniq.size == 1 # ~10x slower than regexp
|
29
|
+
end
|
30
|
+
|
25
31
|
def to_s
|
26
|
-
"text: ".gray +
|
32
|
+
"text: ".gray +
|
33
|
+
if one_char?
|
34
|
+
"[#{text[0].inspect} repeated #{text.size} times]".gray
|
35
|
+
elsif offset == 0
|
36
|
+
# first byte of data is also first char of text
|
37
|
+
text.inspect.bright_red
|
38
|
+
elsif text.size > 10 && text[' '] && text =~ /\A[a-z0-9 .,:!_-]+\Z/i
|
39
|
+
# text is ASCII with spaces
|
40
|
+
text.inspect.bright_red
|
41
|
+
else
|
42
|
+
text.inspect
|
43
|
+
end
|
27
44
|
end
|
28
45
|
end
|
29
46
|
|
@@ -33,9 +50,15 @@ module ZSteg
|
|
33
50
|
# part of data is text
|
34
51
|
class PartialText < Text; end
|
35
52
|
|
53
|
+
# unicode text
|
54
|
+
class UnicodeText < Text; end
|
55
|
+
|
36
56
|
class Zlib < Struct.new(:data, :offset)
|
57
|
+
MAX_SHOW_SIZE = 100
|
37
58
|
def to_s
|
38
|
-
|
59
|
+
x = data
|
60
|
+
x=x[0,MAX_SHOW_SIZE] + "..." if x.size > MAX_SHOW_SIZE
|
61
|
+
"zlib: data=#{x.inspect.bright_red}, offset=#{offset}, size=#{data.size}"
|
39
62
|
end
|
40
63
|
end
|
41
64
|
|
@@ -45,34 +68,6 @@ module ZSteg
|
|
45
68
|
end
|
46
69
|
end
|
47
70
|
|
48
|
-
class FileCmd < Struct.new(:title, :data)
|
49
|
-
COLORMAP = {
|
50
|
-
/bitmap|jpeg|pdf|zip|rar|7z/i => :red,
|
51
|
-
/DBase 3 data/i => :gray
|
52
|
-
}
|
53
|
-
|
54
|
-
def to_s
|
55
|
-
if title[/UTF-8 Unicode text/i]
|
56
|
-
begin
|
57
|
-
t = data.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")
|
58
|
-
rescue
|
59
|
-
t = data.force_encoding('binary')
|
60
|
-
end
|
61
|
-
return "utf8: " + t
|
62
|
-
end
|
63
|
-
COLORMAP.each do |re,color|
|
64
|
-
if title[re]
|
65
|
-
if color == :gray
|
66
|
-
return "file: #{title}".send(color)
|
67
|
-
else
|
68
|
-
return "file: " + title.send(color)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
"file: " + title.yellow
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
71
|
class Camouflage < Struct.new(:hidden_data_len, :host_orig_len)
|
77
72
|
def initialize(data)
|
78
73
|
self.hidden_data_len = (data[0x1a,4] || '').unpack('V').first
|
@@ -83,7 +78,7 @@ module ZSteg
|
|
83
78
|
end
|
84
79
|
|
85
80
|
def to_s
|
86
|
-
super.
|
81
|
+
super.bright_red
|
87
82
|
end
|
88
83
|
end
|
89
84
|
end
|