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
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
#coding: binary
|
4
|
+
module ZSteg
|
5
|
+
class Checker
|
6
|
+
module Zlib
|
7
|
+
|
8
|
+
MIN_UNPACKED_SIZE = 4
|
9
|
+
|
10
|
+
class Result < Struct.new(:data, :offset)
|
11
|
+
MAX_SHOW_SIZE = 100
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
x = data
|
15
|
+
x=x[0,MAX_SHOW_SIZE] + "..." if x.size > MAX_SHOW_SIZE
|
16
|
+
"zlib: data=#{x.inspect.bright_red}, offset=#{offset}, size=#{data.size}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# try to find zlib
|
21
|
+
# http://blog.w3challs.com/index.php?post/2012/03/25/NDH2k12-Prequals-We-are-looking-for-a-real-hacker-Wallpaper-image
|
22
|
+
# http://blog.w3challs.com/public/ndh2k12_prequalls/sp113.bmp
|
23
|
+
def self.check_data data
|
24
|
+
return unless idx = data.index(/\x78[\x9c\xda\x01]/)
|
25
|
+
|
26
|
+
zi = ::Zlib::Inflate.new
|
27
|
+
x = zi.inflate data[idx..-1]
|
28
|
+
# decompress OK
|
29
|
+
return Result.new x, idx if x.size >= MIN_UNPACKED_SIZE
|
30
|
+
rescue ::Zlib::BufError
|
31
|
+
# tried to decompress, but got EOF - need more data
|
32
|
+
return Result.new x, idx
|
33
|
+
rescue ::Zlib::DataError, ::Zlib::NeedDict
|
34
|
+
# not a zlib
|
35
|
+
ensure
|
36
|
+
zi.close if zi && !zi.closed?
|
37
|
+
end
|
38
|
+
|
39
|
+
end # Zlib
|
40
|
+
end # Checker
|
41
|
+
end # ZSteg
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
3
|
module ZSteg
|
4
|
-
class CLI
|
4
|
+
class CLI::Cli
|
5
5
|
DEFAULT_ACTIONS = %w'check'
|
6
6
|
|
7
7
|
def initialize argv = ARGV
|
@@ -22,7 +22,11 @@ module ZSteg
|
|
22
22
|
opts.on("-c", "--channels X", /[rgba,1-8]+/,
|
23
23
|
"channels (R/G/B/A) or any combination, comma separated",
|
24
24
|
"valid values: r,g,b,a,rg,bgr,rgba,r3g2b3,..."
|
25
|
-
)
|
25
|
+
) do |x|
|
26
|
+
@options[:channels] = x.split(',')
|
27
|
+
# specifying channels on command line disables extra checks
|
28
|
+
@options[:extra_checks] = false
|
29
|
+
end
|
26
30
|
|
27
31
|
opts.on("-l", "--limit N", Integer,
|
28
32
|
"limit bytes checked, 0 = no limit (default: #{@options[:limit]})"
|
@@ -32,6 +36,7 @@ module ZSteg
|
|
32
36
|
"advanced: specify individual bits like '00001110' or '0x88'"
|
33
37
|
) do |x|
|
34
38
|
a = []
|
39
|
+
x = '1-8' if x == 'all'
|
35
40
|
x.split(',').each do |x1|
|
36
41
|
if x1['-']
|
37
42
|
t = x1.split('-')
|
@@ -41,6 +46,8 @@ module ZSteg
|
|
41
46
|
end
|
42
47
|
end
|
43
48
|
@options[:bits] = a.flatten.uniq
|
49
|
+
# specifying bits on command line disables extra checks
|
50
|
+
@options[:extra_checks] = false
|
44
51
|
end
|
45
52
|
|
46
53
|
opts.on "--lsb", "least significant BIT comes first" do
|
@@ -52,7 +59,13 @@ module ZSteg
|
|
52
59
|
|
53
60
|
opts.on "-P", "--prime", "analyze/extract only prime bytes/pixels" do
|
54
61
|
@options[:prime] = true
|
62
|
+
# specifying prime on command line disables extra checks
|
63
|
+
@options[:extra_checks] = false
|
64
|
+
end
|
65
|
+
opts.on "--invert", "invert bits (XOR 0xff)" do
|
66
|
+
@options[:invert] = true
|
55
67
|
end
|
68
|
+
|
56
69
|
# opts.on "--pixel-align", "pixel-align hidden data (EasyBMP)" do
|
57
70
|
# @options[:pixel_align] = true
|
58
71
|
# end
|
@@ -60,6 +73,9 @@ module ZSteg
|
|
60
73
|
opts.on "-a", "--all", "try all known methods" do
|
61
74
|
@options[:prime] = :all
|
62
75
|
@options[:order] = :all
|
76
|
+
@options[:bits] = (1..8).to_a
|
77
|
+
# specifying --all on command line explicitly enables extra checks
|
78
|
+
@options[:extra_checks] = true
|
63
79
|
end
|
64
80
|
|
65
81
|
opts.on("-o", "--order X", /all|auto|[bxy,]+/i,
|
@@ -68,9 +84,34 @@ module ZSteg
|
|
68
84
|
){ |x| @options[:order] = x.split(',') }
|
69
85
|
|
70
86
|
opts.on "-E", "--extract NAME", "extract specified payload, NAME is like '1b,rgb,lsb'" do |x|
|
87
|
+
@options[:verbose] = -2 # silent ZPNG warnings
|
71
88
|
@actions << [:extract, x]
|
72
89
|
end
|
73
90
|
|
91
|
+
opts.separator ""
|
92
|
+
|
93
|
+
opts.on "--[no-]file", "use 'file' command to detect data type (default: YES)" do |x|
|
94
|
+
@options[:file] = x
|
95
|
+
end
|
96
|
+
|
97
|
+
# TODO
|
98
|
+
# opts.on "--[no-]binwalk", "use 'binwalk' command to detect data type (default: NO)" do |x|
|
99
|
+
# @options[:binwalk] = x
|
100
|
+
# end
|
101
|
+
|
102
|
+
opts.on "--no-strings", "disable ASCII strings finding (default: enabled)" do |x|
|
103
|
+
@options[:strings] = false
|
104
|
+
end
|
105
|
+
opts.on "-s", "--strings X", %w'first all longest none no',
|
106
|
+
"ASCII strings find mode: first, all, longest, none",
|
107
|
+
"(default: first)" do |x|
|
108
|
+
@options[:strings] = x[0] == 'n' ? false : x.downcase.to_sym
|
109
|
+
end
|
110
|
+
opts.on "-n", "--min-str-len X", Integer,
|
111
|
+
"minimum string length (default: #{Checker::DEFAULT_MIN_STR_LEN})" do |x|
|
112
|
+
@options[:min_str_len] = x
|
113
|
+
end
|
114
|
+
|
74
115
|
opts.separator ""
|
75
116
|
opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
|
76
117
|
@options[:verbose] += 1
|
@@ -123,7 +164,7 @@ module ZSteg
|
|
123
164
|
if File.directory?(fname)
|
124
165
|
puts "[?] #{fname} is a directory".yellow
|
125
166
|
else
|
126
|
-
ZPNG::Image.load(fname)
|
167
|
+
ZPNG::Image.load(fname, :verbose => @options[:verbose]+1)
|
127
168
|
end
|
128
169
|
rescue ZPNG::Exception, Errno::ENOENT
|
129
170
|
puts "[!] #{$!.inspect}".red
|
@@ -167,6 +208,24 @@ module ZSteg
|
|
167
208
|
h
|
168
209
|
end
|
169
210
|
|
211
|
+
def _extract_data name
|
212
|
+
case name
|
213
|
+
when /scanline/
|
214
|
+
Checker::ScanlineChecker.check_image @img
|
215
|
+
when /extradata:imagedata/
|
216
|
+
@img.imagedata[@img.scanlines.map(&:size).inject(&:+)..-1]
|
217
|
+
when /extradata:(\d+)/
|
218
|
+
# accessing imagedata implicitly unpacks zlib stream
|
219
|
+
# zlib stream may contain extradata
|
220
|
+
@img.imagedata
|
221
|
+
@img.extradata[$1.to_i]
|
222
|
+
else
|
223
|
+
h = decode_param_string name
|
224
|
+
h[:limit] = @options[:limit] if @options[:limit] != Checker::DEFAULT_LIMIT
|
225
|
+
Extractor.new(@img, @options).extract(h)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
170
229
|
###########################################################################
|
171
230
|
# actions
|
172
231
|
|
@@ -175,14 +234,7 @@ module ZSteg
|
|
175
234
|
end
|
176
235
|
|
177
236
|
def extract name
|
178
|
-
|
179
|
-
print @img.extradata
|
180
|
-
return
|
181
|
-
end
|
182
|
-
|
183
|
-
h = decode_param_string name
|
184
|
-
h[:limit] = @options[:limit] if @options[:limit] != Checker::DEFAULT_LIMIT
|
185
|
-
print Extractor.new(@img, @options).extract(h)
|
237
|
+
print _extract_data(name)
|
186
238
|
end
|
187
239
|
|
188
240
|
end
|
@@ -3,7 +3,7 @@ require 'set'
|
|
3
3
|
require 'digest/md5'
|
4
4
|
|
5
5
|
module ZSteg
|
6
|
-
class
|
6
|
+
class CLI::Mask
|
7
7
|
DEFAULT_ACTIONS = %w'mask'
|
8
8
|
|
9
9
|
COMMON_MASKS = [
|
@@ -234,7 +234,8 @@ module ZSteg
|
|
234
234
|
|
235
235
|
def masks2fname masks
|
236
236
|
masks = masks.dup.delete_if{ |k,v| !CHANNELS.include?(k) }
|
237
|
-
|
237
|
+
ext = File.extname(@fname)
|
238
|
+
bname = @fname.chomp(ext)
|
238
239
|
color = nil
|
239
240
|
raise "TODO" if masks.values.all?(&:nil?)
|
240
241
|
if masks.values.uniq.size == 1
|
@@ -256,10 +257,11 @@ module ZSteg
|
|
256
257
|
else nil
|
257
258
|
end
|
258
259
|
end
|
259
|
-
tail = a.join("
|
260
|
+
tail = a.join("_")
|
260
261
|
end
|
261
262
|
|
262
|
-
|
263
|
+
# we always export as PNG
|
264
|
+
fname = [bname, "mask_#{tail}", "png"].join('.')
|
263
265
|
fname = File.join(@options[:dir], File.basename(fname)) if @options[:dir]
|
264
266
|
[fname, color]
|
265
267
|
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'stringio'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ZSteg
|
6
|
+
class CLI::Reflow
|
7
|
+
DEFAULT_ACTIONS = %w'reflow'
|
8
|
+
|
9
|
+
def initialize argv = ARGV
|
10
|
+
@argv = argv
|
11
|
+
@cache = {}
|
12
|
+
@wasfiles = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
@actions = []
|
17
|
+
@options = {
|
18
|
+
:verbose => 0,
|
19
|
+
}
|
20
|
+
optparser = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: #{File.basename($0)} [options] filename.png [param_string]"
|
22
|
+
opts.separator ""
|
23
|
+
|
24
|
+
opts.on( "-W", "--width X", "reflow to specified width(s)",
|
25
|
+
"single value: '999', range: '100-200'",
|
26
|
+
"or comma-separated: '100,200,300-350'"
|
27
|
+
) do |x|
|
28
|
+
# if @options[:heights]
|
29
|
+
# STDERR.puts "[!] width _OR_ height can be set".red
|
30
|
+
# exit 1
|
31
|
+
# end
|
32
|
+
@options[:widths] = parse_dimension(x)
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on "-H", "--height X", "reflow to specified height(s)" do |x|
|
36
|
+
# if @options[:widths]
|
37
|
+
# STDERR.puts "[!] width _OR_ height can be set".red
|
38
|
+
# exit 1
|
39
|
+
# end
|
40
|
+
@options[:heights] = parse_dimension(x)
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.separator ""
|
44
|
+
|
45
|
+
opts.on "-a", "--all", "try all possible sizes (default)" do
|
46
|
+
@options[:try_all] = true
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.separator ""
|
50
|
+
|
51
|
+
opts.on "-O", "--outfile FILENAME", "output single result to specified file" do |x|
|
52
|
+
@options[:outfile] = x
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on "-D", "--dir DIRNAME", "output multiple results to specified dir" do |x|
|
56
|
+
@options[:dir] = x
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.separator ""
|
60
|
+
opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
|
61
|
+
@options[:verbose] += 1
|
62
|
+
end
|
63
|
+
opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
|
64
|
+
@options[:verbose] -= 1
|
65
|
+
end
|
66
|
+
opts.on "-C", "--[no-]color", "Force (or disable) color output (default: auto)" do |x|
|
67
|
+
Sickill::Rainbow.enabled = x
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if (argv = optparser.parse(@argv)).empty?
|
72
|
+
puts optparser.help
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
@actions = DEFAULT_ACTIONS if @actions.empty?
|
77
|
+
|
78
|
+
argv.each do |arg|
|
79
|
+
if arg[','] && !File.exist?(arg)
|
80
|
+
@options.merge!(decode_param_string(arg))
|
81
|
+
argv.delete arg
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
argv.each_with_index do |fname,idx|
|
86
|
+
if argv.size > 1 && @options[:verbose] >= 0
|
87
|
+
puts if idx > 0
|
88
|
+
puts "[.] #{fname}".green
|
89
|
+
end
|
90
|
+
next unless @image=load_image(@fname=fname)
|
91
|
+
|
92
|
+
@actions.each do |action|
|
93
|
+
if action.is_a?(Array)
|
94
|
+
self.send(*action) if self.respond_to?(action.first)
|
95
|
+
else
|
96
|
+
self.send(action) if self.respond_to?(action)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
rescue Errno::EPIPE
|
101
|
+
# output interrupt, f.ex. when piping output to a 'head' command
|
102
|
+
# prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_dimension s
|
106
|
+
s.split(',').map do |x|
|
107
|
+
case x
|
108
|
+
when /\A\d+\Z/ # single value
|
109
|
+
x.to_i
|
110
|
+
when /-/ # range
|
111
|
+
Range.new(*x.split('-').map(&:to_i)).to_a
|
112
|
+
end
|
113
|
+
end.flatten.uniq
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_image fname
|
117
|
+
if File.directory?(fname)
|
118
|
+
puts "[?] #{fname} is a directory".yellow
|
119
|
+
else
|
120
|
+
ZPNG::Image.load(fname)
|
121
|
+
end
|
122
|
+
rescue ZPNG::Exception, Errno::ENOENT
|
123
|
+
puts "[!] #{$!.inspect}".red
|
124
|
+
end
|
125
|
+
|
126
|
+
###########################################################################
|
127
|
+
# actions
|
128
|
+
|
129
|
+
def reflow
|
130
|
+
if @image.format != :bmp
|
131
|
+
STDERR.puts "[!] only BMP format supported for now!"
|
132
|
+
return
|
133
|
+
end
|
134
|
+
|
135
|
+
sl = @image.scanlines.first
|
136
|
+
@bpp = sl.bpp
|
137
|
+
@old_significant_sl_bytes = (sl.width*sl.bpp/8.0).ceil
|
138
|
+
@old_total_sl_bytes = sl.size
|
139
|
+
|
140
|
+
if @options[:heights]
|
141
|
+
@options[:heights].each do |h|
|
142
|
+
t = 1.0*@image.width*@image.height/h
|
143
|
+
t.floor.upto(t.ceil).each do |w|
|
144
|
+
next if @options[:widths] && !@options[:widths].include?(w)
|
145
|
+
_reflow w,h
|
146
|
+
end
|
147
|
+
end
|
148
|
+
elsif @options[:widths]
|
149
|
+
@options[:widths].each do |w|
|
150
|
+
h = @image.width*@image.height/w
|
151
|
+
_reflow w,h
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# enum all
|
155
|
+
2.upto(@image.width*@image.height/2) do |w|
|
156
|
+
h = @image.width*@image.height/w
|
157
|
+
_reflow w,h
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def _gen_fname w,h
|
165
|
+
ext = @fname[/\.\w{3}$/].to_s
|
166
|
+
fname = "%s.reflow_%05dx%05d%s" % [@fname.chomp(ext), w, h, ext]
|
167
|
+
fname = File.join(@options[:dir], File.basename(fname)) if @options[:dir]
|
168
|
+
fname
|
169
|
+
end
|
170
|
+
|
171
|
+
def _reflow w,h
|
172
|
+
fname = @options[:outfile] || _gen_fname(w,h)
|
173
|
+
raise "already written to #{fname}" if @wasfiles.include?(fname)
|
174
|
+
@wasfiles << fname
|
175
|
+
|
176
|
+
new_significant_sl_bytes = (w*@bpp/8.0).ceil
|
177
|
+
padding = "\x00" * (4-new_significant_sl_bytes%4)
|
178
|
+
padding = "" if padding.size == 4
|
179
|
+
|
180
|
+
# p @old_significant_sl_bytes
|
181
|
+
# p @old_total_sl_bytes
|
182
|
+
# p new_significant_sl_bytes
|
183
|
+
# p padding
|
184
|
+
|
185
|
+
puts "[.] #{fname} .."
|
186
|
+
File.open(@fname, "rb") do |fi|
|
187
|
+
File.open(fname, "wb") do |fo|
|
188
|
+
# 2 bytes - "BM" signature
|
189
|
+
# 4 bytes - the size of the BMP file in bytes
|
190
|
+
# 2 bytes - reserved
|
191
|
+
# 2 bytes - reserved
|
192
|
+
fo.write fi.read(2+4+2+2)
|
193
|
+
|
194
|
+
# 4 bytes - imagedata offset
|
195
|
+
data = fi.read(4)
|
196
|
+
imagedata_offset = data.unpack('V').first
|
197
|
+
fo.write data
|
198
|
+
|
199
|
+
# 4 bytes - BITMAPINFOHEADER.biSize (keep)
|
200
|
+
# 4 bytes - BITMAPINFOHEADER.biWidth (rewrite)
|
201
|
+
# 4 bytes - BITMAPINFOHEADER.biHeight (rewrite)
|
202
|
+
data = fi.read(4+4+4)
|
203
|
+
fo.write(data[0,4] + [w,h].pack("V2")) # write new size
|
204
|
+
|
205
|
+
# copy remaining header bytes
|
206
|
+
fo.write fi.read(imagedata_offset-fi.tell)
|
207
|
+
|
208
|
+
# FIXME: if scanline sizes differ in BITS, not bytes...
|
209
|
+
|
210
|
+
# scanline padding needs to be respected...
|
211
|
+
imagedata = StringIO.new
|
212
|
+
@image.height.times do
|
213
|
+
data = fi.read @old_total_sl_bytes
|
214
|
+
imagedata << data[0, @old_significant_sl_bytes]
|
215
|
+
#p data[@old_significant_sl_bytes..-1]
|
216
|
+
end
|
217
|
+
imagedata << fi.read # read extradata, if any
|
218
|
+
|
219
|
+
imagedata.rewind
|
220
|
+
imagedata_start = fo.tell
|
221
|
+
h.times do
|
222
|
+
fo << imagedata.read(new_significant_sl_bytes)
|
223
|
+
fo << padding
|
224
|
+
end
|
225
|
+
file_size = fo.tell
|
226
|
+
imagedata_size = fo.tell - imagedata_start
|
227
|
+
fo << imagedata.read # write extradata, if any
|
228
|
+
|
229
|
+
# write new BITMAPFILEHEADER.bfSize
|
230
|
+
fo.seek 2
|
231
|
+
fo.write [file_size].pack('V')
|
232
|
+
|
233
|
+
# write new BITMAPINFOHEADER.biSizeImage
|
234
|
+
fo.seek 14+20 # BITMAPFILEHEADER::SIZE + 20
|
235
|
+
fo.write [imagedata_size].pack('V')
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|