Class: PacketFu::Packet

Inherits:
Object
  • Object
show all
Defined in:
lib/packetfu/packet.rb

Overview

Packet is the parent class of EthPacket, IPPacket, UDPPacket, TCPPacket, and all other packets. It acts as both a singleton class, so things like Packet.parse can happen, and as an abstract class to provide subclasses some structure.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Packet

the Packet class should not be instantiated directly, since it’s an abstract class that real packet types inherit from. Sadly, this makes the Packet class more difficult to test directly.



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/packetfu/packet.rb', line 477

def initialize(args={})
  if self.class.name =~ /(::|^)PacketFu::Packet$/
    raise NoMethodError, "method `new' called for abstract class #{self.class.name}"
  end
  @inspect_style = args[:inspect_style] || PacketFu.inspect_style || :dissect
  if args[:config]
    args[:config].each_pair do |k,v|
      case k
      when :eth_daddr; @eth_header.eth_daddr=v if @eth_header
      when :eth_saddr; @eth_header.eth_saddr=v if @eth_header
      when :ip_saddr; @ip_header.ip_saddr=v		 if @ip_header
      when :iface; @iface = v
      end
    end
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(sym, *args, &block) ⇒ Object

method_missing() delegates protocol-specific field actions to the apporpraite class variable (which contains the associated packet type) This register-of-protocols style switch will work for the forseeable future (there aren’t /that/ many packet types), and it’s a handy way to know at a glance what packet types are supported.



506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'lib/packetfu/packet.rb', line 506

def method_missing(sym, *args, &block)
  case sym.to_s
  when /^is_([a-zA-Z0-9]+)\?/
    ptype = $1
    if PacketFu.packet_prefixes.index(ptype)
      self.send(:handle_is_identity, $1)
    else
      super
    end
  when /^([a-zA-Z0-9]+)_.+/
    ptype = $1
    if PacketFu.packet_prefixes.index(ptype)
      self.instance_variable_get("@#{ptype}_header").send(sym,*args, &block)
    else
      super
    end
  else
    super
  end
end

Instance Attribute Details

#flavorObject (readonly)

Packet Headers are responsible for their own specific flavor methods.



11
12
13
# File 'lib/packetfu/packet.rb', line 11

def flavor
  @flavor
end

#headersObject

All packets have a header collection, useful for determining protocol trees.



12
13
14
# File 'lib/packetfu/packet.rb', line 12

def headers
  @headers
end

#ifaceObject

Default inferface to send packets to



13
14
15
# File 'lib/packetfu/packet.rb', line 13

def iface
  @iface
end

#inspect_styleObject

Default is :dissect, can also be :hex or :default



14
15
16
# File 'lib/packetfu/packet.rb', line 14

def inspect_style
  @inspect_style
end

Class Method Details

.can_parse?(str) ⇒ Boolean

Packet subclasses must override this, since the Packet superclass can’t actually parse anything.

Returns:

  • (Boolean)


292
293
294
# File 'lib/packetfu/packet.rb', line 292

def self.can_parse?(str)
  false
end

.force_binary(str) ⇒ Object

Force strings into binary.



23
24
25
# File 'lib/packetfu/packet.rb', line 23

def self.force_binary(str)
  str.force_encoding Encoding::BINARY if str.respond_to? :force_encoding
end

.inherited(subclass) ⇒ Object

Register subclasses in PacketFu.packet_class to do all kinds of neat things that obviates those long if/else trees for parsing. It’s pretty sweet.



18
19
20
# File 'lib/packetfu/packet.rb', line 18

def self.inherited(subclass)
  PacketFu.add_packet_class(subclass)
end

.layerObject

Defines the layer this packet type lives at, based on the number of headers it requires. Note that this has little to do with the OSI model, since TCP/IP doesn’t really have Session and Presentation layers.

Ethernet and the like are layer 1, IP, IPv6, and ARP are layer 2, TCP, UDP, and other transport protocols are layer 3, and application protocols are at layer 4 or higher. InvalidPackets have an arbitrary layer 0 to distinguish them.

Because these don’t change much, it’s cheaper just to case through them, and only resort to counting headers if we don’t have a match – this makes adding protocols somewhat easier, but of course you can just override this method over there, too. This is merely optimized for the most likely protocols you see on the Internet.



261
262
263
264
265
266
267
268
269
270
# File 'lib/packetfu/packet.rb', line 261

def self.layer
  case self.name # Lol ran into case's fancy treatment of classes
  when /InvalidPacket$/; 0
  when /EthPacket$/; 1
  when /IPPacket$/, /ARPPacket$/, /LLDPPacket$/, /IPv6Packet$/; 2
  when /TCPPacket$/, /UDPPacket$/, /ICMPPacket$/; 3
  when /HSRPPacket$/; 4
  else; self.new.headers.size
  end
end

.layer_symbolObject



276
277
278
279
280
281
282
283
284
# File 'lib/packetfu/packet.rb', line 276

def self.layer_symbol
  case self.layer
  when 0; :invalid
  when 1; :link
  when 2; :internet
  when 3; :transport
  else; :application
  end
end

.parse(packet = nil, args = {}) ⇒ Object

Parse() creates the correct packet type based on the data, and returns the apporpiate Packet subclass object.

There is an assumption here that all incoming packets are either EthPacket or InvalidPacket types. This will be addressed pretty soon.

If application-layer parsing is /not/ desired, that should be indicated explicitly with an argument of :parse_app => false. Otherwise, app-layer parsing will happen.

It is no longer neccisary to manually add packet types here.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/packetfu/packet.rb', line 37

def self.parse(packet=nil,args={})
  parse_app = true if(args[:parse_app].nil? or args[:parse_app])
  force_binary(packet)
  if parse_app
    classes = PacketFu.packet_classes_by_layer
  else
    classes = PacketFu.packet_classes_by_layer_without_application
  end

  new_args = {}
  new_args[:on_ipv6] = true if PacketFu::IPv6Packet.can_parse?(packet)
  p = classes.detect { |pclass| pclass.can_parse?(packet) }.new(new_args)
  parsed_packet = p.read(packet,args)
end

Instance Method Details

#==(other) ⇒ Object

If two packets are represented as the same binary string, and they’re both actually PacketFu packets of the same sort, they’re equal.

The intuitive result is that a packet of a higher layer (like DNSPacket) can be equal to a packet of a lower level (like UDPPacket) as long as the bytes are equal (this can come up if a transport-layer packet has a hand-crafted payload that is identical to what would have been created by using an application layer packet)



199
200
201
202
203
# File 'lib/packetfu/packet.rb', line 199

def ==(other)
  return false unless other.kind_of? self.class
  return false unless other.respond_to? :to_s
  self.to_s == other.to_s
end

#cloneObject

Packets are bundles of lots of objects, so copying them is a little complicated – a dup of a packet is actually full of pass-by-reference stuff in the @headers, so if you change one, you’re changing all this copies, too.

Normally, this doesn’t seem to be a big deal, and it’s a pretty decent performance tradeoff. But, if you’re going to be creating a template packet to base a bunch of slightly different ones off of (like a fuzzer might), you’ll want to use clone()



187
188
189
# File 'lib/packetfu/packet.rb', line 187

def clone
  Packet.parse(self.to_s)
end

#dissectObject

Renders the dissection_table suitable for screen printing. Can take one or two arguments. If just the one, only that layer will be displayed take either a range or a number – if a range, only protos within that range will be rendered. If an integer, only that proto will be rendered.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/packetfu/packet.rb', line 370

def dissect
  dtable = self.dissection_table
  hex_body = nil
  if dtable.last.kind_of?(Array) and dtable.last.first == :body
    body = dtable.pop
    hex_body = hexify(body[1])
  end
  elem_widths = [0,0,0]
  dtable.each do |proto_table|
    proto_table[1].each do |elems|
      elems.each_with_index do |e,i|
        width = e.size
        elem_widths[i] = width if width > elem_widths[i]
      end
    end
  end
  total_width = elem_widths.inject(0) {|sum,x| sum+x}
  table = ""
  dtable.each do |proto|
    table << "--"
    table << proto[0]
    if total_width > proto[0].size
      table << ("-" * (total_width - proto[0].size + 2))
    else
      table << ("-" * (total_width + 2))
    end
    table << "\n"
    proto[1].each do |elems|
      table << "  "
      elems_table = []
      (0..2).each do |i|
        elems_table << ("%-#{elem_widths[i]}s" % elems[i])
      end
      table << elems_table.join("\s")
      table << "\n"
    end
  end
  if hex_body && !hex_body.empty?
    table << "-" * 66
    table << "\n"
    table << "00-01-02-03-04-05-06-07-08-09-0a-0b-0c-0d-0e-0f---0123456789abcdef\n"
    table << "-" * 66
    table << "\n"
    table << hex_body
  end
  table
end

#dissection_tableObject



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/packetfu/packet.rb', line 338

def dissection_table
  table = []
  @headers.each_with_index do |header,table_idx|
    proto = header.class.name.sub(/^.*::/,"")
    table << [proto,[]]
    header.class.members.each do |elem|
      elem_sym = elem.to_sym # to_sym needed for 1.8
      next if elem_sym == :body
      elem_type_value = []
      elem_type_value[0] = elem
      readable_element = "#{elem}_readable"
      if header.respond_to? readable_element
        elem_type_value[1] = header.send(readable_element)
      else
        elem_type_value[1] = header.send(elem)
      end
      elem_type_value[2] = header[elem.to_sym].class.name
      table[table_idx][1] << elem_type_value
    end
  end
  table
  if @headers.last.members.map {|x| x.to_sym }.include? :body
    body_part = [:body, self.payload, @headers.last.body.class.name]
  end
  table << body_part
end

#handle_is_identity(ptype) ⇒ Object



52
53
54
55
56
57
58
59
# File 'lib/packetfu/packet.rb', line 52

def handle_is_identity(ptype)
  idx = PacketFu.packet_prefixes.index(ptype.to_s.downcase)
  if idx
    self.kind_of? PacketFu.packet_classes[idx]
  else
    raise NoMethodError, "Undefined method `is_#{ptype}?' for #{self.class}."
  end
end

#hexify(str) ⇒ Object

Hexify provides a neatly-formatted dump of binary data, familar to hex readers.



297
298
299
300
301
302
303
304
305
306
# File 'lib/packetfu/packet.rb', line 297

def hexify(str)
  str.force_encoding(Encoding::BINARY) if str.respond_to? :force_encoding
  hexascii_lines = str.to_s.unpack("H*")[0].scan(/.{1,32}/)
  regex = Regexp.new('[\x00-\x1f\x7f-\xff]'.force_encoding('ASCII-8BIT'), Regexp::NOENCODING)
  chars = str.to_s.gsub(regex,'.')
  chars_lines = chars.scan(/.{1,16}/)
  ret = []
  hexascii_lines.size.times {|i| ret << "%-48s  %s" % [hexascii_lines[i].gsub(/(.{2})/,"\\1 "),chars_lines[i]]}
  ret.join("\n")
end

#inspectObject

For packets, inspect is overloaded as inspect_hex(0). Not sure if this is a great idea yet, but it sure makes the irb output more sane.

If you hate this, you can run PacketFu.toggle_inspect to return to the typical (and often unreadable) Object#inspect format.



439
440
441
442
443
444
445
446
447
448
# File 'lib/packetfu/packet.rb', line 439

def inspect
  case @inspect_style
  when :dissect
    self.dissect
  when :hex
    self.proto.join("|") + "\n" + self.inspect_hex
  else
    super
  end
end

#inspect_hex(arg = 0) ⇒ Object

If @inspect_style is :default (or :ugly), the inspect output is the usual inspect.

If @inspect_style is :hex (or :pretty), the inspect output is a much more compact hexdump-style, with a shortened set of packet header names at the top.

If @inspect_style is :dissect (or :verbose), the inspect output is the longer, but more readable, dissection of the packet. This is the default.

TODO: Have an option for colors. Everyone loves colorized irb output.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/packetfu/packet.rb', line 319

def inspect_hex(arg=0)
  case arg
  when :layers
    ret = []
    @headers.size.times do |i|
      ret << hexify(@headers[i])
    end
    ret
  when (0..9)
    if @headers[arg]
      hexify(@headers[arg])
    else
      nil
    end
  when :all
    inspect_hex(0)
  end
end

#kind_of?(klass) ⇒ Boolean

Returns:

  • (Boolean)


420
421
422
423
424
425
426
427
428
429
430
431
# File 'lib/packetfu/packet.rb', line 420

def kind_of?(klass)
  return true if orig_kind_of? klass
  packet_types = proto.map {|p| PacketFu.const_get("#{p}Packet")}
  match = false
  packet_types.each do |p|
    if p.ancestors.include? klass
      match =  true
      break
    end
  end
  return match
end

#layerObject



272
273
274
# File 'lib/packetfu/packet.rb', line 272

def layer
  self.class.layer
end

#layer_symbolObject



286
287
288
# File 'lib/packetfu/packet.rb', line 286

def layer_symbol
  self.class.layer_symbol
end

#orig_kind_of?Object



418
# File 'lib/packetfu/packet.rb', line 418

alias :orig_kind_of? :kind_of?

#payloadObject

Get the outermost payload (body) of the packet; this is why all packet headers should have a body type.



73
74
75
# File 'lib/packetfu/packet.rb', line 73

def payload
  @headers.last.body
end

#payload=(args) ⇒ Object

Set the outermost payload (body) of the packet.



78
79
80
# File 'lib/packetfu/packet.rb', line 78

def payload=(args)
  @headers.last.body=(args)
end

#peek(args = {}) ⇒ Object

Peek provides summary data on packet contents.

Each packet type should provide a peek_format.



208
209
210
211
212
213
214
215
# File 'lib/packetfu/packet.rb', line 208

def peek(args={})
  idx = @headers.reverse.map {|h| h.respond_to? peek_format}.index(true)
  if idx
    @headers.reverse[idx].peek_format
  else
    peek_format
  end
end

#peek_formatObject

The peek_format is used to display a single line of packet data useful for eyeballing. It should not exceed 80 characters. The Packet superclass defines an example peek_format, but it should hardly ever be triggered, since peek traverses the @header list in reverse to find a suitable format.

Format

* A one or two character protocol initial. It should be unique
* The packet size
* Useful data in a human-usable form.

Ideally, related peek_formats will all line up with each other when printed to the screen.

Example

tcp_packet.peek
#=> "T  1054 10.10.10.105:55000   ->   192.168.145.105:80 [......] S:adc7155b|I:8dd0"
tcp_packet.peek.size
#=> 79


240
241
242
243
244
245
# File 'lib/packetfu/packet.rb', line 240

def peek_format
  peek_data = ["?  "]
  peek_data << "%-5d" % self.to_s.size
  peek_data << "%68s" % self.to_s[0,34].unpack("H*")[0]
  peek_data.join
end

#protoObject Also known as: protocol

Returns an array of protocols contained in this packet. For example:

t = PacketFu::TCPPacket.new
=> 00 1a c5 00 00 00 00 1a c5 00 00 00 08 00 45 00   ..............E.
00 28 3c ab 00 00 ff 06 7f 25 00 00 00 00 00 00   .(<......%......
00 00 93 5e 00 00 ad 4f e4 a4 00 00 00 00 50 00   ...^...O......P.
40 00 4a 92 00 00                                 @.J...
t.proto
=> ["Eth", "IP", "TCP"]


465
466
467
468
469
# File 'lib/packetfu/packet.rb', line 465

def proto
  type_array = []
  self.headers.each {|header| type_array << header.class.to_s.split('::').last.gsub(/Header$/,'')}
  type_array
end

#read(str = nil, args = {}) ⇒ Object

Read() takes (and trusts) the io input and shoves it all into a well-formed Packet. Note that read is a destructive process, so any existing data will be lost.

A note on the :strip => true argument: If :strip is set, defined lengths of data will be believed, and any trailers (such as frame check sequences) will be chopped off. This helps to ensure well-formed packets, at the cost of losing perhaps important FCS data.

If :strip is false, header lengths are /not/ believed, and all data will be piped in. When capturing from the wire, this is usually fine, but recalculating the length before saving or re-transmitting will absolutely change the data payload; FCS data will become part of the TCP data as far as tcp_len is concerned. Some effort has been made to preserve the “real” payload for the purposes of checksums, but currently, it’s impossible to seperate new payload data from old trailers, so things like pkt.payload += “some data” will not work correctly.

So, to summarize; if you intend to alter the data, use :strip. If you don’t, don’t. Also, this is a horrid hack. Stripping is useful (and fun!), but the default behavior really should be to create payloads correctly, and /not/ treat extra FCS data as a payload.

Finally, packet subclasses should take two arguments: the string that is the data to be transmuted into a packet, as well as args. This superclass method is merely concerned with handling args common to many packet formats (namely, fixing packets on the fly)



166
167
168
169
170
171
172
173
174
175
# File 'lib/packetfu/packet.rb', line 166

def read(str=nil, args={})
  raise "Cannot parse `#{str}'" unless self.class.can_parse?(str)
  @eth_header.read(str)
  if args[:fix] || args[:recalc]
    ip_recalc(:ip_sum) if self.is_ip?
    recalc(:tcp) if self.is_tcp?
    recalc(:udp) if self.is_udp?
  end
  self
end

#recalc(arg = :all) ⇒ Object

Recalculates all the calcuated fields for all headers in the packet. This is important since read() wipes out all the calculated fields such as length and checksum and what all.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/packetfu/packet.rb', line 119

def recalc(arg=:all)
  case arg
  when :ip
    ip_recalc(:all)
  when :ipv6
    ipv6_recalc(:all)
  when :icmp
    icmp_recalc(:all)
  when :udp
    udp_recalc(:all)
  when :tcp
    tcp_recalc(:all)
  when :all
    ip_recalc(:all) if @ip_header
    ipv6_recalc(:all) if @ipv6_header
    icmp_recalc(:all) if @icmp_header
    udp_recalc(:all) if @udp_header
    tcp_recalc(:all) if @tcp_header
  else
    raise ArgumentError, "Recalculating `#{arg}' unsupported. Try :all"
  end
  @headers[0]
end

#respond_to?(sym, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/packetfu/packet.rb', line 527

def respond_to?(sym, include_private = false)
  if sym.to_s =~ /^(invalid|eth|arp|ip|icmp|udp|hsrp|tcp|ipv6)_/
    self.instance_variable_get("@#{$1}_header").respond_to? sym
  elsif sym.to_s =~ /^is_([a-zA-Z0-9]+)\?/
    if PacketFu.packet_prefixes.index($1)
      true
    else
      super
    end
  else
    super
  end
end

#sizeObject Also known as: length

Returns the size of the packet (as a binary string)



451
452
453
# File 'lib/packetfu/packet.rb', line 451

def size
  self.to_s.size
end

#to_f(filename = nil, mode = 'w') ⇒ Object

Put the entire packet into a libpcap file. XXX: this is a hack for now just to confirm that packets are getting created correctly. Now with append! XXX: Document this!

Raises:

  • (ArgumentError)


94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/packetfu/packet.rb', line 94

def to_f(filename=nil,mode='w')
  filename ||= 'out.pcap'
  mode = mode.to_s[0,1] + "b"
  raise ArgumentError, "Unknown mode: #{mode.to_s}" unless mode =~ /^[wa]/
  if(mode == 'w' || !(File.exist?(filename)))
    data = [PcapHeader.new, self.to_pcap].map {|x| x.to_s}.join
  else
    data = self.to_pcap
  end
  File.open(filename, mode) {|f| f.write data}
  return [filename, 1, data.size]
end

#to_pcap(args = {}) ⇒ Object

Converts a packet to libpcap format. Bit of a hack?



83
84
85
86
87
88
89
# File 'lib/packetfu/packet.rb', line 83

def to_pcap(args={})
  p = PcapPacket.new(:endian => args[:endian],
                    :timestamp => Timestamp.new.to_s,
                    :incl_len => self.to_s.size,
                    :orig_len => self.to_s.size,
                    :data => self)
end

#to_sObject

Get the binary string of the entire packet.



62
63
64
# File 'lib/packetfu/packet.rb', line 62

def to_s
  @headers[0].to_s
end

#to_w(iface = nil) ⇒ Object

Put the entire packet on the wire by creating a temporary PacketFu::Inject object. TODO: Do something with auto-checksumming?



109
110
111
112
113
114
# File 'lib/packetfu/packet.rb', line 109

def to_w(iface=nil)
  iface = (iface || self.iface || PacketFu::Config.new.config[:iface]).to_s
  inj = PacketFu::Inject.new(:iface => iface)
  inj.array = [@headers[0].to_s]
  inj.inject
end

#write(io) ⇒ Object

In the event of no proper decoding, at least send it to the inner-most header.



67
68
69
# File 'lib/packetfu/packet.rb', line 67

def write(io)
  @headers[0].write(io)
end