From 8c15ce9fd2c753fc21b5f71c8ab81fd145180f7f Mon Sep 17 00:00:00 2001
From: Toni Uhlig <matzeton@googlemail.com>
Date: Fri, 18 Jan 2019 18:11:04 +0100
Subject: python based fuzzing script to detect security related issues
 (work-in-progress)

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
---
 test/fuzzer.py | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 275 insertions(+)
 create mode 100755 test/fuzzer.py

(limited to 'test')

diff --git a/test/fuzzer.py b/test/fuzzer.py
new file mode 100755
index 0000000..3778952
--- /dev/null
+++ b/test/fuzzer.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python2.7
+"""
+    This is a python ICMP/Ping script,
+    which can be used to fuzz the proxy/forwarder
+    to detect security issues.
+
+    This script was originally taken from:
+    https://gist.githubusercontent.com/pklaus/856268/raw/a4e295d0dbd1140bddc90616e93ab3b19718a87b/ping.py
+"""
+
+import os
+import time
+import socket
+import struct
+import select
+import random
+import asyncore
+
+UINT_MAX=4294967295
+USHORT_MAX=65535
+ICMP_ECHO_REQUEST = 8
+ICMP_PROTO = socket.getprotobyname('icmp')
+ERROR_DESCR = {
+    1:     ' - Note that ICMP messages can only be '
+           'sent from processes running as root.',
+    10013: ' - Note that ICMP messages can only be sent by'
+           ' users or processes with administrator rights.'
+}
+
+__all__ = ['create_packet', 'do_one', 'verbose_ping', 'PingQuery',
+           'multi_ping_query']
+
+
+def checksum(source_string):
+    # I'm not too confident that this is right but testing seems to
+    # suggest that it gives the same answers as in_cksum in ping.c.
+    sum = 0
+    count_to = (len(source_string) / 2) * 2
+    count = 0
+    while count < count_to:
+        this_val = ord(source_string[count + 1])*256+ord(source_string[count])
+        sum = sum + this_val
+        sum = sum & 0xffffffff # Necessary?
+        count = count + 2
+    if count_to < len(source_string):
+        sum = sum + ord(source_string[len(source_string) - 1])
+        sum = sum & 0xffffffff # Necessary?
+    sum = (sum >> 16) + (sum & 0xffff)
+    sum = sum + (sum >> 16)
+    answer = ~sum
+    answer = answer & 0xffff
+    # Swap bytes. Bugger me if I know why.
+    answer = answer >> 8 | (answer << 8 & 0xff00)
+    return answer
+
+
+def create_packet(id, data=None):
+    """Create a new echo request packet based on the given "id"."""
+    # Header is type (8), code (8), checksum (16), id (16), sequence (16)
+    header = struct.pack('bbHHh', ICMP_ECHO_REQUEST, 0, 0, id, 1)
+    if data is None:
+        data = 192 * 'Q'
+    # Calculate the checksum on the data and the dummy header.
+    my_checksum = checksum(header + data)
+    # Now that we have the right checksum, we put that in. It's just easier
+    # to make up a new header than to stuff it into the dummy.
+    header = struct.pack('bbHHh', ICMP_ECHO_REQUEST, 0,
+                         socket.htons(my_checksum), id, 1)
+    return header + data
+
+
+def do_one(dest_addr, timeout=1, data=None):
+    """
+    Sends one ping to the given "dest_addr" which can be an ip or hostname.
+    "timeout" can be any integer or float except negatives and zero.
+
+    Returns either the delay (in seconds) or None on timeout and an invalid
+    address, respectively.
+
+    """
+    try:
+        my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, ICMP_PROTO)
+    except socket.error as e:
+        if e.errno in ERROR_DESCR:
+            # Operation not permitted
+            raise socket.error(''.join((e.args[1], ERROR_DESCR[e.errno])))
+        raise # raise the original error
+    try:
+        host = socket.gethostbyname(dest_addr)
+    except socket.gaierror:
+        return
+    # Maximum for an unsigned short int c object counts to 65535 so
+    # we have to sure that our packet id is not greater than that.
+    packet_id = int((id(timeout) * random.random()) % 65535)
+    packet = create_packet(packet_id, data)
+    while packet:
+        # The icmp protocol does not use a port, but the function
+        # below expects it, so we just give it a dummy port.
+        sent = my_socket.sendto(packet, (dest_addr, 1))
+        packet = packet[sent:]
+    delay = receive_ping(my_socket, packet_id, time.time(), timeout)
+    my_socket.close()
+    return delay
+
+
+def receive_ping(my_socket, packet_id, time_sent, timeout):
+    # Receive the ping from the socket.
+    time_left = timeout
+    while True:
+        started_select = time.time()
+        ready = select.select([my_socket], [], [], time_left)
+        how_long_in_select = time.time() - started_select
+        if ready[0] == []: # Timeout
+            return
+        time_received = time.time()
+        rec_packet, addr = my_socket.recvfrom(1024)
+        icmp_header = rec_packet[20:28]
+        type, code, checksum, p_id, sequence = struct.unpack(
+            'bbHHh', icmp_header)
+        if p_id == packet_id:
+            return time_received - time_sent
+        time_left -= time_received - time_sent
+        if time_left <= 0:
+            return
+
+
+def verbose_ping(dest_addr, data=None, timeout=2, count=4):
+    """
+    Sends one ping to the given "dest_addr" which can be an ip or hostname.
+
+    "timeout" can be any integer or float except negatives and zero.
+    "count" specifies how many pings will be sent.
+
+    Displays the result on the screen.
+ 
+    """
+    for i in range(count):
+        print('ping {}...'.format(dest_addr))
+        delay = do_one(dest_addr, timeout, data)
+        if delay == None:
+            print('failed. (Timeout within {} seconds.)'.format(timeout))
+        else:
+            delay = round(delay * 1000.0, 4)
+            print('get ping in {} milliseconds.'.format(delay))
+    print('')
+
+
+class PingQuery(asyncore.dispatcher):
+    def __init__(self, host, p_id, timeout=0.5, ignore_errors=False, data=None):
+        """
+       Derived class from "asyncore.dispatcher" for sending and
+       receiving an icmp echo request/reply.
+       
+       Usually this class is used in conjunction with the "loop"
+       function of asyncore.
+       
+       Once the loop is over, you can retrieve the results with
+       the "get_result" method. Assignment is possible through
+       the "get_host" method.
+       
+       "host" represents the address under which the server can be reached.
+       "timeout" is the interval which the host gets granted for its reply.
+       "p_id" must be any unique integer or float except negatives and zeros.
+       
+       If "ignore_errors" is True, the default behaviour of asyncore
+       will be overwritten with a function which does just nothing.
+       
+       """
+        asyncore.dispatcher.__init__(self)
+        try:
+            self.create_socket(socket.AF_INET, socket.SOCK_RAW, ICMP_CODE)
+        except socket.error as e:
+            if e.errno in ERROR_DESCR:
+                # Operation not permitted
+                raise socket.error(''.join((e.args[1], ERROR_DESCR[e.errno])))
+            raise # raise the original error
+        self.time_received = 0
+        self.time_sent = 0
+        self.timeout = timeout
+        # Maximum for an unsigned short int c object counts to 65535 so
+        # we have to sure that our packet id is not greater than that.
+        self.packet_id = int((id(timeout) / p_id) % 65535)
+        self.host = host
+        self.packet = create_packet(self.packet_id, data)
+        if ignore_errors:
+            # If it does not care whether an error occured or not.
+            self.handle_error = self.do_not_handle_errors
+            self.handle_expt = self.do_not_handle_errors
+
+    def writable(self):
+        return self.time_sent == 0
+
+    def handle_write(self):
+        self.time_sent = time.time()
+        while self.packet:
+            # The icmp protocol does not use a port, but the function
+            # below expects it, so we just give it a dummy port.
+            sent = self.sendto(self.packet, (self.host, 1))
+            self.packet = self.packet[sent:]
+
+    def readable(self):
+        # As long as we did not sent anything, the channel has to be left open.
+        if (not self.writable()
+            # Once we sent something, we should periodically check if the reply
+            # timed out.
+            and self.timeout < (time.time() - self.time_sent)):
+            self.close()
+            return False
+        # If the channel should not be closed, we do not want to read something
+        # until we did not sent anything.
+        return not self.writable()
+
+    def handle_read(self):
+        read_time = time.time()
+        packet, addr = self.recvfrom(1024)
+        header = packet[20:28]
+        type, code, checksum, p_id, sequence = struct.unpack("bbHHh", header)
+        if p_id == self.packet_id:
+            # This comparison is necessary because winsocks do not only get
+            # the replies for their own sent packets.
+            self.time_received = read_time
+            self.close()
+
+    def get_result(self):
+        """Return the ping delay if possible, otherwise None."""
+        if self.time_received > 0:
+            return self.time_received - self.time_sent
+
+    def get_host(self):
+        """Return the host where to the request has or should been sent."""
+        return self.host
+
+    def do_not_handle_errors(self):
+        # Just a dummy handler to stop traceback printing, if desired.
+        pass
+
+    def create_socket(self, family, type, proto):
+        # Overwritten, because the original does not support the "proto" arg.
+        sock = socket.socket(family, type, proto)
+        sock.setblocking(0)
+        self.set_socket(sock)
+        # Part of the original but is not used. (at least at python 2.7)
+        # Copied for possible compatiblity reasons.
+        self.family_and_type = family, type
+
+    # If the following methods would not be there, we would see some very
+    # "useful" warnings from asyncore, maybe. But we do not want to, or do we?
+    def handle_connect(self):
+        pass
+
+    def handle_accept(self):
+        pass
+
+    def handle_close(self):
+        self.close()
+
+
+
+if __name__ == '__main__':
+    # Testing
+    while True:
+        pt_pkt = struct.pack('!IIIIIIHHs',
+            0xdeadc0de,                     # magic
+            random.randint(0, UINT_MAX),    # ip
+            random.randint(0, UINT_MAX),    # port
+            random.randint(0, 4),           # state
+            random.randint(0, UINT_MAX),    # ack
+            random.randint(0, UINT_MAX),    # length
+            random.randint(0, USHORT_MAX),  # seq
+            random.randint(0, USHORT_MAX),  # rsv
+            '2222'                          # data..
+        )
+
+        verbose_ping('127.0.0.1', pt_pkt, 1, 1)
+        random.seed(time.clock())
-- 
cgit v1.2.3


From 44b32c31eb575ec20b8bb2f0b1642c78d556cb91 Mon Sep 17 00:00:00 2001
From: Toni Uhlig <matzeton@googlemail.com>
Date: Mon, 21 Jan 2019 17:00:35 +0100
Subject: fuzzer.py: build pt icmp pkt

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
---
 test/fuzzer.py | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

(limited to 'test')

diff --git a/test/fuzzer.py b/test/fuzzer.py
index 3778952..766122c 100755
--- a/test/fuzzer.py
+++ b/test/fuzzer.py
@@ -254,22 +254,25 @@ class PingQuery(asyncore.dispatcher):
     def handle_close(self):
         self.close()
 
+def build_pt_pkt(ip, port, state, ack, seq, rsv, data):
+    if type(ip) is int:
+        dst_ip = ip
+    elif type(ip) is str:
+        dst_ip = struct.unpack('<L', socket.inet_aton(ip))[0]
+    else:
+        raise Exception('ip is not of type str|int')
+    dst_port = int(port)
+    return struct.pack('!IIIIIIHH',
+        0xdeadc0de, dst_ip, dst_port, state, ack, len(data),
+        seq, rsv) + data
 
 
 if __name__ == '__main__':
     # Testing
     while True:
-        pt_pkt = struct.pack('!IIIIIIHHs',
-            0xdeadc0de,                     # magic
-            random.randint(0, UINT_MAX),    # ip
-            random.randint(0, UINT_MAX),    # port
-            random.randint(0, 4),           # state
-            random.randint(0, UINT_MAX),    # ack
-            random.randint(0, UINT_MAX),    # length
-            random.randint(0, USHORT_MAX),  # seq
-            random.randint(0, USHORT_MAX),  # rsv
-            '2222'                          # data..
-        )
+        pt_pkt = build_pt_pkt('127.0.0.1', '22', 1, 2,
+            random.randint(0, USHORT_MAX),
+            random.randint(0, USHORT_MAX), 'blah')
 
         verbose_ping('127.0.0.1', pt_pkt, 1, 1)
         random.seed(time.clock())
-- 
cgit v1.2.3