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