# Copyright (c) 2012 CyberLeo, All Rights Reserved.
# http://wiki.cyberleo.net/wiki/CyberLeo/COPYRIGHT?version=4
require 'openssl'
require 'socket'
require 'zlib'
# Public: CocKnocker, the Cryptographically Obfuscated Contact Knocker
#
# This class is responsible for computing a cryptographic sequence of ports to
# knock, via UDP packets, to tickle a firewall to allow the source IP through.
class CocKnocker
# Public: Raised when 'generation' wraps 8 bits and none of the packets have
# been deemed acceptable. This should, in practice, never occur unless you
# have done something seriously wrong.
class Unacceptable < StandardError; end
# Public: See initializer documentation for descriptions of these attributes.
attr_accessor :key, :tgt_ip, :my_ip
# Public: A list of ports which should be avoided when knocking, due to their
# special meaning or likelihood of being blocked by either source or target
# internet service providers.
BAD_PORTS = [ 0, 25, 135, 139, 445 ].freeze
# Public: Packet encapsulation and handling class.
#
# Packet format is as follows:
# uint8_t generation - Integer to alter packet; increment this to generate
# a different packet in case the generated port list
# contains items that are found in the BAD_PORTS list.
# uint32_t ip - IP address of the node attempting to knock (the
# 'client'), to mitigate replay attacks.
# uint32_t time - UNIX Epoch timestamp of when the knock is occuring,
# to mitigate replay attacks.
# uint32_t crc - CRC32 checksum of the first 9 bytes of the encoded
# representation of the packet, to ensure a successful
# decode.
# All values are encoded in network byte order.
class Packet
# Public: See class documentation for descriptions of these attributes.
attr_accessor :generation, :ip, :time
# Public: Compute the CRC32 checksum of the passed binary packet string, to
# ensure that it matches what is encoded in the packet itself.
#
# string packet - Encoded packet to verify
#
# Returns true if the computed CRC32 checksum of the first 9 bytes of the
# provided packet matches the CRC32 checksum embedded in the last four
# bytes; false otherwise.
def self.valid?(packet)
packet.unpack('C5NN').last == Zlib.crc32(packet[0..8])
end
# Public: Unpack a packet for use.
#
# string packet - Encoded packet to decode
# Returns a Packet instance containing the decoded values; or false if the
# checksum does not match.
def self.unpack(packet)
return false unless self.valid?(packet)
data = packet.unpack('C5NN')
generation = data[0]
ip = data[1..4].map(&:to_s).join('.')
time = data[5]
crc = data[6]
self.new(generation, ip, time)
end
# Public: Instance initializer
#
# integer generation - See class documentation
# string ip - See class documentation
# integer time - See class documentation
def initialize(generation, ip, time)
@generation = generation
@ip = ip
@time = time
end
# Internal: Transform the generation, IP, time, and checksum into a packed
# binary string, according to the format set forth in the class docs.
#
# Returns a string containing the packed information.
def pack
a = [ generation ]
a.concat(ip.split('.').map(&:to_i))
a << time
p = a.pack('C5N')
a << Zlib.crc32(p)
a.pack('C5NN')
end
alias :to_s :pack
end
# Public: Instance initializer
#
# key - OpenSSL::PKey::RSA object representing a 192-bit RSA private key;
# used to obfuscate packets prior to their transmission, and to offer
# a means of authenticating the knocker during decryption.
# tgt_ip - String IP address of the firewall that should receive the knocks.
# my_ip - String public IP address of the device knocking; should match the
# IP seen by the firewall, to ensure security against replay attacks.
def initialize(key, tgt_ip, my_ip)
@key = key
@tgt_ip = tgt_ip
@my_ip = my_ip
@generation = 0
@time = Time.now
end
# Internal: Craft and return a packet object corresponding to the attributes
def packet
Packet.new(@generation, my_ip, @time.to_i)
end
# Internal: Encrypt the packet to the provided key
#
# Returns a string containing the packet encrypted against the key.
def encrypted_packet
key.private_encrypt(packet.to_s)
end
# Internal: Checks if the provided port list is acceptable.
#
# Returns true if the provided list does not contain any ports on the
# BAD_PORTS list, false otherwise.
def acceptable?(list)
BAD_PORTS - list == BAD_PORTS
end
# Internal: Format a packet into a sequenced series of port numbers suitable for
# knocking.
#
# Returns an array of integers between 0 and 65535 inclusive.
def port_list
out = []
stack = encrypted_packet.unpack('C*')
while a = stack.shift
b = stack.shift
c = stack.shift
out << ( ( a & 0xff ) << 4 ) + ( ( b & 0xf0 ) >> 4 )
out << ( ( b & 0x0f ) << 8 ) + ( c & 0xff )
end
out.each_with_index {|p, i| out[i] = ( ( p << 4 ) + i ) }
out
end
# Internal: Same as #port_list, but makes sure the port list is #acceptable?
#
# Returns an array of integers between 0 and 65535 inclusive, excluding ports
# in the BAD_PORTS list.
def safe_port_list
return @list if @list
@list = nil
until @list && acceptable?(@list)
@list = port_list
@generation += 1
raise Unacceptable if @generation > 255
end
@list
end
# Public: Perform a knock sequence
def knock!
sock = UDPSocket.new
safe_port_list.each {|port|
sock.send('', 0, tgt_ip, port)
}
end
class Listener
def self.decode(key, list)
# Sort the stack, to ensure the packets are in the correct order, and
# strip off packet indexes
stack = list.sort {|a,b|
( a & 0x000f ) <=> ( b & 0x000f )
}.map {|port|
( port & 0xfff0 ) >> 4
}
# Decode 12b16b encoding
out = []
while one = stack.shift
two = stack.shift
out << ( ( one & 0xff0 ) >> 4 )
out << ( ( one & 0x00f ) << 4 ) + ( ( two & 0xf00 ) >> 8 )
out << ( two & 0x0ff )
end
# Decrypt packet
data = out.pack('C*')
data = key.public_decrypt(data)
Packet.unpack(data)
end
end
end
key = OpenSSL::PKey::RSA.new(192)
my_ip = '10.0.0.2'
tgt_ip = '10.0.0.1'
#CocKnocker.new(key, tgt_ip, my_ip).knock!
c = CocKnocker.new(key, tgt_ip, my_ip)
puts c.packet.inspect
p = c.safe_port_list.sort
puts CocKnocker::Listener.decode(key, p).inspect