##
# packet.rb
# Created March, 2013
# By Ron Bowes
#
# See: LICENSE.md
#
# Builds and parses dnscat2 packets.
##
require 'controller/crypto_helper'
require 'libs/dnscat_exception'
require 'libs/hex'
module PacketHelper
def at_least?(data, needed)
return (data.length >= needed)
end
def exactly?(data, needed)
return (data.length == needed)
end
end
class Packet
extend PacketHelper
# Message types
MESSAGE_TYPE_SYN = 0x00
MESSAGE_TYPE_MSG = 0x01
MESSAGE_TYPE_FIN = 0x02
MESSAGE_TYPE_PING = 0xFF
MESSAGE_TYPE_ENC = 0x03
OPT_NAME = 0x0001
# OPT_TUNNEL = 0x0002 # Deprecated
# OPT_DATAGRAM = 0x0004 # Deprecated
# OPT_DOWNLOAD = 0x0008 # Deprecated
# OPT_CHUNKED_DOWNLOAD = 0x0010 # Deprecated
OPT_COMMAND = 0x0020
attr_reader :packet_id, :type, :session_id, :body
class SynBody
extend PacketHelper
attr_reader :seq, :options, :name
def initialize(options, params = {})
@options = options || raise(DnscatException, "options can't be nil!")
@seq = params[:seq] || raise(DnscatException, "params[:seq] can't be nil!")
if((@options & OPT_NAME) == OPT_NAME)
@name = params[:name] || raise(DnscatException, "params[:name] can't be nil when OPT_NAME is set!")
else
@name = "(unnamed)"
end
end
def SynBody.parse(data)
at_least?(data, 4) || raise(DnscatException, "Packet is too short (SYN)")
seq, options, data = data.unpack("nna*")
# Parse the option name, if it has one
name = nil
if((options & OPT_NAME) == OPT_NAME)
if(data.index("\0").nil?)
raise(DnscatException, "OPT_NAME set, but no null-terminated name given")
end
name, data = data.unpack("Z*a*")
else
name = "[unnamed]"
end
# Verify that that was the entire packet
if(data.length > 0)
raise(DnscatException, "Extra data on the end of an SYN packet :: #{data.unpack("H*")}")
end
return SynBody.new(options, {
:seq => seq,
:name => name,
})
end
def to_s()
return "[[SYN]] :: isn = %04x, options = %04x, name = %s" % [@seq, @options, @name]
end
def to_bytes()
result = [@seq, @options].pack("nn")
if((@options & OPT_NAME) == OPT_NAME)
result += [@name].pack("Z*")
end
return result
end
end
class MsgBody
extend PacketHelper
attr_reader :seq, :ack, :data
def initialize(options, params = {})
@options = options
@seq = params[:seq] || raise(DnscatException, "params[:seq] can't be nil!")
@ack = params[:ack] || raise(DnscatException, "params[:ack] can't be nil!")
@data = params[:data] || raise(DnscatException, "params[:data] can't be nil!")
end
def MsgBody.parse(options, data)
at_least?(data, 4) || raise(DnscatException, "Packet is too short (MSG norm)")
seq, ack = data.unpack("nn")
data = data[4..-1] # Remove the first four bytes
return MsgBody.new(options, {
:seq => seq,
:ack => ack,
:data => data,
})
end
def MsgBody.header_size(options)
return MsgBody.new(options, {
:seq => 0,
:ack => 0,
:data => '',
}).to_bytes().length()
end
def to_s()
return "[[MSG]] :: seq = %04x, ack = %04x, data = 0x%x bytes" % [@seq, @ack, data.length]
end
def to_bytes()
result = ""
seq = @seq || 0
ack = @ack || 0
result += [seq, ack, @data].pack("nna*")
return result
end
end
class FinBody
extend PacketHelper
attr_reader :reason
def initialize(options, params = {})
@options = options
@reason = params[:reason] || raise(DnscatException, "params[:reason] can't be nil!")
end
def FinBody.parse(options, data)
at_least?(data, 1) || raise(DnscatException, "Packet is too short (FIN)")
reason = data.unpack("Z*").pop
data = data[(reason.length+1)..-1]
if(data.length > 0)
raise(DnscatException, "Extra data on the end of a FIN packet")
end
return FinBody.new(options, {
:reason => reason,
})
end
def to_s()
return "[[FIN]] :: %s" % [@reason]
end
def to_bytes()
[@reason].pack("Z*")
end
end
class PingBody
extend PacketHelper
attr_reader :data
def initialize(options, params = {})
@options = options
@data = params[:data] || raise(DnscatException, "params[:data] can't be nil!")
end
def PingBody.parse(options, data)
at_least?(data, 3) || raise(DnscatException, "Packet is too short (PING)")
data = data.unpack("Z*").pop
return PingBody.new(options, {
:data => data,
})
end
def to_s()
return "[[PING]] :: %s" % [@data]
end
def to_bytes()
[@data].pack("Z*")
end
end
class EncBody
extend PacketHelper
SUBTYPE_INIT = 0x0000
SUBTYPE_AUTH = 0x0001
attr_reader :subtype, :flags
attr_reader :public_key_x, :public_key_y # SUBTYPE_INIT
attr_reader :authenticator # SUBTYPE_AUTH
def initialize(params = {})
@subtype = params[:subtype] || raise(DnscatException, "params[:subtype] is required!")
@flags = params[:flags] || raise(DnscatException, "params[:flags] is required!")
if(@subtype == SUBTYPE_INIT)
@public_key_x = params[:public_key_x] || raise(DnscatException, "params[:public_key_x] is required!")
@public_key_y = params[:public_key_y] || raise(DnscatException, "params[:public_key_y] is required!")
if(!@public_key_x.is_a?(Bignum) || !@public_key_y.is_a?(Bignum))
raise(DnscatException, "Public keys have to be Bignums! (Seen: #{@public_key_x.class} #{@public_key_y.class})")
end
elsif(@subtype == SUBTYPE_AUTH)
@authenticator = params[:authenticator] || raise(DnscatException, "params[:authenticator] is required!")
if(@authenticator.length != 32)
raise(DnscatException, "params[:authenticator] was the wrong size!")
end
else
raise(DnscatException, "Unknown subtype: #{@subtype}")
end
end
def EncBody.parse(data)
at_least?(data, 4) || raise(DnscatException, "ENC packet is too short!")
subtype, flags, data = data.unpack("nna*")
params = {
:subtype => subtype,
:flags => flags,
}
if(subtype == SUBTYPE_INIT)
exactly?(data, 64) || raise(DnscatException, "ENC packet is too short!")
public_key_x, public_key_y, data = data.unpack("a32a32a*")
params[:public_key_x] = CryptoHelper.binary_to_bignum(public_key_x)
params[:public_key_y] = CryptoHelper.binary_to_bignum(public_key_y)
elsif(subtype == SUBTYPE_AUTH)
exactly?(data, 32) || raise(DnscatException, "ENC packet is too short!")
authenticator, data = data.unpack("a32a*")
params[:authenticator] = authenticator
else
raise(DnscatException, "Unknown subtype: #{subtype}")
end
if(data != "")
raise(DnscatException, "Extra data on the end of an ENC packet")
end
return EncBody.new(params)
end
def to_s()
if(@subtype == SUBTYPE_INIT)
return "[[ENC|INIT]] :: flags = 0x%04x, pubkey = %s,%s" % [@flags, CryptoHelper.bignum_to_text(@public_key_x), CryptoHelper.bignum_to_text(@public_key_y)]
elsif(@subtype == SUBTYPE_AUTH)
return "[[ENC|AUTH]] :: flags = 0x%04x, authenticator = %s" % [@flags, @authenticator.unpack("H*").pop()]
else
raise(DnscatException, "Unknown subtype: #{@subtype}")
end
end
def to_bytes()
if(@subtype == SUBTYPE_INIT)
public_key_x = CryptoHelper.bignum_to_binary(@public_key_x)
public_key_y = CryptoHelper.bignum_to_binary(@public_key_y)
return [@subtype, @flags, public_key_x, public_key_y].pack("nna32a32")
elsif(@subtype == SUBTYPE_AUTH)
return [@subtype, @flags, @authenticator].pack("nna32")
else
raise(DnscatException, "Unknown subtype: #{@subtype}")
end
end
end
def ==(other_packet)
return self.to_bytes() == other_packet.to_bytes()
end
# You probably don't ever want to use this, call Packet.parse() or Packet.create_*() instead
def initialize(packet_id, type, session_id, body)
@packet_id = packet_id || rand(0xFFFF)
@type = type || raise(DnscatException, "type can't be nil!")
@session_id = session_id || raise(DnscatException, "session_id can't be nil!")
@body = body
end
def Packet.header_size(options)
return Packet.new(0, 0, 0, nil).to_bytes().length()
end
def Packet.parse_header(data)
at_least?(data, 5) || raise(DnscatException, "Packet is too short (header)")
# (uint16_t) packet_id
# (uint8_t) message_type
# (uint16_t) session_id
packet_id, type, session_id = data.unpack("nCn")
data = data[5..-1]
return packet_id, type, session_id, data
end
def Packet.peek_session_id(data)
_, _, session_id, _ = Packet.parse_header(data)
return session_id
end
def Packet.peek_type(data)
_, type, _, _ = Packet.parse_header(data)
return type
end
def Packet.parse(data, options = nil)
packet_id, type, session_id, data = Packet.parse_header(data)
if(type == MESSAGE_TYPE_SYN)
body = SynBody.parse(data)
elsif(type == MESSAGE_TYPE_MSG)
if(options.nil?)
raise(DnscatException, "Options are required when parsing MSG packets!")
end
body = MsgBody.parse(options, data)
elsif(type == MESSAGE_TYPE_FIN)
if(options.nil?)
raise(DnscatException, "Options are required when parsing FIN packets!")
end
body = FinBody.parse(options, data)
elsif(type == MESSAGE_TYPE_PING)
body = PingBody.parse(nil, data)
elsif(type == MESSAGE_TYPE_ENC)
body = EncBody.parse(data)
else
raise(DnscatException, "Unknown message type: 0x%x" % type)
end
return Packet.new(packet_id, type, session_id, body)
end
def Packet.create_syn(options, params = {})
return Packet.new(params[:packet_id], MESSAGE_TYPE_SYN, params[:session_id], SynBody.new(options, params))
end
def Packet.create_msg(options, params = {})
return Packet.new(params[:packet_id], MESSAGE_TYPE_MSG, params[:session_id], MsgBody.new(options, params))
end
def Packet.create_fin(options, params = {})
return Packet.new(params[:packet_id], MESSAGE_TYPE_FIN, params[:session_id], FinBody.new(options, params))
end
def Packet.create_ping(params = {})
return Packet.new(params[:packet_id], MESSAGE_TYPE_PING, params[:session_id], PingBody.new(nil, params))
end
def Packet.create_enc(params = {})
return Packet.new(params[:packet_id], MESSAGE_TYPE_ENC, params[:session_id], EncBody.new(params))
end
def to_s()
result = "[0x%04x] session = %04x :: %s\n" % [@packet_id, @session_id, @body.to_s]
result += Hex.to_s(to_bytes(), 2)
return result
end
def to_bytes()
result = [@packet_id, @type, @session_id].pack("nCn")
# If we set the body to nil, just return a header (this happens when determining the header length)
if(!@body.nil?)
result += @body.to_bytes()
end
return result
end
end