zsteg 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -145,6 +145,9 @@ module ZSteg
145
145
  result ||= Result.read(data)
146
146
  result.color = force_color if result && force_color
147
147
  result
148
+
149
+ rescue
150
+ STDERR.puts "[!] wbStego: #{$!.inspect}".red
148
151
  end
149
152
 
150
153
  end
@@ -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
- ){ |x| @options[:channels] = x.split(',') }
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
- if ['extradata', 'data after IEND'].include?(name)
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 MaskCLI
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
- bname = @fname.sub(/#{Regexp.escape(File.extname(@fname))}$/,'')
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
- fname = [bname, tail, "png"].join('.')
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