def __init__(self, client_ip: str, sniff_interface: str, stratum_file_path: str = '/etc/stratum_override.txt', num_pck_per_bit=1, log: logging.Logger = logging.getLogger('CP2Server-logging')): """ A CP2 Server that listens for incoming NTP requests and injects the manipulated stratum value to a config file on the harddrive :param log: """ self.log = log self.stratum_file_path = stratum_file_path self.num_pck_per_bit = num_pck_per_bit self.client_ip = client_ip self.scapy_wrapper = ScapyWrapper() self.sniff_interface = sniff_interface self.payload_to_send = 'Hello World' self.payload_pointer = -1 self.payload_bits = '' self.current_pck_num = 0 self.current_stratum = None self.zero_mode = False self.init_done = False self.init_counter = 0
def __init__(self, static_key_bits: str, log: logging.Logger = logging.getLogger('CP3Handler-logger')): """ The core of every CP3 server and client. Provides functions to work with packages and process them. :param static_key_bits: :param log: """ self.log = log self.static_key_bits = static_key_bits self.msg: str = '' self.scappy_wrapper = ScapyWrapper()
def __init__(self, address: str, static_key: str, sniff_interface: str = 'lo', self_ip_addr='192.168.0.1', payload: str = PAYLOAD_BITS_120, client_address: str = ADDR_2, log=logging.getLogger('CP1Interceptor-Logger')): super().__init__(address, static_key, sniff_interface, log) self.scapy_wrapper = ScapyWrapper() self.crypto_tools = NTPCrypto() self.self_ip_addr = self_ip_addr self.payload = payload self.client_address = client_address
def __init__(self, cp1_address_bits: str, static_decryption_key_bits: str, sniff_interface: str = 'lo', self_ip_addr: str = None, log: logging.Logger = logging.getLogger('CP1Server-logger')): self.log = log self._address = cp1_address_bits self.static_key_bits = static_decryption_key_bits self.init_pck_field = NTPField.TRANSMIT_TIMESTAMP self.sniff_interface = sniff_interface self.listen_session: CP1ClientSession = None self._listen_lock = Lock() self._release_listen = False self.self_ip_addr = self_ip_addr self.scapy_wrapper = ScapyWrapper() self.crypto_tools = NTPCrypto()
class CP3Handler: def __init__(self, static_key_bits: str, log: logging.Logger = logging.getLogger('CP3Handler-logger')): """ The core of every CP3 server and client. Provides functions to work with packages and process them. :param static_key_bits: :param log: """ self.log = log self.static_key_bits = static_key_bits self.msg: str = '' self.scappy_wrapper = ScapyWrapper() def read_incoming_pck(self, pck: CP3Package) -> bool: mode = pck.get_cp3_mode() if mode is CP3Mode.PCK_1: self.msg = pck.extract_payload() self.log.debug("CP3_Mode_1 package received and payload set: " + self.msg) return True elif mode is CP3Mode.PCK_2: self.msg += pck.extract_payload() self.log.debug( "CP3_Mode_1 package received and complete payload now: " + self.msg) decrypted_bytes = decrypt_bits( self.msg, BitArray(bin=self.static_key_bits).bytes) self.log.info("Decrypted payload: " + str(decrypted_bytes)) return True else: self.log.debug("Package had no corresponding CP3 mode") return False def create_cp3_pck(self, payload_bits: str, cp3_mode: CP3Mode, ntp_mode: NTPMode) -> NTP: assert len(payload_bits) == 64 pck = CP3Package() pck.add_payload(payload_bits) if cp3_mode is CP3Mode.PCK_1: pck.set_cp3_mode_1() elif cp3_mode is CP3Mode.PCK_2: pck.set_cp3_mode_2() pck.set_mode(NTPMode.to_bit_string(ntp_mode)) return pck.ntp() def _send_cp3_pck(self, payload_bits: str, ip_addr: str, cp3_mode: CP3Mode, ntp_mode: NTPMode): ntp = self.create_cp3_pck(payload_bits, cp3_mode, ntp_mode) self.log.debug("Sending of NTP Mode " + str(cp3_mode) + " package with payload: " + payload_bits + " to " + str(ip_addr)) self.scappy_wrapper.send(IP(dst=ip_addr) / UDP() / ntp) def send_pck_1(self, payload_bits: str, ip_addr: str, ntp_mode: NTPMode = NTPMode.CLIENT): self._send_cp3_pck(payload_bits, ip_addr, CP3Mode.PCK_1, ntp_mode) def send_pck_2(self, payload_bits: str, ip_addr: str, ntp_mode: NTPMode = NTPMode.CLIENT): self._send_cp3_pck(payload_bits, ip_addr, CP3Mode.PCK_2, ntp_mode) def __restore_ntp_pck(self, ntp: NTP) -> NTP: self.log.debug('Send timestamp for reconstruction: ' + str(ntp.sent)) sent_time_stamp = datetime.fromtimestamp( ntplib.ntp_to_system_time(ntp.sent)) sent_time_stamp = sent_time_stamp.replace(year=datetime.now().year) sent_time_stamp_as_ntp = ntplib.system_to_ntp_time( sent_time_stamp.timestamp()) ntp.sent = sent_time_stamp_as_ntp self.log.debug('Send timestamp after reconstruction: ' + str(ntp.sent)) pck = CP3Package(ntp) if NTPMode.from_bit_string(pck.mode()) is NTPMode.CLIENT: self.log.debug("Restored in Client mode") ntp.ref = 0 ntp.orig = 0 ntp.recv = 0 if NTPMode.from_bit_string(pck.mode()) is NTPMode.SERVER \ or NTPMode.from_bit_string(pck.mode()) is NTPMode.BROADCAST_SERVER: self.log.debug("Restored in Server mode") origin_last_32 = pck.origin_timestamp()[32:64] received_last_32 = pck.receive_timestamp()[32:64] transmit_first_32 = pck.origin_timestamp()[0:32] pck.set_origin_timestamp(transmit_first_32 + origin_last_32) pck.set_receive_timestamp(transmit_first_32 + received_last_32) ntp = pck.ntp() self.log.debug("Reconstruction complete.") #ntp.show() return ntp def restore_pck(self, pck: CP3Package) -> NTP: """ Restores the original values of the given CP3 packages, depending on the NTP mode. :param pck: :return: """ ntp = pck.ntp() return self.__restore_ntp_pck(ntp)
class CP1Interceptor(CP1Client): def __init__(self, address: str, static_key: str, sniff_interface: str = 'lo', self_ip_addr='192.168.0.1', payload: str = PAYLOAD_BITS_120, client_address: str = ADDR_2, log=logging.getLogger('CP1Interceptor-Logger')): super().__init__(address, static_key, sniff_interface, log) self.scapy_wrapper = ScapyWrapper() self.crypto_tools = NTPCrypto() self.self_ip_addr = self_ip_addr self.payload = payload self.client_address = client_address def listen(self): self.log.info( "Starting to listen for incoming NTP packages on interface: " + self.sniff_interface) while True: if self.self_ip_addr is None: pck = self.scapy_wrapper.next_ntp_packet(self.sniff_interface) else: pck = self.scapy_wrapper.next_ntp_packet_for_target( self.sniff_interface, self.self_ip_addr) self.log.info("NTP-Package received") self.handle_incoming_pck(pck) def handle_incoming_pck(self, pck: Packet): ntp_pck = pck[NTP] cp1_pck = CP1Package(ntp_pck) self.log.info('Received pck bits: ' + str(cp1_pck._raw)) if self.send_session is None: self.log.debug("Init new session (1).") self.send_session = CP1Session() next_pck = self.send_session.generate_init_pck(self.client_address) self.add_secret_payload(self.payload, self.static_key) else: next_bits_to_send = self.send_session.secret_to_send.next_bits( self.payload_size) self.log.debug("Next payload bits to send: " + str(next_bits_to_send)) new_cp1_pck = CP1Package(ntp_pck=init_ntp_client_pck()) new_cp1_pck.add_payload(next_bits_to_send) next_pck = new_cp1_pck.ntp() upstream_pck = self.scapy_wrapper.get_upstream_ntp() upstream_pck[NTP].mode = 4 # upstream_pck[NTP].orig = ntp_pck.sent upstream_pck[NTP].sent = next_pck[NTP].sent upstream_pck[NTP].ref = next_pck[NTP].ref upstream_pck[IP].src = pck[IP].dst upstream_pck[IP].dst = pck[IP].src upstream_pck[UDP].sport = pck[UDP].dport upstream_pck[UDP].dport = pck[UDP].sport up_raw = RawNTP(upstream_pck[NTP]) pck_raw = RawNTP(ntp_pck) up_raw.set_origin_timestamp(pck_raw.transmit_timestamp()) upstream_pck[NTP] = up_raw.ntp() self.log.debug("Created new CP1 packet to send...") upstream_pck.show() send(upstream_pck) if not self.has_next_pck(): self.log.debug("Init new session (2).") self.send_session = None
class CP2Server: def __init__(self, client_ip: str, sniff_interface: str, stratum_file_path: str = '/etc/stratum_override.txt', num_pck_per_bit=1, log: logging.Logger = logging.getLogger('CP2Server-logging')): """ A CP2 Server that listens for incoming NTP requests and injects the manipulated stratum value to a config file on the harddrive :param log: """ self.log = log self.stratum_file_path = stratum_file_path self.num_pck_per_bit = num_pck_per_bit self.client_ip = client_ip self.scapy_wrapper = ScapyWrapper() self.sniff_interface = sniff_interface self.payload_to_send = 'Hello World' self.payload_pointer = -1 self.payload_bits = '' self.current_pck_num = 0 self.current_stratum = None self.zero_mode = False self.init_done = False self.init_counter = 0 def run(self): self.log.info("Starting CP2 Server and listening on interface " + str(self.sniff_interface) + " for client requests from " + str(self.client_ip)) self.payload_bits = ascii_to_bit(self.payload_to_send) self.log.info('Sending payload "' + str(self.payload_to_send) + '" decoded as ' + str(self.payload_bits)) self.set_current_stratum() self.inject_next_bit() while True: next_pck = self.scapy_wrapper.next_ntp_packet( sniff_interface=self.sniff_interface) if next_pck[IP].src != self.client_ip: self.log.debug('Packet with ip ' + str(next_pck[IP]) + ' was not meant for this server.') continue if not self.init_done: self.init_counter += 1 if self.init_counter >= 3: self.init_done = True continue self.current_pck_num += 1 if self.current_pck_num >= self.num_pck_per_bit: self.current_pck_num = 0 self.set_current_stratum() self.inject_next_bit() else: self.log.debug('No change in stratum so far...') def set_current_stratum(self): """ Sets the current stratum value based on the last bit value. It toggles the current stratum value in case the next bit is from the same type. This methods also sets and resets the current payload pointer accordingly. :return: the current Stratum value that was just set. """ if self.payload_pointer % 8 == 0 and self.payload_pointer != 0 and not self.zero_mode: self.log.debug("One byte transfer complete " + str(self.payload_bits[self.payload_pointer - 8:self.payload_pointer])) # self.zero_mode = True # self.current_stratum = CP2_NO_BITS[0] # return # if self.zero_mode: # self.zero_mode = False if self.payload_pointer == len(self.payload_bits) - 1: self.log.info("Resetting the payload pointer") self.payload_pointer = -1 self.payload_pointer += 1 current_bit = self.payload_bits[self.payload_pointer] if current_bit == '0': self.current_stratum = CP2_ZERO_BITS[0] else: self.current_stratum = CP2_ONE_BITS[0] return self.current_stratum current_bit = self.payload_bits[self.payload_pointer] next_bit = self.payload_bits[self.payload_pointer + 1] self.payload_pointer += 1 if current_bit == '0' and next_bit == '0': if self.current_stratum == CP2_ZERO_BITS[0]: self.current_stratum = CP2_ZERO_BITS[1] else: self.current_stratum = CP2_ZERO_BITS[0] elif current_bit == '1' and next_bit == '1': if self.current_stratum == CP2_ONE_BITS[0]: self.current_stratum = CP2_ONE_BITS[1] else: self.current_stratum = CP2_ZERO_BITS[0] elif next_bit == '1': self.current_stratum = CP2_ONE_BITS[0] else: self.current_stratum = CP2_ZERO_BITS[0] return self.current_stratum def inject_next_bit(self): """ Writes the current stratum value to a file, where it can be read by a manipulated chrony NTP server. :return: """ self.log.debug("Injecting next stratum " + str(self.current_stratum) + " for bit: " + str(self.payload_bits[self.payload_pointer])) with open(self.stratum_file_path, 'w') as file: file.write(str(self.current_stratum))
class CP1Server: """ Provides functionality for a CP1 client (receive and send hidden messages). """ def __init__(self, cp1_address_bits: str, static_decryption_key_bits: str, sniff_interface: str = 'lo', self_ip_addr: str = None, log: logging.Logger = logging.getLogger('CP1Server-logger')): self.log = log self._address = cp1_address_bits self.static_key_bits = static_decryption_key_bits self.init_pck_field = NTPField.TRANSMIT_TIMESTAMP self.sniff_interface = sniff_interface self.listen_session: CP1ClientSession = None self._listen_lock = Lock() self._release_listen = False self.self_ip_addr = self_ip_addr self.scapy_wrapper = ScapyWrapper() self.crypto_tools = NTPCrypto() def address_and_version_check(self, pck: CP1Package) -> bool: """ Checks whether a pck is meant for this client: Does the address and version number match? :param pck: The package to check :return: True in case it is meant for this client, False otherwise. """ init_field = pck.get_field(self.init_pck_field) init_field = init_field[self.init_pck_field.length() - 8:self.init_pck_field.length()] received_address_bits_hashed = init_field[0:6] own_address_bits_hashed = generate_address_hash( pck.hash_nonce(), self._address) if received_address_bits_hashed != own_address_bits_hashed: self.log.debug('The received address hash did not match: ' + received_address_bits_hashed) return False received_version_bits_hashed = init_field[6:8] own_version_bits_hashed = generate_version_hash( pck.hash_nonce(), _PROTOCOL_VERSION) if received_version_bits_hashed != own_version_bits_hashed: self.log.debug( 'The received version hash did not match the expected value: ' + str(received_version_bits_hashed)) return False return True def listen(self): """ Starts to listen for incoming NTP packages. This action is blocking so far. :return: """ # TODO: Move this action to a new thread. if self._listen_lock.locked(): return False self._listen_async() return True def _listen_async(self): """ A synchronized method which waits for matching incoming NTP-packages and handles them accordingly to the current Session status (initialized, or non-initialized). :return: """ self._listen_lock.acquire() try: self.log.info( "Lock acquired, starting to listen for incoming NTP packages on interface: " + self.sniff_interface) while True: if self.self_ip_addr is None: pck = self.scapy_wrapper.next_ntp_packet( self.sniff_interface) else: pck = self.scapy_wrapper.next_ntp_packet_for_target( self.sniff_interface, self.self_ip_addr) self.log.info("NTP-Package received") if self._release_listen: # The listening should be aborted. self._release_listen = False return self.handle_incoming_ntp_pck(pck[NTP]) finally: self.log.error("Session cleared after exception.") self.listen_session = None self._listen_lock.release() def handle_incoming_ntp_pck(self, ntp_pck: NTP): cp1_pck = CP1Package(ntp_pck) self.log.info('Received pck bits: ' + str(cp1_pck._raw)) if self.listen_session is None: if not self.address_and_version_check(cp1_pck): self.log.info( 'Package did not contain matching address or version') else: self.log.info("Init pck. Creating new session") self.listen_session = CP1ClientSession(self.static_key_bits, cp1_pck) return self.listen_session.add_next_pck(cp1_pck) if self.listen_session.is_complete(): self.log.info("Payload completely received: " + str(self.listen_session.secret_received_in_bits)) self.log.info("Using key to decrypt: " + str(self.listen_session.get_decryption_key_bytes())) decoded_bits = decrypt_bits_raw( encrypted_bits=self.listen_session.secret_received_in_bits, decryption_key_bytes=self.listen_session. get_decryption_key_bytes()) self.log.info("DECRYPTED Payload: " + str(decoded_bits)) self.listen_session = None