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
@@ -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
|