Changed directory structure, divided changeable modules and less changeable core. Added new classes for communication - CommunicationDictionary and ConvertTable.

This commit is contained in:
Zloooy 2019-03-24 01:02:55 +03:00
parent 78ba4ddf76
commit b16e248712
23 changed files with 734 additions and 2 deletions

7
config.py Normal file
View File

@ -0,0 +1,7 @@
#modules selection
config = {
"parser" : "Parser",
"address_generator" : "IpGenerator",
"scanner" : "CoreModel",
"storage" : "JSONStorage"
}

125
core/MainPresenter.py Normal file
View File

@ -0,0 +1,125 @@
from core import import_utils
from core.communication.ConvertTable import ConvertTable
from threading import RLock
import datetime
from PyQt5.Qt import QThread, pyqtSignal
from PyQt5.QtCore import QObject, pyqtSlot
from config import config
from inspect import isfunction
CoreModel = import_utils.import_class("modules/network_scan/%s.py" %
config["scanner"])
Parser = import_utils.import_class("modules/address_generation/%s.py" %
config["parser"])
IpGenerator = import_utils.import_class(
"modules/address_generation/%s.py" %
config["address_generator"]
)
JSONStorage = import_utils.import_class("modules/storage/%s.py" %
config["storage"])
class MainPresenter:
def __init__(self, ui):
self.ui = ui
self.threads = []
self.isScanEnabled = False
self.convert_table = ConvertTable()
for func in import_utils.import_matching(
"modules/convert_functions/",
lambda name, value:
isfunction(value) and hasattr(value, "__from__")
):
self.convert_table.add_function(func)
self.parser = Parser()
#needed config to specify path
self.storage = JSONStorage("results.json")
self.exit_lock = RLock()
def startScan(self, ipRanges, portsStr, threadNumber, timeout):
timeout = 3 if not timeout else int(timeout)
addresses = self.parser.parse_address_field(ipRanges)
ports = self.parser.parse_port_field(portsStr)
self.ip_generator = IpGenerator(addresses, ports, self.convert_table)
self.scanner = CoreModel(timeout)
threadNumber = int(threadNumber)
for i in range(threadNumber):
scan_worker = ScanWorker(
self.ip_generator,
self.scanner,
self.storage
)
scan_thread = QThread()
scan_worker.log_signal.connect(self.log_text)
scan_worker.moveToThread(scan_thread)
scan_worker.exit_signal.connect(scan_thread.exit)
scan_worker.exit_signal.connect(self.on_worker_exit)
scan_thread.started.connect(scan_worker.work)
self.threads.append((scan_worker, scan_thread))
self.changeThreadLabel(threadNumber)
for thread in self.threads:
scan_worker, scan_thread = thread
scan_thread.start()
def changeThreadLabel(self, number_of_threads):
self.number_of_threads = number_of_threads
self.ui.currentThreadsLabel.setText(str(number_of_threads))
def on_worker_exit(self):
self.changeThreadLabel(self.number_of_threads - 1)
with self.exit_lock:
for num, thread in enumerate(self.threads):
scan_worker, scan_thread = thread
if not scan_worker.isRunning:
self.threads.pop(num)
break
if self.number_of_threads == 0:
self.on_end_scanning()
def on_end_scanning(self):
self.isScanEnabled = False
self.ui.startButton.setText("Start")
self.storage.save()
def stopScan(self):
while self.threads:
scan_worker, scan_thread = self.threads[0]
if scan_worker.isRunning:
scan_worker.stop()
def log_text(self, string):
self.ui.dataText.append("[" + str(datetime.datetime.now()) + "] " + str(string))
class ScanWorker(QObject):
log_signal = pyqtSignal(str)
exit_signal = pyqtSignal()
def __init__(self, ip_generator, scanner, storage, **kwargs):
super().__init__()
self.ip_generator = ip_generator
self.storage = storage
self.scanner = scanner
self.previous_address = None
self.isRunning = True
@pyqtSlot()
def work(self):
while self.isRunning:
scan_address = self.ip_generator.get_next_address(self.previous_address)
if not scan_address:
break
self.previous_address = scan_address
scan_result = self.scanner.scan_address(scan_address)
self.storage.put_responce(scan_address, scan_result)
string_scan_address = " ".join(key + ":" + str(scan_address[key]) for
key in scan_address.keys())
if scan_result == 0:
self.log_signal.emit('%s is open' % string_scan_address)
else:
self.log_signal.emit('%s is closed' % string_scan_address)
self.stop()
def stop(self):
self.isRunning = False
self.exit_signal.emit()

6
core/__init__.py Normal file
View File

@ -0,0 +1,6 @@
import sys
import os
fil = __file__[:__file__.rindex(os.sep)]
sys.path.insert(0,fil)

View File

@ -0,0 +1,22 @@
class CommunicationDictionary(dict):
'''This class is used to provide communication between classes using
key-value interface'''
def __init__(self, convert_table):
'''Convert table stores functions used to get value of unable key if
it's possible'''
super(CommunicationDictionary, self).__init__()
self.convert_table = convert_table
def __getitem__(self, key):
item = None
try:
item = dict.__getitem__(self,key)
except KeyError:
key_to_convert, convert_function = self.convert_table.get_converter(
self.keys(),
key
)
if key_to_convert is not None:
item = convert_function(dict.__getitem__(self,key_to_convert))
return item

View File

@ -0,0 +1,31 @@
def convert_function(_from, _to):
'''This decorator is simple way to declare ability of function to be
converter from name _from to name _to in convert table'''
def real_decorator(function):
function.__from__ = _from
function.__to__ = _to
return function
return real_decorator
class ConvertTable():
'''The class is used to store and find the right function to convert value from
one key to another'''
def __init__(self):
self.convert_functions = []
def add_function(self, function):
'''Here you can add function to ConvertTable.'''
#TODO: make this method produce new functions, that will be able to
#create converter chains
print("adding function", function)
self.convert_functions.append(function)
def get_converter(self, from_keys, to_key):
'''This function returns converter function, that can convert one key
to another'''
for function in self.convert_functions:
if function.__from__ in from_keys and function.__to__ == to_key:
return function.__from__, function
print("Can't find converter!")
return None, None

55
core/import_utils.py Normal file
View File

@ -0,0 +1,55 @@
import importlib
from glob import glob
from os import extsep
from os.path import realpath, dirname, join, basename, splitext
import sys
from inspect import getmembers, isfunction, isclass
def module_paths_list(path):
return glob(join(path, "*" + extsep + "py"))
def modulename(file_path):
return splitext(basename(file_path))[0]
def module_name_list(path):
return [modulename(module_path) for module_path in
module_paths_list(path)]
def import_module(path):
if dirname(path) not in sys.path:
sys.path.insert(0, dirname(path))
name = modulename(path)
module_spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
return module
def import_class(path):
return getattr(import_module(path), modulename(path))
def import_matching(path, matcher_function):
matching = []
for modulefile in module_paths_list(path):
module = import_module(modulefile)
for name, value in getmembers(module):
if matcher_function(name, value):
matching.append(value)
return matching
def import_functions(path):
def matcher(name, value):
return isfunction(value)
return import_matching(path, matcher)
def import_classes(path):
def matcher(name, value):
return isclass(value)
return import_matching(path, matcher)

View File

@ -4,7 +4,7 @@ from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from main_ui import *
import MainPresenter
import core.MainPresenter
class MyWin(QtWidgets.QMainWindow):
@ -14,7 +14,7 @@ class MyWin(QtWidgets.QMainWindow):
self.ui = Ui_Form()
self.ui.setupUi(self)
self.presenter = MainPresenter.MainPresenter(self.ui)
self.presenter = core.MainPresenter.MainPresenter(self.ui)
self.ui.startButton.clicked.connect(self.startButtonClicked)
self.isScanActive = False

98
modules/RawTCP.py Normal file
View File

@ -0,0 +1,98 @@
from struct import pack, unpack
import socket
class TCPHeader():
# TCP header class. Thanks to Silver Moon for the flags calculation and packing order
# This was designed to be re-used. You might want to randomize the seq number
# get_struct performs packing based on if you have a valid checksum or not
def __init__(self, src_port=47123, dst_port=80, seqnum=1000, acknum=0, data_offset=80, fin=0, syn=1, rst=0, psh=0, ack=0, urg=0, window=5840, check=0, urg_ptr=0):
# !=network(big-endian), H=short(2), L=long(4),B=char(1)
self.order = "!HHLLBBHHH"
self.src_port = src_port
self.dst_port = dst_port
self.seqnum = seqnum
self.acknum = acknum
# size of tcp header; size is specified by 4-byte words; This is 80
# decimal, which is 0x50, which is 20bytes (5words*4bytes).
self.data_offset = data_offset
self.fin = fin
self.syn = syn
self.rst = rst
self.psh = psh
self.ack = ack
self.urg = urg
self.window = socket.htons(window)
self.check = check
self.urg_ptr = urg_ptr
def flags(self):
return self.fin + (self.syn << 1) + (self.rst << 2) + (self.psh << 3) + (self.ack << 4) + (self.urg << 5)
def get_struct(self, check=False, checksummed=False):
if check is not False:
self.check = check
if checksummed:
return pack('!HHLLBBH', self.src_port, self.dst_port, self.seqnum, self.acknum, self.data_offset, self.flags(), self.window) + pack('H', self.check) + pack('!H', self.urg_ptr)
else:
return pack(self.order, self.src_port, self.dst_port, self.seqnum, self.acknum, self.data_offset, self.flags(), self.window, self.check, self.urg_ptr)
def checksum(msg):
# Shoutout to Silver Moon @ binarytides for this checksum algo.
sum = 0
for i in range(0, len(msg), 2):
w = msg[i] + (msg[i + 1] << 8)
sum = sum + w
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
sum = ~sum & 0xffff
return sum
def tcp_checksum(source_ip, dest_ip, tcp_header, user_data=b''):
# Calculates the correct checksum for the tcp header
tcp_length = len(tcp_header) + len(user_data)
# This is an IP header w/ TCP as protocol.
ip_header = pack('!4s4sBBH', socket.inet_aton(source_ip), socket.inet_aton(
dest_ip), 0, socket.IPPROTO_TCP, tcp_length)
# Assemble the packet (IP Header + TCP Header + data, and then send it to
# checksum function)
packet = ip_header + tcp_header + user_data
return checksum(packet)
def handle_packet(raw_packet):
# Now we need to unpack the packet. It will be an IP/TCP packet
# We are looking for SYN-ACKs from our SYN scan
# Fields to check: IP - src addr; TCP - src port, flags
# We want to pull out and compare only these three
# Heres the math for unpacking: B=1, H=2, L=4, 4s=4 (those are bytes)
packet = raw_packet[0]
# This is the IP header, not including any self.options OR THE DST ADDR.
# Normal length is 20!! Im parsing as little as possible
ip_header = unpack('!BBHHHBBH4s', packet[0:16])
# If there are any self.options, the length of the IP header will be >20. We
# dont care about self.options
ip_header_length = (ip_header[0] & 0xf) * 4
# This is the source address (position 8, or the first "4s" in our
# unpack)
src_addr = socket.inet_ntoa(ip_header[8])
# We had to get the proper IP Header length to find the TCP header
# offset.
tcp_header_raw = packet[ip_header_length:ip_header_length + 14]
# TCP header structure is pretty straight-forward. We want PORTS and
# FLAGS, so we partial unpack it
tcp_header = unpack('!HHLLBB', tcp_header_raw)
src_port = tcp_header[0] # self-explanatory
dst_port = tcp_header[1] # self-explanatory FIXME: notused
# We only care about syn-ack, which will be 18 (0x12)
flag = tcp_header[5]
if flag == 18:
return (src_addr, src_port)
else:
return None

89
modules/TCPScan.py Normal file
View File

@ -0,0 +1,89 @@
from RawTCP import TCPHeader, handle_packet, tcp_checksum
import socket
import sys
class TCPScanner:
def __init__(self):
self.source_ips = {}
# TODO: options, threading, input/output queues
self.options = None
return
def in_scope(self):
"""Check that IP is in scanning scope"""
# TODO
pass
def tcp_listener(self):
# Raw socket listener for when send_raw_syn() is used. This will catch
# return SYN-ACKs
listen = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
while True:
# packet = ('E \x00(\x1f\xaa@\x00w\x06\x99w2\xe0\xc8\xa2\xa2\xf3\xac\x18\xdf\xb3\x00\x16\xb6\x80\xc1\xa0/\xa6=$P\x10\xce\xab\xd1\xe4\x00\x00', ('50.XXX.200.162', 0))
raw_packet = listen.recvfrom(65565)
ret = handle_packet(raw_packet)
if ret is None:
continue
src_addr, src_port = ret
if self.in_scope(src_addr) and src_port in self.ports:
self.output_queue.put((src_addr, src_port))
def send_raw_syn(self, dest_ip, dst_port):
# Use raw sockets to send a SYN packet.
# If you want, you could use the IP header assembled in the tcp_checksum
# function to have a fully custom TCP/IP stack
try:
# Using IPPROTO_TCP so the kernel will deal with the IP packet for us.
# Change to IPPROTO_IP if you want control of IP header as well
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
except Exception:
sys.stderr.write("Error creating socket in send_raw_syn\n")
return
if self.options.source == "auto":
# This gets the correct source IP. Just in case of multiple interfaces,
# it will pick the right one
src_addr = self.get_source_ip(dest_ip)
else:
src_addr = self.options.source
src_port = 54321
make_tcpheader = TCPHeader(src_port, dst_port)
tcp_header = make_tcpheader.get_struct()
packet = make_tcpheader.get_struct(check=tcp_checksum(
src_addr, dest_ip, tcp_header), checksummed=True)
try:
s.sendto(packet, (dest_ip, 0))
except Exception as e:
sys.stderr.write("Error utilizing raw socket in send_raw_syn: {}\n".format(e))
def get_source_ip(self, dst_addr):
# Credit: 131264/alexander from stackoverflow. This gets the correct IP for sending. Useful if you have multiple interfaces
# NOTE: This will send an additional packet for every single IP to confirm
# the route. (but just one packet)
try:
if dst_addr in self.source_ips:
return self.source_ips[dst_addr]
else:
self.source_ips[dst_addr] = [(s.connect((dst_addr, 53)), s.getsockname()[0], s.close(
)) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]
return self.source_ips[dst_addr]
except Exception:
sys.stderr.write(
"Something went wrong in get_source_ip, results might be wrong\n")
def send_full_connect_syn(ip, port, timeout):
# Normal scan using socket to connect. Does 3-way handshack, then graceful
# teardown using FIN
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
except Exception as e:
sys.stderr.write("Error creating socket in send_full_connect_syn: {}\n".format(e))
return False
try:
s.connect((ip, port))
return True
s.close()
except Exception:
return False

View File

@ -0,0 +1,15 @@
from abc import abstractmethod, ABC
from core.communication import CommunicationDictionary
class AbstractAddressGenerator(ABC):
'''The class describes addess generation mechanism.
In __init__ method it should get results of parsing fields
and then it returns addresses.'''
@abstractmethod
def get_next_address(self, previous_address, **kwargs) -> CommunicationDictionary:
'''Address - an only, indivisible object, that describes single scan
target address. This method should return next address to scan based on
previous scanned address and result of scanning previous address, that
can be placed in kwargs.'''
pass

View File

@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
class AbstractParser(ABC):
'''The class describes fields parsing mechanisms'''
@abstractmethod
def parse_address_field(self, field):
'''In address field can be plased any text, describing address of
scanning target'''
pass
@abstractmethod
def parse_port_field(self, field):
'''In port field only numbers, whitespaces, comma and '-' allowed'''
pass

View File

@ -0,0 +1,32 @@
from AbstractAddressGenerator import AbstractAddressGenerator
from threading import RLock
from core.communication.CommunicationDictionary import CommunicationDictionary
class IpGenerator(AbstractAddressGenerator):
def __init__(self, ip_generator, ports, convert_table):
self.convert_table = convert_table
self.ip_generator = ip_generator
self.ports = ports
self.lock = RLock()
def get_next_port_number(self, previous_port):
return (self.ports.index(previous_port) + 1) % len(self.ports)
def get_next_address(self, previous_address):
result = CommunicationDictionary(self.convert_table)
with self.lock:
portnum = 0
next_ip = None
if previous_address:
next_ip = previous_address["ipv4"]
port = previous_address["port"]
portnum = self.get_next_port_number(port)
if (portnum == 0):
try:
next_ip = next(self.ip_generator)
except StopIteration:
return None
result["ipv4"] = next_ip
result["port"] = self.ports[portnum]
return result

View File

@ -0,0 +1,80 @@
import ipaddress
from AbstractParser import AbstractParser
class Parser(AbstractParser):
def parse_port_field(self, ports):
"""
Parses ports from string, returns them as integers in the list.
Handles non-existent ports and non-port values.
"""
if ports:
# Using set to avoid repetitions
parsed = set()
ports = ports.split(",")
for port in ports:
try:
# Input is in range form ("100-200"):
if '-' in port:
start, end = map(int, port.split('-'))
parsed.update(
[p for p in range(start, end + 1) if 65355 >= p > 0])
# Input is a single port ("80"):
else:
parsed.add(int(port))
except ValueError as e:
# If we get any not integer just ignore it
pass
return sorted(list(parsed))
else:
# Change to default ports from constant
return [21, 22, 23, 25, 80, 443, 110, 111, 135, 139, 445, 8080, 8443, 53, 143, 989, 990, 3306, 1080, 5554, 6667, 2222, 4444, 666, 6666, 1337, 2020, 31337]
def parse_address_field(self, ips):
"""
Parses ip input string, returns the generator over them.
Supports next inputs:
1) 1.2.3.4
2) 192.168.0.0/24
3) 1.2.3.4 - 5.6.7.8
Any non-ip value will be ignored.
"""
# A set to contain non repeating ip objects from ipaddress
ip_objects = set()
inputs = [ip.strip() for ip in ips.split(',')]
for input_ in inputs:
try:
# Input is in range form ("1.2.3.4 - 5.6.7.8"):
if '-' in input_:
input_ips = input_.split('-')
ranges = set(
ipaddress.summarize_address_range(
*map(lambda x: ipaddress.IPv4Address(x.strip()), input_ips)
)
)
ip_objects.update(ranges)
# Input is in CIDR form ("192.168.0.0/24"):
elif '/' in input_:
network = ipaddress.ip_network(input_)
ip_objects.add(network)
# Input is a single ip ("1.1.1.1"):
else:
ip = ipaddress.ip_address(input_)
ip_objects.add(ip)
except ValueError as e:
print(e)
# If we get any non-ip value just ignore it
pass
for ip_obj in ip_objects:
# The object is just one ip, simply yield it:
if isinstance(ip_obj, ipaddress.IPv4Address):
yield ip_obj
# The object is a network, yield every host in it:
else:
for host in ip_obj:
yield host

View File

@ -0,0 +1,6 @@
import sys
import os
fil = __file__[:__file__.rindex(os.sep)]
sys.path.insert(0,fil)

View File

@ -0,0 +1,27 @@
from unittest import TestCase, main
from ipaddress import IPv4Address
from IpGenerator import IpGenerator
from Parser import Parser
class testIpGenerator(TestCase):
def setUp(self):
p = Parser()
self.ipgen = IpGenerator(
p.parse_address_field("192.168.1.1 - 192.168.1.10"), [80, 90])
def testIpGeneration(self):
'''self.assertEqual(
self.ipgen.get_next_address(None),
("192.168.1.1", 80))'''
previous_address = None
a = True
while previous_address or a:
previous_address=self.ipgen.get_next_address(previous_address)
a = False
self.assertEqual(self.ipgen.get_next_address(None), None)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,37 @@
from unittest import main, TestCase
from Parser import Parser
import ipaddress
class TestParser(TestCase):
def setUp(self):
self.parser = Parser()
def test_port_parsing(self):
self.assertEqual(self.parser.parse_port_field("80,90"), [80, 90])
def test_ip_parsing(self):
self.assertEqual(
set(self.parser.parse_address_field("192.168.1.1 - 192.168.1.3")),
{ipaddress.IPv4Address(ip) for ip in
[
"192.168.1.1",
"192.168.1.2",
"192.168.1.3"
]})
self.assertEqual(
set(self.parser.parse_address_field("192.168.1.1")),
{ipaddress.IPv4Address("192.168.1.1")}
)
self.assertEqual(
set(self.parser.parse_address_field("192.168.1.0/31")),
{ipaddress.IPv4Address(ip) for ip in
[
'192.168.1.1',
'192.168.1.0'
]})
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
from core.communication.ConvertTable import convert_function
@convert_function("ipv4","str")
def ipv4tostring(ip):
return str(ip)

View File

@ -0,0 +1,17 @@
from abc import ABC, abstractmethod
from core.communication.CommunicationDictionary import CommunicationDictionary
class AbstractScanner(ABC):
'''The class is used by one thread to scan targets'''
@abstractmethod
def __init__(self, **kwargs):
'''In this method you can init some
reusable resources needed for scan'''
pass
@abstractmethod
def scan_address(self, address) -> CommunicationDictionary:
'''This method should contain scanning process of given address. All
items returned will be passed to AbstractStorage and ui'''
pass

View File

@ -0,0 +1,17 @@
import socket
from AbstractScanner import AbstractScanner
from core.communication.CommunicationDictionary import CommunicationDictionary
class CoreModel(AbstractScanner):
def __init__(self, timeout):
self.defSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.defSocket.settimeout(int(timeout))
def scan_address(self, address: CommunicationDictionary) -> CommunicationDictionary:
host = address["str"]
port = address["port"]
if not host: raise Exception
result = self.defSocket.connect_ex((host, port))
self.defSocket.close()
self.defSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
return result

View File

@ -0,0 +1,6 @@
import sys
import os
fil = __file__[:__file__.rindex(os.sep)]
sys.path.insert(0,fil)

View File

@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
class AbstractStorage(ABC):
@abstractmethod
def put_responce(self, address, responce):
pass
@abstractmethod
def save(self):
pass

View File

@ -0,0 +1,23 @@
from AbstractStorage import AbstractStorage
import json
from threading import RLock
class JSONStorage(AbstractStorage):
def __init__(self, path):
self.path = path
self.respdict = dict()
self.lock = RLock()
def put_responce(self, address, responce):
ip, port = address
if ip not in self.respdict.keys():
self.respdict[ip] = {"open": [], "close": []}
self.respdict[ip]["open" if responce != 0 else "close"].append(port)
def save(self):
print("saving")
with open(self.path, "w") as f:
json.dump(self.respdict, f)
self.respdict = {}

View File

@ -0,0 +1,6 @@
import sys
import os
fil = __file__[:__file__.rindex(os.sep)]
sys.path.insert(0,fil)