metasploit-framework/lib/packetfu/tcp.rb

361 lines
11 KiB
Ruby

require 'packetfu/tcpopts'
module PacketFu
# Implements the Explict Congestion Notification for TCPHeader.
#
# ==== Header Definition
#
#
# bit1 :n
# bit1 :c
# bit1 :e
class TcpEcn < BinData::MultiValue
bit1 :n
bit1 :c
bit1 :e
# Returns the TcpEcn field as an integer.
def to_i
(n << 2) + (c << 1) + e
end
end
# Implements flags for TCPHeader.
#
# ==== Header Definition
#
# bit1 :urg
# bit1 :ack
# bit1 :psh
# bit1 :rst
# bit1 :syn
# bit1 :fin
class TcpFlags < BinData::MultiValue
bit1 :urg
bit1 :ack
bit1 :psh
bit1 :rst
bit1 :syn
bit1 :fin
# Returns the TcpFlags as an integer.
def to_i
(urg << 5) + (ack << 4) + (psh << 3) + (rst << 2) + (syn << 1) + fin
end
end
# TCPHeader is a complete TCP struct, used in TCPPacket. Most IP traffic is TCP-based, by
# volume.
#
# For more on TCP packets, see http://www.networksorcery.com/enp/protocol/tcp.htm
#
# ==== Header Definition
#
# uint16be :tcp_src, :initial_value => lambda {tcp_calc_src}
# uint16be :tcp_dst
# uint32be :tcp_seq, :initial_value => lambda {tcp_calc_seq}
# uint32be :tcp_ack
# bit4 :tcp_hlen, :initial_value => 5 # Must recalc as options are set.
# bit3 :tcp_reserved
# tcp_ecn :tcp_ecn
# tcp_flags :tcp_flags
# uint16be :tcp_win, :initial_value => 0x4000 # WinXP's default syn packet
# uint16be :tcp_sum, :initial_value => 0 # Must set this upon generation.
# uint16be :tcp_urg
# string :tcp_opts
# rest :body
#
# See also TcpEcn, TcpFlags, TcpOpts
class TCPHeader < BinData::MultiValue
uint16be :tcp_src, :initial_value => lambda {tcp_calc_src}
uint16be :tcp_dst
uint32be :tcp_seq, :initial_value => lambda {tcp_calc_seq}
uint32be :tcp_ack
bit4 :tcp_hlen, :initial_value => 5 # Must recalc as options are set.
bit3 :tcp_reserved
tcp_ecn :tcp_ecn
tcp_flags :tcp_flags
uint16be :tcp_win, :initial_value => 0x4000 # WinXP's default syn packet
uint16be :tcp_sum, :initial_value => 0 # Must set this upon generation.
uint16be :tcp_urg
string :tcp_opts
rest :body
# Create a new TCPHeader object, and intialize with a random sequence number.
def initialize(args={})
@random_seq = rand(0xffffffff)
@random_src = rand_port
super
end
attr_accessor :flavor
# tcp_calc_hlen adjusts the header length to account for tcp_opts. Note
# that if tcp_opts does not fall on a 32-bit boundry, tcp_calc_hlen will
# additionally pad the option string with nulls. Most stacks avoid this
# eventuality by padding with NOP options at OS-specific points in the
# option field. The practical effect of this is, you should tcp_calc_hlen
# only when all the options are already set; otherwise, additional options
# will be lost to the reciever as \x00 is an EOL option. Additionally,
# (and this is almost certainly a bug), there is no sanity checking to
# ensure the final tcp_opts value is 44 bytes or less (any more will bleed
# over into the tcp payload). You are forewarned!
#
# If you would like to craft specifically malformed packets with
# nonsense lengths of opts fields, you should avoid tcp_calc_hlen
# altogether, and simply set the values for tcp_hlen and tcp_opts manually.
def tcp_calc_hlen
pad = (self.tcp_opts.to_s.size % 4)
if (pad > 0)
self.tcp_opts += ("\x00" * pad)
end
self.tcp_hlen = ((20 + self.tcp_opts.to_s.size) / 4)
end
def tcp_calc_seq
@random_seq
end
def tcp_calc_src
@random_src
end
# Generates a random high port. This is affected by packet flavor.
def rand_port
rand(0xffff - 1025) + 1025
end
# Returns the actual length of the TCP options.
def tcp_opts_len
tcp_opts.to_s.size * 4
end
# Returns a more readable option list. Note, it can lack fidelity on bad option strings.
# For more on TCP options, see the TcpOpts class.
def tcp_options
TcpOpts.decode(self.tcp_opts)
end
# Allows a more writable version of TCP options.
# For more on TCP options, see the TcpOpts class.
def tcp_options=(arg)
self.tcp_opts=TcpOpts.encode(arg)
end
# Equivalent to tcp_src
def tcp_sport
self.tcp_src
end
# Equivalent to tcp_src=
def tcp_sport=(arg)
self.tcp_src=(arg)
end
# Equivalent to tcp_dst
def tcp_dport
self.tcp_dst
end
# Equivalent to tcp_dst=
def tcp_dport=(arg)
self.tcp_dst=(arg)
end
# Recalculates calculated fields for TCP (except checksum which is at the Packet level).
def tcp_recalc(arg=:all)
case arg
when :tcp_hlen
tcp_calc_hlen
when :tcp_src
@random_tcp_src = rand_port
when :tcp_sport
@random_tcp_src = rand_port
when :tcp_seq
@random_tcp_seq = rand(0xffffffff)
when :all
tcp_calc_hlen
@random_tcp_src = rand_port
@random_tcp_seq = rand(0xffffffff)
else
raise ArgumentError, "No such field `#{arg}'"
end
end
end
# TCPPacket is used to construct TCP packets. They contain an EthHeader, an IPHeader, and a TCPHeader.
#
# == Example
#
# tcp_pkt = PacketFu::TCPPacket.new
# tcp_pkt.tcp_flags.syn=1
# tcp_pkt.tcp_dst=80
# tcp_pkt.tcp_win=5840
# tcp_pkt.tcp_options="mss:1460,sack.ok,ts:#{rand(0xffffffff)};0,nop,ws:7"
#
# tcp_pkt.ip_saddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
# tcp_pkt.ip_daddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
#
# tcp_pkt.recalc
# tcp_pkt.to_f('/tmp/tcp.pcap')
#
# == Parameters
# :eth
# A pre-generated EthHeader object.
# :ip
# A pre-generated IPHeader object.
# :flavor
# TODO: Sets the "flavor" of the TCP packet. This will include TCP options and the initial window
# size, per stack. There is a lot of variety here, and it's one of the most useful methods to
# remotely fingerprint devices. :flavor will span both ip and tcp for consistency.
# :type
# TODO: Set up particular types of packets (syn, psh_ack, rst, etc). This can change the initial flavor.
# :config
# A hash of return address details, often the output of Utils.whoami?
class TCPPacket < Packet
attr_accessor :eth_header, :ip_header, :tcp_header, :headers
def ethernet?; true; end
def ip?; true; end
def tcp?; true; end
def initialize(args={})
@eth_header = (args[:eth] || EthHeader.new)
@ip_header = (args[:ip] || IPHeader.new)
@tcp_header = (args[:tcp] || TCPHeader.new)
@tcp_header.flavor = args[:flavor].to_s.downcase
@ip_header.body = @tcp_header
@eth_header.body = @ip_header
@headers = [@eth_header, @ip_header, @tcp_header]
@ip_header.ip_proto=0x06
super
if args[:flavor]
tcp_calc_flavor(@tcp_header.flavor)
else
tcp_calc_sum
end
end
# Sets the correct flavor for TCP Packets. Recognized flavors are:
# windows, linux, freebsd
def tcp_calc_flavor(str)
ts_val = Time.now.to_i + rand(0x4fffffff)
ts_sec = rand(0xffffff)
case @tcp_header.flavor = str.to_s.downcase
when "windows" # WinXP's default syn
@tcp_header.tcp_win = 0x4000
@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
@tcp_header.tcp_src = rand(5000 - 1026) + 1026
@ip_header.ip_ttl = 64
when "linux" # Ubuntu Linux 2.6.24-19-generic default syn
@tcp_header.tcp_win = 5840
@tcp_header.tcp_options="MSS:1460,SACK.OK,TS:#{ts_val};0,NOP,WS:7"
@tcp_header.tcp_src = rand(61_000 - 32_000) + 32_000
@ip_header.ip_ttl = 64
when "freebsd" # Freebsd
@tcp_header.tcp_win = 0xffff
@tcp_header.tcp_options="MSS:1460,NOP,WS:3,NOP,NOP,TS:#{ts_val};#{ts_sec},SACK.OK,EOL,EOL"
@ip_header.ip_ttl = 64
else
@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
end
tcp_calc_sum
end
# tcp_calc_sum() computes the TCP checksum, and is called upon intialization. It usually
# should be called just prior to dropping packets to a file or on the wire.
#--
# This is /not/ delegated down to @tcp_header since we need info
# from the IP header, too.
#++
def tcp_calc_sum
checksum = (ip_src.to_i >> 16)
checksum += (ip_src.to_i & 0xffff)
checksum += (ip_dst.to_i >> 16)
checksum += (ip_dst.to_i & 0xffff)
checksum += 0x06 # TCP Protocol.
checksum += (ip_len.to_i - ((ip_hl.to_i) * 4))
checksum += tcp_src
checksum += tcp_dst
checksum += (tcp_seq.to_i >> 16)
checksum += (tcp_seq.to_i & 0xffff)
checksum += (tcp_ack.to_i >> 16)
checksum += (tcp_ack.to_i & 0xffff)
checksum += ((tcp_hlen << 12) +
(tcp_reserved << 9) +
(tcp_ecn.to_i << 6) +
tcp_flags.to_i
)
checksum += tcp_win
checksum += tcp_urg
chk_tcp_opts = (tcp_opts.to_s.size % 2 == 0 ? tcp_opts.to_s : tcp_opts.to_s + "\x00")
chk_tcp_opts.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
if (ip_len - ((ip_hl + tcp_hlen) * 4)) >= 0
real_tcp_payload = payload[0,( ip_len - ((ip_hl + tcp_hlen) * 4) )] # Can't forget those pesky FCSes!
else
real_tcp_payload = payload # Something's amiss here so don't bother figuring out where the real payload is.
end
chk_payload = (real_tcp_payload.size % 2 == 0 ? real_tcp_payload : real_tcp_payload + "\x00") # Null pad if it's odd.
chk_payload.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
checksum = checksum % 0xffff
checksum = 0xffff - checksum
checksum == 0 ? 0xffff : checksum
@tcp_header.tcp_sum = checksum
end
# Recalculates various fields of the TCP packet.
#
# ==== Parameters
#
# :all
# Recomputes all calculated fields.
# :tcp_sum
# Recomputes the TCP checksum.
# :tcp_hlen
# Recomputes the TCP header length. Useful after options are added.
def tcp_recalc(arg=:all)
case arg
when :tcp_sum
tcp_calc_sum
when :tcp_hlen
@tcp_header.tcp_recalc :tcp_hlen
when :all
@tcp_header.tcp_recalc :all
tcp_calc_sum
else
raise ArgumentError, "No such field `#{arg}'"
end
end
# Peek provides summary data on packet contents.
def peek(args={})
peek_data = ["T "]
peek_data << "%-5d" % self.to_s.size
peek_data << "%-21s" % "#{self.ip_saddr}:#{self.tcp_src}"
peek_data << "->"
peek_data << "%21s" % "#{self.ip_daddr}:#{self.tcp_dst}"
flags = ' ['
flags << (self.tcp_flags.urg.zero? ? "." : "U")
flags << (self.tcp_flags.ack.zero? ? "." : "A")
flags << (self.tcp_flags.psh.zero? ? "." : "P")
flags << (self.tcp_flags.rst.zero? ? "." : "R")
flags << (self.tcp_flags.syn.zero? ? "." : "S")
flags << (self.tcp_flags.fin.zero? ? "." : "F")
flags << '] '
peek_data << flags
peek_data << "S:"
peek_data << "%08x" % self.tcp_seq
peek_data << "|I:"
peek_data << "%04x" % self.ip_id
peek_data.join
end
end
end # module PacketFu