metasploit-framework/lib/packetfu/tcpopts.rb

212 lines
7.7 KiB
Ruby

module PacketFu
# TcpOpts handles the translation of TCP option strings to human-readable form,
# and vice versa. It is nearly certain to be completely rewritten, though the
# syntax will remain the same.
#
# == Example
#
# tcp_pkt = PacketFu::TCPPacket.new
# tcp_pkt.tcp_options="nop,nop,sack.ok,ws:7,eol"
#
# It's usable now, but you should not trust attacker data, since certain
# combinations of malformed options will raise execeptions.
#
# See http://www.iana.org/assignments/tcp-parameters/ for the rules on sizes and such.
class TcpOpts
include Singleton
# XXX: Here be dragons!
#
# Like decode, encode requires fairly strict adherance to the various
# RFCs when it comes to lengths and expected data types. If you're
# trying to enter well-formed data, it should all work fine. If you're
# trying to something like fuzzing, then you'll probably have better
# luck using tcp_opts= instead.
#
# There are a few opportunities for attackers get get bad data passed
# from decode over to encode to produce unexpected results (SACK
# and timestamp manipulation seeming to be the most disasterous).
# So don't make any life-critical decisions based on these options
# remaining the same between the decode and encode functions.
def self.encode(str)
opts = str.split(/\s*,\s*/)
binary_opts = ''
opts.each do |opt|
binary_opts << case opt
when /^EOL$/i; "\x00"
when /^NOP$/i; "\x01"
when /^MSS\s*:/i; tcp_opts_short(2,opt)
when /^WS\s*:/i; tcp_opts_char(3,opt)
when /^SACK\.OK$/i; "\x04\x02"
when /^SACK\s*:/i
sack_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
[0x05,sack_opts.size+2,sack_opts].pack("CCa*")
when /^ECHO\s*:/i; tcp_opts_long(6,opt)
when /^ECHO.REPLY\s*:/i; tcp_opts_long(7,opt)
when /^TS\s*:/i
ts_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
[0x08,ts_opts.size+2,ts_opts].pack("CCa*")
when /^POCP$/i; "\x09\x02"
when /^POSP\s*:/i
posp = [10,3,(opt.split(/\s*:\s*/)[1].to_i << 6)].pack("C3")
when /^CC\s*:/i; tcp_opts_long(11,opt)
when /^CC.NEW\s*:/i; tcp_opts_long(12,opt)
when /^CC.ECHO\s*:/i; tcp_opts_long(13,opt)
when /^ALT.CRC\s*:/i; tcp_opts_char(14,opt)
when /^ALT.DATA\s*:/i; tcp_opts_variable(15,opt)
when /^Skeeter\s*:/i; tcp_opts_variable(16,opt)
when /^Bubba\s*:/i; tcp_opts_variable(17,opt)
when /^TCO\s*:/i; tcp_opts_char(18,opt)
when /^MD5\s*:/i; tcp_opts_variable(19,opt)
when /^QSR\s*:/i; tcp_opts_variable(27,opt) # TODO: Do the bitwise math to match the decode.
when /^0x[0-9a-f][0-9a-f]\s*:/i; tcp_opts_variable(opt[0,4].to_i(16),opt)
else
raise ArgumentError, "Invalid tcp_options format or entry. Perhaps you want tcp_opts?"
end
end
binary_opts
end
def self.tcp_opts_variable(optnum,optstr)
ret = [optnum,0,optstr.split(/\s*:\s*/)[1]]
ret[1] = ret.pack("CCH*").size
ret.pack("CCH*")
end
def self.tcp_opts_char(optnum,optstr)
[optnum,3,optstr.split(/\s*:\s*/)[1][0,3].to_i].pack("C3")
end
def self.tcp_opts_short(optnum,optstr)
[optnum,4,optstr.split(/\s*:\s*/)[1].to_i].pack("CCn")
end
def self.tcp_opts_long(optnum,optstr)
[optnum,6,optstr.split(/\s*:\s*/)[1].to_i].pack("CCN")
end
# XXX: Here be dragons!
#
# There are a few opportunities for attackers get get bad data passed
# from decode over to encode to produce unexpected results (SACK
# and timestamp manipulation seeming to be the most disasterous).
# So don't make any life-critical decisions based on these options
# remaining the same between the decode and encode functions.
def self.decode(str)
bare_opts = []
invalid_opts = false
invalid_opts = true if str.size > 44
opts = StringIO.new(str)
while opts.pos < opts.size
bare_opts << case opts.read(1)
when "\x00"; "\x00"
when "\x01"; "\x01"
else
arr = []
opts.seek(opts.pos-1,0) # No StringIO.read(-1)? Lame.
arr << opts.read(1)
sz = opts.read(1)
if sz.nil? # Every option needs a size.
invalid_opts = true
else
sz = sz.unpack("C")[0]
if sz <= 1 # Every option's size is 2 or greater.
invalid_opts = true
else
arr << sz
arr << opts.read(sz-2)
end
end
end
end
if invalid_opts
"INVALID:#{str}"
else
TcpOpts.translate(bare_opts)
end
end
# Get an array of TCP options as produced by decode() and translate it into
# something passing for human readable.
def self.translate(arr)
translated_opts = arr.collect do |opt|
case opt
when "\x00"; "EOL"
when "\x01"; "NOP"
else
if opt[2].class == String
TcpOpts.option_to_s(opt[0].unpack("C")[0],opt[2])
else
"INVALID:#{opt.pack("aC")}"
end
end
end
translated_opts.join(",")
end
# Option_to_s translates TCP option strings into a human-readable
# form (really, a nerd-readable form, and only if that nerd has
# a copy of the various RFC's handy). It makes big assumptions
# that the sizes are RFC correct by this point, what with the
# unpacks and other presentations; for example, SACK-OK options
# are always two bytes, and no payload, so if you get a SACK-OK
# with a payload, it will be invisible when you process it with
# option_to_s.
#
# Underruns and other argument errors /should/ be impossible.
# If you find one, please file a bug!
#
# If you require more precision than this (eg, to
# account for malformed options), you should probably do
# your own opts processing using the bare tcp_opts values.
#
# Eventually, option_to_s should get a lot smarter about
# weirdly-formed options. And it at least shouldn't raise
# exceptions.
def self.option_to_s(optnum,value)
case optnum
when 2; "MSS:#{value.unpack("n")}" # Max Segment Size
when 3; "WS:#{value.unpack("C")}" # Window Scale
when 4; "SACK.OK" # SACK Permitted
when 5; sack_opt = "SACK:" # SACK values
if value.size % 4 == 0 # Well formed or not.
edges = value.scan(/[\x00-\xff]{4}/).collect {|h| h.unpack("N")}.join(';')
else
edges = value.unpack("H*")[0]
end
sack_opt + edges
when 6; "ECHO:#{value.unpack("N")}" # Echo
when 7; "ECHO.REPLY:#{value.unpack("N")}" # Echo Reply
when 8; ts_opt = "TS:" # Timestamp and TS-echo reply
ts_opt << value[0,4].unpack("N")[0].to_s
ts_opt << ";"
ts_opt << value[4,4].unpack("N")[0].to_s
when 9; "POCP" # Partial Order Connection Permitted
when 10; "POSP:" + # Partial Order Service Profile
"%02d" % ((value.unpack("C")[0] >> 6).to_s(2)) # Partial Order bits
when 11; "CC:#{value.unpack("N")}" # Connection Count. RFC 1644 is hi-larious, btw.
when 12; "CC.NEW:#{value.unpack("N")}" # Connection Count New.
when 13; "CC.EHCO:#{value.unpack("N")}" # Conn. Count Echo.
when 14: "ALT.CRC:#{value.unpack("C")}" # Alt Checksum request
when 15: "ALT.DATA:#{value.unpack("H*")}" # Alt checksum data. I'm too dumb for this.
when 16: "Skeeter:#{value.unpack("H*")}" # Skeeter crypto.
when 17: "Bubba:#{value.unpack("H*")}" # Bubba crypto.
when 18: "TCO:#{value.unpack("C")}" # Trailer Checksum Option. Nobody knows what this is.
when 19: "MD5:#{value.unpack("H*")}" # MD5 Signature Option. Hash-signed TCP? Outrageous!
when 27: qsr_opt = "QSR:" # Quick-Start Request. Experimental in Jan 2007.
qsr_val = []
qsr_val << (value[0,1].unpack("C")[0] >> 4)
qsr_val << (value[0,1].unpack("C")[0] & 0x0f)
qsr_val << value[1,1].unpack("C")[0]
qsr_val << value[2,4].unpack("N")[0] # Note bits 30,31 RFC-SHOULD be zero.
qsr_opt + qsr_val.join(';')
# Pretty much everything else is obsolete, experiemental, unused, or undoc'ed.
else; "0x#{[optnum].pack("C").unpack("H*")[0].upcase}:#{value.unpack("H*")}"
end
end
end
end