diff --git a/lib/packetfu.rb b/lib/packetfu.rb index 316b06f525..86bb6e0bb2 100644 --- a/lib/packetfu.rb +++ b/lib/packetfu.rb @@ -1,109 +1,4 @@ - -# :title: PacketFu Documentation -# :include: ../README -# :include: ../INSTALL -# :include: ../LICENSE - -$: << File.expand_path(File.dirname(__FILE__)) -require "packetfu/structfu" -require "ipaddr" -require 'rubygems' if RUBY_VERSION =~ /^1\.[0-8]/ - module PacketFu - - # Sets the expected byte order for a pcap file. See PacketFu::Read.set_byte_order - @byte_order = :little - - # Checks if pcaprub is loaded correctly. - @@pcaprub_loaded = false - - # PacketFu works best with Pcaprub version 0.8-dev (at least) - # The current (Aug 01, 2010) pcaprub gem is 0.9, so should be fine. - def self.pcaprub_platform_require - begin - require 'pcaprub' - rescue LoadError - return false - end - @@pcaprub_loaded = true - end - - pcaprub_platform_require - if @@pcaprub_loaded - if Pcap.version !~ /[0-9]\.[7-9][0-9]?(-dev)?/ # Regex for 0.7-dev and beyond. - @@pcaprub_loaded = false # Don't bother with broken versions - raise LoadError, "PcapRub not at a minimum version of 0.8-dev" - end - require "packetfu/capture" - require "packetfu/inject" - end - end +require 'packetfu/packetfu' -require "packetfu/pcap" -require "packetfu/packet" -require "packetfu/invalid" -require "packetfu/eth" -require "packetfu/ip" -require "packetfu/arp" -require "packetfu/icmp" -require "packetfu/udp" -require "packetfu/hsrp" # Depends on UDP -require "packetfu/tcp" -require "packetfu/ipv6" # This is pretty minimal. -require "packetfu/utils" -require "packetfu/config" - -module PacketFu - - # Version 1.0.0 was released July 31, 2010 - # Version 1.0.1 is unreleased. - VERSION = "1.0.1" - - # Returns the current version of PacketFu. Incremented every once - # in a while, when I remember - def self.version - PacketFu::VERSION - end - - # Returns the version in a binary format for easy comparisons. - def self.binarize_version(str) - if(str.respond_to?(:split) && str =~ /^[0-9]+(\.([0-9]+)(\.[0-9]+)?)?$/) - bin_major,bin_minor,bin_teeny = str.split(/\x2e/).map {|x| x.to_i} - bin_version = (bin_major.to_i << 16) + (bin_minor.to_i << 8) + bin_teeny.to_i - else - raise ArgumentError, "Compare version malformed. Should be \x22x.y.z\x22" - end - end - - # Returns true if the version is equal to or greater than the compare version. - # If the current version of PacketFu is "0.3.1" for example: - # - # PacketFu.at_least? "0" # => true - # PacketFu.at_least? "0.2.9" # => true - # PacketFu.at_least? "0.3" # => true - # PacketFu.at_least? "1" # => true after 1.0's release - # PacketFu.at_least? "1.12" # => false - # PacketFu.at_least? "2" # => false - def self.at_least?(str) - this_version = binarize_version(self.version) - ask_version = binarize_version(str) - this_version >= ask_version - end - - # Returns true if the current version is older than the compare version. - def self.older_than?(str) - this_version = binarize_version(self.version) - ask_version = binarize_version(str) - this_version < ask_version - end - - # Returns true if the current version is newer than the compare version. - def self.newer_than?(str) - return false if str == self.version - !self.older_than?(str) - end - -end - -# vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby diff --git a/lib/packetfu/INSTALL b/lib/packetfu/INSTALL deleted file mode 100644 index 8fc846e466..0000000000 --- a/lib/packetfu/INSTALL +++ /dev/null @@ -1,40 +0,0 @@ -== INSTALL - -Installation is pretty straightforward -- it's a gem now! - -$ rvm gem install packetfu - -Not using rvm? For shame! Get it now, it will make your life 100x better. - -$ links http://rvm.beginrescueend.com/ - -If you are installing from a source checkout, just run (as root / rvmsudo): - -$ rvmsudo ./setup.rb -$ sudo ruby ./setup.rb # If not on rvm, and seriously what is wrong with you? - -== Testing - -The easiest way to test the installation is to run PacketFu via -irb, using the example shell in the "examples" directory: - -% sudo irb -r packetfu-shell.rb - -After the banner, you should see something like: - ->>> Use $packetfu_default.config for salient networking details. -IP: 192.168.1.100 Mac: 00:1d:e0:54:2f:7e Gateway: 00:03:2f:32:a5:3c -Net: 192.168.1.0 Iface: wlan0 ->>> Packet capturing/injecting enabled. -<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><> - -If not, then Something Went Wrong. It's most likely that you have either -an older or broken version of pcaprub (try installing the version provided -with Metasploit), or you have a very, very old version of libpcap (version -0.9.4 is the oldest tested version, and there's really no reason to not -be at least on 1.0.0). - -== Complaints - -If things don't work out, please contact todb@planb-security.net, and I'll -try to get you all sorted out. diff --git a/lib/packetfu/LICENSE b/lib/packetfu/LICENSE deleted file mode 100644 index 7fbaa8b9b0..0000000000 --- a/lib/packetfu/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -== LICENSE - -Copyright (c) 2008-2010, Tod Beardsley -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Tod Beardsley nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY TOD BEARDSLEY ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL TOD BEARDSLEY BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/lib/packetfu/README b/lib/packetfu/README deleted file mode 100644 index 918420a8b7..0000000000 --- a/lib/packetfu/README +++ /dev/null @@ -1,28 +0,0 @@ -= PacketFu - -A library for reading a writing packets to an interface or to a libpcap-formatted file. -It is maintained at http://code.google.com/p/packetfu - -== Documentation - -PacketFu is rdoc-compatable. In the same directory as this file, run "rdoc" by itself, and then view doc/index.html with your favored browser. Once that's done, navigate at the top, and read up on how to create a Packet or Capture from an interface with show_live or whatever. - -== Requirements - -PcapRub: - -$ svn co http://www.metasploit.com/svn/framework3/trunk/external/pcaprub - -or - -$ rvm gem install pcaprub - -Marshall Beddoe's PcapRub is required only for packet reading and writing from a network interfaces (which is a pretty big only). PcapRub itself relies on libpcap 0.9.8 or later for packet injection. PcapRub also requires root privilieges to access the interface directly. - -== Examples - -PacketFu ships with dozens and dozens of tests, built on Test::Unit. These should give good pointers on how you're expected to use it. See the /tests directory. Furthermore, PacketFu also ships with packetfu-shell.rb, which should be run via IRB (as root, if you intend to use your interfaces). - -== Author - -PacketFu is maintained primarily by Tod Beardsley diff --git a/lib/packetfu/packetfu.rb b/lib/packetfu/packetfu.rb new file mode 100644 index 0000000000..e22b38917c --- /dev/null +++ b/lib/packetfu/packetfu.rb @@ -0,0 +1,93 @@ + +# :title: PacketFu Documentation +# :include: ../README +# :include: ../INSTALL +# :include: ../LICENSE + +cwd = File.expand_path(File.dirname(__FILE__)) + +$: << cwd + +require File.join(cwd,"packetfu","structfu") +require "ipaddr" +require 'rubygems' if RUBY_VERSION =~ /^1\.[0-8]/ + +module PacketFu + + # Sets the expected byte order for a pcap file. See PacketFu::Read.set_byte_order + @byte_order = :little + + # Checks if pcaprub is loaded correctly. + @pcaprub_loaded = false + + # PacketFu works best with Pcaprub version 0.8-dev (at least) + # The current (Aug 01, 2010) pcaprub gem is 0.9, so should be fine. + def self.pcaprub_platform_require + begin + require 'pcaprub' + rescue LoadError + return false + end + @pcaprub_loaded = true + end + + pcaprub_platform_require + if @pcaprub_loaded + if Pcap.version !~ /[0-9]\.[7-9][0-9]?(-dev)?/ # Regex for 0.7-dev and beyond. + @pcaprub_loaded = false # Don't bother with broken versions + raise LoadError, "PcapRub not at a minimum version of 0.8-dev" + end + require "packetfu/capture" + require "packetfu/inject" + end + + def self.pcaprub_loaded? + @pcaprub_loaded + end + + # Returns an array of classes defined in PacketFu + def self.classes + constants.map { |const| const_get(const) if const_get(const).kind_of? Class}.compact + end + + def self.add_packet_class(klass) + raise "Need a class" unless klass.kind_of? Class + if klass.name !~ /[A-Za-z0-9]Packet/ + raise "Packet classes should be named 'ProtoPacket'" + end + @packet_classes ||= [] + @packet_classes << klass + @packet_classes.sort! {|x,y| x.name <=> y.name} + end + + def self.packet_classes + @packet_classes || [] + end + + def self.packet_prefixes + return [] unless @packet_classes + @packet_classes.map {|p| p.to_s.split("::").last.to_s.downcase.gsub(/packet$/,"")} + end + +end + +def require_protos(cwd) + protos_dir = File.join(cwd, "packetfu", "protos") + Dir.new(protos_dir).each do |fname| + next unless fname[/\.rb$/] + begin + require File.join(protos_dir,fname) + rescue + warn "Warning: Could not load `#{fname}'. Skipping." + end + end +end + +require File.join(cwd,"packetfu","version") +require File.join(cwd,"packetfu","pcap") +require File.join(cwd,"packetfu","packet") +require_protos(cwd) +require File.join(cwd,"packetfu","utils") +require File.join(cwd,"packetfu","config") + +# vim: nowrap sw=2 sts=0 ts=2 ff=unix ft=ruby diff --git a/lib/packetfu/capture.rb b/lib/packetfu/packetfu/capture.rb similarity index 100% rename from lib/packetfu/capture.rb rename to lib/packetfu/packetfu/capture.rb diff --git a/lib/packetfu/config.rb b/lib/packetfu/packetfu/config.rb similarity index 100% rename from lib/packetfu/config.rb rename to lib/packetfu/packetfu/config.rb diff --git a/lib/packetfu/inject.rb b/lib/packetfu/packetfu/inject.rb similarity index 100% rename from lib/packetfu/inject.rb rename to lib/packetfu/packetfu/inject.rb diff --git a/lib/packetfu/packet.rb b/lib/packetfu/packetfu/packet.rb similarity index 68% rename from lib/packetfu/packet.rb rename to lib/packetfu/packetfu/packet.rb index 3b34d94087..217c8a7721 100644 --- a/lib/packetfu/packet.rb +++ b/lib/packetfu/packetfu/packet.rb @@ -1,136 +1,54 @@ module PacketFu # Packet is the parent class of EthPacket, IPPacket, UDPPacket, TCPPacket, and all - # other packets. + # 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. class Packet + attr_reader :flavor # Packet Headers are responsible for their own specific flavor methods. attr_accessor :headers # All packets have a header collection, useful for determining protocol trees. attr_accessor :iface # Default inferface to send packets to + # 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. + def self.inherited(subclass) + PacketFu.add_packet_class(subclass) + end + # Force strings into binary. def self.force_binary(str) str.force_encoding "binary" if str.respond_to? :force_encoding end - # parse_app makes a valiant attempt at picking out particular applications (beyond - # the transport layer). As of right now, this only accounts for HSRP. I don't really - # intend to get very far with this because I need a better way to parse packets anyway -- - # each packet type (including application layers) should be responsible for their own - # parsing rules. But, let's assume that'll never happen, so continue with this folly. - # - # This is an optional step, since it can lead to misidentified applications, depending - # on the strategy used to pick out app layers. For example, we really shouldn't have - # a rule that says that HTTP must be port 80, since it can easily be on 3128 or any - # other arbitrary port. However, we can say with certainty that HSRP must be dst port - # 1985, because that's what the RFC dictates. - def self.parse_app(parsed_packet,packet) - if parsed_packet.is_udp? - # Figure out UDP protocols (DNS, DHCP, etc) - # All HSRP is dst 224.0.0.2:1985 with a TTL of 1, so sayeth RFC 2281. - if( - parsed_packet.ip_ttl == 1 and - parsed_packet.ip_dst == 0xe0000002 and - parsed_packet.udp_dst == 1985 - ) - return HSRPPacket.new.read(packet) - else - return parsed_packet - end - elsif parsed_packet.is_tcp? - # Figure out TCP protocols (HTTP, SSH, etc) - return parsed_packet - else - # I don't know any others. - return parsed_packet - end - end - # Parse() creates the correct packet type based on the data, and returns the apporpiate - # Packet subclass. + # Packet subclass object. # # There is an assumption here that all incoming packets are either EthPacket - # or InvalidPacket types. + # 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. + # with an argument of :parse_app => false. Otherwise, app-layer parsing will happen. # - # New packet types should get an entry here. - def self.parse(packet,args={}) + # It is no longer neccisary to manually add packet types here. + def self.parse(packet=nil,args={}) parse_app = true if(args[:parse_app].nil? or args[:parse_app]) force_binary(packet) - if packet.size >= 14 # Min size for Ethernet. No check for max size, yet. - case packet[12,2] # Check the Eth protocol field. - when "\x08\x00" # It's IP. - if 1.respond_to? :ord - ipv = packet[14,1][0].ord >> 4 - else - ipv = packet[14,1][0] >> 4 - end - case ipv # Check the IP version field. - when 4; # It's IPv4. - case packet[23,1] # Check the IP protocol field. - when "\x06"; p = TCPPacket.new # Returns a TCPPacket. - when "\x11"; p = UDPPacket.new # Returns a UDPPacket. - when "\x01"; p = ICMPPacket.new # Returns an ICMPPacket. - else; p = IPPacket.new # Returns an IPPacket since we can't tell the transport layer. - end - else; p = IPPacket.new # Returns an IPPacket of this crazy IP version. - end - when "\x08\x06" # It's arp - if packet.size >= 28 # Min size for complete arp - p = ARPPacket.new - else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny arps. - end - when "\x86\xdd" # It's IPv6 - if packet.size >= 54 # Min size for a complete IPv6 packet. - p = IPv6Packet.new - else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny Ipv6. - end - else; p = EthPacket.new # Returns an EthPacket since we can't tell the network layer. - end + if parse_app + classes = PacketFu.packet_classes.select {|pclass| pclass.can_parse? packet} else - p = InvalidPacket.new # Not the right size for Ethernet (jumbo frames are okay) + classes = PacketFu.packet_classes.select {|pclass| pclass.can_parse? packet}.reject {|pclass| pclass.layer_symbol == :application} end + p = classes.sort {|x,y| x.layer <=> y.layer}.last.new parsed_packet = p.read(packet,args) - app_parsed_packet = parse_app ? parse_app(parsed_packet,packet) : nil - return app_parsed_packet || parsed_packet end - #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. - def method_missing(sym, *args, &block) - case sym.to_s - when /^invalid_/ - @invalid_header.send(sym,*args) - when /^eth_/ - @eth_header.send(sym,*args) - when /^arp_/ - @arp_header.send(sym,*args) - when /^ip_/ - @ip_header.send(sym,*args) - when /^icmp_/ - @icmp_header.send(sym,*args) - when /^udp_/ - @udp_header.send(sym,*args) - when /^hsrp_/ - @hsrp_header.send(sym,*args) - when /^tcp_/ - @tcp_header.send(sym,*args) - when /^ipv6_/ - @ipv6_header.send(sym,*args) + 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, "Unknown method `#{sym}' for this packet object." - end - end - - 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 - else - super + raise NoMethodError, "Undefined method `is_#{ptype}?' for #{self.class}." end end @@ -216,13 +134,6 @@ module PacketFu # 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. # - # TODO: This giant if tree is a mess, and worse, is decieving. You need to define - # actions both here and in parse(). All read() does is make a (good) guess as to - # what @headers to expect, and reads data to them. - # - # To take strings and turn them into packets without knowing ahead of time what kind of - # packet it is, use Packet.parse instead; parse() handles the figuring-out part. - # # 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. @@ -236,123 +147,18 @@ module PacketFu # correctly. # # So, to summarize; if you intend to alter the data, use :strip. If you don't, don't. Also, - # this is a horrid XXX hack. Stripping is useful (and fun!), but the default behavior really + # 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. # - # Update: This scheme is so lame. Need to fix. Seriously. - # Update: still sucks. Really. - def read(io,args={}) - begin - if io.size >= 14 - @eth_header.read(io) - eth_proto_num = io[12,2].unpack("n")[0] - if eth_proto_num == 0x0800 # It's IP. - if 1.respond_to? :ord - ipv = io[14].ord - else - ipv = io[14] - end - ip_hlen=(ipv & 0x0f) * 4 - ip_ver=(ipv >> 4) # It's IPv4. Other versions, all bets are off! - if ip_ver == 4 - ip_proto_num = io[23,1].unpack("C")[0] - @ip_header.read(io[14,ip_hlen]) - if ip_proto_num == 0x06 # It's TCP. - tcp_len = io[16,2].unpack("n")[0] - 20 - if args[:strip] # Drops trailers like frame check sequence (FCS). Often desired for cleaner packets. - tcp_all = io[ip_hlen+14,tcp_len] # Believe the tcp_len value; chop off anything that's not in range. - else - tcp_all = io[ip_hlen+14,0xffff] # Don't believe the tcp_len value; suck everything up. - end - tcp_hlen = ((tcp_all[12,1].unpack("C")[0]) >> 4) * 4 - if tcp_hlen.to_i >= 20 - @tcp_header.read(tcp_all) - @ip_header.body = @tcp_header - else # It's a TCP packet with an impossibly small hlen, so it can't be real TCP. Abort! Abort! - @ip_header.body = io[16,io.size-16] - end - elsif ip_proto_num == 0x11 # It's UDP. - udp_len = io[16,2].unpack("n")[0] - 20 - if args[:strip] # Same deal as with TCP. We might have stuff at the end of the packet that's not part of the payload. - @udp_header.read(io[ip_hlen+14,udp_len]) - else # ... Suck it all up. BTW, this will change the lengths if they are ever recalc'ed. Bummer. - @udp_header.read(io[ip_hlen+14,0xffff]) - end - @ip_header.body = @udp_header - elsif ip_proto_num == 1 # It's ICMP - @icmp_header.read(io[ip_hlen+14,0xffff]) - @ip_header.body = @icmp_header - else # It's an IP packet for a protocol we don't have a decoder for. - @ip_header.body = io[16,io.size-16] - end - else # It's not IPv4, so no idea what should come next. Just dump it all into an ip_header and ip payload. - @ip_header.read(io[14,ip_hlen]) - @ip_header.body = io[16,io.size-16] - end - @eth_header.body = @ip_header - elsif eth_proto_num == 0x0806 # It's ARP - @arp_header.read(io[14,0xffff]) # You'll nearly have a trailer and you'll never know what size. - @eth_header.body=@arp_header - @eth_header.body - elsif eth_proto_num == 0x86dd # It's IPv6 - @ipv6_header.read(io[14,0xffff]) - @eth_header.body=@ipv6_header - else # It's an Ethernet packet for a protocol we don't have a decoder for - @eth_header.body = io[14,io.size-14] - end - if (args[:fix] || args[:recalc]) - # Unfortunately, we cannot simply recalc with abandon, since - # we may have unaccounted trailers that will sneak into the checksum. - # The better way to handle this is to put trailers in their own - # StructFu field, but I'm not a-gonna right now. :/ - ip_recalc(:ip_sum) if respond_to? :ip_header - recalc(:tcp) if respond_to? :tcp_header - recalc(:udp) if respond_to? :udp_header - end - else # You're not big enough for Ethernet. - @invalid_header.read(io) - end - # @headers[0] - self - rescue ::Exception => e - # remove last header - # nested_types = self.headers.collect {|header| header.class} - # nested_types.pop # whatever this packet type is, we weren't able to parse it - self.headers.pop - return_header_type = self.headers[self.headers.length-1].class.to_s - retklass = PacketFu::InvalidPacket - seekpos = 0 - target_header = @invalid_header - case return_header_type.to_s - when "PacketFu::EthHeader" - retklass = PacketFu::EthPacket - seekpos = 0x0e - target_header = @eth_header - when "PacketFu::IPHeader" - retklass = PacketFu::IPPacket - seekpos = 0x0e + @ip_header.ip_hl * 4 - target_header = @ip_header - when "PacketFu::TCPHeader" - retklass = PacketFu::TCPPacket - seekpos = 0x0e + @ip_header.ip_hl * 4 + @tcpheader.tcp_hlen - target_header = @tcp_header - when "PacketFu::UDPHeader" - retklass = PacketFu::UDPPacket - when "PacketFu::ARPHeader" - retklass = PacketFu::ARPPacket - when "PacketFu::ICMPHeader" - retklass = PacketFu::ICMPPacket - when "PacketFu::IPv6Header" - retklass = PacketFu::IPv6Packet - else - end - - io = io[seekpos,io.length - seekpos] - target_header.body = io - p = retklass.new - p.headers = self.headers - p - raise e if $debug + # 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) + def read(args={}) + 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 end @@ -366,6 +172,55 @@ module PacketFu peek_data.join end + # 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. + def self.layer + case self.name # Lol ran into case's fancy treatment of classes + when /InvalidPacket$/; 0 + when /EthPacket$/; 1 + when /IPPacket$/, /ARPPacket$/, /IPv6Packet$/; 2 + when /TCPPacket$/, /UDPPacket$/, /ICMPPacket$/; 3 + when /HSRPPacket$/; 4 + else; self.new.headers.size + end + end + + def layer + self.class.layer + end + + def self.layer_symbol + case self.layer + when 0; :invalid + when 1; :link + when 2; :internet + when 3; :transport + else; :application + end + end + + def layer_symbol + self.class.layer_symbol + end + + # Packet subclasses must override this, since the Packet superclass + # can't actually parse anything. + def self.can_parse?(str) + false + end + # Hexify provides a neatly-formatted dump of binary data, familar to hex readers. def hexify(str) if str.respond_to? :force_encoding @@ -426,6 +281,21 @@ module PacketFu end end + alias :orig_kind_of? :kind_of? + + 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 + # 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. @@ -458,31 +328,6 @@ module PacketFu end alias_method :protocol, :proto - - # Returns true if this is an Invalid packet. Else, false. - def is_invalid? ; self.proto.include? "Invalid"; end - # Returns true if this is an Ethernet packet. Else, false. - def is_ethernet? ; self.proto.include? "Eth"; end - alias_method :is_eth?, :is_ethernet? - # Returns true if this is an IP packet. Else, false. - def is_ip? ; self.proto.include? "IP"; end - # Returns true if this is an TCP packet. Else, false. - def is_tcp? ; self.proto.include? "TCP"; end - # Returns true if this is an UDP packet. Else, false. - def is_udp? ; self.proto.include? "UDP"; end - # Returns true if this is an HSRP packet. Else, false. - def is_hsrp? ; self.proto.include? "HSRP"; end - # Returns true if this is an ARP packet. Else, false. - def is_arp? ; self.proto.include? "ARP"; end - # Returns true if this is an IPv6 packet. Else, false. - def is_ipv6? ; self.proto.include? "IPv6" ; end - # Returns true if this is an ICMP packet. Else, false. - def is_icmp? ; self.proto.include? "ICMP" ; end - # Returns true if this is an IPv6 packet. Else, false. - def is_ipv6? ; self.proto.include? "IPv6" ; end - # Returns true if the outermost layer has data. Else, false. - def has_data? ; self.payload.size.zero? ? false : true ; end - alias_method :length, :size def initialize(args={}) @@ -498,6 +343,46 @@ module PacketFu end end + #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. + 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 + + 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 + end # class Packet @@inspect_style = :pretty diff --git a/lib/packetfu/pcap.rb b/lib/packetfu/packetfu/pcap.rb similarity index 98% rename from lib/packetfu/pcap.rb rename to lib/packetfu/packetfu/pcap.rb index 56a5a9af01..e1a9134f4c 100644 --- a/lib/packetfu/pcap.rb +++ b/lib/packetfu/packetfu/pcap.rb @@ -416,7 +416,7 @@ module PacketFu # set_byte_order is pretty much totally deprecated. def set_byte_order(byte_order) - PacketFu.instance_variable_set("@byte_order",byte_order) + PacketFu.instance_variable_set(:@byte_order,byte_order) return true end @@ -450,7 +450,7 @@ module PacketFu arr = args[:arr] || args[:array] || [] ts = args[:ts] || args[:timestamp] || Time.now.to_i ts_inc = args[:ts_inc] || args[:timestamp_increment] - pkts = PcapFile.new.array_to_file(:endian => PacketFu.instance_variable_get("@byte_order"), + pkts = PcapFile.new.array_to_file(:endian => PacketFu.instance_variable_get(:@byte_order), :arr => arr, :ts => ts, :ts_inc => ts_inc) @@ -468,7 +468,7 @@ module PacketFu append = args[:append] Read.set_byte_order(byte_order) if [:big, :little].include? byte_order pf = PcapFile.new - pf.array_to_file(:endian => PacketFu.instance_variable_get("@byte_order"), + pf.array_to_file(:endian => PacketFu.instance_variable_get(:@byte_order), :arr => arr, :ts => ts, :ts_inc => ts_inc) diff --git a/lib/packetfu/arp.rb b/lib/packetfu/packetfu/protos/arp.rb similarity index 94% rename from lib/packetfu/arp.rb rename to lib/packetfu/packetfu/protos/arp.rb index c2c759c764..aebe84c7db 100644 --- a/lib/packetfu/arp.rb +++ b/lib/packetfu/packetfu/protos/arp.rb @@ -173,13 +173,29 @@ module PacketFu attr_accessor :eth_header, :arp_header + def self.can_parse?(str) + return false unless EthPacket.can_parse? str + return false unless str.size >= 28 + return false unless str[12,2] == "\x08\x06" + true + end + + def read(str=nil,args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @arp_header.read(str[14,str.size]) + @eth_header.body = @arp_header + super(args) + self + end + def initialize(args={}) @eth_header = EthHeader.new(args).read(args[:eth]) @arp_header = ARPHeader.new(args).read(args[:arp]) @eth_header.eth_proto = "\x08\x06" @eth_header.body=@arp_header - # Please send more flavors to todb-packetfu@planb-security.net. + # Please send more flavors to todb+packetfu@planb-security.net. # Most of these initial fingerprints come from one (1) sample. case (args[:flavor].nil?) ? :nil : args[:flavor].to_s.downcase.intern when :windows; @arp_header.body = "\x00" * 64 # 64 bytes of padding @@ -197,7 +213,6 @@ module PacketFu @headers = [@eth_header, @arp_header] super - end # Generates summary data for ARP packets. diff --git a/lib/packetfu/eth.rb b/lib/packetfu/packetfu/protos/eth.rb similarity index 97% rename from lib/packetfu/eth.rb rename to lib/packetfu/packetfu/protos/eth.rb index 3bc9227fc7..1b0f361273 100644 --- a/lib/packetfu/eth.rb +++ b/lib/packetfu/packetfu/protos/eth.rb @@ -245,10 +245,15 @@ module PacketFu class EthPacket < Packet attr_accessor :eth_header - def initialize(args={}) - @eth_header = EthHeader.new(args).read(args[:eth]) - @headers = [@eth_header] - super + def self.can_parse?(str) + str.size >= 14 + end + + def read(str=nil,args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + super(args) + return self end # Does nothing, really, since there's no length or @@ -257,6 +262,12 @@ module PacketFu @headers[0].inspect end + def initialize(args={}) + @eth_header = EthHeader.new(args).read(args[:eth]) + @headers = [@eth_header] + super + end + end end diff --git a/lib/packetfu/hsrp.rb b/lib/packetfu/packetfu/protos/hsrp.rb similarity index 86% rename from lib/packetfu/hsrp.rb rename to lib/packetfu/packetfu/protos/hsrp.rb index 29b38b709f..4d5a2fd9f5 100644 --- a/lib/packetfu/hsrp.rb +++ b/lib/packetfu/packetfu/protos/hsrp.rb @@ -141,6 +141,33 @@ module PacketFu attr_accessor :eth_header, :ip_header, :udp_header, :hsrp_header + def self.can_parse?(str) + return false unless str.size >= 54 + return false unless EthPacket.can_parse? str + return false unless IPPacket.can_parse? str + return false unless UDPPacket.can_parse? str + temp_packet = UDPPacket.new + temp_packet.read(str) + if temp_packet.ip_ttl == 1 and [temp_packet.udp_sport,temp_packet.udp_dport] == [1985,1985] + return true + else + return false + end + end + + def read(str=nil, args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ip_header.read(str[14,str.size]) + @eth_header.body = @ip_header + @udp_header.read(str[14+(@ip_header.ip_hlen),str.size]) + @ip_header.body = @udp_header + @hsrp_header.read(str[14+(@ip_header.ip_hlen)+8,str.size]) + @udp_header.body = @hsrp_header + super(args) + self + end + def initialize(args={}) @eth_header = EthHeader.new(args).read(args[:eth]) @ip_header = IPHeader.new(args).read(args[:ip]) @@ -150,7 +177,6 @@ module PacketFu @udp_header.body = @hsrp_header @ip_header.body = @udp_header @eth_header.body = @ip_header - @headers = [@eth_header, @ip_header, @udp_header, @hsrp_header] super end diff --git a/lib/packetfu/icmp.rb b/lib/packetfu/packetfu/protos/icmp.rb similarity index 88% rename from lib/packetfu/icmp.rb rename to lib/packetfu/packetfu/protos/icmp.rb index 5cfcadc305..a43e0f3c82 100644 --- a/lib/packetfu/icmp.rb +++ b/lib/packetfu/packetfu/protos/icmp.rb @@ -114,7 +114,26 @@ module PacketFu class ICMPPacket < Packet attr_accessor :eth_header, :ip_header, :icmp_header - + + def self.can_parse?(str) + return false unless str.size >= 54 + return false unless EthPacket.can_parse? str + return false unless IPPacket.can_parse? str + return false unless str[23,1] == "\x01" + return true + end + + def read(str=nil, args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ip_header.read(str[14,str.size]) + @eth_header.body = @ip_header + @icmp_header.read(str[14+(@ip_header.ip_hlen),str.size]) + @ip_header.body = @icmp_header + super(args) + self + end + def initialize(args={}) @eth_header = EthHeader.new(args).read(args[:eth]) @ip_header = IPHeader.new(args).read(args[:ip]) diff --git a/lib/packetfu/invalid.rb b/lib/packetfu/packetfu/protos/invalid.rb similarity index 82% rename from lib/packetfu/invalid.rb rename to lib/packetfu/packetfu/protos/invalid.rb index 3720f7327b..d63de22834 100644 --- a/lib/packetfu/invalid.rb +++ b/lib/packetfu/packetfu/protos/invalid.rb @@ -30,6 +30,20 @@ module PacketFu class InvalidPacket < Packet attr_accessor :invalid_header + # Any packet is potentially an invalid packet + def self.can_parse?(str) + true + end + + def self.layer + 0 + end + + def read(str=nil,args={}) + @invalid_header.read(str) + self + end + def initialize(args={}) @invalid_header = (args[:invalid] || InvalidHeader.new) @headers = [@invalid_header] diff --git a/lib/packetfu/ip.rb b/lib/packetfu/packetfu/protos/ip.rb similarity index 93% rename from lib/packetfu/ip.rb rename to lib/packetfu/packetfu/protos/ip.rb index 9b44665efa..d443080a6e 100644 --- a/lib/packetfu/ip.rb +++ b/lib/packetfu/packetfu/protos/ip.rb @@ -175,6 +175,11 @@ module PacketFu (ip_hl * 4) + body.to_s.length end + # Return the claimed header length + def ip_hlen + (ip_hl * 4) + end + # Calculate the true checksum of the packet. # (Yes, this is the long way to do it, but it's e-z-2-read for mathtards like me.) def ip_calc_sum @@ -289,6 +294,30 @@ module PacketFu attr_accessor :eth_header, :ip_header + def self.can_parse?(str) + return false unless str.size >= 34 + return false unless EthPacket.can_parse? str + if str[12,2] == "\x08\x00" + if 1.respond_to? :ord + ipv = str[14,1][0].ord >> 4 + else + ipv = str[14,1][0] >> 4 + end + return true if ipv == 4 + else + return false + end + end + + def read(str=nil, args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ip_header.read(str[14,str.size]) + @eth_header.body = @ip_header + super(args) + self + end + # Creates a new IPPacket object. def initialize(args={}) @eth_header = EthHeader.new(args).read(args[:eth]) diff --git a/lib/packetfu/ipv6.rb b/lib/packetfu/packetfu/protos/ipv6.rb similarity index 94% rename from lib/packetfu/ipv6.rb rename to lib/packetfu/packetfu/protos/ipv6.rb index c39df73de0..41f413a04b 100644 --- a/lib/packetfu/ipv6.rb +++ b/lib/packetfu/packetfu/protos/ipv6.rb @@ -203,12 +203,27 @@ module PacketFu attr_accessor :eth_header, :ipv6_header + def self.can_parse?(str) + return false unless EthPacket.can_parse? str + return false unless str.size >= 54 + return false unless str[12,2] == "\x86\xdd" + true + end + + def read(str=nil,args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ipv6_header.read(str[14,str.size]) + @eth_header.body = @ipv6_header + super(args) + self + end + def initialize(args={}) @eth_header = (args[:eth] || EthHeader.new) @ipv6_header = (args[:ipv6] || IPv6Header.new) @eth_header.eth_proto = 0x86dd @eth_header.body=@ipv6_header - @headers = [@eth_header, @ipv6_header] super end diff --git a/lib/packetfu/tcp.rb b/lib/packetfu/packetfu/protos/tcp.rb similarity index 97% rename from lib/packetfu/tcp.rb rename to lib/packetfu/packetfu/protos/tcp.rb index b69e46d06c..5c2a4d8f68 100644 --- a/lib/packetfu/tcp.rb +++ b/lib/packetfu/packetfu/protos/tcp.rb @@ -920,8 +920,32 @@ module PacketFu # A hash of return address details, often the output of Utils.whoami? class TCPPacket < Packet - attr_accessor :eth_header, :ip_header, :tcp_header, :headers - + attr_accessor :eth_header, :ip_header, :tcp_header + + def self.can_parse?(str) + return false unless str.size >= 54 + return false unless EthPacket.can_parse? str + return false unless IPPacket.can_parse? str + return false unless str[23,1] == "\x06" + return true + end + + def read(str=nil, args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ip_header.read(str[14,str.size]) + @eth_header.body = @ip_header + if args[:strip] + tcp_len = str[16,2].unpack("n")[0] - 20 + @tcp_header.read(str[14+(@ip_header.ip_hlen),tcp_len]) + else + @tcp_header.read(str[14+(@ip_header.ip_hlen),str.size]) + end + @ip_header.body = @tcp_header + super(args) + self + end + def initialize(args={}) @eth_header = (args[:eth] || EthHeader.new) @ip_header = (args[:ip] || IPHeader.new) diff --git a/lib/packetfu/udp.rb b/lib/packetfu/packetfu/protos/udp.rb similarity index 89% rename from lib/packetfu/udp.rb rename to lib/packetfu/packetfu/protos/udp.rb index 71410f65a3..159852d48e 100644 --- a/lib/packetfu/udp.rb +++ b/lib/packetfu/packetfu/protos/udp.rb @@ -127,7 +127,31 @@ module PacketFu class UDPPacket < Packet attr_accessor :eth_header, :ip_header, :udp_header - + + def self.can_parse?(str) + return false unless str.size >= 54 + return false unless EthPacket.can_parse? str + return false unless IPPacket.can_parse? str + return false unless str[23,1] == "\x11" + return true + end + + def read(str=nil, args={}) + raise "Cannot parse `#{str}'" unless self.class.can_parse?(str) + @eth_header.read(str) + @ip_header.read(str[14,str.size]) + @eth_header.body = @ip_header + if args[:strip] + udp_len = str[16,2].unpack("n")[0] - 20 + @udp_header.read(str[14+(@ip_header.ip_hlen),udp_len]) + else + @udp_header.read(str[14+(@ip_header.ip_hlen),str.size]) + end + @ip_header.body = @udp_header + super(args) + self + end + def initialize(args={}) @eth_header = EthHeader.new(args).read(args[:eth]) @ip_header = IPHeader.new(args).read(args[:ip]) diff --git a/lib/packetfu/structfu.rb b/lib/packetfu/packetfu/structfu.rb similarity index 96% rename from lib/packetfu/structfu.rb rename to lib/packetfu/packetfu/structfu.rb index bdde71e962..0e1dd9135d 100644 --- a/lib/packetfu/structfu.rb +++ b/lib/packetfu/packetfu/structfu.rb @@ -2,6 +2,7 @@ # to create meaningful binary data. module StructFu + # Normally, self.size and self.length will refer to the Struct # size as an array. It's a hassle to redefine, so this introduces some # shorthand to get at the size of the resultant string. @@ -106,6 +107,7 @@ module StructFu # Returns a two byte value as a packed string. def to_s + @packstr = (self.e == :big) ? "n" : "v" [(self.v || self.d)].pack(@packstr) end @@ -113,10 +115,12 @@ module StructFu # Int16be is a two byte value in big-endian format. class Int16be < Int16 + undef :endian= end # Int16le is a two byte value in little-endian format. class Int16le < Int16 + undef :endian= def initialize(v=nil, e=:little) super(v,e) @packstr = (self.e == :big) ? "n" : "v" @@ -132,6 +136,7 @@ module StructFu # Returns a four byte value as a packed string. def to_s + @packstr = (self.e == :big) ? "N" : "V" [(self.v || self.d)].pack(@packstr) end @@ -139,10 +144,12 @@ module StructFu # Int32be is a four byte value in big-endian format. class Int32be < Int32 + undef :endian= end # Int32le is a four byte value in little-endian format. class Int32le < Int32 + undef :endian= def initialize(v=nil, e=:little) super(v,e) end @@ -166,11 +173,11 @@ module StructFu class IntString < Struct.new(:int, :string, :mode) def initialize(string='',int=Int8,mode=nil) - unless int.respond_to?(:ancestors) && int.ancestors.include?(StructFu::Int) - raise StandardError, "Invalid length (#{int.inspect}) associated with this String." - else + if int < Int super(int.new,string,mode) calc + else + raise "IntStrings need a StructFu::Int for a length." end end diff --git a/lib/packetfu/utils.rb b/lib/packetfu/packetfu/utils.rb similarity index 100% rename from lib/packetfu/utils.rb rename to lib/packetfu/packetfu/utils.rb diff --git a/lib/packetfu/packetfu/version.rb b/lib/packetfu/packetfu/version.rb new file mode 100644 index 0000000000..1c2f149131 --- /dev/null +++ b/lib/packetfu/packetfu/version.rb @@ -0,0 +1,50 @@ +module PacketFu + + # Version 1.0.0 was released July 31, 2010 + # Version 1.0.1 is unreleased. + VERSION = "1.0.2" + + def self.version + VERSION + end + + # Returns the version in a binary format for easy comparisons. + def self.binarize_version(str) + if(str.respond_to?(:split) && str =~ /^[0-9]+(\.([0-9]+)(\.[0-9]+)?)?$/) + bin_major,bin_minor,bin_teeny = str.split(/\x2e/).map {|x| x.to_i} + bin_version = (bin_major.to_i << 16) + (bin_minor.to_i << 8) + bin_teeny.to_i + else + raise ArgumentError, "Compare version malformed. Should be \x22x.y.z\x22" + end + end + + # Returns true if the version is equal to or greater than the compare version. + # If the current version of PacketFu is "0.3.1" for example: + # + # PacketFu.at_least? "0" # => true + # PacketFu.at_least? "0.2.9" # => true + # PacketFu.at_least? "0.3" # => true + # PacketFu.at_least? "1" # => true after 1.0's release + # PacketFu.at_least? "1.12" # => false + # PacketFu.at_least? "2" # => false + def self.at_least?(str) + this_version = binarize_version(self.version) + ask_version = binarize_version(str) + this_version >= ask_version + end + + # Returns true if the current version is older than the compare version. + def self.older_than?(str) + return false if str == self.version + this_version = binarize_version(self.version) + ask_version = binarize_version(str) + this_version < ask_version + end + + # Returns true if the current version is newer than the compare version. + def self.newer_than?(str) + return false if str == self.version + !self.older_than?(str) + end + +end