binstruct 1.0.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/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
|