binstruct 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +9 -0
- data/README.txt +106 -0
- data/Rakefile +12 -0
- data/bin/binstruct +3 -0
- data/lib/binstruct.rb +252 -0
- data/lib/fields.rb +310 -0
- data/lib/objhax.rb +42 -0
- data/test/test_binstruct.rb +8 -0
- metadata +74 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
= binstruct
|
2
|
+
|
3
|
+
http://metafuzz.rubyforge.org/binstruct
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
BinStruct is a small class for defining structures that can be used to parse /
|
8
|
+
manipulate binary data. It is similar in concept to BitStruct, but supports
|
9
|
+
some things that are difficult to do with a pure pack / unpack approach. It is
|
10
|
+
mainly designed to work with the Metafuzz fuzzing tools, but would possibly be
|
11
|
+
useful to anyone working with binary data.
|
12
|
+
|
13
|
+
== FEATURES/PROBLEMS:
|
14
|
+
|
15
|
+
* Parsing is done 'as you go' which means you can refer to previous fields or
|
16
|
+
values when making parser decisions (variable length fields etc)
|
17
|
+
* Supports byte-swapped multi-byte bitfields for little endian file formats and
|
18
|
+
substructs for nested parsing
|
19
|
+
* Full Ruby inside the class definition, with a little bit of syntactic sugar
|
20
|
+
for field type definitions
|
21
|
+
* All field data remains in the original binary internally, but get / setters
|
22
|
+
are available for signed, unsigned, string, octets, hexstring and bitstring
|
23
|
+
and can be easily extended.
|
24
|
+
* Key instance methods are get / setters for the fields (defined when the
|
25
|
+
subclass is constructed), directly access the raw field object with #[:afield]
|
26
|
+
#each {|item| (item may be a substruct), #deep_each {|field| (for nested
|
27
|
+
structs), #flatten and of course #to_s to re-pack as a string.
|
28
|
+
* Because this class was designed for use with a fuzzer it does not do very
|
29
|
+
stringent checking of alignment, length matching etc, and raw contents are
|
30
|
+
available where possible. This is either a bug or a feature, depending on
|
31
|
+
your point of view.
|
32
|
+
|
33
|
+
== SYNOPSIS:
|
34
|
+
|
35
|
+
This example is from some Word binary file work...
|
36
|
+
|
37
|
+
class WordDgg < BinStruct
|
38
|
+
parse{ |bitbuf|
|
39
|
+
endian :little
|
40
|
+
# These two bytes will be swapped before they are carved up, and
|
41
|
+
# swapped back whenever to_s is called. The bitfield itself cannot
|
42
|
+
# be directly accessed, just the fields inside.
|
43
|
+
bitfield(bitbuf, 16) do |buf|
|
44
|
+
unsigned buf, :recInstance, 12, "Object Identifier"
|
45
|
+
unsigned buf, :recVer, 4, "Object version, 0xF for container"
|
46
|
+
end
|
47
|
+
# Normal field definitions take effect immediately, and consume
|
48
|
+
# the portion of the bit buffer they used.
|
49
|
+
unsigned bitbuf, :recType, 16, "RecType"
|
50
|
+
unsigned bitbuf, :recLen, 32, "Content length"
|
51
|
+
# Context sensitive parsing...
|
52
|
+
if self.recVer==0xF
|
53
|
+
substruct_name=:substruct0
|
54
|
+
# Manually manipulate the buffer parameter, if you like.
|
55
|
+
mychunk=[bitbuf.slice!(0,self.recLen*8)].pack('B*')
|
56
|
+
while mychunk.length > 0
|
57
|
+
# Nested parsing, here...
|
58
|
+
substruct(mychunk, substruct_name, mychunk.length, WordDgg)
|
59
|
+
substruct_name=substruct_name.succ
|
60
|
+
end
|
61
|
+
else
|
62
|
+
string bitbuf, :contents, self.recLen*8, "Contents"
|
63
|
+
end
|
64
|
+
# groups link sets of fields, using a given name. The groups are used
|
65
|
+
# by Metafuzz to create cartestian product output of the fuzz items
|
66
|
+
# for all fields in the group.
|
67
|
+
if self[:contents]
|
68
|
+
group :tv, :recType, :contents
|
69
|
+
else
|
70
|
+
group :tl, :recType, :recLen
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
== REQUIREMENTS:
|
76
|
+
|
77
|
+
None.
|
78
|
+
|
79
|
+
== INSTALL:
|
80
|
+
|
81
|
+
Just install the gem.
|
82
|
+
|
83
|
+
== LICENSE:
|
84
|
+
|
85
|
+
(The MIT License)
|
86
|
+
|
87
|
+
Copyright (c) 2006 Ben Nagy
|
88
|
+
|
89
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
90
|
+
a copy of this software and associated documentation files (the
|
91
|
+
'Software'), to deal in the Software without restriction, including
|
92
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
93
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
94
|
+
permit persons to whom the Software is furnished to do so, subject to
|
95
|
+
the following conditions:
|
96
|
+
|
97
|
+
The above copyright notice and this permission notice shall be
|
98
|
+
included in all copies or substantial portions of the Software.
|
99
|
+
|
100
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
101
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
102
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
103
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
104
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
105
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
106
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/binstruct.rb'
|
6
|
+
|
7
|
+
Hoe.new('binstruct', Binstruct::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'metafuzz' # if different than lowercase project name
|
9
|
+
p.developer('Ben Nagy', 'metafuzz@ben.iagu.net')
|
10
|
+
end
|
11
|
+
|
12
|
+
# vim: syntax=Ruby
|
data/bin/binstruct
ADDED
data/lib/binstruct.rb
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'fields'
|
2
|
+
require 'objhax'
|
3
|
+
|
4
|
+
# There are two main classes of methods for a Binstruct, but because of the
|
5
|
+
# implementation they are all actually instance methods. The "construction"
|
6
|
+
# methods are +endian+, +bitfield+, +substruct+ and all the builders for field
|
7
|
+
# subtypes. Each class in the Fields module has a builder, so UnsignedField is
|
8
|
+
# just unsigned. Those methods are created at runtime, so they can't be
|
9
|
+
# seen by rdoc, but they all look like:
|
10
|
+
# unsigned, buffer_name, :accessor_sym, length_in_bits, "Description"
|
11
|
+
# Inside a parse block, the buffer name used for the field builders should match
|
12
|
+
# the block parameter, unless you are manually messing with the buffer. The
|
13
|
+
# same applies to fields created inside the blocks for substructs and bitfields.
|
14
|
+
#
|
15
|
+
# The other methods are 'proper' instance methods, to work with the structure
|
16
|
+
# once it is created and filled with data. Each field creates a getter and
|
17
|
+
# setter method matching its symbol, and also a direct access via [:field_sym]
|
18
|
+
# that returns the field itself, which gives you access to the field's own
|
19
|
+
# instance methods like +set_raw+ (see Fields).
|
20
|
+
class Binstruct
|
21
|
+
VERSION="1.0.0"
|
22
|
+
|
23
|
+
class Bitfield < Binstruct # :nodoc:
|
24
|
+
end
|
25
|
+
attr_reader :groups
|
26
|
+
attr_accessor :fields, :endian
|
27
|
+
#---------------CONSTRUCTION-------------------
|
28
|
+
|
29
|
+
# Set the endianness for the whole structure. Default is +:big+, options
|
30
|
+
# are +:big+ or +:little+. Fields created after this method is invoked in
|
31
|
+
# a construction block will be created with the new endianness. You can also
|
32
|
+
# set the endianness after construction with <code>somestruct.endian=:little
|
33
|
+
# </code>.
|
34
|
+
def endian( sym )
|
35
|
+
unless sym==:little || sym==:big
|
36
|
+
raise RuntimeError, "Binstruct: Construction: Unknown endianness #{sym.to_s}"
|
37
|
+
end
|
38
|
+
@endian=sym
|
39
|
+
meta_def :endian do @endian end
|
40
|
+
meta_def :endianness do @endian end
|
41
|
+
end
|
42
|
+
|
43
|
+
# In little endian structures, byte-swaps 16, 32 or 64 bits and presents
|
44
|
+
# them for carving into fields. Only really neccessary for non-byte aligned
|
45
|
+
# field sets in little endian structures. The bitfield itself is invisible
|
46
|
+
# but the fields are created with the usual accessors.
|
47
|
+
def bitfield(bitbuf, len, &blk)
|
48
|
+
if @endian==:little
|
49
|
+
unless len==16||len==32||len==64
|
50
|
+
raise RuntimeError, "Binstruct: Bitfield: Don't know how to endian swap #{len} bits. :("
|
51
|
+
end
|
52
|
+
instr=bitbuf.slice!(0,len).scan(/.{8}/).reverse.join
|
53
|
+
else
|
54
|
+
instr=bitbuf.slice!(0,len)
|
55
|
+
end
|
56
|
+
new=Bitfield.new([instr].pack('B*'), &blk)
|
57
|
+
if @endian==:little
|
58
|
+
# This is so we know to flip the bytes back in #to_s
|
59
|
+
new.instance_variable_set :@endian_flip_hack, true
|
60
|
+
end
|
61
|
+
@fields << new
|
62
|
+
# Add direct references and accessor methods to the containing Binstruct
|
63
|
+
new.fields.each {|f|
|
64
|
+
unless f.is_a? Fields::Field
|
65
|
+
raise RuntimeError, "Binstruct: Construction: Illegal content #{f.class} in bitfield - use only Fields"
|
66
|
+
end
|
67
|
+
@hash_references[f.name]=f
|
68
|
+
meta_def f.name do f.get_value end
|
69
|
+
meta_def (f.name.to_s + '=').to_sym do |val| f.set_value(val) end
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates a nested structure, and a direct accesor for it that returns the
|
74
|
+
# structure itself, so accessors like <code>main.sub1.sub2.some_val</code>
|
75
|
+
# are possible.
|
76
|
+
# When iterating over the Binstruct contents, see +#each+, which will pass
|
77
|
+
# substructs to the block and +#deep_each+ which recursively enters
|
78
|
+
# substructs and passes only Fields.
|
79
|
+
def substruct(strbuf, name, len, klass)
|
80
|
+
new=klass.new(strbuf)
|
81
|
+
@fields << new
|
82
|
+
@hash_references[name]=new
|
83
|
+
meta_def name do new end
|
84
|
+
# More informative than the NoMethodError they would normally get.
|
85
|
+
meta_def (name.to_s + '=').to_sym do raise NoMethodError, "Binstruct: Illegal call of '=' on a substruct." end
|
86
|
+
end
|
87
|
+
|
88
|
+
#fieldtype builders
|
89
|
+
Fields::Field_Subtypes.each {|fieldname|
|
90
|
+
field_klass=Fields.const_get(fieldname.capitalize.to_s+"Field")
|
91
|
+
define_method fieldname do |*args|
|
92
|
+
bitbuf, name, len, desc=args
|
93
|
+
@fields << thisfield=field_klass.new(bitbuf.slice!(0,len),name,len,desc,nil,@endian||:big)
|
94
|
+
@hash_references[name.to_sym]=thisfield
|
95
|
+
meta_def name do thisfield.get_value end
|
96
|
+
meta_def (name.to_s + '=').to_sym do |val| thisfield.set_value(val) end
|
97
|
+
end
|
98
|
+
}
|
99
|
+
|
100
|
+
# Groups a list of fields under +:groupname+. Designed for use in Metafuzz.
|
101
|
+
# +somestruct.groups+ will return the hash of <code>{:group_sym=>[field1,
|
102
|
+
# field2...]}</code>
|
103
|
+
def group( groupname, *fieldsyms )
|
104
|
+
@groups[groupname] << fieldsyms
|
105
|
+
end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
attr_reader :init_block
|
109
|
+
end
|
110
|
+
|
111
|
+
# There are two ways to create a Binstruct subclass, one is by calling
|
112
|
+
# parse inside the structure definition:
|
113
|
+
# class Foo < Binstruct
|
114
|
+
# parse {|buffer_as_binary|
|
115
|
+
# #definitions here
|
116
|
+
# }
|
117
|
+
# end
|
118
|
+
# and the other is by just supplying a block to new:
|
119
|
+
# quick_struct=Binstruct.new {|b| string, b, :foo, 32, "Some String"}
|
120
|
+
# Otherwise, +Binstruct.new+ will just create a blank structure (this can
|
121
|
+
# be useful if you want to fill in the fields at runtime).
|
122
|
+
def self.parse( &blk )
|
123
|
+
@init_block=blk
|
124
|
+
end
|
125
|
+
|
126
|
+
#------------END CONSTRUCTION------------------
|
127
|
+
|
128
|
+
def initialize(buffer=nil, &blk)
|
129
|
+
@fields=[]
|
130
|
+
@hash_references={}
|
131
|
+
@endian_flip_hack=false
|
132
|
+
@groups=Hash.new {|h,k| h[k]=[]}
|
133
|
+
buffer||=""
|
134
|
+
@bitbuf=buffer.unpack('B*').join
|
135
|
+
if block_given?
|
136
|
+
instance_exec(@bitbuf, &blk)
|
137
|
+
elsif self.class.init_block
|
138
|
+
instance_exec(@bitbuf, &self.class.init_block)
|
139
|
+
else
|
140
|
+
# do nothing, user probably just wants a blank struct to manually add fields.
|
141
|
+
end
|
142
|
+
endian :big unless @endian
|
143
|
+
@groups.each {|group, contents|
|
144
|
+
unless contents.flatten.all? {|sym| @hash_references.keys.any? {|othersym| othersym==sym}}
|
145
|
+
raise RuntimeError, "Binstruct: Construction: group #{group} contains invalid field name(s)"
|
146
|
+
end
|
147
|
+
}
|
148
|
+
# This is not ideal for structures that aren't byte aligned, but raising an exception
|
149
|
+
# would be less flexible.
|
150
|
+
buffer.replace @bitbuf.scan(/.{8}/).map {|e| e.to_i(2).chr}.join unless buffer.nil?
|
151
|
+
end
|
152
|
+
|
153
|
+
#----------------INSTANCE----------------------
|
154
|
+
|
155
|
+
# return an object, specified by symbol. May be a field or a substruct.
|
156
|
+
# not designed for bitfields, since they're supposed to be invisible
|
157
|
+
# containers.
|
158
|
+
def []( sym )
|
159
|
+
@hash_references[sym]
|
160
|
+
end
|
161
|
+
|
162
|
+
# yield each object to the block. This is a little messy, because
|
163
|
+
# substructs are not Fields::Field types. For Bitfields, just silently
|
164
|
+
# yield each component, not the container field. The upshot of all this
|
165
|
+
# is that the caller needs to be prepared for a Field or a Binstruct in the
|
166
|
+
# block. This is the 'shallow' each.
|
167
|
+
def each( &blk ) #:yields: a Field or a Bitstruct
|
168
|
+
@fields.each {|atom|
|
169
|
+
if atom.is_a? Bitfield
|
170
|
+
atom.fields.each {|f| yield f}
|
171
|
+
else
|
172
|
+
yield atom
|
173
|
+
end
|
174
|
+
}
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
# yield all fields in the structure, entering nested substructs as necessary
|
179
|
+
def deep_each( &blk ) #:yields: a Field
|
180
|
+
@fields.each {|atom|
|
181
|
+
if atom.is_a? Binstruct
|
182
|
+
atom.deep_each &blk unless atom.fields.empty?
|
183
|
+
else
|
184
|
+
yield atom
|
185
|
+
end
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
# Searches a Binstruct, recursing through nested structures as neccessary,
|
190
|
+
# and replaces a given object with a new object. Note that this replaces
|
191
|
+
# the object that ==oldthing, so a reference to it is needed first.
|
192
|
+
def replace(oldthing, newthing)
|
193
|
+
k,v=@hash_references.select {|k,v| v==oldthing}.flatten
|
194
|
+
@hash_references[k]=newthing
|
195
|
+
@fields.map! {|atom|
|
196
|
+
if atom==oldthing
|
197
|
+
newthing
|
198
|
+
else
|
199
|
+
if atom.is_a? Binstruct
|
200
|
+
atom.replace(oldthing,newthing)
|
201
|
+
end
|
202
|
+
atom
|
203
|
+
end
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
# Flattens all fields in nested structures into an array, preserving order.
|
208
|
+
# In some cases (eg little endian structures with bitfields) this will mean
|
209
|
+
# that struct.flatten.join will not be the same as struct.to_s.
|
210
|
+
def flatten
|
211
|
+
a=[]
|
212
|
+
self.deep_each {|f| a << f}
|
213
|
+
a
|
214
|
+
end
|
215
|
+
|
216
|
+
#pack current struct as a string - for Fields, it will use the bitstring,
|
217
|
+
#for anything else (including Bitfields and Binstructs) it will use
|
218
|
+
#<code>to_s.unpack('B*')</code>. Via a filthy internal hack, bitfields
|
219
|
+
#get byte-swapped
|
220
|
+
#back in here. Finally, once the bitstring is assembled, it is right-padded
|
221
|
+
#with 0 and packed as a string.
|
222
|
+
def to_s
|
223
|
+
bits=@fields.inject("") {|str,field|
|
224
|
+
field.kind_of?(Fields::Field) ? str << field.bitstring : str << field.to_s.unpack('B*').join
|
225
|
+
}
|
226
|
+
unless bits.length % 8 == 0
|
227
|
+
#puts "Warning, structure not byte aligned, right padding with 0"
|
228
|
+
end
|
229
|
+
return "" if bits.empty?
|
230
|
+
bytearray=bits.scan(/.{1,8}/)
|
231
|
+
# If not byte aligned, right pad with 0
|
232
|
+
bytearray.last << '0'*(8-bytearray.last.length) if bytearray.last.length < 8
|
233
|
+
# This only happens for Binstructs that have the endian_flip_hack ivar
|
234
|
+
# set, so only inside the to_s of a Bitfield when little endian.
|
235
|
+
bytearray=bytearray.reverse if @endian_flip_hack
|
236
|
+
bytearray.map {|byte| "" << byte.to_i(2)}.join
|
237
|
+
end
|
238
|
+
|
239
|
+
# Packed length in bytes.
|
240
|
+
def length
|
241
|
+
self.to_s.length
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns an array of terse field descriptions which show the index, field
|
245
|
+
# class, name and length in bits, plus a hexdumped snippet of the contents.
|
246
|
+
def inspect
|
247
|
+
# This could possibly be more readable...
|
248
|
+
self.flatten.map {|field| "<IDX:#{self.flatten.index(field)}><#{field.class.to_s.match(/::(.+)Field/)[1]}><#{field.name}><#{field.length}><#{field.to_s[0..12].each_byte.to_a.map {|b| "%.2x" % b}.join(' ') + (field.to_s.length>12?"...":"")}>"}
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
data/lib/fields.rb
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
#If you add new Field subclasses, you need to name them with a capitalized 'Field' at the end, eg CrazyField, and use it in Binstruct by declaring the
|
2
|
+
#first half in lower case. Like this:
|
3
|
+
# module Fields
|
4
|
+
# class CrazyField < StringField
|
5
|
+
# # I act just like a StringField, only crunchy!
|
6
|
+
# end
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class MyStruct < Binstruct
|
10
|
+
# crazy :field_name, 32, "A Crazy Field!"
|
11
|
+
# [...]
|
12
|
+
#
|
13
|
+
#Take a look at the documentation for Fields::Field for some other guidlines on creating new Field subclasses that can't trivially
|
14
|
+
#inherit one of the base classes like Fields::StringField or Fields::HexstringField.
|
15
|
+
module Fields
|
16
|
+
|
17
|
+
#Subclasses of Fields::Field should probably at least overload <tt>input_to_bitstring</tt> and <tt>get_value</tt> and
|
18
|
+
#set <tt>@length_type</tt>. Check the source code for the base subclasses for ideas. Remember that for the
|
19
|
+
#purposes of fuzzing you can probably get away with
|
20
|
+
#inheriting from one of the base classes a lot of the time - an EmailField can probably just inherit StringField unless you want
|
21
|
+
#stringent checking in your parser.
|
22
|
+
class Field
|
23
|
+
|
24
|
+
attr_reader :bitstring, :desc, :length, :type, :default_value, :name, :length_type, :endianness
|
25
|
+
|
26
|
+
def initialize(bitstring, name, length, desc, default, endian)
|
27
|
+
@name=name
|
28
|
+
@length=length
|
29
|
+
@desc=desc
|
30
|
+
default&&=self.input_to_bitstring(default)
|
31
|
+
@default_value=default # can still be nil
|
32
|
+
@bitstring=self.parse_buffer(bitstring)
|
33
|
+
@type=self.class.to_s[/\w+(?=Field)/].downcase #fricking ugly
|
34
|
+
@endianness=endian
|
35
|
+
unless @endianness.downcase==:big or @endianness.downcase==:little
|
36
|
+
raise ArgumentError, "#{self.class.to_s[/\w+$/]} (#{@name}): Unknown endianness #{endianness}, use :little or :big (default)."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Provided for classes that require strict lengths. Left pads with 0 to @length
|
41
|
+
# if the input buffer is incomplete. Truncates at @length if too long. Called by <tt>parse_buffer</tt> internally.
|
42
|
+
def parse_buffer_strict( bitstring )
|
43
|
+
return "" unless bitstring
|
44
|
+
bitstring||=""
|
45
|
+
unless bitstring.is_a? String and bitstring=~/^[10]*$/
|
46
|
+
raise ArgumentError, "Field: <Internal> bitstring buffer borked?? #{bitstring.inspect}"
|
47
|
+
end
|
48
|
+
if bitstring.length <= @length
|
49
|
+
"0"*(@length-bitstring.length)+bitstring
|
50
|
+
elsif bitstring.length > @length
|
51
|
+
bitstring.slice(0,@length)
|
52
|
+
else
|
53
|
+
raise RuntimeError, "Universe Broken Error: value neither less/equal nor greater"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Provided for classes that don't care about length matching
|
58
|
+
# when assigning the contents. Called by <tt>parse_buffer</tt> internally.
|
59
|
+
def parse_buffer_lazy( bitstring )
|
60
|
+
return "" unless bitstring
|
61
|
+
bitstring||=""
|
62
|
+
unless bitstring.is_a? String and bitstring=~/^[10]*$/
|
63
|
+
raise ArgumentError, "#{self.class.to_s[/\w+$/]} (#{@name}): <Internal> bitstring buffer borked??"
|
64
|
+
end
|
65
|
+
bitstring
|
66
|
+
end
|
67
|
+
|
68
|
+
# Placeholder, subclasses should probably redefine for clarity. Defaults to calling parse_buffer_strict, but uses
|
69
|
+
# parse_buffer_lazy for variable length fields.
|
70
|
+
def parse_buffer( bitstring )
|
71
|
+
return self.parse_buffer_lazy( bitstring ) if self.length_type=="variable"
|
72
|
+
self.parse_buffer_strict( bitstring ) # default to this for subclasses that forget to define @length_type
|
73
|
+
end
|
74
|
+
|
75
|
+
#Sets the raw field contents. Can be useful if the set_value method does inconvenient checking when you want
|
76
|
+
#to set crazy values.
|
77
|
+
def set_raw( bitstring )
|
78
|
+
@bitstring=self.parse_buffer( bitstring )
|
79
|
+
end
|
80
|
+
|
81
|
+
# Placeholder, classes should override as neccessary.
|
82
|
+
# Parse a value in whatever format is determined by the class
|
83
|
+
# and return a bitstring.
|
84
|
+
# This default does zero checking, so expects a bitstring as input
|
85
|
+
def input_to_bitstring( value )
|
86
|
+
value
|
87
|
+
end
|
88
|
+
|
89
|
+
# Randomize the bitstring for this field, bit by bit
|
90
|
+
def randomize!
|
91
|
+
random_bitstring=Array.new(self.length).map {|e| e=rand(2).to_s}.join
|
92
|
+
if self.length_type=="variable"
|
93
|
+
slice=random_bitstring[0,(rand((self.length/8)+1)*8)]
|
94
|
+
set_raw(slice)
|
95
|
+
else
|
96
|
+
set_raw(random_bitstring)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#Set the field value. Calls self.input_to_bitstring which is expected to return a binary string.
|
101
|
+
def set_value(new_val)
|
102
|
+
@bitstring=self.input_to_bitstring(new_val)
|
103
|
+
end
|
104
|
+
|
105
|
+
#Subclasses should override this. This default returns the raw bitstring.
|
106
|
+
def get_value
|
107
|
+
@bitstring
|
108
|
+
end
|
109
|
+
|
110
|
+
# This really only makes sense for fields that are byte aligned, but what the hey. For the rest it effectively
|
111
|
+
# packs them as left padded with zeroes to a byte boundary (so a 3 bit field "110" will pack as \006)
|
112
|
+
def to_s
|
113
|
+
@bitstring.reverse.scan(/.{1,8}/).map {|s| s.reverse}.reverse.map {|bin| "" << bin.to_i(2)}.join
|
114
|
+
end
|
115
|
+
|
116
|
+
end # Field
|
117
|
+
|
118
|
+
#Accepts negative numbers on assignment, but stores them as 2s complement.
|
119
|
+
class UnsignedField < Field
|
120
|
+
|
121
|
+
def initialize *args
|
122
|
+
@length_type="fixed"
|
123
|
+
super
|
124
|
+
end
|
125
|
+
|
126
|
+
def input_to_bitstring( value )
|
127
|
+
unless value.kind_of? Integer
|
128
|
+
raise ArgumentError, "UnsignedField (#{@name}): attempted to assign non-integer"
|
129
|
+
end
|
130
|
+
if value.to_s(2).length > @length
|
131
|
+
# works for negative assignments as well, since to_s(2) adds a '-' to the start of the binary string
|
132
|
+
raise ArgumentError, "UnsignedField (#{@name}): value too big for field length"
|
133
|
+
end
|
134
|
+
# accept negative numbers, but store them as their two's complement representation for this bitlength
|
135
|
+
# (get_value will return the positive interpretation)
|
136
|
+
unpadded=value < 0 ? (("1"+"0"*@length).to_i(2)-value.abs).to_s(2) : value.to_s(2)
|
137
|
+
value="0"*(@length-unpadded.length)+unpadded # left pad with zeroes to full length
|
138
|
+
if self.endianness==:little
|
139
|
+
if value.length > 8 && value.length % 8 ==0
|
140
|
+
value=value.scan(/.{8}/).reverse.join
|
141
|
+
end
|
142
|
+
end
|
143
|
+
value
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_value
|
147
|
+
tempstring=@bitstring
|
148
|
+
if self.endianness==:little
|
149
|
+
if tempstring.length > 8 && tempstring.length % 8 ==0
|
150
|
+
tempstring=tempstring.scan(/.{8}/).reverse.join
|
151
|
+
end
|
152
|
+
end
|
153
|
+
tempstring.to_i(2)
|
154
|
+
end
|
155
|
+
|
156
|
+
end #UnsignedField
|
157
|
+
|
158
|
+
#For IP addresses etc
|
159
|
+
class OctetstringField < Field
|
160
|
+
|
161
|
+
def initialize(*args)
|
162
|
+
raise ArgumentError, "OctetstringField (#{args[1]})(CREATE): Length must be a multiple of 8 bits" unless args[2]%8==0
|
163
|
+
@length_type="fixed"
|
164
|
+
super
|
165
|
+
end
|
166
|
+
|
167
|
+
def input_to_bitstring( value )
|
168
|
+
# Expects [0-255][.0-255] .....
|
169
|
+
begin
|
170
|
+
unless value.split('.').all? {|elem| Integer(elem)<256 && Integer(elem) >= 0}
|
171
|
+
raise ArgumentError, "OctetstringField (#{@name})(PARSE): Unable to parse input value"
|
172
|
+
end
|
173
|
+
rescue
|
174
|
+
raise ArgumentError, "OctetstringField (#{@name})(PARSE): Unable to parse input value"
|
175
|
+
end
|
176
|
+
octets=value.split('.')
|
177
|
+
raise ArgumentError, "OctetstringField (#{@name}): Not enough octets?" if (octets.length)*8 < @length
|
178
|
+
raise ArgumentError, "OctetstringField (#{@name}): Too many octets?" if (octets.length*8) > @length
|
179
|
+
octets.inject("") do |str,num|
|
180
|
+
str << "%.8b"%num
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_value
|
185
|
+
@bitstring.scan(/.{8}/).map {|e| e.to_i(2).to_s}.join('.')
|
186
|
+
end
|
187
|
+
end #OctetstringField
|
188
|
+
|
189
|
+
|
190
|
+
#For getting and setting via hex strings.
|
191
|
+
class HexstringField < Field
|
192
|
+
|
193
|
+
def initialize *args
|
194
|
+
@length_type="variable"
|
195
|
+
super
|
196
|
+
end
|
197
|
+
|
198
|
+
def input_to_bitstring( value )
|
199
|
+
if (value.respond_to? :to_int) and value >= 0
|
200
|
+
bs=value.to_int.to_s(2)
|
201
|
+
elsif (value.respond_to? :to_str) and value.to_str=~/^[a-f0-9\s]*$/
|
202
|
+
bs=value.to_str.gsub(/\s/,'').to_i(16).to_s(2)
|
203
|
+
else
|
204
|
+
raise ArgumentError, "HexstringField (#{@name}): Unable to parse input value."
|
205
|
+
end
|
206
|
+
(bs.length % 8 != 0) && bs=("0"*(8-bs.length%8) + bs)
|
207
|
+
bs
|
208
|
+
end
|
209
|
+
|
210
|
+
def get_value
|
211
|
+
return '' if @bitstring==""
|
212
|
+
unless @bitstring.length > 8 && @bitstring.length % 8 == 0
|
213
|
+
#do the best we can..
|
214
|
+
hexstring=@bitstring.to_i(2).to_s(16)
|
215
|
+
(hexstring.length % 2 == 1) && hexstring="0"+hexstring
|
216
|
+
else
|
217
|
+
hexstring=bitstring.scan(/.{8}/).map {|e| "%.2x" % e.to_i(2)}.join
|
218
|
+
end
|
219
|
+
hexstring
|
220
|
+
end
|
221
|
+
|
222
|
+
end #HexstringField
|
223
|
+
|
224
|
+
class SignedField < Field
|
225
|
+
|
226
|
+
def initialize *args
|
227
|
+
@length_type="fixed"
|
228
|
+
super
|
229
|
+
end
|
230
|
+
|
231
|
+
def input_to_bitstring( value )
|
232
|
+
unless value.kind_of? Integer
|
233
|
+
raise ArgumentError, "SignedField (#{@name}): attempted to assign non-integer"
|
234
|
+
end
|
235
|
+
if value < 0 and value.to_s(2).length > @length
|
236
|
+
# int.to_s(2) will return "-1001001" etc for negative numbers in binary, so this length check will work.
|
237
|
+
raise ArgumentError, "SignedField (#{@name}): negative value too long for field length"
|
238
|
+
end
|
239
|
+
if value > 0 and value.to_s(2).length > @length-1 # positive integers shouldn't overflow the sign bit
|
240
|
+
raise ArgumentError, "SignedField (#{@name}): positive value too long for field length"
|
241
|
+
end
|
242
|
+
unpadded=value <= 0 ? (("1"+"0"*@length).to_i(2)-value.abs).to_s(2) : value.to_s(2)
|
243
|
+
value="0"*(@length-unpadded.length)+unpadded # left pad with zeroes to full length
|
244
|
+
if self.endianness==:little
|
245
|
+
if value.length > 8 && value.length % 8==0
|
246
|
+
value=value.scan(/.{8}/).reverse.join
|
247
|
+
end
|
248
|
+
end
|
249
|
+
value
|
250
|
+
end
|
251
|
+
|
252
|
+
def get_value
|
253
|
+
tempstring=@bitstring
|
254
|
+
if self.endianness==:little
|
255
|
+
if tempstring.length > 8 && tempstring.length % 8 ==0
|
256
|
+
tempstring=tempstring.scan(/.{8}/).reverse.join
|
257
|
+
end
|
258
|
+
end
|
259
|
+
if tempstring.slice(0,1)=="1" #sign bit set
|
260
|
+
0-(("1"+"0"*@length).to_i(2)-tempstring.to_i(2))
|
261
|
+
elsif @bitstring.slice(0,1)=="0"
|
262
|
+
tempstring.to_i(2)
|
263
|
+
else
|
264
|
+
raise RuntimeError, "SignedField: Ouch, internal contents screwed somehow."
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
end # SignedField
|
269
|
+
|
270
|
+
class StringField < Field
|
271
|
+
def initialize *args
|
272
|
+
@length_type="variable"
|
273
|
+
super
|
274
|
+
end
|
275
|
+
|
276
|
+
def input_to_bitstring( value )
|
277
|
+
unless value.respond_to? :to_str
|
278
|
+
raise ArgumentError, "StringField(#{@name}): Input value not a string."
|
279
|
+
end
|
280
|
+
value.to_str.unpack('B*').join
|
281
|
+
end
|
282
|
+
|
283
|
+
def get_value
|
284
|
+
return @bitstring if @bitstring==""
|
285
|
+
[@bitstring].pack('B*')
|
286
|
+
end
|
287
|
+
end # StringField
|
288
|
+
|
289
|
+
#For getting and setting via binary strings
|
290
|
+
class BitstringField < Field
|
291
|
+
|
292
|
+
def initialize *args
|
293
|
+
@length_type="fixed"
|
294
|
+
super
|
295
|
+
end
|
296
|
+
|
297
|
+
def input_to_bitstring( value )
|
298
|
+
unless value.respond_to? :to_str and value.to_str=~/^[01\s]*$/
|
299
|
+
raise ArgumentError, "BitstringField(#{@name}) (PARSE): Input value not a bitstring."
|
300
|
+
end
|
301
|
+
parse_buffer(value.to_str.gsub(/\s/,''))
|
302
|
+
end
|
303
|
+
|
304
|
+
def get_value
|
305
|
+
@bitstring
|
306
|
+
end
|
307
|
+
end #BitstringField
|
308
|
+
|
309
|
+
Field_Subtypes=self.constants.map {|const| const.to_s.sub(/Field/,'').downcase.to_sym if const=~/^.+Field/}.compact
|
310
|
+
end # Fields
|
data/lib/objhax.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A collection of useful code bits that have been pilfered from various sources
|
2
|
+
# on the Interweb.
|
3
|
+
|
4
|
+
class Object # :nodoc: all
|
5
|
+
#Return the metaclass of an object.
|
6
|
+
def metaclass
|
7
|
+
class << self
|
8
|
+
self
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
#Run a block in the context of the metaclass.
|
13
|
+
def meta_eval &blk; metaclass.instance_eval &blk; end
|
14
|
+
|
15
|
+
# Adds methods to a metaclass
|
16
|
+
def meta_def name, &blk
|
17
|
+
meta_eval { define_method name, &blk }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Defines an instance method within a class
|
21
|
+
def class_def name, &blk
|
22
|
+
class_eval { define_method name, &blk }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Deep copy of objects that can be handled with Marshal
|
26
|
+
def deep_copy
|
27
|
+
Marshal.load(Marshal.dump(self))
|
28
|
+
end
|
29
|
+
|
30
|
+
#Define, run and then undefine a method to fake instance_eval with arguments
|
31
|
+
#(not needed on 1.9)
|
32
|
+
def instance_exec(*args, &block)
|
33
|
+
mname = "__instance_exec_#{Thread.current.object_id.abs}"
|
34
|
+
class << self; self end.class_eval{ define_method(mname, &block) }
|
35
|
+
begin
|
36
|
+
ret = send(mname, *args)
|
37
|
+
ensure
|
38
|
+
class << self; self end.class_eval{ undef_method(mname) } rescue nil
|
39
|
+
end
|
40
|
+
ret
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: binstruct
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Nagy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-02 00:00:00 +08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.12.1
|
24
|
+
version:
|
25
|
+
description: BinStruct is a small class for defining structures that can be used to parse / manipulate binary data. It is similar in concept to BitStruct, but supports some things that are difficult to do with a pure pack / unpack approach. It is mainly designed to work with the Metafuzz fuzzing tools, but would possibly be useful to anyone working with binary data.
|
26
|
+
email:
|
27
|
+
- metafuzz@ben.iagu.net
|
28
|
+
executables:
|
29
|
+
- binstruct
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- History.txt
|
34
|
+
- Manifest.txt
|
35
|
+
- README.txt
|
36
|
+
files:
|
37
|
+
- History.txt
|
38
|
+
- Manifest.txt
|
39
|
+
- README.txt
|
40
|
+
- Rakefile
|
41
|
+
- bin/binstruct
|
42
|
+
- lib/binstruct.rb
|
43
|
+
- lib/fields.rb
|
44
|
+
- lib/objhax.rb
|
45
|
+
- test/test_binstruct.rb
|
46
|
+
has_rdoc: true
|
47
|
+
homepage: http://metafuzz.rubyforge.org/binstruct
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --main
|
51
|
+
- README.txt
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project: metafuzz
|
69
|
+
rubygems_version: 1.3.1
|
70
|
+
signing_key:
|
71
|
+
specification_version: 2
|
72
|
+
summary: BinStruct is a small class for defining structures that can be used to parse / manipulate binary data
|
73
|
+
test_files:
|
74
|
+
- test/test_binstruct.rb
|