pedump 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,147 @@
1
+ require 'pedump'
2
+ require 'pedump/loader'
3
+
4
+ ########################################################################
5
+ # comparing 2 binaries
6
+ ########################################################################
7
+
8
+ class PEdump::Comparer
9
+ attr_accessor :verbose
10
+ attr_accessor :ignored_data_dirs, :ignored_sections
11
+
12
+ METHODS = [:sections, :data_dirs, :imports, :resources, :pe_hdr]
13
+
14
+ def initialize ldr1, ldr2
15
+ @ldr1,@ldr2 = ldr1,ldr2
16
+ @ignored_data_dirs = []
17
+ @ignored_sections = []
18
+ end
19
+
20
+ def equal?
21
+ METHODS.map{ |m| send("cmp_#{m}") }.uniq == [true]
22
+ end
23
+
24
+ def diff
25
+ METHODS.map{ |m| send("cmp_#{m}") ? nil : m }.compact
26
+ end
27
+
28
+ def cmp_pe_hdr
29
+ @ldr1.pe.ioh.AddressOfEntryPoint == @ldr2.pe.ioh.AddressOfEntryPoint &&
30
+ @ldr1.pe.ioh.ImageBase == @ldr2.pe.ioh.ImageBase
31
+ end
32
+
33
+ def cmp_resources
34
+ PEdump.quiet do
35
+ #@ldr1.pedump.resources == @ldr2.pedump.resources
36
+ @ldr1.pedump.resources.each_with_index do |r1,idx|
37
+ r2 = @ldr2.pedump.resources[idx]
38
+ if (r1.to_a - [r1.file_offset]) != (r2.to_a - [r2.file_offset])
39
+ p r1
40
+ p r2
41
+ return false
42
+ end
43
+ end
44
+ end
45
+ true
46
+ end
47
+
48
+ def cmp_sections
49
+ r = true
50
+ @ldr1.sections.each_with_index do |s1,idx|
51
+ next if @ignored_sections.include?(s1.name)
52
+ s2 = @ldr2.sections[idx]
53
+
54
+ if !s2
55
+ r = false
56
+ printf "[!] extra section %-12s in %s\n".red, s1.name.inspect, f1
57
+ elsif s1.data == s2.data
58
+ printf "[.] section: %s == %s\n".green, s1.name, s2.name if @verbose
59
+ else
60
+ r = false
61
+ printf "[!] section: %s != %s\n".red, s1.name, s2.name
62
+ self.class.cmp_ios *[s1,s2].map{ |section| StringIO.new(section.data) }
63
+ end
64
+ end
65
+ r
66
+ end
67
+
68
+ def cmp_data_dirs
69
+ r = true
70
+ @ldr1.pe.ioh.DataDirectory.each_with_index do |d1,idx|
71
+ break if idx == 15
72
+ d2 = @ldr2.pe.ioh.DataDirectory[idx]
73
+
74
+ case idx
75
+ when PEdump::IMAGE_DATA_DIRECTORY::BASERELOC
76
+ # total 8-byte size relocs == no relocs at all
77
+ next if [d1.va, d2.va].min == 0 && [d1.size, d2.size].max == 8
78
+ end
79
+
80
+ next if @ignored_data_dirs.include?(idx)
81
+
82
+ if d1.va != d2.va && d1.size != d2.size
83
+ r = false
84
+ printf "[!] data_dir: %-12s: SIZE & VA: %6x %6x | %6x %6x\n".red, d1.type,
85
+ d1.va, d1.size, d2.va, d2.size
86
+ elsif d1.va != d2.va
87
+ r = false
88
+ printf "[!] data_dir: %-12s: VA : %x != %x\n".red, d1.type, d1.va, d2.va
89
+ elsif d1.size != d2.size
90
+ r = false
91
+ printf "[!] data_dir: %-12s: SIZE : %x != %x\n".red, d1.type, d1.size, d2.size
92
+ end
93
+ end
94
+ r
95
+ end
96
+
97
+ def cmp_imports
98
+ @ldr1.pedump.imports.each_with_index do |iid1,idx|
99
+ iid2 = @ldr2.pedump.imports[idx]
100
+ if iid1 != iid2
101
+ puts "[!] diff imports".red
102
+ return false
103
+ end
104
+ end
105
+ true
106
+ end
107
+
108
+ class << self
109
+ # arguments can be:
110
+ # a) filenames
111
+ # b) IO instances
112
+ # c) PEdump::Loader instances
113
+ def cmp *args
114
+ handles = []
115
+ if args.all?{|x| x.is_a?(String)}
116
+ handles = args.map{|x| File.open(x,"rb")}
117
+ _cmp(*handles.map{|h| PEdump::Loader.new(h)})
118
+ else
119
+ _cmp(*args)
120
+ end
121
+ ensure
122
+ handles.each(&:close)
123
+ end
124
+
125
+ # each arg is a PEdump::Loader
126
+ def _cmp ldr1, ldr2
127
+ new(ldr1, ldr2).equal?
128
+ end
129
+
130
+ def cmp_ios *ios
131
+ ndiff = 0
132
+ while !ios.any?(&:eof)
133
+ bytes = ios.map(&:readbyte)
134
+ if bytes.uniq.size > 1
135
+ ndiff += 1
136
+ printf ("\t%08x:"+" %02x"*ios.size).yellow+"\n", ios[0].pos-1, *bytes
137
+ if ndiff >= 5
138
+ puts "\t...".yellow
139
+ break
140
+ end
141
+ end
142
+ end
143
+ puts if ndiff > 0
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,56 @@
1
+ class PEdump
2
+ class CompositeIO
3
+ def initialize(*ios)
4
+ @ios = ios.flatten
5
+ @starts = ios.map(&:tell) # respect current position of each IO
6
+ @pos = 0
7
+ end
8
+
9
+ def read(amount = nil, buf = nil)
10
+ buf ||= ''; buf1 = ''
11
+
12
+ # truncates buffer to zero length if nothing read
13
+ @ios.first.read(amount,buf)
14
+
15
+ @ios[1..-1].each do |io|
16
+ break if amount && buf.size >= amount
17
+ io.read(amount ? (amount-buf.size) : nil, buf1)
18
+ buf << buf1
19
+ end
20
+
21
+ @pos += buf.size
22
+
23
+ buf.size > 0 ? buf : (amount ? nil : buf )
24
+ end
25
+
26
+ def tell
27
+ @pos
28
+ end
29
+
30
+ def seek pos
31
+ @pos = pos
32
+ @ios.each_with_index do |io,idx|
33
+ if pos > 0
34
+ sz = io.size-@starts[idx]
35
+ io.seek( @starts[idx] + (pos < sz ? pos : sz) )
36
+ pos -= sz
37
+ else
38
+ # seek all remaining IOs to 0
39
+ io.seek @starts[idx]
40
+ end
41
+ end
42
+ end
43
+
44
+ def rewind
45
+ seek(0)
46
+ end
47
+
48
+ def size
49
+ @ios.map(&:size).inject(&:+)
50
+ end
51
+
52
+ def eof?
53
+ @ios.all?(&:eof?)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ require 'logger'
2
+ require 'pedump/version'
3
+ require 'pedump/logger'
4
+
5
+ class String
6
+ def xor x
7
+ if x.is_a?(String)
8
+ r = ''
9
+ j = 0
10
+ 0.upto(self.size-1) do |i|
11
+ r << (self[i].ord^x[j].ord).chr
12
+ j+=1
13
+ j=0 if j>= x.size
14
+ end
15
+ r
16
+ else
17
+ r = ''
18
+ 0.upto(self.size-1) do |i|
19
+ r << (self[i].ord^x).chr
20
+ end
21
+ r
22
+ end
23
+ end
24
+ end
25
+
26
+ class File
27
+ def checked_seek newpos
28
+ @file_range ||= (0..size)
29
+ @file_range.include?(newpos) && (seek(newpos) || true)
30
+ end
31
+ end
32
+
33
+ class PEdump
34
+ class << self
35
+ def logger; @@logger; end
36
+ def logger= l; @@logger=l; end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ class Object
2
+ # Invokes the method identified by the symbol +method+, passing it any arguments
3
+ # and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does.
4
+ #
5
+ # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised
6
+ # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass.
7
+ #
8
+ # If try is called without a method to call, it will yield any given block with the object.
9
+ #
10
+ # Please also note that +try+ is defined on +Object+, therefore it won't work with
11
+ # subclasses of +BasicObject+. For example, using try with +SimpleDelegator+ will
12
+ # delegate +try+ to target instead of calling it on delegator itself.
13
+ #
14
+ # ==== Examples
15
+ #
16
+ # Without +try+
17
+ # @person && @person.name
18
+ # or
19
+ # @person ? @person.name : nil
20
+ #
21
+ # With +try+
22
+ # @person.try(:name)
23
+ #
24
+ # +try+ also accepts arguments and/or a block, for the method it is trying
25
+ # Person.try(:find, 1)
26
+ # @people.try(:collect) {|p| p.name}
27
+ #
28
+ # Without a method argument try will yield to the block unless the receiver is nil.
29
+ # @person.try { |p| "#{p.first_name} #{p.last_name}" }
30
+ #--
31
+ # +try+ behaves like +Object#send+, unless called on +NilClass+.
32
+ def try(*a, &b)
33
+ if a.empty? && block_given?
34
+ yield self
35
+ else
36
+ __send__(*a, &b)
37
+ end
38
+ end
39
+ end
40
+
41
+ class NilClass
42
+ # Calling +try+ on +nil+ always returns +nil+.
43
+ # It becomes specially helpful when navigating through associations that may return +nil+.
44
+ #
45
+ # === Examples
46
+ #
47
+ # nil.try(:name) # => nil
48
+ #
49
+ # Without +try+
50
+ # @person && !@person.children.blank? && @person.children.first.name
51
+ #
52
+ # With +try+
53
+ # @person.try(:children).try(:first).try(:name)
54
+ def try(*args)
55
+ nil
56
+ end
57
+ end
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pedump'
4
+ require 'stringio'
5
+ require 'pedump/loader/section'
6
+ require 'pedump/loader/minidump'
7
+
8
+ # This class is kinda Virtual Machine that mimics executable loading as real OS does.
9
+ # Can be used for unpacking, emulating, reversing, ...
10
+ class PEdump::Loader
11
+ attr_accessor :mz_hdr, :dos_stub, :pe_hdr, :sections, :pedump, :image_base
12
+ attr_accessor :find_limit
13
+
14
+ DEFAULT_FIND_LIMIT = 2**64
15
+
16
+ # shortcuts
17
+ alias :pe :pe_hdr
18
+ def ep; @pe_hdr.ioh.AddressOfEntryPoint; end
19
+ def ep= v; @pe_hdr.ioh.AddressOfEntryPoint=v; end
20
+
21
+ ########################################################################
22
+ # constructors
23
+ ########################################################################
24
+
25
+ def initialize io = nil, params = {}
26
+ @pedump = PEdump.new(io, params)
27
+ if io
28
+ @mz_hdr = @pedump.mz
29
+ @dos_stub = @pedump.dos_stub
30
+ @pe_hdr = @pedump.pe
31
+ @image_base = params[:image_base] || @pe_hdr.try(:ioh).try(:ImageBase) || 0
32
+ load_sections @pedump.sections, io
33
+ end
34
+ @find_limit = params[:find_limit] || DEFAULT_FIND_LIMIT
35
+ end
36
+
37
+ def load_sections section_hdrs, f = nil
38
+ if section_hdrs.is_a?(Array)
39
+ @sections = section_hdrs.map do |x|
40
+ raise "unknown section hdr: #{x.inspect}" unless x.is_a?(PEdump::IMAGE_SECTION_HEADER)
41
+ Section.new(x, :deferred_load_io => f, :image_base => @image_base )
42
+ end
43
+ if f.respond_to?(:seek) && f.respond_to?(:read)
44
+ #
45
+ # converted to deferred loading
46
+ #
47
+ # section_hdrs.each_with_index do |sect_hdr, idx|
48
+ # f.seek sect_hdr.PointerToRawData
49
+ # @sections[idx].data = f.read(sect_hdr.SizeOfRawData)
50
+ # end
51
+ elsif f
52
+ raise "invalid 2nd arg: #{f.inspect}"
53
+ end
54
+ else
55
+ raise "invalid arg: #{section_hdrs.inspect}"
56
+ end
57
+ end
58
+
59
+ # load MS Minidump (*.dmp) file, that can be created in Task Manager via
60
+ # right click on process -> save memory dump
61
+ def load_minidump io, options = {}
62
+ @sections ||= []
63
+ md = Minidump.new io
64
+ options[:merge] = true unless options.key?(:merge)
65
+ md.memory_ranges(options).each do |mr|
66
+ hdr = PEdump::IMAGE_SECTION_HEADER.new(
67
+ :VirtualAddress => mr.va,
68
+ :PointerToRawData => mr.file_offset,
69
+ :SizeOfRawData => mr.size,
70
+ :VirtualSize => mr.size # XXX may be larger than SizeOfRawData
71
+ )
72
+ @sections << Section.new( hdr, :deferred_load_io => io )
73
+ end
74
+ end
75
+
76
+ def self.load_minidump io
77
+ new.tap{ |ldr| ldr.load_minidump io }
78
+ end
79
+
80
+ ########################################################################
81
+ # VA conversion
82
+ ########################################################################
83
+
84
+ # VA to section
85
+ def va2section va
86
+ @sections.find{ |x| x.range.include?(va) }
87
+ end
88
+
89
+ # RVA (Relative VA) to section
90
+ def rva2section rva
91
+ va2section( rva + @image_base )
92
+ end
93
+
94
+ def va2stream va
95
+ return nil unless section = va2section(va)
96
+ StringIO.new(section.data).tap do |io|
97
+ io.seek va-section.va
98
+ end
99
+ end
100
+
101
+ def rva2stream rva
102
+ va2stream( rva + @image_base )
103
+ end
104
+
105
+ ########################################################################
106
+ # virtual memory read/write
107
+ ########################################################################
108
+
109
+ # read arbitrary string
110
+ def [] va, size, params = {}
111
+ section = va2section(va)
112
+ raise "no section for va=0x#{va.to_s 16}" unless section
113
+ offset = va - section.va
114
+ raise "negative offset #{offset}" if offset < 0
115
+ r = section.data[offset,size]
116
+ return nil if r.nil?
117
+ if r.size < size && params.fetch(:zerofill, true)
118
+ # append some empty data
119
+ r << ("\x00".force_encoding('binary')) * (size - r.size)
120
+ end
121
+ r
122
+ end
123
+
124
+ # write arbitrary string
125
+ def []= va, size, data
126
+ raise "data.size != size" if data.size != size
127
+ section = va2section(va)
128
+ raise "no section for va=0x#{va.to_s 16}" unless section
129
+ offset = va - section.va
130
+ raise "negative offset #{offset}" if offset < 0
131
+ if section.data.size < offset
132
+ # append some empty data
133
+ section.data << ("\x00".force_encoding('binary') * (offset-section.data.size))
134
+ end
135
+ section.data[offset, data.size] = data
136
+ end
137
+
138
+ # returns StringIO with section data, pre-seeked to specified VA
139
+ # TODO: make io cross sections
140
+ def io va
141
+ section = va2section(va)
142
+ raise "no section for va=0x#{va.to_s 16}" unless section
143
+ offset = va - section.va
144
+ raise "negative offset #{offset}" if offset < 0
145
+ StringIO.new(section.data).tap{ |io| io.seek offset }
146
+ end
147
+
148
+ # read single DWord (4 bytes) if no 'n' specified
149
+ # delegate to #dwords otherwise
150
+ def dw va, n=nil
151
+ n ? dwords(va,n) : self[va,4].unpack('L')[0]
152
+ end
153
+ alias :dword :dw
154
+
155
+ # read N DWords, returns array
156
+ def dwords va, n
157
+ self[va,4*n].unpack('L*')
158
+ end
159
+
160
+ # check if any section has specified VA in its range
161
+ def valid_va? va
162
+ @ranges ||= _merge_ranges
163
+ @ranges.any?{ |range| range.include?(va) }
164
+ end
165
+
166
+ # increasing max_diff speed ups the :valid_va? method, but may cause false positives
167
+ def _merge_ranges max_diff = nil
168
+ max_diff ||=
169
+ if sections.size > 100
170
+ 1024*1024
171
+ else
172
+ 0
173
+ end
174
+
175
+ ranges0 = sections.map(&:range).sort_by(&:begin)
176
+ #puts "[.] #{ranges0.size} ranges"
177
+ ranges1 = []
178
+ range = ranges0.shift
179
+ while ranges0.any?
180
+ while (ranges0.first.begin-range.end).abs <= max_diff
181
+ range = range.begin...ranges0.shift.end
182
+ break if ranges0.empty?
183
+ end
184
+ #puts "[.] diff #{ranges0.first.begin-range.end}"
185
+ ranges1 << range
186
+ range = ranges0.shift
187
+ end
188
+ ranges1 << range
189
+ #puts "[=] #{ranges1.size} ranges"
190
+ ranges1.uniq.compact
191
+ end
192
+
193
+ # find first occurence of string
194
+ # returns VA
195
+ def find needle, options = {}
196
+ options[:align] ||= 1
197
+ options[:limit] ||= @find_limit
198
+
199
+ if needle.is_a?(Fixnum)
200
+ # silently convert to DWORD
201
+ needle = [needle].pack('L')
202
+ end
203
+
204
+ if options[:align] == 1
205
+ # fastest find?
206
+ processed_bytes = 0
207
+ sections.each do |section|
208
+ next unless section.data # skip empty sections
209
+ pos = section.data.index(needle)
210
+ return section.va+pos if pos
211
+ processed_bytes += section.vsize
212
+ return nil if processed_bytes >= options[:limit]
213
+ end
214
+ end
215
+ nil
216
+ end
217
+
218
+ # find all occurences of string
219
+ # returns array of VAs or empty array
220
+ def find_all needle, options = {}
221
+ options[:align] ||= 1
222
+ options[:limit] ||= @find_limit
223
+
224
+ if needle.is_a?(Fixnum)
225
+ # silently convert to DWORD
226
+ needle = [needle].pack('L')
227
+ end
228
+
229
+ r = []
230
+ if options[:align] == 1
231
+ # fastest find?
232
+ processed_bytes = 0
233
+ sections.each do |section|
234
+ next unless section.data # skip empty sections
235
+ section.data.scan(needle) do
236
+ r << $~.begin(0) + section.va
237
+ end
238
+ processed_bytes += section.vsize
239
+ return r if processed_bytes >= options[:limit]
240
+ end
241
+ end
242
+ r
243
+ end
244
+
245
+ ########################################################################
246
+ # parsing names
247
+ ########################################################################
248
+
249
+ def names
250
+ return @names if @names
251
+ @names = {}
252
+ if oep = @pe_hdr.try(:ioh).try(:AddressOfEntryPoint)
253
+ oep += @image_base
254
+ @names[oep] = 'start'
255
+ end
256
+ _parse_imports
257
+ _parse_exports
258
+ #TODO: debug info
259
+ @names
260
+ end
261
+
262
+ def _parse_imports
263
+ @pedump.imports.each do |iid| # Image Import Descriptor
264
+ va = iid.FirstThunk + @image_base
265
+ (Array(iid.original_first_thunk) + Array(iid.first_thunk)).uniq.each do |func|
266
+ name = "__imp_" + (func.name || "#{func.ordinal}")
267
+ @names[va] = name
268
+ va += 4
269
+ end
270
+ end
271
+ end
272
+
273
+ def _parse_exports
274
+ return {} unless @pedump.exports
275
+ @pedump.exports.functions.each do |func|
276
+ @names[@image_base + func.va] = func.name || "##{func.ordinal}"
277
+ end
278
+ end
279
+
280
+ ########################################################################
281
+ # generating PE binary
282
+ ########################################################################
283
+
284
+ def section_table
285
+ @sections.map do |section|
286
+ section.hdr.SizeOfRawData = section.data.size
287
+ section.hdr.PointerToRelocations ||= 0
288
+ section.hdr.PointerToLinenumbers ||= 0
289
+ section.hdr.NumberOfRelocations ||= 0
290
+ section.hdr.NumberOfLinenumbers ||= 0
291
+ section.hdr.Characteristics ||= 0
292
+ section.hdr.pack
293
+ end.join
294
+ end
295
+
296
+ # save a new PE file to specified IO
297
+ def export io
298
+ @mz_hdr ||= PEdump::MZ.new("MZ", *[0]*22)
299
+ @dos_stub ||= ''
300
+ @pe_hdr ||= PEdump::PE.new("PE\x00\x00")
301
+ @pe_hdr.ioh ||=
302
+ PEdump::IMAGE_OPTIONAL_HEADER32.read( StringIO.new("\x00" * 224) ).tap do |ioh|
303
+ ioh.Magic = 0x10b # 32-bit executable
304
+ #ioh.NumberOfRvaAndSizes = 0x10
305
+ end
306
+ @pe_hdr.ifh ||= PEdump::IMAGE_FILE_HEADER.new(
307
+ :Machine => 0x14c, # x86
308
+ :NumberOfSections => @sections.size,
309
+ :TimeDateStamp => 0,
310
+ :PointerToSymbolTable => 0,
311
+ :NumberOfSymbols => 0,
312
+ :SizeOfOptionalHeader => @pe_hdr.ioh.pack.size,
313
+ :Characteristics => 0x102 # EXECUTABLE_IMAGE | 32BIT_MACHINE
314
+ )
315
+
316
+ if @pe_hdr.ioh.FileAlignment.to_i == 0
317
+ # default file align = 512 bytes
318
+ @pe_hdr.ioh.FileAlignment = 0x200
319
+ end
320
+ if @pe_hdr.ioh.SectionAlignment.to_i == 0
321
+ # default section align = 4k
322
+ @pe_hdr.ioh.SectionAlignment = 0x1000
323
+ end
324
+
325
+ mz_size = @mz_hdr.pack.size
326
+ raise "odd mz_size #{mz_size}" if mz_size % 0x10 != 0
327
+ @mz_hdr.header_paragraphs = mz_size / 0x10 # offset of dos_stub
328
+ @mz_hdr.lfanew = mz_size + @dos_stub.size # offset of PE hdr
329
+ io.write @mz_hdr.pack
330
+ io.write @dos_stub
331
+ io.write @pe_hdr.pack
332
+ io.write @pe_hdr.ioh.DataDirectory.map(&:pack).join
333
+
334
+ section_tbl_offset = io.tell # store offset for 2nd write of section table
335
+ io.write section_table
336
+
337
+ align = @pe_hdr.ioh.FileAlignment
338
+ @sections.each do |section|
339
+ io.seek(align - (io.tell % align), IO::SEEK_CUR) if io.tell % align != 0
340
+ section.hdr.PointerToRawData = io.tell # fix raw_ptr
341
+ io.write(section.data)
342
+ end
343
+
344
+ eof = io.tell
345
+
346
+ # 2nd write of section table with correct raw_ptr's
347
+ io.seek section_tbl_offset
348
+ io.write section_table
349
+
350
+ io.seek eof
351
+ end
352
+
353
+ alias :dump :export
354
+ end
355
+
356
+ ###################################################################
357
+
358
+ if $0 == __FILE__
359
+ require 'pp'
360
+ require 'zhexdump'
361
+
362
+ io = open ARGV.first
363
+ ldr = PEdump::Loader.load_minidump io
364
+
365
+ File.open(ARGV.first + ".exe", "wb") do |f|
366
+ ldr.sections[100..-1] = []
367
+ ldr.export f
368
+ end
369
+ exit
370
+
371
+ va = 0x3a10000+0xceb00-0x300+0x18c
372
+ ZHexdump.dump ldr[va, 0x200], :add => va
373
+ exit
374
+
375
+ #puts
376
+ #ZHexdump.dump ldr[x,0x100]
377
+
378
+ ldr.find_all(va, :limit => 100_000_000).each do |va0|
379
+ printf "[.] found at VA=%x\n", va0
380
+ 5.times do |i|
381
+ puts
382
+ va = ldr.dw(va0+i*4)
383
+ ZHexdump.dump ldr[va,0x30], :add => va if va != 0
384
+ end
385
+ end
386
+
387
+ puts "---"
388
+ ldr.find_all(0x3dff970, :limit => 100_000_000).each do |va|
389
+ ldr[va,0x20].hexdump
390
+ puts
391
+ end
392
+
393
+ end