├── logs
└── .gitkeep
├── wrapper
├── client
│ ├── __init__.py
│ ├── icmp.py
│ └── http.py
└── server
│ ├── wrap_module
│ ├── __init__.py
│ ├── icmp.py
│ ├── http.py
│ └── dns.py
│ └── wrap_server
│ ├── __init__.py
│ ├── icmpserver.py
│ ├── dnsserver.py
│ └── httpserver.py
├── overlay
├── server
│ ├── __init__.py
│ ├── shell.py
│ ├── io.py
│ ├── tcpconnect.py
│ └── tcplisten.py
└── client
│ ├── __init__.py
│ ├── io.py
│ ├── shell.py
│ ├── tcplisten.py
│ └── tcpconnect.py
├── mistica.png
├── .gitignore
├── Dockerfile
├── sotp
├── route.py
├── packet.py
├── core.py
├── router.py
├── serverworker.py
└── misticathread.py
├── utils
├── buffer.py
├── rc4.py
├── logger.py
├── prompt.py
├── messaging.py
└── icmp.py
├── ms.py
└── mc.py
/logs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/wrapper/client/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["http", "dns", "icmp"]
--------------------------------------------------------------------------------
/wrapper/server/wrap_module/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["http", "dns", "icmp"]
--------------------------------------------------------------------------------
/overlay/server/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["io", "shell", "tcpconnect", "tcplisten"]
--------------------------------------------------------------------------------
/mistica.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IncideDigital/Mistica/HEAD/mistica.png
--------------------------------------------------------------------------------
/overlay/client/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["io", "shell", "tcpconnect", "tcplisten"]
2 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_server/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["httpserver", "dnsserver", "icmpserver"]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | __pycache__
3 | *.log
4 | wiki
5 | .vscode
6 | build
7 | dist
8 | *.bin
9 | *.zip
10 | docs
11 | tests
12 | mc.spec
13 | ms.spec
14 | mc
15 | ms
16 | *.pem
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # [*] First build image with:
2 | #
3 | # sudo docker build --tag mistica:latest .
4 | #
5 | # [*] Second, create the network with:
6 | #
7 | # sudo docker network create misticanw
8 | #
9 | # [*] Third run the server with:
10 | #
11 | # sudo docker run --network misticanw --sysctl net.ipv4.icmp_echo_ignore_all=1 -v $(pwd):/opt/Mistica -it mistica /bin/bash
12 | #
13 | # [*] Fourth run the client with:
14 | #
15 | # sudo docker run --network misticanw -v $(pwd):/opt/Mistica -it mistica /bin/bash
16 |
17 | FROM python:3.7
18 |
19 | LABEL maintainer="rcaro@incide.es"
20 |
21 | RUN python3.7 -m pip install pip && python3.7 -m pip install dnslib
22 |
23 | WORKDIR /opt/Mistica
24 |
25 | ENTRYPOINT /bin/bash
26 |
27 |
--------------------------------------------------------------------------------
/sotp/route.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | class Route:
21 |
22 | def __init__(self, session_id, worker, wrap_module, overlay):
23 | self.session_id = session_id
24 | self.worker = worker
25 | self.wrap_module = wrap_module # MisticaThread
26 | self.overlay = overlay # MisticaThread
27 |
--------------------------------------------------------------------------------
/utils/buffer.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | class Index(object):
21 | def __init__(self):
22 | self.chunks = []
23 |
24 | def add(self, chunk):
25 | self.chunks.append(chunk)
26 |
27 |
28 | class OverlayBuffer(object):
29 | def __init__(self):
30 | self.data = []
31 |
32 | def addIndex(self, index):
33 | self.data.append(index)
34 |
35 | def getChunk(self):
36 | if not self.data:
37 | raise Exception("There is no Index in OverlayBuffer")
38 | if not self.data[0].chunks:
39 | raise Exception("There is no Chunk in OverlayBuffer")
40 | chunk = self.data[0].chunks.pop(0)
41 | if not self.data[0].chunks:
42 | self.data.pop(0)
43 | return chunk,True
44 | return chunk,False
45 |
46 | def anyIndex(self):
47 | return True if self.data else False
48 |
49 |
50 | class WrapperBuffer(object):
51 | def __init__(self):
52 | self.data = Index()
53 |
54 | def addChunk(self, chunk):
55 | self.data.add(chunk)
56 |
57 | def getChunks(self):
58 | if not self.data.chunks:
59 | raise Exception("There is no Chunk in WrapperBuffer")
60 | chunks = self.data.chunks
61 | self.data = Index()
62 | return chunks
--------------------------------------------------------------------------------
/sotp/packet.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from utils.bitstring import BitArray
21 |
22 | BYTE = 8
23 |
24 | class Packet(object):
25 | def __init__(self):
26 | self.session_id = None
27 | self.seq_number = None
28 | self.ack = None
29 | self.data_len = None
30 | self.flags = None
31 | self.sync_type = None
32 | self.content = None
33 | self.optional_headers = False
34 |
35 | # Method for transforming a sotp packet into BitArray
36 | def toBytes(self):
37 | data = BitArray()
38 | data.append('0b'+self.session_id.bin)
39 | data.append('0b'+self.seq_number.bin)
40 | data.append('0b'+self.ack.bin)
41 | data.append('0b'+self.data_len.bin)
42 | data.append('0b'+self.flags.bin)
43 | if self.optional_headers:
44 | data.append('0b'+self.sync_type.bin)
45 | if any(self.content):
46 | data.append('0b'+self.content.bin)
47 | return data.tobytes()
48 |
49 | def isFlagActive(self,checkflag):
50 | return True if self.flags.uint == checkflag else False
51 |
52 | def isSyncType(self,checktype):
53 | return True if self.optional_headers and self.sync_type.uint == checktype else False
54 |
55 | def anyContentAvailable(self):
56 | return True if any(self.data_len) and any(self.content) else False
57 |
--------------------------------------------------------------------------------
/overlay/client/io.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ClientOverlay
21 | from sys import stdout
22 |
23 | class io(ClientOverlay):
24 |
25 | NAME = "io"
26 | CONFIG = {
27 | "prog": NAME,
28 | "description": "Reads from stdin, sends through SOTP connection. Reads from SOTP connection, prints to stdout",
29 | "args": [
30 | {
31 | "--tag": {
32 | "help": "Tag used by the overlay",
33 | "nargs": 1,
34 | "required": False,
35 | "default": ["0x1010"]
36 | }
37 | }
38 | ]
39 | }
40 |
41 | def __init__(self, qsotp, qdata, args, logger=None):
42 | ClientOverlay.__init__(self,type(self).__name__,qsotp,qdata,args,logger)
43 | self.name = type(self).__name__
44 | # Setting input capture
45 | self.hasInput = True
46 | # Logger parameters
47 | self.logger = logger
48 | self._LOGGING_ = False if logger is None else True
49 |
50 | def processInputStream(self, content):
51 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from STDIN: {len(content)} bytes")
52 | return content
53 |
54 | def processSOTPStream(self, content):
55 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to STDOUT: {len(content)} bytes")
56 | stdout.buffer.write(content)
57 | stdout.flush()
--------------------------------------------------------------------------------
/overlay/client/shell.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ClientOverlay
21 | from subprocess import Popen,PIPE,STDOUT
22 | from platform import system
23 |
24 |
25 | class shell(ClientOverlay):
26 |
27 | NAME = "shell"
28 | CONFIG = {
29 | "prog": NAME,
30 | "description": "Executes commands recieved through the SOTP connection and returns the output. Compatible with io module.",
31 | "args": [
32 | {
33 | "--tag": {
34 | "help": "Tag used by the overlay at the server",
35 | "nargs": 1,
36 | "required": False,
37 | "default": ["0x1010"]
38 | }
39 | }
40 | ]
41 | }
42 |
43 |
44 | def __init__(self, qsotp, qdata, args, logger=None):
45 | ClientOverlay.__init__(self,type(self).__name__,qsotp,qdata,args,logger)
46 | self.name = type(self).__name__
47 | # Logger parameters
48 | self.logger = logger
49 | self._LOGGING_ = False if logger is None else True
50 |
51 | def processSOTPStream(self, content):
52 | data = b""
53 | try:
54 | # Windows by default pass Popen data to CreateProcess() Winapi
55 | if system() == "Windows":
56 | commandline = "cmd /c " + str(content,"utf-8")
57 | else:
58 | commandline = str(content,"utf-8")
59 |
60 | p = Popen(commandline.split(), stdout=PIPE, stderr=STDOUT)
61 | data = p.communicate()[0]
62 | except Exception as e:
63 | self._LOGGING_ and self.logger.exception(f"[{self.name}] Exception in shell: {str(e)}")
64 | finally:
65 | return data if (len(data) > 0) else None
--------------------------------------------------------------------------------
/overlay/server/shell.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerOverlay
21 | from subprocess import Popen,PIPE,STDOUT
22 | from platform import system
23 |
24 | class shell(ServerOverlay):
25 |
26 | NAME = "shell"
27 | CONFIG = {
28 | "prog": NAME,
29 | "description": "Executes commands recieved through the SOTP connection and returns the output. Compatible with io module.",
30 | "args": [
31 | {
32 | "--tag": {
33 | "help": "Tag used by the overlay at the server",
34 | "nargs": 1,
35 | "required": False,
36 | "default": ["0x1010"]
37 | }
38 | }
39 | ]
40 | }
41 |
42 |
43 | def __init__(self, id, qsotp, mode, args, logger):
44 | ServerOverlay.__init__(self, type(self).__name__, id, qsotp, mode, args, logger)
45 | self.name = type(self).__name__
46 | self.hasInput = False
47 | # Logger parameters
48 | self.logger = logger
49 | self._LOGGING_ = False if logger is None else True
50 |
51 | def processSOTPStream(self, content):
52 | data = b""
53 | try:
54 | # Windows by default pass Popen data to CreateProcess() Winapi
55 | if system() == "Windows":
56 | commandline = "cmd /c " + str(content,"utf-8")
57 | else:
58 | commandline = str(content,"utf-8")
59 |
60 | p = Popen(commandline.split(), stdout=PIPE, stderr=STDOUT)
61 | data = p.communicate()[0]
62 | except Exception as e:
63 | self._LOGGING_ and self.logger.exception(f"[{self.name}] Exception in shell: {str(e)}")
64 | finally:
65 | # By default, only one worker.
66 | (len(data) > 0) and self.workers[0].datainbox.put(self.streamToSOTPWorker(data,self.workers[0].id))
67 |
--------------------------------------------------------------------------------
/utils/rc4.py:
--------------------------------------------------------------------------------
1 | #
2 | # MIT License
3 | #
4 | # Copyright (c) 2018 David Buchanan
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 | #
24 |
25 | class RC4:
26 | """
27 | https://github.com/DavidBuchanan314/rc4
28 |
29 | This class implements the RC4 streaming cipher.
30 |
31 | Derived from http://cypherpunks.venona.com/archive/1994/09/msg00304.html
32 | """
33 |
34 | def __init__(self, key, streaming=True):
35 | assert(isinstance(key, (bytes, bytearray)))
36 |
37 | # key scheduling
38 | S = list(range(0x100))
39 | j = 0
40 | for i in range(0x100):
41 | j = (S[i] + key[i % len(key)] + j) & 0xff
42 | S[i], S[j] = S[j], S[i]
43 | self.S = S
44 |
45 | # in streaming mode, we retain the keystream state between crypt()
46 | # invocations
47 | if streaming:
48 | self.keystream = self._keystream_generator()
49 | else:
50 | self.keystream = None
51 |
52 | def crypt(self, data):
53 | """
54 | Encrypts/decrypts data (It's the same thing!)
55 | """
56 | assert(isinstance(data, (bytes, bytearray)))
57 | keystream = self.keystream or self._keystream_generator()
58 | return bytes([a ^ b for a, b in zip(data, keystream)])
59 |
60 | def _keystream_generator(self):
61 | """
62 | Generator that returns the bytes of keystream
63 | """
64 | S = self.S.copy()
65 | x = y = 0
66 | while True:
67 | x = (x + 1) & 0xff
68 | y = (S[x] + y) & 0xff
69 | S[x], S[y] = S[y], S[x]
70 | i = (S[x] + S[y]) & 0xff
71 | yield S[i]
--------------------------------------------------------------------------------
/utils/logger.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from logging import DEBUG, INFO, ERROR
21 | from logging import Formatter, FileHandler, getLogger
22 | from glob import glob as Glob
23 | from os import path, mkdir, remove as Remove
24 |
25 | formatter = Formatter('%(asctime)s - %(message)s')
26 |
27 | class Log():
28 |
29 | NONE = 0
30 | LOW = 1
31 | MEDIUM = 2
32 | HIGH = 3
33 |
34 | def __init__(self,prefix="",level=NONE):
35 | self.level = level
36 | if level != self.NONE:
37 | self.clearFiles(prefix)
38 | self.deb = self.setup_logger('debug_log', f"logs/debug{prefix}.log",DEBUG)
39 | self.inf = self.setup_logger('info_log', f"logs/info{prefix}.log",INFO)
40 | self.err = self.setup_logger('error_log', f"logs/error{prefix}.log",ERROR)
41 | self.exc = self.setup_logger('exception_log', f"logs/exception{prefix}.log",ERROR)
42 |
43 | def clearFiles(self,prefix):
44 | if path.exists("logs"):
45 | files = Glob(f"logs/*{prefix}.log")
46 | for f in files:
47 | Remove(f)
48 | else:
49 | mkdir("logs")
50 |
51 | def setup_logger(self, name, log_file, level):
52 | handler = FileHandler(log_file)
53 | handler.setFormatter(formatter)
54 |
55 | logger = getLogger(name)
56 | logger.setLevel(level)
57 | logger.addHandler(handler)
58 |
59 | return logger
60 |
61 | def debug_all(self, message):
62 | if self.level == self.HIGH:
63 | self.deb.debug(message)
64 |
65 | def debug(self, message):
66 | if self.level > self.LOW:
67 | self.deb.debug(message)
68 |
69 | def error(self, message):
70 | if self.level != self.NONE:
71 | self.err.error(message)
72 |
73 | def info(self, message):
74 | if self.level != self.NONE:
75 | self.inf.info(message)
76 |
77 | def exception(self, message):
78 | if self.level != self.NONE:
79 | self.exc.exception(message)
--------------------------------------------------------------------------------
/overlay/server/io.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | from sotp.misticathread import ServerOverlay
22 | from sys import stdout
23 |
24 |
25 | class io(ServerOverlay):
26 |
27 | NAME = "io"
28 | CONFIG = {
29 | "prog": NAME,
30 | "description": "Reads from stdin, sends through SOTP connection. Reads from SOTP connection, prints to stdout",
31 | "args": [
32 | {
33 | "--tag": {
34 | "help": "Tag used by the overlay",
35 | "nargs": 1,
36 | "required": False,
37 | "default": ["0x1010"]
38 | }
39 | }
40 | ]
41 | }
42 |
43 | def __init__(self, id, qsotp, mode, args, logger):
44 | ServerOverlay.__init__(self, type(self).__name__, id, qsotp, mode, args, logger)
45 | self.name = type(self).__name__
46 | self.buffer = []
47 | # Setting input capture
48 | self.hasInput = True
49 | # Logger parameters
50 | self.logger = logger
51 | self._LOGGING_ = False if logger is None else True
52 |
53 | def processInputStream(self, content):
54 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from STDIN: {len(content)}")
55 | return content
56 |
57 | def processSOTPStream(self, content):
58 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to STDOUT: {len(content)}")
59 | stdout.buffer.write(content)
60 | stdout.flush()
61 |
62 | # overriden for pipe scenarios
63 | def handleInputStream(self, msg):
64 | content = self.processInputStream(msg.content)
65 | # By default, only one worker. Must be overriden for more
66 | # In a multi-worker scenario inputs must be mapped to workers
67 | if self.workers:
68 | return self.streamToSOTPWorker(content, self.workers[0].id)
69 |
70 | self.buffer.append(content)
71 |
72 | # overriden for pipe scenarios
73 | def addWorker(self, worker):
74 | if not self.workers: # empty
75 | self.workers.append(worker)
76 | # check if there's some buffered data and pass it
77 | while self.buffer:
78 | self.workers[0].datainbox.put(self.streamToSOTPWorker(self.buffer.pop(0),
79 | self.workers[0].id))
80 | else:
81 | raise(f"Cannot Register worker on overlay module. Module {self.name} only accepts one worker")
82 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_module/icmp.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerWrapper
21 | from base64 import urlsafe_b64encode,urlsafe_b64decode
22 | from wrapper.server.wrap_server.icmpserver import icmpserver
23 |
24 | class icmpwrapper(ServerWrapper):
25 |
26 | SERVER_CLASS = icmpserver
27 | NAME = "icmp"
28 | CONFIG = {
29 | "prog": "icmp",
30 | "wrapserver": "icmpserver",
31 | "description": "Encodes/Decodes data in ICMP echo requests/responses on data section",
32 | "args": [
33 | {
34 | "--max-size": {
35 | "help": "Max size of the SOTP packet. Default is 1024 bytes",
36 | "nargs": 1,
37 | "default": [1024],
38 | "type": int
39 | },
40 | "--max-retries": {
41 | "help": "Maximum number of re-synchronization retries.",
42 | "nargs": 1,
43 | "default": [5],
44 | "type": int
45 | }
46 | }
47 | ]
48 | }
49 |
50 | def __init__(self, id, qsotp, args, logger):
51 | ServerWrapper.__init__(self, id, icmpwrapper.NAME, qsotp, icmpwrapper.SERVER_CLASS.NAME, args, logger)
52 | # Base args
53 | self.max_size = None
54 | self.max_retries = None
55 | # Parsing args
56 | self.argparser = self.generateArgParser()
57 | self.parseArguments(args)
58 | # Logger parameters
59 | self.logger = logger
60 | self._LOGGING_ = False if logger is None else True
61 |
62 | def parseArguments(self, args):
63 | parsed = self.argparser.parse_args(args.split())
64 | self.max_size = parsed.max_size[0]
65 | self.max_retries = parsed.max_retries[0]
66 |
67 | def unpackSotp(self, data):
68 | try:
69 | # We use base64_urlsafe_encode, change if you encode different.
70 | return urlsafe_b64decode(data)
71 | except Exception as e:
72 | self.logger.exception(f"[{self.name}] Exception at unpackSotp: {e}")
73 | return
74 |
75 | def unwrap(self, content):
76 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] unwrap data: {content}")
77 | return self.unpackSotp(content)
78 |
79 | def wrap(self, content):
80 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] wrap data: {content}")
81 | urlSafeEncodedBytes = urlsafe_b64encode(content)
82 | return urlSafeEncodedBytes
83 |
--------------------------------------------------------------------------------
/utils/prompt.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 |
21 | from sotp.misticathread import ClientOverlay, ClientWrapper, ServerOverlay, ServerWrapper
22 | from overlay.client import *
23 | from overlay.server import *
24 | #from wrapper.server.wrap_module import *
25 | from wrapper.client import *
26 | from argparse import ArgumentParser
27 |
28 |
29 | class Prompt(object):
30 | def __init__(self):
31 | self.banner = "[Mistica] >>> "
32 |
33 | def GetFromStdin(self):
34 | data = input(self.banner)
35 | return data
36 |
37 | @staticmethod
38 | def listModules(type, lst):
39 | if lst == "overlays":
40 | output = Prompt.listOverlays(type)
41 | elif lst == "wrappers":
42 | output = Prompt.listWrapModules(type)
43 | else:
44 | output = Prompt.listOverlays(type)
45 | output = output + Prompt.listWrapModules(type)
46 | return output
47 |
48 | @staticmethod
49 | def listOverlays(type):
50 | if type == "server":
51 | overlaylist = [x for x in ServerOverlay.__subclasses__()]
52 | else:
53 | overlaylist = [x for x in ClientOverlay.__subclasses__()]
54 | overlaydict = {}
55 | for elem in overlaylist:
56 | overlaydict[elem.NAME] = elem.CONFIG["description"]
57 | output = "\nOverlay modules:\n\n"
58 | for k in sorted(overlaydict):
59 | output = output + "- {}: {}\n".format(k, overlaydict[k])
60 | return output
61 |
62 |
63 | @staticmethod
64 | def listWrapModules(type):
65 | if type == "server":
66 | overlaylist = [x for x in ServerWrapper.__subclasses__()]
67 | else:
68 | overlaylist = [x for x in ClientWrapper.__subclasses__()]
69 | wmdict = {}
70 | for elem in overlaylist:
71 | wmdict[elem.NAME] = elem.CONFIG["description"]
72 | output = "\nWrap modules:\n\n"
73 | for k in sorted(wmdict):
74 | output = output + "- {}: {}\n".format(k, wmdict[k])
75 | return output
76 |
77 | @staticmethod
78 | def listParameters(type, lst):
79 | module = Prompt.findModule(type, lst)
80 | if not module:
81 | return f"Module {lst} does not exist"
82 | argparser = Prompt.generateArgParser(module)
83 | try:
84 | argparser.parse_args(["-h"])
85 | except SystemExit:
86 | pass
87 | try:
88 | ws = module.SERVER_CLASS
89 | except Exception:
90 | ws = None
91 | if ws:
92 | print(f"\n{lst} uses {ws.NAME} as wrap server\n")
93 | argparser = Prompt.generateArgParser(ws)
94 | try:
95 | argparser.parse_args(["-h"])
96 | except SystemExit:
97 | pass
98 |
99 | @staticmethod
100 | def findModule(type, lst):
101 | if type == "server":
102 | for x in ServerWrapper.__subclasses__():
103 | if x.NAME == lst:
104 | return x
105 | for x in ServerOverlay.__subclasses__():
106 | if x.NAME == lst:
107 | return x
108 | for x in ClientWrapper.__subclasses__():
109 | if x.NAME == lst:
110 | return x
111 | for x in ClientOverlay.__subclasses__():
112 | if x.NAME == lst:
113 | return x
114 | return None
115 |
116 | @staticmethod
117 | def generateArgParser(module):
118 | config = module.CONFIG
119 |
120 | parser = ArgumentParser(prog=config["prog"],description=config["description"])
121 | for arg in config["args"]:
122 | for name,field in arg.items():
123 | opts = {}
124 | for key,value in field.items():
125 | opts[key] = value
126 | parser.add_argument(name, **opts)
127 | return parser
128 |
--------------------------------------------------------------------------------
/utils/messaging.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from enum import Enum
21 | from sotp.core import Header,OptionalHeader,Sizes,Offsets,Status,Flags,Sync
22 | from sotp.core import Core
23 |
24 |
25 | class MessageType(Enum):
26 | STREAM = 0
27 | SIGNAL = 1
28 |
29 |
30 | class SignalType(Enum):
31 | START = 0
32 | TERMINATE = 1
33 | STOP = 2
34 | RESTART = 3
35 | COMMS_FINISHED = 4
36 | COMMS_BROKEN = 5
37 | ERROR = 6
38 | BUFFER_READY = 7
39 |
40 |
41 | class Message():
42 | '''
43 | {
44 | from : string,
45 | from_id : int,
46 | to : string,
47 | to_id : int,
48 | type : int,
49 | content : (Arbitrary),
50 | wrapServerQ: Queue (only in Mistica Server)
51 | }
52 | '''
53 | def __init__(self, sender, sender_id, receiver, receiver_id, msgtype, content, wrapServerQ=None):
54 | self.sender = sender
55 | self.sender_id = sender_id
56 | self.receiver = receiver
57 | self.receiver_id = receiver_id
58 | self.msgtype = msgtype
59 | self.content = content
60 | self.wrapServerQ = wrapServerQ
61 |
62 | def __eq__(self, other):
63 | return self.sender == other.sender and self.receiver == other.receiver and self.msgtype == other.msgtype and self.content == other.content
64 |
65 | def isCommsFinishedMessage(self):
66 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.COMMS_FINISHED:
67 | return True
68 | return False
69 |
70 | def isCommsBrokenMessage(self):
71 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.COMMS_BROKEN:
72 | return True
73 | return False
74 |
75 | def isTerminateMessage(self):
76 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.TERMINATE:
77 | return True
78 | return False
79 |
80 | def isCommunicationEndedMessage(self):
81 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.COMMS_FINISHED:
82 | return True
83 | return False
84 |
85 | def isCommunicationBrokenMessage(self):
86 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.COMMS_BROKEN:
87 | return True
88 | return False
89 |
90 | def isStartMessage(self):
91 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.START:
92 | return True
93 | return False
94 |
95 | def isStopMessage(self):
96 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.STOP:
97 | return True
98 | return False
99 |
100 | def isRestartMessage(self):
101 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.RESTART:
102 | return True
103 | return False
104 |
105 | def isBufferReady(self):
106 | if self.msgtype == MessageType.SIGNAL and self.content == SignalType.BUFFER_READY:
107 | return True
108 | return False
109 |
110 | def isStreamMessage(self):
111 | return (self.msgtype == MessageType.STREAM)
112 |
113 | def isSignalMessage(self):
114 | return (self.msgtype == MessageType.SIGNAL)
115 |
116 | # method for print/debug messages
117 | def printHeader(self):
118 | if self.isSignalMessage():
119 | return f"Signal Message: {self.content}"
120 | if not self.isStreamMessage():
121 | return "Message is not a Stream Message"
122 | try:
123 | p = Core.transformToPacket(self.content)
124 | sid = p.session_id.uint
125 | sq = p.seq_number.uint
126 | ack = p.ack.uint
127 | dl = p.data_len.uint
128 | fl = p.flags.uint
129 | oh = p.optional_headers
130 | st = p.sync_type.uint if fl == 1 else 0
131 | return f"SID: {sid}, SQ: {sq}, ACK: {ack}, DL: {dl}, FL: {fl}, OH: {oh}, SYT: {st}"
132 | except Exception:
133 | return f"Message content is not a SOTP Packet {self.content}"
134 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_module/http.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerWrapper
21 | from base64 import urlsafe_b64encode,urlsafe_b64decode
22 | from wrapper.server.wrap_server.httpserver import httpserver
23 |
24 | class httpwrapper(ServerWrapper):
25 |
26 | SERVER_CLASS = httpserver
27 | NAME = "http"
28 | CONFIG = {
29 | "prog": "http",
30 | "wrapserver": "httpserver",
31 | "description": "Encodes/Decodes data in HTTP requests/responses using different methods",
32 | "args": [
33 | {
34 | "--method": {
35 | "help": "HTTP Method to use",
36 | "nargs": 1,
37 | "default": ["GET"],
38 | "choices": ["GET","POST"],
39 | "type": str
40 | },
41 | "--uri": {
42 | "help": "URI Path before data message",
43 | "nargs": 1,
44 | "default": ["/"],
45 | "type": str
46 | },
47 | "--header": {
48 | "help": "Header key for encapsulate data message",
49 | "nargs": 1,
50 | "type": str
51 | },
52 | "--post-field": {
53 | "help": "Post Field for encapsulate data message",
54 | "nargs": 1,
55 | "type": str
56 | },
57 | "--success-code": {
58 | "help": "HTTP Code for Success Connections. Default is 200",
59 | "nargs": 1,
60 | "default": [200],
61 | "choices": [100,101,102,200,201,202,203,204,205,206,207,
62 | 208,226,300,301,302,303,304,305,306,307,308,
63 | 400,401,402,403,404,405,406,407,408,409,410,
64 | 411,412,413,414,415,416,417,418,421,422,423,
65 | 424,426,428,429,431,500,501,502,503,504,505,
66 | 506,507,508,510,511],
67 | "type": int
68 | },
69 | "--max-size": {
70 | "help": "Max size of the SOTP packet. Default is 10000 bytes",
71 | "nargs": 1,
72 | "default": [10000],
73 | "type": int
74 | },
75 | "--max-retries": {
76 | "help": "Maximum number of re-synchronization retries.",
77 | "nargs": 1,
78 | "default": [5],
79 | "type": int
80 | }
81 | }
82 | ]
83 | }
84 |
85 | def __init__(self, id, qsotp, args, logger):
86 | ServerWrapper.__init__(self, id, httpwrapper.NAME, qsotp, httpwrapper.SERVER_CLASS.NAME, args,logger)
87 | # Logger parameters
88 | self.logger = logger
89 | self._LOGGING_ = False if logger is None else True
90 |
91 | def parseArguments(self, args):
92 | parsed = self.argparser.parse_args(args.split())
93 | self.method = parsed.method[0]
94 | self.header = parsed.header[0] if parsed.header is not None else None
95 | self.uri = parsed.uri[0]
96 | self.post_field = parsed.post_field[0] if parsed.post_field is not None else None
97 | self.max_size = parsed.max_size[0]
98 | self.max_retries = parsed.max_retries[0]
99 | self.success_code = parsed.success_code[0]
100 |
101 | def unpackSotp(self, data):
102 | # We use base64_urlsafe_encode, change if you encode different.
103 | return urlsafe_b64decode(data)
104 |
105 | def parseFromHeaders(self, content):
106 | try:
107 | for key,value in content.items():
108 | if key == self.header:
109 | return self.unpackSotp(value)
110 | return None
111 | except Exception:
112 | return None
113 |
114 | def parseFromURI(self, requestline):
115 | try:
116 | _,uri,_ = requestline.split(' ')
117 | sotpdata = uri.replace(self.uri,'')
118 | return self.unpackSotp(sotpdata)
119 | except Exception:
120 | return None
121 |
122 | def parseFromPostFields(self, fields):
123 | try:
124 | for field in fields.list:
125 | if field.name == self.post_field:
126 | return self.unpackSotp(field.value)
127 | return None
128 | except Exception:
129 | return None
130 |
131 | def parseGET(self, content):
132 | if self.header:
133 | return self.parseFromHeaders(content['headers'])
134 | else:
135 | return self.parseFromURI(content['requestline'])
136 |
137 | def parsePOST(self, content):
138 | if self.header:
139 | return self.parseFromHeaders(content['headers'])
140 | elif self.post_field:
141 | return self.parseFromPostFields(content['content'])
142 | else:
143 | return self.parseFromURI(content['requestline'])
144 |
145 | def unwrap(self, content):
146 | if self.method == "GET":
147 | unwrapped = self.parseGET(content)
148 | else:
149 | unwrapped = self.parsePOST(content)
150 | return unwrapped
151 |
152 | def generateResponse(self,content):
153 | return {
154 | "requestline" : "",
155 | "headers" : {},
156 | "content" : content,
157 | "httpcode" : self.success_code
158 | }
159 |
160 | def wrap(self, content):
161 | urlSafeEncodedBytes = urlsafe_b64encode(content)
162 | urlSafeEncodedStr = str(urlSafeEncodedBytes, "utf-8")
163 | return self.generateResponse(urlSafeEncodedStr)
164 |
--------------------------------------------------------------------------------
/sotp/core.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.packet import Packet
21 | from utils.bitstring import BitArray
22 | from utils.rc4 import RC4
23 | from utils.buffer import Index, OverlayBuffer, WrapperBuffer
24 |
25 |
26 | BYTE = 8
27 |
28 |
29 | class Header(object):
30 | SESSION_ID = 1 * BYTE
31 | SEQ_NUMBER = 2 * BYTE
32 | ACK = 2 * BYTE
33 | DATA_LEN = 2 * BYTE
34 | FLAGS = 1 * BYTE
35 |
36 |
37 | class OptionalHeader(object):
38 | SYNC_TYPE = 1 * BYTE
39 |
40 |
41 | class Sizes(object):
42 | HEADER = Header.SESSION_ID + Header.SEQ_NUMBER + Header.ACK + Header.DATA_LEN + Header.FLAGS
43 | OPTIONAL_HEADER = OptionalHeader.SYNC_TYPE
44 | MAX_MESSAGES = (2**Header.SEQ_NUMBER)-1
45 | TAG = 2 * BYTE
46 |
47 |
48 | class Offsets(object):
49 | SESSION_ID = 0 + Header.SESSION_ID
50 | SEQ_NUMBER = SESSION_ID + Header.SEQ_NUMBER
51 | ACK = SEQ_NUMBER + Header.ACK
52 | DATA_LEN = ACK + Header.DATA_LEN
53 | FLAGS = DATA_LEN + Header.FLAGS
54 | SYNC_TYPE = 0 + OptionalHeader.SYNC_TYPE
55 |
56 |
57 | class Status(object):
58 | NOT_INITIALIZING = 0
59 | INITIALIZING = 1
60 | WORKING = 2
61 | TERMINATING = 3
62 | REINITIALIZING = 4
63 | STOPING = 5
64 |
65 |
66 | class Flags(object):
67 | SYNC = 1
68 | PUSH = 2
69 |
70 |
71 | class Sync(object):
72 | REQUEST_AUTH = 0
73 | RESPONSE_AUTH = 1
74 | REINITIALIZING = 2
75 | POLLING_REQUEST = 5
76 | SESSION_TERMINATION = 6
77 |
78 |
79 | class Core(object):
80 |
81 | def __init__(self, key, maxretries, maxsize):
82 | self.rc4 = RC4(bytes(key, encoding='utf8'), False)
83 | self.st = Status.NOT_INITIALIZING
84 | self.maxretries = maxretries
85 | self.retries = 0
86 | self.lastPacketSent = None
87 | self.lastPacketRecv = None
88 | self.bufOverlay = OverlayBuffer()
89 | self.bufWrapper = WrapperBuffer()
90 | self.checkMaxSizeAvailable(maxsize)
91 |
92 | def checkMaxSizeAvailable(self, maxsize):
93 | if maxsize > 2**Header.DATA_LEN:
94 | raise Exception(f"MaxSize {maxsize} is exced {2**Header.DATA_LEN} which is max representation with {Header.DATA_LEN} bits of Data Length")
95 | self.maxsize = maxsize
96 |
97 | @staticmethod
98 | def fromBytesToBitArray(data):
99 | return BitArray(bytes=data)
100 |
101 | @staticmethod
102 | def parseRawPacket(binarypacket):
103 | if len(binarypacket) < Sizes.HEADER:
104 | raise Exception(f"Raw Packet size {len(binarypacket)} is lower than the minimun Header size {Sizes.HEADER}")
105 | return binarypacket[:Sizes.HEADER], binarypacket[Sizes.HEADER:]
106 |
107 | @staticmethod
108 | def buildPacket(header, body):
109 | p = Packet()
110 | p.session_id = header[0:Offsets.SESSION_ID]
111 | p.seq_number = header[Offsets.SESSION_ID:Offsets.SEQ_NUMBER]
112 | p.ack = header[Offsets.SEQ_NUMBER:Offsets.ACK]
113 | p.data_len = header[Offsets.ACK:Offsets.DATA_LEN]
114 | p.flags = header[Offsets.DATA_LEN:Offsets.FLAGS]
115 | p.sync_type = None
116 | p.content = None
117 | if p.isFlagActive(Flags.SYNC):
118 | p.sync_type = BitArray(bin=body.bin[0:Offsets.SYNC_TYPE])
119 | p.content = BitArray(bin=body.bin[Offsets.SYNC_TYPE:])
120 | p.optional_headers = True
121 | else:
122 | p.sync_type = BitArray()
123 | p.content = BitArray(bin=body.bin)
124 | return p
125 |
126 | @staticmethod
127 | def transformToPacket(rawbytes):
128 | bitarraydata = Core.fromBytesToBitArray(rawbytes)
129 | header, body = Core.parseRawPacket(bitarraydata)
130 | packt = Core.buildPacket(header, body)
131 | return packt
132 |
133 | def checkMainFields(self,packt):
134 | if any(packt.session_id) == False:
135 | return False
136 | if any(packt.seq_number) == False:
137 | return False
138 | if any(packt.ack) == False:
139 | return False
140 | return True
141 |
142 | def checkForRetries(self):
143 | if self.retries == self.maxretries:
144 | self.retries = 0
145 | return True
146 | self.retries = self.retries + 1
147 | return False
148 |
149 | def lostPacket(self):
150 | if self.lastPacketSent is None:
151 | raise Exception("Last sent packet is None, cannot resend")
152 | else:
153 | return self.lastPacketSent
154 |
155 | def decryptWrapperData(self):
156 | packets = self.bufWrapper.getChunks()
157 | content = BitArray()
158 | for packet in packets:
159 | content.append('0b' + packet.content.bin)
160 | bytescontent = content.tobytes()
161 | decryptcontent = self.rc4.crypt(bytescontent)
162 | return decryptcontent
163 |
164 | def storeOverlayContent(self, data):
165 | data = self.rc4.crypt(data)
166 | index = Index()
167 | lendata = len(data)
168 | for i in range(0, lendata, self.maxsize):
169 | index.add(data[i:i + self.maxsize])
170 | self.bufOverlay.addIndex(index)
171 |
172 | def someOverlayData(self):
173 | return self.bufOverlay.anyIndex()
174 |
175 | def checkConfirmation(self,packt):
176 | if self.lastPacketSent is None:
177 | raise Exception("Haven't sent any packet, can't perform confirmation.")
178 | if self.lastPacketSent.seq_number != packt.ack:
179 | return False
180 | return True
181 |
182 | def checkTermination(self,packt):
183 | if self.checkMainFields(packt) == False:
184 | return False
185 | if packt.isFlagActive(Flags.SYNC) == False:
186 | return False
187 | if packt.isSyncType(Sync.SESSION_TERMINATION) == False:
188 | return False
189 | return True
190 |
--------------------------------------------------------------------------------
/overlay/client/tcplisten.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ClientOverlay
21 | from sys import stdout
22 | import socket
23 | from threading import Thread
24 | from utils.messaging import Message, MessageType, SignalType
25 | import select
26 |
27 | class tcplisten(ClientOverlay):
28 |
29 | NAME = "tcplisten"
30 | CONFIG = {
31 | "prog": NAME,
32 | "description": "Binds to TCP port. Reads from socket, sends through SOTP connection. Reads from SOTP connection, sends through socket.",
33 | "args": [
34 | {
35 | "--tag": {
36 | "help": "Tag used by the overlay",
37 | "nargs": 1,
38 | "required": False,
39 | "default": ["0x1010"]
40 | }
41 | },
42 | {
43 | "--address": {
44 | "help": "Address where the module will bind",
45 | "nargs": 1,
46 | "required": True,
47 | "action": "store"
48 | }
49 | },
50 | {
51 | "--port": {
52 | "help": "Port where the module will bind",
53 | "nargs": 1,
54 | "required": True,
55 | "action": "store"
56 | }
57 | },
58 | {
59 | "--persist": {
60 | "help": "Keeps the port open after closing the TCP connection",
61 | "action": "store_true"
62 | }
63 | }
64 | ]
65 | }
66 |
67 |
68 | def __init__(self, qsotp, mode, args, logger):
69 | ClientOverlay.__init__(self, type(self).__name__, qsotp, mode, args, logger)
70 | self.name = type(self).__name__
71 | self.buffer = []
72 | # Setting input capture
73 | self.conn = None
74 | self.hasInput = False
75 | self.id = 0
76 | self.timeout = 1
77 | # Logger parameters
78 | self.logger = logger
79 | self._LOGGING_ = False if logger is None else True
80 | self.tcpthread = Thread(target=self.captureTcpStream)
81 | self.tcpthread.start()
82 |
83 | # Overriden
84 | def parseArguments(self, args):
85 | self.port = int(args.port[0])
86 | self.address = args.address[0]
87 | self.persist = args.persist
88 |
89 | def captureTcpStream(self):
90 | # Create socket and listen
91 | while self.conn is None and not self.exit:
92 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
93 | self.socket.settimeout(self.timeout)
94 | try:
95 |
96 | self.socket.bind((self.address, self.port))
97 | self.socket.listen(0)
98 | self.conn, self.remoteaddr = self.socket.accept()
99 |
100 | except socket.timeout as te:
101 | continue # Allows to check if the application has exited to finish the thread
102 | except Exception as e:
103 | print(e)
104 | self.inbox.put(Message('input',
105 | 0,
106 | 'overlay',
107 | self.id,
108 | MessageType.SIGNAL,
109 | SignalType.TERMINATE))
110 | return
111 | # Empty buffered data, if any
112 | while self.buffer and not self.exit:
113 | self.conn.send(self.buffer.pop(0))
114 | # socket loop
115 | while not self.exit:
116 | try:
117 | # Block on socket until Timeout
118 | result = select.select([self.conn], [], [], self.timeout)
119 | if result[0]:
120 | rawdata = self.conn.recv(4096)
121 | if rawdata and len(rawdata) > 0:
122 | self.inbox.put(Message('input',
123 | 0,
124 | 'overlay',
125 | self.id,
126 | MessageType.STREAM,
127 | rawdata))
128 | elif not self.persist:
129 | self._LOGGING_ and self.logger.debug(f"[{self.name}] TCP communication broken. Sending Terminate signal to overlay")
130 | self.inbox.put(Message('input',
131 | 0,
132 | self.name,
133 | self.id,
134 | MessageType.SIGNAL,
135 | SignalType.TERMINATE))
136 | return
137 | else:
138 | self.conn.close()
139 | self.conn = None
140 | break
141 | except:
142 | self.inbox.put(Message('input',
143 | 0,
144 | self.name,
145 | self.id,
146 | MessageType.SIGNAL,
147 | SignalType.TERMINATE))
148 | return
149 |
150 | def processInputStream(self, content):
151 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from socket: {len(content)}")
152 | return content
153 |
154 | def processSOTPStream(self, content):
155 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to socket: {len(content)}")
156 | if self.conn is None:
157 | self.buffer.append(content) # wait until a connection happens
158 | else:
159 | self.conn.send(content)
160 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_server/icmpserver.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from threading import Thread
21 | from queue import Queue,Empty
22 | from utils.messaging import Message, MessageType, SignalType
23 | from argparse import ArgumentParser
24 | from json import load
25 | from utils.prompt import Prompt
26 |
27 | import socket, select
28 | from utils.icmp import Packet
29 |
30 | class icmpserver(Thread):
31 |
32 | NAME = "icmpserver"
33 | CONFIG = {
34 | "prog": "icmpserver",
35 | "description": "Simple ICMP Server",
36 | "args": [
37 | {
38 | "--iface": {
39 | "help": "Network interface to bind (Ex: eth0, wlp2s0, etc)",
40 | "nargs": 1,
41 | "type": str,
42 | "required": 1
43 | },
44 | "--timeout": {
45 | "help": "Max time, in seconds, that the server will wait for the SOTP layer to reply, before returning an error. Default is 5",
46 | "nargs": 1,
47 | "default": [5],
48 | "type" : int
49 | },
50 | "--request-timeout": {
51 | "help": "Max time, in seconds, that the server will wait blocked on raw socket",
52 | "nargs": 1,
53 | "default": [3],
54 | "type" : int
55 | }
56 | }
57 | ]
58 | }
59 |
60 | def __init__(self, id, args, logger):
61 | Thread.__init__(self)
62 | self.wrappers = []
63 | self.id = id
64 | self.server = None
65 | self.name = type(self).__name__
66 | self.inbox = Queue()
67 | self.shutdown = False
68 | # Server parameters
69 | self.iface = None
70 | self.timeout = None
71 | self.request_timeout = None
72 | # Argparsing
73 | self.argparser = self.generateArgParser()
74 | self.parseArguments(args)
75 | # Logger parameters
76 | self.logger = logger
77 | self._LOGGING_ = False if logger is None else True
78 | # Open Raw Socket and binding to a Network Interface
79 | self.mysocket = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_ICMP)
80 | self.mysocket.setsockopt(socket.SOL_SOCKET,25,str(self.iface+'\0').encode('utf-8'))
81 |
82 | def parseArguments(self, args):
83 | parsed = self.argparser.parse_args(args.split())
84 | self.iface = parsed.iface[0]
85 | self.timeout = parsed.timeout[0]
86 | self.request_timeout = parsed.request_timeout[0]
87 |
88 | def generateArgParser(self):
89 | config = self.CONFIG
90 |
91 | parser = ArgumentParser(prog=config["prog"],description=config["description"])
92 | for arg in config["args"]:
93 | for name,field in arg.items():
94 | opts = {}
95 | for key,value in field.items():
96 | opts[key] = value
97 | parser.add_argument(name, **opts)
98 | return parser
99 |
100 | def SignalThread(self):
101 | while True:
102 | msg = self.inbox.get()
103 | if msg.isTerminateMessage():
104 | self.shutdown = True
105 | break
106 |
107 | def addWrapModule(self, encWrapper):
108 | self.wrappers.append(encWrapper)
109 |
110 | def removeWrapModule(self, encWrapper):
111 | self.wrappers.remove(encWrapper)
112 |
113 | def doMulticast(self,q,data):
114 | for wrap in self.wrappers:
115 | msg = Message(self.name, self.id, wrap.name, wrap.id,
116 | MessageType.STREAM, data, q)
117 | wrap.inbox.put(msg)
118 |
119 | def waitForResponse(self, q, request):
120 | response = None
121 | try:
122 | r = q.get(True,self.timeout)
123 | response = r.content
124 | except (Empty,Exception):
125 | response = request.data
126 | self._LOGGING_ and self.logger.error(f"[{self.name}] queue timeout expired answering: {response}")
127 | finally:
128 | return response
129 |
130 | def returnResponse(self, request, data, addr):
131 | response = Packet()
132 | response.pack_response(request, data)
133 | raw_response = response.toBytes()
134 | self.mysocket.sendto(raw_response, (addr[0], 1))
135 | self.mysocket.setblocking(0)
136 |
137 | def processRequest(self, raw_data, addr):
138 | try:
139 | request = Packet()
140 | request.unpack(raw_data)
141 | q = Queue()
142 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] icmp request data: {request.data}")
143 |
144 | self.doMulticast(q,request.data)
145 | response = self.waitForResponse(q, request)
146 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] icmp response data: {response}")
147 |
148 | if not response:
149 | return
150 |
151 | self.returnResponse(request,response,addr)
152 | except Exception as e:
153 | self._LOGGING_ and self.logger.exception(f"[{self.name}] exception in processRequest: {e}")
154 | return
155 |
156 | def run(self):
157 | self._LOGGING_ and self.logger.info(f"[{self.name}] Server started. Passing messages...")
158 | st = Thread(target=self.SignalThread)
159 | st.start()
160 |
161 | while not self.shutdown:
162 | ready = select.select([self.mysocket], [], [], self.request_timeout)
163 | if ready[0]:
164 | rec_packet, addr = self.mysocket.recvfrom(65535)
165 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] recv raw data: {rec_packet}")
166 |
167 | # Handle every request in separate thread
168 | it = Thread(target=self.processRequest, args=(rec_packet,addr,))
169 | it.start()
170 |
171 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] Terminated")
172 |
173 |
--------------------------------------------------------------------------------
/overlay/client/tcpconnect.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ClientOverlay
21 | from sys import stdout
22 | import socket
23 | from threading import Thread, Lock
24 | from utils.messaging import Message, MessageType, SignalType
25 | import select
26 |
27 | class tcpconnect(ClientOverlay):
28 |
29 | NAME = "tcpconnect"
30 | CONFIG = {
31 | "prog": NAME,
32 | "description": "Connects to TCP port. Reads from socket, sends through SOTP connection. Reads from SOTP connection, sends through socket.",
33 | "args": [
34 | {
35 | "--tag": {
36 | "help": "Tag used by the overlay",
37 | "nargs": 1,
38 | "required": False,
39 | "default": ["0x1010"]
40 | }
41 | },
42 | {
43 | "--address": {
44 | "help": "Address where the module will connect",
45 | "nargs": 1,
46 | "required": True,
47 | "action": "store"
48 | }
49 | },
50 | {
51 | "--port": {
52 | "help": "Port where the module will connect",
53 | "nargs": 1,
54 | "required": True,
55 | "action": "store"
56 | }
57 | },
58 | {
59 | "--persist": {
60 | "help": "Retries the TCP connection, if closed",
61 | "action": "store_true"
62 | }
63 | },
64 | {
65 | "--wait": {
66 | "help": "Waits until data is received through the SOTP channel to connect",
67 | "action": "store_true"
68 | }
69 | }
70 | ]
71 | }
72 |
73 |
74 | def __init__(self, qsotp, mode, args, logger):
75 | ClientOverlay.__init__(self, type(self).__name__, qsotp, mode, args, logger)
76 | self.name = type(self).__name__
77 | self.buffer = []
78 | # Setting input capture
79 | self.hasInput = False
80 | self.id = 0
81 | self.timeout = 1
82 | self.started = False
83 | self.lock = Lock()
84 | # Logger parameters
85 | self.logger = logger
86 | self._LOGGING_ = False if logger is None else True
87 | self.tcpthread = Thread(target=self.captureTcpStream)
88 | self.lock.acquire()
89 | if not self.wait:
90 | self.tcpthread.start()
91 | self.started = True
92 |
93 | # Overriden
94 | def parseArguments(self, args):
95 | self.port = int(args.port[0])
96 | self.address = args.address[0]
97 | self.persist = args.persist
98 | self.wait = args.wait
99 |
100 | def captureTcpStream(self):
101 | # Create socket and connect
102 | while not self.exit:
103 | try:
104 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
105 | self.socket.connect((self.address, self.port))
106 | except Exception as e:
107 | print(e)
108 | self.inbox.put(Message('input',
109 | 0,
110 | self.name,
111 | self.id,
112 | MessageType.SIGNAL,
113 | SignalType.TERMINATE))
114 | if self.lock.locked():
115 | self.lock.release()
116 | return
117 | if self.lock.locked():
118 | self.lock.release()
119 | while not self.exit:
120 | try:
121 | # Block on socket until Timeout
122 | result = select.select([self.socket], [], [], self.timeout)
123 | if result[0]:
124 | rawdata = self.socket.recv(4096)
125 | if rawdata and len(rawdata) > 0:
126 | self.inbox.put(Message('input',
127 | 0,
128 | 'overlay',
129 | self.id,
130 | MessageType.STREAM,
131 | rawdata))
132 | elif not self.persist:
133 | self._LOGGING_ and self.logger.debug(f"[{self.name}] TCP communication broken. Sending Terminate signal to overlay")
134 | self.inbox.put(Message('input',
135 | 0,
136 | self.name,
137 | self.id,
138 | MessageType.SIGNAL,
139 | SignalType.TERMINATE))
140 | return
141 | else:
142 | self.socket.close()
143 | break
144 | except:
145 | self.inbox.put(Message('input',
146 | 0,
147 | self.name,
148 | self.id,
149 | MessageType.SIGNAL,
150 | SignalType.TERMINATE))
151 | return
152 | return
153 |
154 | def processInputStream(self, content):
155 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from socket: {len(content)}")
156 | return content
157 |
158 | def processSOTPStream(self, content):
159 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to socket: {len(content)}")
160 | if not self.started:
161 | self.tcpthread.start()
162 | self.lock.acquire()
163 | self.started = True
164 | try:
165 | self.socket.send(content)
166 | except Exception as e:
167 | print(e)
168 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_server/dnsserver.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from threading import Thread
21 | from queue import Queue,Empty
22 | from utils.messaging import Message, MessageType, SignalType
23 | from dnslib import DNSRecord, DNSHeader, QTYPE, CLASS, RR, TXT
24 | from argparse import ArgumentParser
25 | from utils.prompt import Prompt
26 | from socketserver import ThreadingUDPServer
27 | from socketserver import BaseRequestHandler
28 |
29 |
30 | class CustomBaseRequestHandler(BaseRequestHandler):
31 |
32 | def genDefaultError(self, request):
33 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
34 | reply.add_answer(RR(rname=request.q.qname,
35 | rtype=QTYPE.TXT,
36 | rclass=CLASS.IN,
37 | ttl=self.server.ttl,
38 | rdata=TXT("google-site-verification=qt5d8b2252742f0bcab14623d9714bee9ba7e82da3")))
39 | return reply
40 |
41 | def waitForResponse(self,q, request):
42 | response = None
43 | try:
44 | r = q.get(True,self.server.timeout)
45 | response = r.content
46 | except (Empty,Exception):
47 | response = self.genDefaultError(request)
48 | self.server._LOGGING_ and self.server.logger.error(f"[{self.server.sname}] expired timeout in waitForResponse()")
49 | finally:
50 | return response
51 |
52 | def doMulticast(self,q,data):
53 | for wrap in self.server.wrappers:
54 | msg = Message(self.server.sname, self.server.sid, wrap.name, wrap.id,
55 | MessageType.STREAM, data, q)
56 | wrap.inbox.put(msg)
57 |
58 | def returnResponse(self,reply):
59 | self.send_data(reply.pack())
60 |
61 | def processRequest(self,request):
62 | q = Queue()
63 | self.doMulticast(q,request)
64 | response = self.waitForResponse(q,request)
65 | self.returnResponse(response)
66 |
67 | def get_data(self):
68 | raise NotImplementedError
69 |
70 | def send_data(self, data):
71 | raise NotImplementedError
72 |
73 | def handle(self):
74 | try:
75 | data = self.get_data()
76 | request = DNSRecord.parse(data)
77 | self.processRequest(request)
78 | except Exception as e:
79 | self.server._LOGGING_ and self.server.logger.exception(f"[{self.server.sname}] Exception on handle: {e}")
80 |
81 |
82 | class UDPRequestHandler(CustomBaseRequestHandler):
83 |
84 | def get_data(self):
85 | return self.request[0]
86 |
87 | def send_data(self, data):
88 | return self.request[1].sendto(data, self.client_address)
89 |
90 |
91 | class WrapDNSServer(ThreadingUDPServer):
92 | def __init__(self, server_address, RequestHandlerClass, wrappers, sname, sid, ttl, timeout, logger):
93 | ThreadingUDPServer.__init__(self, server_address, RequestHandlerClass)
94 | self.wrappers = wrappers
95 | self.sname = sname
96 | self.sid = sid
97 | self.ttl = ttl
98 | self.timeout = timeout
99 | # Logger parameters
100 | self.logger = logger
101 | self._LOGGING_ = False if logger is None else True
102 |
103 |
104 | class dnsserver(Thread):
105 |
106 | NAME = "dnsserver"
107 | CONFIG = {
108 | "prog": NAME,
109 | "description": "Simple DNS server",
110 | "args": [
111 | {
112 | "--hostname": {
113 | "help": "Hostname or IP address. Default is localhost",
114 | "nargs": 1,
115 | "default": ["localhost"],
116 | "type": str
117 | },
118 | "--port": {
119 | "help": "Port where the server will listen. Default is 5355",
120 | "nargs": 1,
121 | "default": [5355],
122 | "type" : int
123 | },
124 | "--ttl": {
125 | "help": "TTL of DNS Responses",
126 | "nargs": 1,
127 | "default": [300],
128 | "type" : int
129 | },
130 | "--timeout": {
131 | "help": "Max time in seconds that the server will wait for the SOTP layer to reply, before returning an error. Default is 3",
132 | "nargs": 1,
133 | "default": [3],
134 | "type" : int
135 | }
136 | }
137 | ]
138 | }
139 |
140 | def __init__(self, id, args, logger):
141 | Thread.__init__(self)
142 | self.wrappers = []
143 | self.id = id
144 | self.server = None
145 | self.name = type(self).__name__
146 | self.inbox = Queue()
147 | # Argparsing
148 | self.argparser = self.generateArgParser()
149 | self.parseArguments(args)
150 | # Logger parameters
151 | self.logger = logger
152 | self._LOGGING_ = False if logger is None else True
153 |
154 | def parseArguments(self, args):
155 | parsed = self.argparser.parse_args(args.split())
156 | self.hostname = parsed.hostname[0]
157 | self.port = parsed.port[0]
158 | self.ttl = parsed.ttl[0]
159 | self.timeout = parsed.timeout[0]
160 |
161 | def generateArgParser(self):
162 | config = self.CONFIG
163 |
164 | parser = ArgumentParser(prog=config["prog"],description=config["description"])
165 | for arg in config["args"]:
166 | for name,field in arg.items():
167 | opts = {}
168 | for key,value in field.items():
169 | opts[key] = value
170 | parser.add_argument(name, **opts)
171 | return parser
172 |
173 | def SignalThread(self):
174 | while True:
175 | msg = self.inbox.get()
176 | if msg.isTerminateMessage():
177 | self.server.shutdown()
178 | break
179 |
180 | def addWrapModule(self, encWrapper):
181 | self.wrappers.append(encWrapper)
182 |
183 | def removeWrapModule(self, encWrapper):
184 | self.wrappers.remove(encWrapper)
185 |
186 | def run(self):
187 | self._LOGGING_ and self.logger.info(f"[{self.name}] Server started. Passing messages...")
188 | self.server = WrapDNSServer(
189 | (self.hostname, self.port),
190 | UDPRequestHandler,
191 | self.wrappers,
192 | self.name,
193 | self.id,
194 | self.ttl,
195 | self.timeout,
196 | self.logger)
197 | st = Thread(target=self.SignalThread)
198 | st.start()
199 | self.server.serve_forever()
200 | self.server.server_close()
201 |
--------------------------------------------------------------------------------
/wrapper/client/icmp.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ClientWrapper
21 | from base64 import urlsafe_b64encode,urlsafe_b64decode
22 | from sotp.core import BYTE,Header,OptionalHeader,Sizes
23 |
24 | import socket, select
25 | from utils.icmp import Packet
26 |
27 | class ICMPClient(object):
28 |
29 | def __init__(self, hostname, request_timeout, name, logger):
30 | self.name = name
31 | self.hostname = hostname
32 | self.request_timeout = request_timeout
33 | # Logger parameters
34 | self.logger = logger
35 | self._LOGGING_ = False if logger is None else True
36 | # Opening Raw Socket and resolve the hostname
37 | self.mysocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
38 | self.mysocket.setblocking(0)
39 | socket.gethostbyname(self.hostname)
40 |
41 | def send_data(self, data):
42 | request = Packet()
43 | request.pack_request(data)
44 | raw_request = request.toBytes()
45 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] send_data() will send: {raw_request}")
46 | self.mysocket.sendto(raw_request, (self.hostname, 1))
47 |
48 | def get_data(self):
49 | ready = select.select([self.mysocket], [], [], self.request_timeout)
50 | if ready[0]:
51 | raw_response = self.mysocket.recv(65535)
52 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] get_data() recv: {raw_response}")
53 | response = Packet()
54 | response.unpack(raw_response)
55 | return response.data
56 |
57 |
58 | class icmp(ClientWrapper):
59 |
60 | # 65535 bytes (Max IP Packet) - 20 bytes (IP Header) - 8 bytes (ICMP Header)
61 | # = 65507 bytes; see rfc 792 for more info
62 | MAX_ICMP_DATA_LEN = 65507
63 |
64 | NAME = "icmp"
65 |
66 | CONFIG = {
67 | "prog": "icmp",
68 | "description": "Encodes/Decodes data in the data section of ICMP Echo requests/responses",
69 | "args": [
70 | {
71 | "--hostname": {
72 | "help": "Remote Server Addresses (not working for 127.0.0.1, localhost, etc)",
73 | "nargs": 1,
74 | "type": str,
75 | "required": 1
76 | },
77 | "--request-timeout": {
78 | "help": "Timeout in second to wait for a socket reply.",
79 | "nargs": 1,
80 | "default": [1],
81 | "type": int
82 | },
83 | "--max-size": {
84 | "help": "Maximum size in bytes of the sotp packet to be embedded in the icmp data section (49120 bytes max)",
85 | "nargs": 1,
86 | "default": [1024],
87 | "type": int
88 | },
89 | "--poll-delay": {
90 | "help": "Time in seconds between pollings (in order not to saturate when not transmitting)",
91 | "nargs": 1,
92 | "default": [3],
93 | "type": int
94 | },
95 | "--response-timeout": {
96 | "help": "Waiting time in seconds for wrapper data.",
97 | "nargs": 1,
98 | "default": [2],
99 | "type": int
100 | },
101 | "--max-retries": {
102 | "help": "Maximum number of re-synchronization retries.",
103 | "nargs": 1,
104 | "default": [10],
105 | "type": int
106 | }
107 | }
108 | ]
109 | }
110 |
111 | def __init__(self, qsotp, args, logger):
112 | ClientWrapper.__init__(self,type(self).__name__,qsotp,logger)
113 | self.name = type(self).__name__
114 | self.exit = False
115 | # Generate argparse
116 | self.argparser = self.generateArgParser()
117 | # Parse arguments
118 | self.hostname = None
119 | self.request_timeout = None
120 | # Base arguments
121 | self.max_size = None
122 | self.poll_delay = None
123 | self.response_timeout = None
124 | self.max_retries = None
125 | self.parseArguments(args)
126 | # Logger parameters
127 | self.logger = logger
128 | self._LOGGING_ = False if logger is None else True
129 | # ICMPclient parameters
130 | self.icmpclient = ICMPClient(self.hostname,
131 | self.request_timeout,
132 | self.name,
133 | self.logger)
134 |
135 | def checkMaxProtoSize(self,max_size):
136 | headerlen = int(Sizes.HEADER/BYTE)
137 | totalen = len(urlsafe_b64encode(b'A' * (headerlen + max_size)))
138 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] checkMaxProtoSize() headerlen:{headerlen},max_size:{max_size},totalen:{totalen}")
139 |
140 | if totalen > self.MAX_ICMP_DATA_LEN:
141 | raise BaseException(f"Total Length is {totalen} and max available for this module is {self.MAX_ICMP_DATA_LEN}. Please, reduce max_size value")
142 |
143 | def parseArguments(self, args):
144 | args = self.argparser.parse_args(args.split())
145 | self.hostname = args.hostname[0]
146 | self.request_timeout = args.request_timeout[0]
147 | self.max_size = args.max_size[0]
148 | self.poll_delay = args.poll_delay[0]
149 | self.response_timeout = args.response_timeout[0]
150 | self.max_retries = args.max_retries[0]
151 | self.checkMaxProtoSize(self.max_size)
152 |
153 | def wrap(self,content):
154 | # make your own routine to encapsulate sotp content in dns packet (i use b64 in subdomain space)
155 | sotpDataBytes = urlsafe_b64encode(content)
156 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] wrap() data to icmp request: {str(sotpDataBytes,'utf-8')}")
157 |
158 | self.icmpclient.send_data(sotpDataBytes)
159 | raw_reply = self.icmpclient.get_data()
160 | if not raw_reply:
161 | self._LOGGING_ and self.logger.error(f"[{self.name}] wrap() get_data return None")
162 | return
163 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] wrap() recv icmp raw response: {raw_reply}")
164 | self.inbox.put(self.messageToWrapper(raw_reply))
165 |
166 | def unwrap(self,content):
167 | data = urlsafe_b64decode(content)
168 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] unwrap() data icmp response: {data}")
169 | return data
--------------------------------------------------------------------------------
/overlay/server/tcpconnect.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerOverlay
21 | from sys import stdout
22 | import socket
23 | from threading import Thread, Lock
24 | from utils.messaging import Message, MessageType, SignalType
25 | import select
26 |
27 |
28 | class tcpconnect(ServerOverlay):
29 |
30 | NAME = "tcpconnect"
31 | CONFIG = {
32 | "prog": NAME,
33 | "description": "Connects to TCP port. Reads from socket, sends through SOTP connection. Reads from SOTP connection, sends through socket.",
34 | "args": [
35 | {
36 | "--tag": {
37 | "help": "Tag used by the overlay",
38 | "nargs": 1,
39 | "required": False,
40 | "default": ["0x1010"]
41 | }
42 | },
43 | {
44 | "--address": {
45 | "help": "Address where the module will connect",
46 | "nargs": 1,
47 | "required": True,
48 | "action": "store"
49 | }
50 | },
51 | {
52 | "--port": {
53 | "help": "Port where the module will connect",
54 | "nargs": 1,
55 | "required": True,
56 | "action": "store"
57 | }
58 | },
59 | {
60 | "--persist": {
61 | "help": "Retries the TCP connection, if closed",
62 | "action": "store_true"
63 | }
64 | },
65 | {
66 | "--wait": {
67 | "help": "Waits until data is received through the SOTP channel to connect",
68 | "action": "store_true"
69 | }
70 | }
71 | ]
72 | }
73 |
74 | def __init__(self, id, qsotp, mode, args, logger):
75 | ServerOverlay.__init__(self, type(self).__name__, id, qsotp, mode, args, logger)
76 | self.name = type(self).__name__
77 | self.buffer = []
78 | self.timeout = 1
79 | self.started = False
80 | self.lock = Lock()
81 | # Setting input capture
82 | self.hasInput = False
83 | # Logger parameters
84 | self.logger = logger
85 | self._LOGGING_ = False if logger is None else True
86 | self.tcpthread = Thread(target=self.captureTcpStream)
87 | self.lock.acquire()
88 | if not self.wait:
89 | self.tcpthread.start()
90 | self.started = True
91 |
92 | # Overriden
93 | def parseArguments(self, args):
94 | self.port = int(args.port[0])
95 | self.address = args.address[0]
96 | self.persist = args.persist
97 | self.wait = args.wait
98 |
99 | def captureTcpStream(self):
100 | # Create socket and connect
101 | while not self.exit:
102 | try:
103 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
104 | self.socket.connect((self.address, self.port))
105 | except Exception as e:
106 | print(e)
107 | self.inbox.put(Message('input',
108 | 0,
109 | self.name,
110 | self.id,
111 | MessageType.SIGNAL,
112 | SignalType.TERMINATE))
113 | if self.lock.locked():
114 | self.lock.release()
115 | return
116 | if self.lock.locked():
117 | self.lock.release()
118 | while not self.exit:
119 | try:
120 | # Block on socket
121 | result = select.select([self.socket], [], [], self.timeout)
122 | if result[0]:
123 | rawdata = self.socket.recv(4096)
124 | if rawdata and len(rawdata) > 0:
125 | self.inbox.put(Message('input',
126 | 0,
127 | 'overlay',
128 | self.id,
129 | MessageType.STREAM,
130 | rawdata))
131 | elif not self.persist:
132 | self._LOGGING_ and self.logger.debug(f"[{self.name}] TCP communication broken. Sending Terminate signal to overlay")
133 | self.inbox.put(Message('input',
134 | 0,
135 | self.name,
136 | self.id,
137 | MessageType.SIGNAL,
138 | SignalType.TERMINATE))
139 | return
140 | else:
141 | self.socket.close()
142 | break
143 | except Exception as e:
144 | self.inbox.put(Message('input',
145 | 0,
146 | self.name,
147 | self.id,
148 | MessageType.SIGNAL,
149 | SignalType.TERMINATE))
150 | return
151 | return
152 |
153 | def processInputStream(self, content):
154 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from socket: {len(content)}")
155 | return content
156 |
157 | def processSOTPStream(self, content):
158 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to socket: {len(content)}")
159 | if not self.started:
160 | self.tcpthread.start()
161 | self.lock.acquire()
162 | self.started = True
163 | try:
164 | self.socket.send(content)
165 | except Exception as e:
166 | print(e)
167 |
168 | # overriden for pipe scenarios
169 | def handleInputStream(self, msg):
170 | content = self.processInputStream(msg.content)
171 | # By default, only one worker. Must be overriden for more
172 | # In a multi-worker scenario inputs must be mapped to workers
173 | if self.workers:
174 | return self.streamToSOTPWorker(content, self.workers[0].id)
175 |
176 | self.buffer.append(content)
177 |
178 | # overriden for pipe scenarios
179 | def addWorker(self, worker):
180 | if not self.workers: # empty
181 | self.workers.append(worker)
182 | # check if there's some buffered data and pass it
183 | while self.buffer:
184 | self.workers[0].datainbox.put(self.streamToSOTPWorker(self.buffer.pop(0),
185 | self.workers[0].id))
186 | else:
187 | raise(f"Cannot Register worker on overlay module. Module {self.name} only accepts one worker")
188 |
--------------------------------------------------------------------------------
/utils/icmp.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Raul Caro.
3 | #
4 | # This file is part of ICMPack
5 | # (see https://github.com/rcaroncd/ICMPack).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sys import byteorder
21 | from socket import htons
22 | from random import random
23 |
24 |
25 | class Sizes(object):
26 | '''
27 | See https://tools.ietf.org/html/rfc792 Page 14
28 | '''
29 | IP_HEADER = 20
30 | ICMP_HEADER = 8
31 | ICMP_TYPE = 1
32 | ICMP_CODE = 1
33 | ICMP_CHECKSUM = 2
34 | ICMP_IDENTIFIER = 2
35 | ICMP_SEQUENCE = 2
36 | # extra field of icmp data (sent by ping program by default)
37 | ICMP_TIMESTAMP = 8
38 | ICMP_UNKNOWN = 8
39 |
40 |
41 | class Offsets(object):
42 | '''
43 | Precalculate Offsets to parse ICMP Packet.
44 | '''
45 | IP_HEADER = Sizes.IP_HEADER
46 | ICMP_HEADER = Sizes.IP_HEADER + Sizes.ICMP_HEADER
47 | # after ip header, start icmp header
48 | ICMP_TYPE = Sizes.ICMP_TYPE
49 | ICMP_CODE = ICMP_TYPE + Sizes.ICMP_CODE
50 | ICMP_CHECKSUM = ICMP_CODE + Sizes.ICMP_CHECKSUM
51 | ICMP_IDENTIFIER = ICMP_CHECKSUM + Sizes.ICMP_IDENTIFIER
52 | ICMP_SEQUENCE = ICMP_IDENTIFIER + Sizes.ICMP_SEQUENCE
53 | # extra field of icmp data (sent by ping program by default)
54 | ICMP_TIMESTAMP = ICMP_SEQUENCE + Sizes.ICMP_TIMESTAMP
55 | ICMP_UNKNOWN = ICMP_TIMESTAMP + Sizes.ICMP_UNKNOWN
56 |
57 |
58 | class Packet(object):
59 | """
60 | Class for handling ICMP Echo Request and Echo Response packets
61 | from an array of bytes passed through input.
62 | Represents an ICMP package Echo Request/Response
63 |
64 | ICMP Echo / Echo Reply Message header info from RFC792
65 | -> http://tools.ietf.org/html/rfc792
66 |
67 | 0 1 2 3
68 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
69 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
70 | | Type | Code | Checksum |
71 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
72 | | Identifier | Sequence Number |
73 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
74 | | Data ...
75 | +-+-+-+-+-
76 |
77 | *Max data (available) per ICMP packet 65507 bytes:
78 |
79 | 65535 bytes (Max IP Packet)
80 | -20 bytes (IP Header)
81 | -8 bytes (ICMP Header)
82 | =====
83 | 65507 bytes ~= 65 Kb (1Mb ~= 16 ICMP Packets)
84 |
85 | """
86 |
87 | REQUEST_TYPE = 8
88 | REQUEST_CODE = 0
89 | REQUEST_CHECKSUM = 0
90 | REQUEST_IDENTIFIER = 0
91 | REQUEST_SEQUENCE = 1
92 |
93 | RESPONSE_TYPE = 0
94 | RESPONSE_CODE = 0
95 | RESPONSE_CHECKSUM = 0
96 | RESPONSE_IDENTIFIER = 0
97 | RESPONSE_SEQUENCE = 1
98 |
99 | def __init__(self, rawdata=None):
100 | self.type = b''
101 | self.code = b''
102 | self.checksum = b''
103 | self.id = b''
104 | self.seq = b''
105 | self.data = b''
106 | # passing raw data, try to unpack within icmp packet
107 | if rawdata != None:
108 | self.unpack(rawdata)
109 |
110 | def __repr__(self):
111 | return "Packet()"
112 |
113 | def __str__(self):
114 | r = f"Type: '{self.type.hex()}', "
115 | r += f"Code: '{self.code.hex()}', "
116 | r += f"Checksum: '{self.checksum.hex()}', "
117 | r += f"Identifier: '{self.id.hex()}', "
118 | r += f"Sequence Number: '{self.seq.hex()}', "
119 | r += f"Data: '{self.data.hex()}'"
120 | return r
121 |
122 | def unpack(self, data):
123 | """
124 | Extracts data from a received ICMP Packet (Echo Request or Echo Response).
125 | Does not ensure that the resulting data is that of a
126 | valid icmp package, so the attributes should be
127 | checked after performing this transformation.
128 | """
129 | ip_header = data[:Offsets.IP_HEADER]
130 | icmp_data = data[Offsets.IP_HEADER:]
131 | self.type = icmp_data[:Offsets.ICMP_TYPE]
132 | self.code = icmp_data[Offsets.ICMP_TYPE:Offsets.ICMP_CODE]
133 | self.checksum = icmp_data[Offsets.ICMP_CODE:Offsets.ICMP_CHECKSUM]
134 | self.id = icmp_data[Offsets.ICMP_CHECKSUM:Offsets.ICMP_IDENTIFIER]
135 | self.seq = icmp_data[Offsets.ICMP_IDENTIFIER:Offsets.ICMP_SEQUENCE]
136 | self.data = icmp_data[Offsets.ICMP_SEQUENCE:]
137 |
138 | def calc_checksum(self, source_string):
139 | """
140 | A port of the functionality of in_cksum() from ping.c
141 | Ideally this would act on the string as a series of 16-bit ints (host
142 | packed), but this works.
143 | Network data is big-endian, hosts are typically little-endian.
144 |
145 | From https://github.com/mjbright/python3-ping/blob/master/ping.py
146 | """
147 | countTo = (int(len(source_string)/2))*2
148 | sum = 0
149 | count = 0
150 |
151 | # Handle bytes in pairs (decoding as short ints)
152 | loByte = 0
153 | hiByte = 0
154 | while count < countTo:
155 | if (byteorder == "little"):
156 | loByte = source_string[count]
157 | hiByte = source_string[count + 1]
158 | else:
159 | loByte = source_string[count + 1]
160 | hiByte = source_string[count]
161 | sum = sum + (hiByte * 256 + loByte)
162 | count += 2
163 |
164 | # Handle last byte if applicable (odd-number of bytes)
165 | # Endianness should be irrelevant in this case
166 | if countTo < len(source_string): # Check for odd length
167 | loByte = source_string[len(source_string)-1]
168 | sum += loByte
169 |
170 | sum &= 0xffffffff # Truncate sum to 32 bits (a variance from ping.c, which
171 | # uses signed ints, but overflow is unlikely in ping)
172 |
173 | sum = (sum >> 16) + (sum & 0xffff) # Add high 16 bits to low 16 bits
174 | sum += (sum >> 16) # Add carry from above (if any)
175 | answer = ~sum & 0xffff # Invert and truncate to 16 bits
176 | answer = htons(answer)
177 | return answer
178 |
179 | def pack_request(self, data=None):
180 | """
181 | Create an ICMP Echo Request package with default data (like ping)
182 | or allow custom data to be passed to it.
183 | """
184 | self.type = self.REQUEST_TYPE.to_bytes(Sizes.ICMP_TYPE,byteorder=byteorder)
185 | self.code = self.REQUEST_CODE.to_bytes(Sizes.ICMP_CODE,byteorder=byteorder)
186 | self.checksum = self.REQUEST_CHECKSUM.to_bytes(Sizes.ICMP_CHECKSUM,byteorder='big')
187 | new_identifier = int((id(data) * random()) % 65535)
188 | self.id = new_identifier.to_bytes(Sizes.ICMP_IDENTIFIER,byteorder=byteorder)
189 | self.seq = self.REQUEST_SEQUENCE.to_bytes(Sizes.ICMP_SEQUENCE,byteorder='big')
190 | self.data = data
191 |
192 | # calculate new checksum of icmp request packet
193 | raw_packet = self.toBytes()
194 | newchecksum = self.calc_checksum(raw_packet)
195 | self.checksum = newchecksum.to_bytes(Sizes.ICMP_CHECKSUM,byteorder='big')
196 |
197 | def pack_response(self, request, data=None):
198 | """
199 | Create an ICMP Echo Response package
200 | """
201 | assert isinstance(request, Packet)
202 | self.type = self.RESPONSE_TYPE.to_bytes(Sizes.ICMP_TYPE,byteorder=byteorder)
203 | self.code = self.RESPONSE_CODE.to_bytes(Sizes.ICMP_CODE,byteorder=byteorder)
204 | self.checksum = self.RESPONSE_CHECKSUM.to_bytes(Sizes.ICMP_CHECKSUM,byteorder='big')
205 | self.id = request.id
206 | self.seq = request.seq
207 | assert isinstance(data, bytes)
208 | self.data = data
209 |
210 | # calculate new checksum of icmp response packet
211 | raw_packet = self.toBytes()
212 | newchecksum = self.calc_checksum(raw_packet)
213 | self.checksum = newchecksum.to_bytes(Sizes.ICMP_CHECKSUM,byteorder='big')
214 |
215 | def toBytes(self):
216 | """
217 | Returns an array of bytes corresponding to the icmp package built.
218 | """
219 | return (self.type + self.code + self.checksum + self.id + self.seq + self.data)
220 |
--------------------------------------------------------------------------------
/overlay/server/tcplisten.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerOverlay
21 | from sys import stdout
22 | import socket
23 | from threading import Thread
24 | from utils.messaging import Message, MessageType, SignalType
25 | import select
26 |
27 |
28 | class tcplisten(ServerOverlay):
29 |
30 | NAME = "tcplisten"
31 | CONFIG = {
32 | "prog": NAME,
33 | "description": "Binds to TCP port. Reads from socket, sends through SOTP connection. Reads from SOTP connection, sends through socket.",
34 | "args": [
35 | {
36 | "--tag": {
37 | "help": "Tag used by the overlay",
38 | "nargs": 1,
39 | "required": False,
40 | "default": ["0x1010"]
41 | }
42 | },
43 | {
44 | "--address": {
45 | "help": "Address where the module will bind",
46 | "nargs": 1,
47 | "required": True,
48 | "action": "store"
49 | }
50 | },
51 | {
52 | "--port": {
53 | "help": "Port where the module will bind",
54 | "nargs": 1,
55 | "required": True,
56 | "action": "store"
57 | }
58 | },
59 | {
60 | "--persist": {
61 | "help": "Keeps the port open after closing the TCP connection",
62 | "action": "store_true"
63 | }
64 | }
65 | ]
66 | }
67 |
68 |
69 | def __init__(self, id, qsotp, mode, args, logger):
70 | ServerOverlay.__init__(self, type(self).__name__, id, qsotp, mode, args, logger)
71 | self.name = type(self).__name__
72 | self.buffer = []
73 | self.buffer2 = []
74 | self.conn = None
75 | self.timeout = 1
76 | # Setting input capture
77 | self.hasInput = False
78 | # Logger parameters
79 | self.logger = logger
80 | self._LOGGING_ = False if logger is None else True
81 | self.tcpthread = Thread(target=self.captureTcpStream)
82 | self.tcpthread.start()
83 |
84 | # Overriden
85 | def parseArguments(self, args):
86 | self.port = int(args.port[0])
87 | self.address = args.address[0]
88 | self.persist = args.persist
89 |
90 | def captureTcpStream(self):
91 | # Create socket and listen
92 | while self.conn is None and not self.exit:
93 |
94 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
95 | self.socket.settimeout(self.timeout)
96 | try:
97 | self.socket.bind((self.address, self.port))
98 | self.socket.listen(0)
99 | self.conn, self.remoteaddr = self.socket.accept()
100 |
101 | except socket.timeout as te:
102 | continue # Allows to check if the application has exited to finish the thread
103 | except Exception as e:
104 | print(e)
105 | self.inbox.put(Message('input',
106 | 0,
107 | self.name,
108 | self.id,
109 | MessageType.SIGNAL,
110 | SignalType.TERMINATE))
111 | return
112 | # Empty buffered data, if any
113 | while self.buffer2 and not self.exit:
114 | self.conn.send(self.buffer2.pop(0))
115 | # socket loop
116 | while not self.exit:
117 | try:
118 | # Block on socket
119 | result = select.select([self.conn], [], [], self.timeout)
120 | if result[0]:
121 | rawdata = self.conn.recv(4096)
122 | if rawdata and len(rawdata) > 0:
123 | self.inbox.put(Message('input',
124 | 0,
125 | 'overlay',
126 | self.id,
127 | MessageType.STREAM,
128 | rawdata))
129 | elif not self.persist:
130 | self._LOGGING_ and self.logger.debug(f"[{self.name}] TCP communication broken. Sending Terminate signal to overlay")
131 | self.inbox.put(Message('input',
132 | 0,
133 | self.name,
134 | self.id,
135 | MessageType.SIGNAL,
136 | SignalType.TERMINATE))
137 | return
138 | else:
139 | self.conn.close()
140 | self.conn = None
141 | break
142 | except Exception as e:
143 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Exception: {e}. Sending Terminate signal to overlay")
144 | self.inbox.put(Message('input',
145 | 0,
146 | self.name,
147 | self.id,
148 | MessageType.SIGNAL,
149 | SignalType.TERMINATE))
150 | return
151 | return
152 |
153 | def processInputStream(self, content):
154 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Read from socket: {len(content)}")
155 | return content
156 |
157 | def processSOTPStream(self, content):
158 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Write to socket: {len(content)}")
159 | if self.conn is None:
160 | self.buffer2.append(content) # wait until a connection happens
161 | else:
162 | self.conn.send(content)
163 |
164 | # overriden for pipe scenarios
165 | def handleInputStream(self, msg):
166 | content = self.processInputStream(msg.content)
167 | # By default, only one worker. Must be overriden for more
168 | # In a multi-worker scenario inputs must be mapped to workers
169 | if self.workers:
170 | return self.streamToSOTPWorker(content, self.workers[0].id)
171 |
172 | self.buffer.append(content)
173 |
174 | # overriden for pipe scenarios
175 | def addWorker(self, worker):
176 | if not self.workers: # empty
177 | self.workers.append(worker)
178 | # check if there's some buffered data and pass it
179 | while self.buffer:
180 | self.workers[0].datainbox.put(self.streamToSOTPWorker(self.buffer.pop(0),
181 | self.workers[0].id))
182 | else:
183 | raise(f"Cannot Register worker on overlay module. Module {self.name} only accepts one worker")
184 |
--------------------------------------------------------------------------------
/wrapper/client/http.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from utils.messaging import Message, MessageType, SignalType
21 | from sotp.misticathread import ClientWrapper
22 | from http.client import HTTPConnection, HTTPSConnection
23 | from base64 import urlsafe_b64encode,urlsafe_b64decode
24 | from ssl import _create_unverified_context
25 |
26 | class http(ClientWrapper):
27 |
28 | NAME = "http"
29 | CONFIG = {
30 | "prog": NAME,
31 | "description": "Encodes/Decodes data in HTTP requests/responses using different methods",
32 | "args": [
33 | {
34 | "--hostname": {
35 | "help": "Hostname or IP address. Default is localhost",
36 | "nargs": 1,
37 | "default": ["localhost"],
38 | "type": str
39 | },
40 | "--port": {
41 | "help": "Server Port",
42 | "nargs": 1,
43 | "default": [8080],
44 | "type": int
45 | },
46 | "--timeout": {
47 | "help": "HTTPConnection Timeout",
48 | "nargs": 1,
49 | "default": [5],
50 | "type": int
51 | },
52 | "--method": {
53 | "help": "HTTP Method to use",
54 | "nargs": 1,
55 | "default": ["GET"],
56 | "choices": ["GET","POST"],
57 | "type": str
58 | },
59 | "--uri": {
60 | "help": "URI Path before embedded message. Default is '/'",
61 | "nargs": 1,
62 | "default": ["/"],
63 | "type": str
64 | },
65 | "--header": {
66 | "help": "Header field to embed the packets",
67 | "nargs": 1,
68 | "type": str
69 | },
70 | "--post-field": {
71 | "help": "Post param to embed the packet",
72 | "nargs": 1,
73 | "type": str
74 | },
75 | "--success-code": {
76 | "help": "HTTP Code for Success Connections. Default is 200",
77 | "nargs": 1,
78 | "default": [200],
79 | "choices": [100,101,102,200,201,202,203,204,205,206,207,
80 | 208,226,300,301,302,303,304,305,306,307,308,
81 | 400,401,402,403,404,405,406,407,408,409,410,
82 | 411,412,413,414,415,416,417,418,421,422,423,
83 | 424,426,428,429,431,500,501,502,503,504,505,
84 | 506,507,508,510,511],
85 | "type": int
86 | },
87 | "--proxy": {
88 | "help": "Proxy Address for tunneling communication format 'ip:port'",
89 | "nargs": 1,
90 | "type": str
91 | },
92 | "--max-size": {
93 | "help": "Maximum size in bytes of the SOTP packet. You can change it depending http method used (see rfc2616 page 69)",
94 | "nargs": 1,
95 | "default": [4096],
96 | "type": int
97 | },
98 | "--poll-delay": {
99 | "help": "Time in seconds between pollings",
100 | "nargs": 1,
101 | "default": [5],
102 | "type": int
103 | },
104 | "--response-timeout": {
105 | "help": "Waiting time in seconds for wrapper data.",
106 | "nargs": 1,
107 | "default": [3],
108 | "type": int
109 | },
110 | "--max-retries": {
111 | "help": "Maximum number of re-synchronization retries.",
112 | "nargs": 1,
113 | "default": [20],
114 | "type": int
115 | },
116 | "--ssl": {
117 | "help": "Flag to indicate that SSL will be used.",
118 | "action": "store_true"
119 | }
120 | }
121 | ]
122 | }
123 |
124 | def __init__(self, qsotp, args, logger):
125 | ClientWrapper.__init__(self,type(self).__name__,qsotp,logger)
126 | self.args = args
127 | self.name = type(self).__name__
128 | self.exit = False
129 | self.parseArguments(args)
130 | # Logger parameters
131 | self.logger = logger
132 | self._LOGGING_ = False if logger is None else True
133 |
134 | def parseArguments(self, args):
135 | args = self.argparser.parse_args(args.split())
136 | self.hostname = args.hostname[0]
137 | self.port = args.port[0]
138 | self.timeout = args.timeout[0]
139 | self.uri = args.uri[0]
140 | self.method = args.method[0]
141 | self.header = args.header[0] if args.header is not None else None
142 | self.post_field = args.post_field[0] if args.post_field is not None else None
143 | self.success_code = args.success_code[0]
144 | self.proxy = args.proxy[0] if args.proxy is not None else None
145 | self.max_size = args.max_size[0]
146 | self.poll_delay = args.poll_delay[0]
147 | self.response_timeout = args.response_timeout[0]
148 | self.max_retries = args.max_retries[0]
149 | self.ssl = args.ssl
150 |
151 | def doReqInURI(self, conn, content, method):
152 | data_headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
153 | conn.request(method, f"{self.uri}{content}", headers=data_headers)
154 | r = conn.getresponse()
155 | return (r.read(),r.status)
156 |
157 | def doReqInHeaders(self, conn, content, method):
158 | data_headers = {
159 | "Content-type": "application/x-www-form-urlencoded",
160 | "Accept": "text/plain",
161 | f"{self.header}": f"{content}"
162 | }
163 | conn.request(method, f"{self.uri}", headers=data_headers)
164 | r = conn.getresponse()
165 | return (r.read(),r.status)
166 |
167 | def doGet(self, conn, content):
168 | if self.header:
169 | return self.doReqInHeaders(conn, content, "GET")
170 | else:
171 | return self.doReqInURI(conn, content, "GET")
172 |
173 | def doPostField(self, conn, content):
174 | post_data = f"{self.post_field}={content}"
175 | data_headers = {
176 | "Content-type": "application/x-www-form-urlencoded",
177 | "Accept": "text/plain"
178 | }
179 | conn.request("POST", f"{self.uri}", post_data, data_headers)
180 | r = conn.getresponse()
181 | return (r.read(),r.status)
182 |
183 | def doPost(self, conn, content):
184 | if self.post_field:
185 | return self.doPostField(conn, content)
186 | elif self.header:
187 | return self.doReqInHeaders(conn, content, "POST")
188 | else:
189 | return self.doReqInURI(conn, content, "POST")
190 |
191 | def dispatchByMethod(self, conn, content):
192 | if self.method == "GET":
193 | return self.doGet(conn, content)
194 | elif self.method == "POST":
195 | return self.doPost(conn, content)
196 | else:
197 | raise Exception("None HTTP Method Available")
198 |
199 | def packSotp(self, content):
200 | # we encode sotp data with urlsafe_b64encode but change
201 | # here (and in wrap_server) if you use other encoding.
202 | urlSafeEncodedBytes = urlsafe_b64encode(content)
203 | urlSafeEncodedStr = str(urlSafeEncodedBytes, "utf-8")
204 | return urlSafeEncodedStr
205 |
206 | def wrap(self,content):
207 | self._LOGGING_ and self.logger.debug(f"[{self.name}] wrap: {len(content)} bytes")
208 | packedSotp = self.packSotp(content)
209 |
210 | if self.proxy:
211 | proxy_ip, proxy_port = self.proxy.split(":")
212 |
213 | if self.ssl:
214 | conn = HTTPSConnection(self.hostname, self.port, timeout=self.timeout, context=_create_unverified_context())
215 | else:
216 | conn = HTTPConnection(proxy_ip, proxy_port, self.timeout)
217 |
218 | conn.set_tunnel(self.hostname, self.port)
219 | else:
220 | if self.ssl:
221 | conn = HTTPSConnection(self.hostname, self.port, timeout=self.timeout, context=_create_unverified_context())
222 | else:
223 | conn = HTTPConnection(self.hostname, self.port, self.timeout)
224 |
225 | data_response, code_response = self.dispatchByMethod(conn, packedSotp)
226 | conn.close()
227 | self.inbox.put(self.messageToWrapper((data_response,code_response)))
228 |
229 | def unpackSotp(self, data):
230 | # we decode sotp data with urlsafe_b64decode but change
231 | # here (and in wrap_server) if you use other encoding.
232 | return urlsafe_b64decode(data)
233 |
234 | def unwrap(self,content):
235 | data, httpcode = content
236 | if httpcode != self.success_code:
237 | self._LOGGING_ and self.logger.error(f"[{self.name}] unwrap: Invalid HTTP Response {httpcode}")
238 | raise Exception(f"Invalid HTTP Response Code {httpcode} waited: {self.success_code}")
239 | return self.unpackSotp(data)
240 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_server/httpserver.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from threading import Thread
21 | from queue import Queue, Empty
22 | from utils.messaging import Message, MessageType, SignalType
23 | from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
24 | from argparse import ArgumentParser
25 | from utils.prompt import Prompt
26 | from cgi import FieldStorage
27 | from ssl import wrap_socket
28 |
29 |
30 | class WrapHTTPServer(ThreadingHTTPServer):
31 | def __init__(self, server_address, RequestHandlerClass, wrappers, sname, sid, timeout, error_file, error_code, logger):
32 | ThreadingHTTPServer.__init__(self, server_address, RequestHandlerClass)
33 | self.wrappers = wrappers
34 | self.sname = sname
35 | self.sid = sid
36 | self.timeout = timeout
37 | self.error_file = error_file
38 | self.error_code = error_code
39 | # Logger parameters
40 | self.logger = logger
41 | self._LOGGING_ = False if logger is None else True
42 |
43 |
44 | class httpserverHandler(BaseHTTPRequestHandler):
45 |
46 | # Overide log function to disable verbose outputs.
47 | def log_message(self, format, *args):
48 | return
49 |
50 | def getDefaultErrorView(self):
51 | content = '
408 Request Timeout408 Request Timeout
nginx/1.5.10'
52 | return self.packRequest("", {"Server": "nginx 1.5.10"}, content, 408)
53 |
54 | def readErrorFile(self):
55 | try:
56 | with open(self.server.error_file, "r") as errfile:
57 | content = errfile.read()
58 | return self.packRequest("", {"Server": "nginx 1.13.1"}, content, self.server.error_code)
59 | except Exception:
60 | return self.getDefaultErrorView()
61 |
62 | def generateErrorView(self):
63 | if self.server.error_file and self.server.error_code:
64 | return self.readErrorFile()
65 | else:
66 | return self.getDefaultErrorView()
67 |
68 | # Send the request message to all wrappers (just the right wrapper will process and make an answer).
69 | def doMulticast(self, q, data):
70 | for wrap in self.server.wrappers:
71 | msg = Message(self.server.sname, self.server.sid, wrap.name, wrap.id,
72 | MessageType.STREAM, data, q)
73 | wrap.inbox.put(msg)
74 |
75 | def waitForResponse(self, q):
76 | response = None
77 | try:
78 | r = q.get(True, self.server.timeout)
79 | response = r.content
80 | except (Empty, Exception):
81 | response = self.generateErrorView()
82 | finally:
83 | return response
84 |
85 | def packRequest(self, requestline, headers, content=None, httpcode=200):
86 | return {
87 | "requestline": requestline,
88 | "headers": headers,
89 | "content": content,
90 | "httpcode": httpcode
91 | }
92 |
93 | def returnResponse(self, res):
94 | self.protocol_version = "HTTP/1.1"
95 |
96 | if 'Server' in res['headers']:
97 | # Server header must be 'werbserver+space+version'
98 | versions = res['headers']['Server'].split(' ')
99 | self.server_version = versions[0]
100 | self.sys_version = versions[1]
101 | del res['headers']['Server']
102 | else:
103 | self.server_version = "nginx"
104 | self.sys_version = "1.5.10"
105 |
106 | self.send_response(res['httpcode'])
107 |
108 | for key, value in res['headers'].items():
109 | self.send_header(key, value)
110 |
111 | if res['content'] == "":
112 | self.end_headers()
113 | elif 'Content-Length' not in res['headers']:
114 | self.send_header("Content-Length", len(res['content']))
115 | self.end_headers()
116 | self.wfile.write(bytes(res['content'], "utf8"))
117 | else:
118 | self.end_headers()
119 | self.wfile.write(bytes(res['content'], "utf8"))
120 |
121 | # Generate a Queue (for recieve responses *thread), send request to wrappers,
122 | # and wait for response from only one and return to HTTP Client.
123 | def processRequest(self, request):
124 | try:
125 | q = Queue()
126 | self.doMulticast(q, request)
127 | response = self.waitForResponse(q)
128 | self.returnResponse(response)
129 | except Exception as e:
130 | self.server.logger.exception(
131 | f"[{self.server.sname}] Exception in handle: {e}")
132 |
133 | def do_GET(self):
134 | req = self.packRequest(self.requestline, self.headers)
135 | self.processRequest(req)
136 |
137 | def do_POST(self):
138 | form = FieldStorage(
139 | fp=self.rfile,
140 | headers=self.headers,
141 | environ={'REQUEST_METHOD': 'POST',
142 | 'CONTENT_TYPE': self.headers['Content-Type'],
143 | })
144 | req = self.packRequest(self.requestline, self.headers, form)
145 | self.processRequest(req)
146 |
147 |
148 | class httpserver(Thread, BaseHTTPRequestHandler):
149 |
150 | NAME = "httpserver"
151 | CONFIG = {
152 | "prog": NAME,
153 | "description": "Simple HTTP Server",
154 | "args": [
155 | {
156 | "--hostname": {
157 | "help": "Hostname or IP address. Default is localhost",
158 | "nargs": 1,
159 | "default": ["localhost"],
160 | "type": str
161 | },
162 | "--port": {
163 | "help": "Port where the server will listen. Default is 8080",
164 | "nargs": 1,
165 | "default": [8080],
166 | "type": int
167 | },
168 | "--timeout": {
169 | "help": "Max time, in seconds, that the server will wait for the SOTP layer to reply, before returning an error. Default is 5",
170 | "nargs": 1,
171 | "default": [5],
172 | "type": int
173 | },
174 | "--error-file": {
175 | "help": "HTML File for custom error page when timeout expires.",
176 | "nargs": 1,
177 | "type": str
178 | },
179 | "--error-code": {
180 | "help": "HTTP Code for custom http code when timeout expires.",
181 | "nargs": 1,
182 | "type": int
183 | },
184 | "--ssl": {
185 | "help": "Flag to indicate that SSL will be used",
186 | "action": "store_true"
187 | },
188 | "--ssl-cert": {
189 | "help": "Path of the ssl certificate file. You can generate one with the following command: 'openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes'",
190 | "nargs": 1,
191 | "type": str
192 | }
193 | }
194 | ]
195 | }
196 |
197 | def __init__(self, id, args, logger):
198 | Thread.__init__(self)
199 | self.wrappers = []
200 | self.id = id
201 | self.server = None
202 | self.name = type(self).__name__
203 | self.inbox = Queue()
204 | # Argparsing
205 | self.argparser = self.generateArgParser()
206 | self.parseArguments(args)
207 | # Logger parameters
208 | self.logger = logger
209 | self._LOGGING_ = False if logger is None else True
210 |
211 | def generateArgParser(self):
212 | config = self.CONFIG
213 |
214 | parser = ArgumentParser(prog=config["prog"],description=config["description"])
215 | for arg in config["args"]:
216 | for name,field in arg.items():
217 | opts = {}
218 | for key,value in field.items():
219 | opts[key] = value
220 | parser.add_argument(name, **opts)
221 | return parser
222 |
223 | def parseArguments(self, args):
224 | parsed = self.argparser.parse_args(args.split())
225 | self.hostname = parsed.hostname[0]
226 | self.port = parsed.port[0]
227 | self.timeout = parsed.timeout[0]
228 | self.error_file = parsed.error_file[0] if parsed.error_file else None
229 | self.error_code = parsed.error_code[0] if parsed.error_code else None
230 | self.ssl = parsed.ssl
231 | self.ssl_cert = parsed.ssl_cert[0] if parsed.ssl_cert else None
232 |
233 | def SignalThread(self):
234 | while True:
235 | msg = self.inbox.get()
236 | if msg.isTerminateMessage():
237 | self.server.shutdown()
238 | break
239 |
240 | def addWrapModule(self, encWrapper):
241 | self.wrappers.append(encWrapper)
242 |
243 | def removeWrapModule(self, encWrapper):
244 | self.wrappers.remove(encWrapper)
245 |
246 | def run(self):
247 | self._LOGGING_ and self.logger.info(f"[{self.name}] Server started. Passing messages...")
248 | self.server = WrapHTTPServer((self.hostname, self.port),
249 | httpserverHandler,self.wrappers,self.name,
250 | self.id, self.timeout, self.error_file,
251 | self.error_code, self.logger)
252 |
253 | # Checking if SSL should be used
254 | if self.ssl and self.ssl_cert:
255 | self.server.socket = wrap_socket(self.server.socket,certfile=self.ssl_cert, server_side=True)
256 |
257 | st = Thread(target=self.SignalThread)
258 | st.start()
259 | self.server.serve_forever()
260 | self.server.server_close()
--------------------------------------------------------------------------------
/ms.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3.7
2 | #
3 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
4 | #
5 | # This file is part of Mística
6 | # (see https://github.com/IncideDigital/Mistica).
7 | #
8 | # This program is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation, either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with this program. If not, see .
20 | #
21 |
22 | import argparse
23 | from utils.logger import Log
24 | from utils.messaging import Message, SignalType, MessageType
25 | from sotp.router import Router
26 | from importlib import import_module
27 | from signal import signal, SIGINT
28 | from random import choice
29 | from utils.prompt import Prompt
30 | from sys import exit, stdin
31 | from platform import system
32 | if system() != "Windows":
33 | from select import poll, POLLIN
34 | from time import sleep
35 | from sotp.misticathread import ServerOverlay, ServerWrapper
36 |
37 | from wrapper.server.wrap_module import *
38 | from overlay.server import *
39 |
40 | class MisticaMode:
41 | SINGLE = 0
42 | MULTI = 1
43 |
44 |
45 | class ModuleType:
46 | WRAP_MODULE = 0
47 | WRAP_SERVER = 2
48 | OVERLAY = 3
49 |
50 |
51 | class MisticaServer():
52 |
53 | def __init__(self, mode, key, verbose, moduleargs):
54 | # Get args and set attributes
55 | self.args = moduleargs
56 | # Logger params
57 | self.logger = Log('_server', verbose) if verbose > 0 else None
58 | self._LOGGING_ = False if self.logger is None else True
59 | # init Router
60 | self.key = key
61 | self.Router = Router(self.key, self.logger)
62 | self.Router.start()
63 | self.mode = mode
64 | self.procid = 0
65 | if self.mode == MisticaMode.SINGLE:
66 | self.overlayname = self.args["overlay"]
67 | self.wrappername = self.args["wrapper"]
68 |
69 | # Checks if the wrap_server of a certain wrap_module is up and running.
70 | def dependencyLaunched(self, wmitem):
71 | dname = wmitem.SERVER_CLASS
72 | for elem in self.Router.wrapServers:
73 | if dname == elem.name:
74 | return True
75 | return False
76 |
77 | # Returns a wrap_module, wrap_server or overlay module
78 | def getModuleInstance(self, type, name, args):
79 | self.procid += 1
80 | if (type == ModuleType.WRAP_MODULE):
81 | return [x for x in ServerWrapper.__subclasses__() if x.NAME == name][0](self.procid, self.Router.inbox, args, self.logger)
82 | else:
83 | return [x for x in ServerOverlay.__subclasses__() if x.NAME == name][0](self.procid, self.Router.inbox, self.mode, args, self.logger)
84 |
85 | def sigintDetect(self, signum, frame):
86 | self._LOGGING_ and self.logger.info("[Sotp] SIGINT detected")
87 | if (self.mode == MisticaMode.SINGLE):
88 | targetoverlay = self.Router.overlayModules[0]
89 | targetoverlay.inbox.put(Message('input',
90 | 0,
91 | self.Router.overlayModules[0].name,
92 | self.Router.overlayModules[0].id,
93 | MessageType.SIGNAL,
94 | SignalType.TERMINATE))
95 | else:
96 | # TODO: Depends on who's on the foreground
97 | pass
98 |
99 | def captureInput(self, overlay):
100 | while True:
101 | if overlay.exit:
102 | break
103 | if system() != 'Windows':
104 | polling = poll()
105 | try:
106 | if system() == 'Windows':
107 | # Ugly loop for windows
108 | rawdata = stdin.buffer.raw.read(50000)
109 | sleep(0.1)
110 | else:
111 | # Nice loop for unix
112 | polling.register(stdin.buffer.raw.fileno(), POLLIN)
113 | polling.poll()
114 |
115 | rawdata = stdin.buffer.raw.read(50000)
116 | if rawdata == b'': # assume EOF
117 | self._LOGGING_ and self.logger.info("[MísticaServer] Input is dead")
118 | self.Router.join()
119 |
120 | if rawdata and len(rawdata) > 0 and overlay.hasInput:
121 | overlay.inbox.put(Message('input', 0, 'overlay', overlay.id, MessageType.STREAM, rawdata))
122 | except KeyboardInterrupt:
123 | self._LOGGING_ and self.logger.info("[MísticaServer] CTRL+C detected. Passing to overlay")
124 | overlay.inbox.put(Message('input',
125 | 0,
126 | self.Router.overlayModules[0].name,
127 | self.Router.overlayModules[0].id,
128 | MessageType.SIGNAL,
129 | SignalType.TERMINATE))
130 | break
131 | return
132 |
133 | def run(self):
134 |
135 | # If the mode is single-handler only a wrapper module and overlay module is used
136 | if self.mode == MisticaMode.SINGLE:
137 |
138 | # Launch wrap_module
139 | wmitem = self.getModuleInstance(ModuleType.WRAP_MODULE, self.wrappername, self.args["wrapper_args"])
140 | wmitem.start()
141 | self.Router.wrapModules.append(wmitem)
142 |
143 | # Check wrap_server dependency of wrap_module and launch it
144 | if not self.dependencyLaunched(wmitem):
145 | self.procid += 1
146 | wsitem = wmitem.SERVER_CLASS(self.procid, self.args["wrap_server_args"], self.logger)
147 | wsitem.start()
148 | self.Router.wrapServers.append(wsitem)
149 | else:
150 | wsitem = [elem for elem in self.Router.wrapServers if wsname == wmitem.SERVER_CLASS.NAME][0]
151 |
152 | # add wrap_module to wrap_server list
153 | wsitem.addWrapModule(wmitem)
154 |
155 | # Launch overlay module
156 | omitem = self.getModuleInstance(ModuleType.OVERLAY, self.overlayname, self.args["overlay_args"])
157 | omitem.start()
158 | self.Router.overlayModules.append(omitem)
159 | targetoverlay = self.Router.overlayModules[0]
160 | if targetoverlay.hasInput:
161 | self.captureInput(targetoverlay)
162 | self.Router.join()
163 | self._LOGGING_ and self.logger.debug("[MísticaServer] Terminated")
164 | elif self.mode == MisticaMode.MULTI:
165 | # Launch prompt etc.
166 | # Before registering a wrapper or an overlay, we must make sure that there is no other
167 | # module with incompatible parameters (e.g 2 DNS base64-based wrap_modules)
168 | self.Router.inbox.put(Message("Mistica", 0, "sotp", 0, MessageType.SIGNAL, SignalType.TERMINATE))
169 | print("Multi-handler mode is not implemented yet! use -h")
170 | exit(0)
171 |
172 |
173 | if __name__ == '__main__':
174 | fortunes = ["The Last Protocol Bender.",
175 | "Your friendly data smuggler.",
176 | "Anything is a tunnel if you're brave enough.",
177 | "It's a wrap!",
178 | "The Overlay Overlord."]
179 | parser = argparse.ArgumentParser(description=f"Mistica server. {choice(fortunes)} Run without parameters to launch multi-handler mode.")
180 | parser.add_argument('-k', "--key", action='store', default="", required=False, help="RC4 key used to encrypt the comunications")
181 | parser.add_argument("-l", "--list", action='store', required=False, help="Lists modules or parameters. Options are: all, overlays, wrappers, , ")
182 | parser.add_argument("-m", "--modules", action='store', required=False, help="Module pair in single-handler mode. format: 'overlay:wrapper'")
183 | parser.add_argument("-w", "--wrapper-args", action='store', required=False, default='', help="args for the selected overlay module (Single-handler mode)")
184 | parser.add_argument("-o", "--overlay-args", action='store', required=False, default='', help="args for the selected wrapper module (Single-handler mode)")
185 | parser.add_argument("-s", "--wrap-server-args", action='store', required=False, default='', help="args for the selected wrap server (Single-handler mode)")
186 | parser.add_argument('-v', '--verbose', action='count', default=0, help="Level of verbosity in logger (no -v None, -v Low, -vv Medium, -vvv High)")
187 |
188 |
189 | args = parser.parse_args()
190 | moduleargs = {}
191 |
192 | if args.list:
193 | # list and quit
194 | if args.list == "all" or args.list == "overlays" or args.list == "wrappers":
195 | print(Prompt.listModules("server", args.list))
196 | else:
197 | Prompt.listParameters("server", args.list)
198 | exit(0)
199 | elif args.modules:
200 | if args.key == "":
201 | print("You must suply a key for RC4 encryption. Use -k or --key")
202 | exit(1)
203 | moduleargs["overlay"] = args.modules.partition(":")[0]
204 | moduleargs["wrapper"] = args.modules.partition(":")[2]
205 | if Prompt.findModule("server", moduleargs["overlay"]) is None:
206 | print(f"Invalid overlay module {moduleargs['overlay']}. Please specify a valid module.")
207 | exit(1)
208 | if Prompt.findModule("server", moduleargs["wrapper"]) is None:
209 | print(f"Invalid wrap module {moduleargs['wrapper']}. Please specify a valid module.")
210 | exit(1)
211 | moduleargs["overlay_args"] = args.overlay_args
212 | moduleargs["wrapper_args"] = args.wrapper_args
213 | moduleargs["wrap_server_args"] = args.wrap_server_args
214 | mode = MisticaMode.SINGLE
215 | elif args.overlay_args:
216 | print("Error: Overlay parameters without overlay module. Please use -m")
217 | exit(1)
218 | elif args.wrapper_args:
219 | print("Error: Wrapper parameters without wrapper module. Please use -m")
220 | exit(1)
221 | else:
222 | mode = MisticaMode.MULTI
223 |
224 | s = MisticaServer(mode, args.key, args.verbose, moduleargs)
225 | s.run()
226 |
--------------------------------------------------------------------------------
/sotp/router.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from threading import Thread
21 | from queue import Queue
22 | from random import randint
23 | from utils.messaging import Message, MessageType, SignalType
24 | from utils.bitstring import BitArray
25 | from sotp.packet import Packet
26 | from sotp.serverworker import ServerWorker
27 | from sotp.core import Header, OptionalHeader, Sizes, Offsets, Status, Flags, Sync
28 | from sotp.core import Core
29 | from sotp.route import Route
30 | from sys import stderr
31 |
32 |
33 | class Router(Thread):
34 |
35 | def __init__(self, key, logger):
36 | Thread.__init__(self)
37 | self.inbox = Queue()
38 | self.wrapModules = []
39 | self.wrapServers = []
40 | self.overlayModules = []
41 | self.workers = []
42 | self.routes = []
43 | self.pendingInit = []
44 | self.workerID = 1
45 | self.rc4 = key
46 | self.id = 0
47 | self.name = "router"
48 | self.exit = False
49 | # Logger parameters
50 | self.logger = logger
51 | self._LOGGING_ = False if logger is None else True
52 |
53 | def errorMessage(self, destination, destination_id):
54 | return Message(self.name, self.id, destination, destination_id,
55 | MessageType.SIGNAL, SignalType.ERROR)
56 |
57 | def addRoute(self, session_id, worker, wrap_module, overlay):
58 | self.routes.append(Route(session_id, worker, wrap_module, overlay))
59 |
60 | def routeMessage(self, msg, sessionID):
61 | if (msg.sender == "serverworker"): # Outgoing
62 | for route in self.routes:
63 | if (route.session_id == sessionID):
64 | route.wrap_module.inbox.put(msg)
65 | break
66 |
67 | else: # incoming from wrapper
68 | for route in self.routes:
69 | if (route.session_id == sessionID):
70 | route.worker.inbox.put(msg)
71 | break
72 | else: # worker not found
73 | # Place error reply to unlock the server.
74 | for route in self.routes:
75 | if (route.wrap_module.id == msg.sender_id):
76 | route.wrap_module.inbox.put(self.errorMessage(route.wrap_module.name,
77 | route.wrap_module.id))
78 | break
79 |
80 | def craftTerminateMessage(self, receiver, receiver_id):
81 | return Message(self.name, self.id, receiver, receiver_id,
82 | MessageType.SIGNAL, SignalType.TERMINATE)
83 |
84 | def handleSignal(self, msg):
85 | self._LOGGING_ and self.logger.debug("[Router] Terminating all threads")
86 | if msg.isTerminateMessage():
87 | # shutdown everything
88 | for wm in self.wrapModules:
89 | wm.inbox.put(self.craftTerminateMessage(wm.name, wm.id))
90 | for wm in self.wrapServers:
91 | wm.inbox.put(self.craftTerminateMessage(wm.name, wm.id))
92 | for wm in self.workers:
93 | wm.inbox.put(self.craftTerminateMessage(wm.name, wm.id))
94 | for wm in self.overlayModules:
95 | wm.inbox.put(self.craftTerminateMessage(wm.name, wm.id))
96 | self.exit = True
97 |
98 | def sessionAlreadyExists(self, sessionID):
99 | for route in self.routes:
100 | if (route.session_id == sessionID):
101 | return True
102 | return False
103 |
104 | def newSessionID(self):
105 | while True:
106 | sessionID = BitArray(uint=randint(1, ((2**Header.SESSION_ID)-1)), length=Header.SESSION_ID)
107 | if not self.sessionAlreadyExists(sessionID):
108 | break
109 | return sessionID
110 |
111 | def generateAuthResponsePacket(self, req, sessionID):
112 | p = Packet()
113 | p.session_id = sessionID
114 | p.seq_number = BitArray(uint=1, length=Header.SEQ_NUMBER)
115 | p.ack = req.seq_number
116 | p.data_len = BitArray(bin='0' * Header.DATA_LEN)
117 | p.flags = BitArray(uint=Flags.SYNC, length=Header.FLAGS)
118 | p.optional_headers = True
119 | p.sync_type = BitArray(uint=Sync.RESPONSE_AUTH, length=OptionalHeader.SYNC_TYPE)
120 | p.content = BitArray()
121 | return p
122 |
123 | def validOverlayTag(self, tag):
124 | for overlay in self.overlayModules:
125 | if overlay.tag == tag:
126 | return True
127 | return False
128 |
129 | def initializeSOTPSession(self, msg):
130 | # Get sender:
131 | sender = None
132 | for wrapper in self.wrapModules:
133 | if wrapper.id == msg.sender_id:
134 | sender = wrapper
135 | self._LOGGING_ and self.logger.debug(f"[Router] Found {msg.sender} with id {msg.sender_id} in the WrapModule list")
136 | break
137 | else: # not a valid wrapper?
138 | self._LOGGING_ and self.logger.error(f"[Router] Error: Wrapper does not exist")
139 | return
140 |
141 | # Check if valid packet
142 | try:
143 | pkt = Core.transformToPacket(msg.content)
144 | except Exception as e:
145 | sender.inbox.put(self.errorMessage(sender.name, sender.id))
146 | self._LOGGING_ and self.logger.exception(f"[Router] Exception on transformToPacket() {e}")
147 | return
148 |
149 | # Check if valid overlay tag
150 | if not self.validOverlayTag(pkt.content):
151 | sender.inbox.put(self.errorMessage(sender.name, sender.id))
152 | self._LOGGING_ and self.logger.error(f"[Router] Error: Not a valid Overlay tag")
153 | return
154 |
155 | # Generate new random ID
156 | try:
157 | sessionID = self.newSessionID()
158 | except Exception as e:
159 | self._LOGGING_ and self.logger.exception(f"[Router] Exception on newSessionID() {e}")
160 | sender.inbox.put(self.errorMessage(sender.name, sender.id))
161 | return
162 |
163 | # Add Session ID and overlay tag to pending and send response to wrapper
164 | authpkt = self.generateAuthResponsePacket(pkt, sessionID)
165 | self.pendingInit.append({
166 | "sessionID": sessionID,
167 | "tag": pkt.content,
168 | "lastpkt": authpkt
169 | })
170 |
171 | # Avoid DoS by rejecting old pendings:
172 | if len(self.pendingInit) > (Header.SESSION_ID / 2):
173 | self.pendingInit.pop(0)
174 | self._LOGGING_ and self.logger.debug(f"[Router] Passing Session Response back to {msg.sender}")
175 | sender.inbox.put(Message(self.name, self.id,
176 | sender.name, sender.id, MessageType.STREAM,
177 | authpkt.toBytes(),msg.wrapServerQ))
178 |
179 | def spawnRoute(self, msg, sessionID, tag, lastpkt):
180 | overlay = None
181 | wrapper = None
182 | # Get overlay MisticaThread
183 | for available in self.overlayModules:
184 | if available.tag == tag:
185 | overlay = available
186 | break
187 | else:
188 | self._LOGGING_ and self.logger.error(f"[Router] Error: Overlay module no longer available")
189 | return
190 |
191 | # Get wrapper MisticaThread
192 | for available in self.wrapModules:
193 | if available.id == msg.sender_id:
194 | wrapper = available
195 | break
196 | else:
197 | self._LOGGING_ and self.logger.error(f"[Router] Error: Wrapper module no longer available")
198 | return
199 |
200 | self._LOGGING_ and self.logger.debug(f"[Router] Creating route for session 0x{sessionID.hex} from {wrapper.name} to {overlay.name}. Spawning worker...")
201 | worker = ServerWorker(overlay, self.workerID, self.inbox, wrapper.max_retries,
202 | wrapper.max_size, self.logger, self.rc4, sessionID, lastpkt)
203 | self.workers.append(worker)
204 | self.workerID += 1
205 | self.routes.append(Route(sessionID, worker, wrapper, overlay))
206 | self.pendingInit.remove({
207 | "sessionID": sessionID,
208 | "tag": tag,
209 | "lastpkt": lastpkt
210 | })
211 | worker.start()
212 |
213 | # ONLY gets the first N bytes where N is the size of the session_id field
214 | def getSessionID(self, binarypacket):
215 | return Core.fromBytesToBitArray(binarypacket)[0:Offsets.SESSION_ID]
216 |
217 | def run(self):
218 | self._LOGGING_ and self.logger.info(f"[Router] Staring up and waiting for messages...")
219 |
220 | while (not self.exit):
221 | msg = self.inbox.get()
222 | # inbox contains signal?
223 | if msg.isSignalMessage():
224 | self._LOGGING_ and self.logger.debug(f"[Router] Signal received from {msg.sender} with content {msg.content}")
225 | self.handleSignal(msg)
226 | continue
227 | # This inbox contains a sotp packet from a worker or a wrapper
228 | try:
229 | sessionID = self.getSessionID(msg.content)
230 | except Exception as e:
231 | print(e)
232 | continue
233 | # New session? create pending init
234 | if (sessionID.hex == '00'):
235 | self._LOGGING_ and self.logger.info(f"[Router] New Session Request. Initializing...")
236 | self.initializeSOTPSession(msg)
237 | continue
238 | # Session init confirmed?
239 | for elem in self.pendingInit:
240 | if sessionID == elem['sessionID']:
241 | self.spawnRoute(msg, sessionID, elem['tag'], elem['lastpkt'])
242 | break
243 |
244 | # Established session! Route message
245 | self.routeMessage(msg, sessionID)
246 | self._LOGGING_ and self.logger.debug("[Router] Terminated")
247 |
--------------------------------------------------------------------------------
/wrapper/server/wrap_module/dns.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.misticathread import ServerWrapper
21 | from base64 import urlsafe_b64encode,urlsafe_b64decode
22 | from dnslib import QTYPE, CLASS, RR
23 | from dnslib import DNSHeader, DNSRecord
24 | from dnslib import TXT, CNAME, MX, NS, SOA
25 | from wrapper.server.wrap_server.dnsserver import dnsserver
26 |
27 | class dnswrapper(ServerWrapper):
28 |
29 | SERVER_CLASS = dnsserver
30 | NAME = "dns"
31 | CONFIG = {
32 | "prog": NAME,
33 | "wrapserver": "dnsserver",
34 | "description": "Encodes/Decodes data in DNS queries/responses using different methods",
35 | "args": [
36 | {
37 | "--domains": {
38 | "help": "Domain names to accept packets. (Ex: mistica.dev)",
39 | "nargs": "*",
40 | "default": ["mistica.dev"],
41 | "type": str
42 | },
43 | "--ttl": {
44 | "help": "TTL of DNS Response",
45 | "nargs": 1,
46 | "default": [300],
47 | "type" : int
48 | },
49 | "--queries": {
50 | "help": "Type of DNS Query (NS,CNAME,SOA,MX,TXT) not supported (A,AAAA) yet",
51 | "nargs": "*",
52 | "default": ["TXT"],
53 | "choices": ["NS","CNAME","SOA","MX","TXT"],
54 | "type": str
55 | },
56 | "--max-size": {
57 | "help": "Maximum size in bytes of the sotp packet to be embedded in the dns packet. Not recommended change it (8 sotp_header + 37 raw_data = 45 rc4 = 60 b64 < 63 max_idna)",
58 | "nargs": 1,
59 | "default": [37],
60 | "type" : int
61 | },
62 | "--max-retries": {
63 | "help": "Maximum number of re-synchronization retries.",
64 | "nargs": 1,
65 | "default": [5],
66 | "type": int
67 | }
68 | }
69 | ]
70 | }
71 |
72 | def __init__(self, id, qsotp, args, logger):
73 | ServerWrapper.__init__(self, id, dnswrapper.NAME, qsotp, dnswrapper.SERVER_CLASS.NAME, args, logger)
74 | self.request = []
75 | # Logger parameters
76 | self.logger = logger
77 | self._LOGGING_ = False if logger is None else True
78 |
79 | def parseArguments(self, args):
80 | parsed = self.argparser.parse_args(args.split())
81 | self.domains = parsed.domains
82 | self.ttl = parsed.ttl[0]
83 | self.queries = parsed.queries
84 | self.max_size = parsed.max_size[0]
85 | self.max_retries = parsed.max_retries[0]
86 |
87 |
88 | def extractFromSubdomain(self, qname):
89 | reqhostname = qname.idna()[:-1]
90 | for hostname in self.domains:
91 | if reqhostname.endswith(f".{hostname}"):
92 | # urlsafe_b64decode ignore '.' characters, so its okey for decode packets in multiple mode
93 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] extractFromSubdomain() extract data from query: {reqhostname}")
94 | return urlsafe_b64decode(reqhostname.replace(f".{hostname}",""))
95 | else:
96 | self._LOGGING_ and self.logger.error(f"[{self.name}] Extracting SOTP from Subdomain and not in hostname list")
97 | return None
98 |
99 | def parseQuestion(self,request):
100 | if request.q.qtype is QTYPE.NS:
101 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Received a dns question with qtype NS")
102 | return self.extractFromSubdomain(request.q.qname)
103 | elif request.q.qtype is QTYPE.CNAME:
104 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Received a dns question with qtype CNAME")
105 | return self.extractFromSubdomain(request.q.qname)
106 | elif request.q.qtype is QTYPE.SOA:
107 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Received a dns question with qtype SOA")
108 | return self.extractFromSubdomain(request.q.qname)
109 | elif request.q.qtype is QTYPE.MX:
110 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Received a dns question with qtype MX")
111 | return self.extractFromSubdomain(request.q.qname)
112 | elif request.q.qtype is QTYPE.TXT:
113 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Received a dns question with qtype TXT")
114 | return self.extractFromSubdomain(request.q.qname)
115 | else:
116 | # A, AAAA, PTR for future releases
117 | self._LOGGING_ and self.logger.error(f"[{self.name}] parseQuestion() recieved a dns with invalid question type: {request.q.qtype}")
118 | return None
119 |
120 | def inHostnameList(self, request):
121 | reqhostname = request.q.qname.idna()[:-1]
122 | for hostname in self.domains:
123 | if reqhostname == hostname or reqhostname.endswith(f".{hostname}"):
124 | return True
125 | self._LOGGING_ and self.logger.error(f"[{self.name}] received dns query to {reqhostname} which is not in the hostname list")
126 | return False
127 |
128 | def inQueryList(self,request):
129 | for q in self.queries:
130 | if getattr(QTYPE,q) is request.q.qtype:
131 | return True
132 | self._LOGGING_ and self.logger.error(f"[{self.name}] received dns query {request.q.qtype} which is not in the query list {self.queries}")
133 | return False
134 |
135 | def unwrap(self, content):
136 | if not self.inHostnameList(content):
137 | return None
138 | if not self.inQueryList(content):
139 | return None
140 | self.request.append(content)
141 | return self.parseQuestion(content)
142 |
143 | def getDomainFromRequest(self, reqhostname):
144 | for hostname in self.domains:
145 | if reqhostname.endswith(f".{hostname}"):
146 | return hostname
147 | raise f"Request Hostname not found in domain list: {reqhostname}"
148 |
149 | def createNsResponse(self, data, request):
150 | dataRawEnc = urlsafe_b64encode(data)
151 | dataEnc = str(dataRawEnc, "utf-8")
152 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] createNSResponse() with sotp_data: {dataEnc}")
153 | rdomain = self.getDomainFromRequest(request.q.qname.idna()[:-1])
154 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
155 | reply.add_answer(RR(rname=request.q.qname,
156 | rtype=QTYPE.NS,
157 | rclass=CLASS.IN,
158 | ttl=self.ttl,
159 | rdata=NS(f"{dataEnc}.{rdomain}")))
160 | return reply
161 |
162 | def createCnameResponse(self, data, request):
163 | dataRawEnc = urlsafe_b64encode(data)
164 | dataEnc = str(dataRawEnc, "utf-8")
165 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] createCnameResponse() with sotp_data: {dataEnc}")
166 | rdomain = self.getDomainFromRequest(request.q.qname.idna()[:-1])
167 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
168 | reply.add_answer(RR(rname=request.q.qname,
169 | rtype=QTYPE.CNAME,
170 | rclass=CLASS.IN,
171 | ttl=self.ttl,
172 | rdata=CNAME(f"{dataEnc}.{rdomain}")))
173 | return reply
174 |
175 | def createSoaResponse(self, data, request):
176 | dataRawEnc = urlsafe_b64encode(data)
177 | dataEnc = str(dataRawEnc, "utf-8")
178 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] createSoaResponse() with sotp_data: {dataEnc}")
179 | rdomain = self.getDomainFromRequest(request.q.qname.idna()[:-1])
180 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
181 | reply.add_answer(RR(rname=request.q.qname,
182 | rtype=QTYPE.SOA,
183 | rclass=CLASS.IN,
184 | ttl=self.ttl,
185 | rdata=SOA(f"{dataEnc}.{rdomain}")))
186 | return reply
187 |
188 | def createMxResponse(self, data, request):
189 | dataRawEnc = urlsafe_b64encode(data)
190 | dataEnc = str(dataRawEnc, "utf-8")
191 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] createMxResponse() with sotp_data: {dataEnc}")
192 | rdomain = self.getDomainFromRequest(request.q.qname.idna()[:-1])
193 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
194 | reply.add_answer(RR(rname=request.q.qname,
195 | rtype=QTYPE.MX,
196 | rclass=CLASS.IN,
197 | ttl=self.ttl,
198 | rdata=MX(f"{dataEnc}.{rdomain}")))
199 | return reply
200 |
201 | def createTxtResponse(self, data, request):
202 | # I embebed sopt data in one RR in TXT Response (but you can split sotp data in multiple RR)
203 | dataRawEnc = urlsafe_b64encode(data)
204 | dataEnc = str(dataRawEnc, "utf-8")
205 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] createTxtResponse() with sotp_data: {dataEnc}")
206 | reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
207 | reply.add_answer(RR(rname=request.q.qname,
208 | rtype=QTYPE.TXT,
209 | rclass=CLASS.IN,
210 | ttl=self.ttl,
211 | rdata=TXT(dataEnc)))
212 | return reply
213 |
214 | def generateResponse(self, data, request):
215 | if request.q.qtype is QTYPE.NS:
216 | return self.createNsResponse(data, request)
217 | elif request.q.qtype is QTYPE.CNAME:
218 | return self.createCnameResponse(data, request)
219 | elif request.q.qtype is QTYPE.SOA:
220 | return self.createSoaResponse(data, request)
221 | elif request.q.qtype is QTYPE.MX:
222 | return self.createMxResponse(data, request)
223 | elif request.q.qtype is QTYPE.TXT:
224 | return self.createTxtResponse(data, request)
225 | else:
226 | # A, AAAA, PTR for future releases
227 | self._LOGGING_ and self.logger.error(f"[{self.name}] generateResponse() invalid request qtype: {request.q.qtype}")
228 | return None
229 |
230 | def wrap(self, content):
231 | request = self.request.pop(0)
232 | reply = self.generateResponse(content,request)
233 | return reply
234 |
--------------------------------------------------------------------------------
/mc.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3.7
2 | #
3 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
4 | #
5 | # This file is part of Mística
6 | # (see https://github.com/IncideDigital/Mistica).
7 | #
8 | # This program is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation, either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with this program. If not, see .
20 | #
21 |
22 | from argparse import ArgumentParser
23 | from threading import Thread, Semaphore
24 | from queue import Queue, Empty
25 | from importlib import import_module
26 | from sotp.clientworker import ClientWorker
27 | from utils.messaging import Message, SignalType, MessageType
28 | from utils.logger import Log
29 | from time import sleep
30 | from sys import exit, stdin
31 | from platform import system
32 | from signal import signal, SIGINT
33 | from utils.prompt import Prompt
34 | if system() != "Windows":
35 | from select import poll, POLLIN
36 | from sotp.misticathread import ClientOverlay, ClientWrapper
37 | from wrapper.client import *
38 | from overlay.client import *
39 |
40 | class MisticaClient(object):
41 |
42 | def __init__(self, key, args, verbose):
43 | self.name = type(self).__name__
44 | self.wrapper = None
45 | self.overlay = None
46 | self.qsotp = Queue()
47 | self.qdata = Queue()
48 | self.sem = Semaphore(1)
49 | self.sotp_sem = Semaphore(1)
50 | self.released = False
51 | # Overlay and Wrapper args
52 | self.overlayname = args["overlay"]
53 | self.wrappername = args["wrapper"]
54 | self.overlayargs = args["overlay_args"]
55 | self.wrapperargs = args["wrapper_args"]
56 | # Mistica Client args
57 | self.key = key
58 | # Arguments depended of overlay used
59 | self.tag = None
60 | # Arguments depended of wrapper used
61 | self.max_size = None
62 | self.poll_delay = None
63 | self.response_timeout = None
64 | self.max_retries = None
65 | # Logger parameters
66 | self.logger = Log('_client', verbose) if verbose > 0 else None
67 | self._LOGGING_ = False if self.logger is None else True
68 |
69 |
70 | def doWrapper(self):
71 | self.sem.acquire()
72 | self._LOGGING_ and self.logger.info(f"[Wrapper] Initializing...")
73 | self.wrapper = [x for x in ClientWrapper.__subclasses__() if x.NAME == self.wrappername][0](self.qsotp, self.wrapperargs, self.logger)
74 | self.wrapper.start()
75 | # setting sotp arguments depending on the wrapper to be used
76 | self.max_size = self.wrapper.max_size
77 | self.response_timeout = self.wrapper.response_timeout
78 | self.poll_delay = self.wrapper.poll_delay
79 | self.max_retries = self.wrapper.max_retries
80 | self.sem.release()
81 |
82 |
83 | def doSotp(self):
84 | try:
85 | self.sem.acquire()
86 | self._LOGGING_ and self.logger.info(f"[{self.name}] Initializing...")
87 | self.sem.release()
88 | self.sotp_sem.acquire()
89 | i = 1
90 | s = ClientWorker(self.key,
91 | self.max_retries,
92 | self.max_size,
93 | self.tag,
94 | self.overlayname,
95 | self.wrappername,
96 | self.qdata,
97 | self.logger)
98 | dataThread = Thread(target=s.dataEntry, args=(self.qsotp,))
99 | dataThread.start()
100 | while not s.exit:
101 | answers = []
102 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Iteration nº{i} Status: {s.st} Seq: {s.seqnumber}/65535")
103 |
104 | if s.wait_reply:
105 | timeout = self.response_timeout
106 | else:
107 | timeout = self.poll_delay
108 | try:
109 | dataEntry = self.qsotp.get(True,timeout)
110 | answers = s.Entrypoint(dataEntry)
111 | except Empty:
112 | if s.wait_reply:
113 | answers = s.lookForRetries()
114 | else:
115 | answers = s.getPollRequest()
116 |
117 | for answer in answers:
118 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Header Sent: {answer.printHeader()}")
119 | if answer.receiver == self.wrapper.name:
120 | self._LOGGING_ and self.logger.debug_all(f"[{self.name}] Retries {s.retries}/{self.max_retries}")
121 | self.wrapper.inbox.put(answer)
122 | elif answer.receiver == self.overlay.name:
123 | self.overlay.inbox.put(answer)
124 | else:
125 | raise Exception(f"Invalid answer to {answer.receiver} in client loop")
126 |
127 | if self.released == False and s.sotp_first_push:
128 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Initialized! Unblocked sem")
129 | self.sotp_sem.release()
130 | self.released = True
131 | i += 1
132 | pass
133 | dataThread.join()
134 | self._LOGGING_ and self.logger.info(f"[DataThread] Terminated")
135 | self._LOGGING_ and self.logger.info(f"[{s.name}] Terminated")
136 | except Exception as e:
137 | print(e)
138 | self._LOGGING_ and self.logger.exception(f"[ClientWorker] Exception in doSotp: {e}")
139 | self.overlay.inbox.put(Message("clientworker", 0, self.overlayname, 0, MessageType.SIGNAL, SignalType.TERMINATE))
140 | self.wrapper.inbox.put(Message("clientworker", 0, self.wrappername, 0, MessageType.SIGNAL, SignalType.TERMINATE))
141 |
142 |
143 | def doOverlay(self):
144 | self.sem.acquire()
145 | self._LOGGING_ and self.logger.info(f"[Overlay] Initializing...")
146 | self.overlay = [x for x in ClientOverlay.__subclasses__() if x.NAME == self.overlayname][0](self.qsotp, self.qdata, self.overlayargs, self.logger)
147 | self.overlay.start()
148 | # setting sotp arguments depending on the overlay to be used
149 | self.tag = self.overlay.tag
150 | self.sem.release()
151 |
152 |
153 | def captureInput(self):
154 | self.sem.acquire()
155 | self._LOGGING_ and self.logger.info(f"[Input] Initializing...")
156 | self.sem.release()
157 | if system() != 'Windows':
158 | polling = poll()
159 | while True:
160 | if self.overlay.exit:
161 | break
162 | try:
163 | if system() == 'Windows':
164 | # Ugly loop for windows
165 | rawdata = stdin.buffer.raw.read(300000)
166 | sleep(0.1)
167 | else:
168 | # Nice loop for unix
169 | polling.register(stdin.buffer.raw.fileno(), POLLIN)
170 | polling.poll()
171 |
172 | rawdata = stdin.buffer.raw.read(300000)
173 | if rawdata == b'': # assume EOF
174 | self.sotp_sem.acquire()
175 | self._LOGGING_ and self.logger.debug(f"[Input] SOTP initialized, sending terminate because input recv EOF")
176 | self.overlay.inbox.put(Message("input", 0, self.overlayname, 0, MessageType.SIGNAL, SignalType.TERMINATE))
177 | self.sotp_sem.release()
178 | break
179 |
180 | if rawdata and len(rawdata) > 0 and self.overlay.hasInput:
181 | self.overlay.inbox.put(Message("input", 0, self.overlayname, 0, MessageType.STREAM, rawdata))
182 | except KeyboardInterrupt:
183 | self._LOGGING_ and self.logger.debug("[Input] CTRL+C detected. Passing to wrapper")
184 | self.overlay.inbox.put(Message("input", 0, self.overlayname, 0, MessageType.SIGNAL, SignalType.TERMINATE))
185 | break
186 | self._LOGGING_ and self.logger.info(f"[Input] Terminated")
187 | return
188 |
189 |
190 | def captureExit(self, signal_received, frame):
191 | self._LOGGING_ and self.logger.debug("[Input] CTRL+C detected. Passing to overlay")
192 | self.overlay.inbox.put(Message("input", 0, self.overlayname, 0, MessageType.SIGNAL, SignalType.TERMINATE))
193 |
194 |
195 | def run(self):
196 | try:
197 | self.doWrapper()
198 | self.doOverlay()
199 | sotpThread = Thread(target=self.doSotp)
200 | sotpThread.start()
201 | if self.overlay.hasInput:
202 | self.captureInput()
203 | else:
204 | signal(SIGINT, self.captureExit)
205 | # Crappy loop for windows
206 | if system() == 'Windows':
207 | while not self.overlay.exit:
208 | sleep(0.5)
209 | # Nice sync primitive for unix
210 | else:
211 | sotpThread.join()
212 | except Exception as e:
213 | self._LOGGING_ and self.logger.exception(f"Exception at run(): {e}")
214 | finally:
215 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Terminated")
216 |
217 |
218 | if __name__ == '__main__':
219 | parser = ArgumentParser(description=f"Mistica client.")
220 |
221 | # Sotp Client arguments.
222 | parser.add_argument('-k', "--key", action='store', default="",required=False, help="RC4 key used to encrypt the comunications")
223 | parser.add_argument("-l", "--list", action='store', required=False, help="Lists modules or parameters. Options are: all, overlays, wrappers, , ")
224 | # Wrapper and Overlay arguments.
225 | parser.add_argument("-m", "--modules", action='store', required=False, help="Module pair. Format: 'overlay:wrapper'")
226 | parser.add_argument("-w", "--wrapper-args", action='store', required=False, default='', help="args for the selected overlay module")
227 | parser.add_argument("-o", "--overlay-args", action='store', required=False, default='', help="args for the selected wrapper module")
228 | parser.add_argument('-v', '--verbose', action='count', default=0, help="Level of verbosity in logger (no -v None, -v Low, -vv Medium, -vvv High)")
229 | args = parser.parse_args()
230 | moduleargs = {}
231 |
232 | if args.list:
233 | # list and quit
234 | if args.list == "all" or args.list == "overlays" or args.list == "wrappers":
235 | print(Prompt.listModules("client", args.list))
236 | else:
237 | Prompt.listParameters("client", args.list)
238 | exit(0)
239 | elif args.modules:
240 | if args.key == "":
241 | print("You must suply a key for RC4 encryption. Use -k or --key")
242 | exit(1)
243 | moduleargs["overlay"] = args.modules.partition(":")[0]
244 | moduleargs["wrapper"] = args.modules.partition(":")[2]
245 | if Prompt.findModule("client", moduleargs["overlay"]) is None:
246 | print(f"Invalid overlay module {moduleargs['overlay']}. Please specify a valid module.")
247 | exit(1)
248 | if Prompt.findModule("client", moduleargs["wrapper"]) is None:
249 | print(f"Invalid wrap module {moduleargs['wrapper']}. Please specify a valid module.")
250 | exit(1)
251 | moduleargs["overlay_args"] = args.overlay_args
252 | moduleargs["wrapper_args"] = args.wrapper_args
253 | elif args.overlay_args:
254 | print("Error: Overlay parameters without overlay module. Please use -m")
255 | exit(1)
256 | elif args.wrapper_args:
257 | print("Error: Wrapper parameters without wrapper module. Please use -m")
258 | exit(1)
259 | else:
260 | parser.print_help()
261 | exit(0)
262 |
263 | c = MisticaClient(args.key,moduleargs,args.verbose)
264 | c.run()
265 |
--------------------------------------------------------------------------------
/sotp/serverworker.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from sotp.core import Header, OptionalHeader, Sizes, Offsets, Status, Flags, Sync
21 | from sotp.core import Core, BYTE
22 | from sotp.packet import Packet
23 | from threading import Thread
24 | from queue import Queue
25 | from utils.messaging import Message, MessageType, SignalType
26 | from utils.bitstring import BitArray
27 |
28 |
29 | class ServerWorker(Core, Thread):
30 |
31 | def __init__(self, overlay, id, SotpServerInbox, retries, maxsize, logger, key, sid, lastpkt):
32 | Core.__init__(self, key, retries, maxsize)
33 | Thread.__init__(self)
34 | self.overlay = overlay
35 | self.st = Status.WORKING # Server establishes session before a route is created
36 | self.inbox = Queue()
37 | self.datainbox = Queue()
38 | self.id = id
39 | self.sid = sid
40 | self.outbox = SotpServerInbox
41 | self.lastPacketSent = lastpkt
42 | self.lastPacketRecv = None
43 | self.seqnumber = lastpkt.seq_number.uint
44 | self.overlay.addWorker(self)
45 | self.exit = False
46 | # Logger parameters
47 | self.logger = logger
48 | self._LOGGING_ = False if logger is None else True
49 |
50 | # Method that checks if a packet is a Polling request
51 | def seemsPollingRequest(self,packet):
52 | if packet.isFlagActive(Flags.SYNC) == False:
53 | return False
54 | if packet.isSyncType(Sync.POLLING_REQUEST) == False:
55 | return False
56 | if any(packet.data_len) or any(packet.content):
57 | return False
58 | return True
59 |
60 | # Method that checks if a packet is a Data Transfer Packet
61 | def seemsPollingChunk(self,packet):
62 | if any(packet.flags):
63 | return False
64 | if any(packet.data_len) == False:
65 | return False
66 | if packet.data_len.uint != int(packet.content.length/BYTE):
67 | return False
68 | return True
69 |
70 | # Method that checks if a packet is a Data Transfer Packet with PUSH Flag
71 | def seemsPollingDataPush(self,packet):
72 | if packet.isFlagActive(Flags.PUSH) == False:
73 | return False
74 | if packet.optional_headers:
75 | return False
76 | if any(packet.data_len) == False:
77 | return False
78 | if packet.data_len.uint != int(packet.content.length/BYTE):
79 | return False
80 | return True
81 |
82 | # Method that checks if a packet is a confirmation.
83 | def seemsConfirmation(self,packet):
84 | if any(packet.flags):
85 | return False
86 | if any(packet.data_len) or any(packet.content):
87 | return False
88 | return True
89 |
90 | # Method that checks if a packet is a session reinitialization request.
91 | def seemsReinitRequest(self, packet):
92 | if not packet.isFlagActive(Flags.SYNC):
93 | return False
94 | if not packet.isSyncType(Sync.REINITIALIZING):
95 | return False
96 | if any(packet.data_len) or any(packet.content):
97 | return False
98 | return True
99 |
100 | # Method to check if the packet is a valid request from mistica client.
101 | # first it checks that the package has the sotp structure and then
102 | # if it fits any of the expected package types
103 | def checkWorkRequest(self,packet):
104 | if self.checkMainFields(packet) == False:
105 | return False
106 | if self.seemsPollingRequest(packet):
107 | return True
108 | if self.seemsConfirmation(packet):
109 | return True
110 | if self.seemsPollingChunk(packet):
111 | return True
112 | if self.seemsPollingDataPush(packet):
113 | return True
114 | return False
115 |
116 | # Method to check if the packet is valid reinitialization request
117 | def checkReinitialization(self, packet):
118 | if self.seemsReinitRequest(packet):
119 | return True
120 | return False
121 |
122 | # Method for generating a response to a Polling request
123 | def generatePollResponse(self,packet):
124 | p = Packet()
125 | p.session_id = self.sid
126 | self.seqnumber+=1
127 | p.seq_number = BitArray(uint=self.seqnumber,length=Header.SEQ_NUMBER)
128 | p.ack = packet.seq_number
129 | p.data_len = BitArray(bin='0'*Header.DATA_LEN)
130 | p.flags = BitArray(bin='0'*Header.FLAGS)
131 | p.content = BitArray()
132 | return p
133 |
134 | # Method for generating a session reinitialization response
135 | def generateReinitResponse(self,packet):
136 | p = Packet()
137 | p.session_id = self.sid
138 | self.seqnumber=1
139 | p.seq_number = BitArray(uint=self.seqnumber,length=Header.SEQ_NUMBER)
140 | p.ack = packet.seq_number
141 | p.data_len = BitArray(bin='0'*Header.DATA_LEN)
142 | p.flags = BitArray(bin='0'*Header.FLAGS)
143 | p.content = BitArray()
144 | return p
145 |
146 | # Method to generate a transfer packet (with data from the overlay)
147 | def generateTransferPacket(self,packt,content,push):
148 | p = Packet()
149 | p.session_id = self.sid
150 | self.seqnumber+=1
151 | p.seq_number = BitArray(uint=self.seqnumber,length=Header.SEQ_NUMBER)
152 | p.ack = packt.seq_number
153 | p.data_len = BitArray(uint=len(content),length=Header.DATA_LEN)
154 | if push:
155 | p.flags = BitArray(uint=Flags.PUSH,length=Header.FLAGS)
156 | else:
157 | p.flags = BitArray(bin='0'*Header.FLAGS)
158 | p.sync_type = BitArray()
159 | p.content = BitArray(bytes=content)
160 | return p
161 |
162 | # Method that updates the reference to the last packet sent and/or received
163 | def storePackets(self,packetrecv,packetsent):
164 | if packetsent is not None:
165 | self._LOGGING_ and self.logger.debug(f"[ServerWorker {self.id}] Storing Sent Packet {packetsent.seq_number}")
166 | self.lastPacketSent = packetsent
167 | if packetrecv is not None:
168 | self._LOGGING_ and self.logger.debug(f"[ServerWorker {self.id}] Storing Recv Packet {packetrecv.seq_number}")
169 | self.lastPacketRecv = packetrecv
170 |
171 | # Method of starting a data transfer
172 | def makeTransferPacket(self,packet):
173 | response = None
174 | chunk, push = self.bufOverlay.getChunk()
175 | transpacket = self.generateTransferPacket(packet,chunk,push)
176 | response = transpacket
177 | if push and not self.bufOverlay.anyIndex():
178 | self._LOGGING_ and self.logger.debug(f"[ServerWorker {self.id}] makeTransferPacket with PUSH")
179 | return response
180 |
181 | # Method that extracts data (if any) from a packet received from the client (by full-duplex).
182 | def extractIncomingData(self,packet):
183 | if any(packet.data_len) == False or any(packet.content) == False:
184 | return
185 | self.bufWrapper.addChunk(packet)
186 | return
187 |
188 | # Method that manages Polling Requests, Confirmations and Data Transfer packets
189 | # Data transfers can be full duplex
190 | def doWork(self,packet,wsrvinbox):
191 | response = None
192 | packettosend = None
193 | if packet.anyContentAvailable():
194 | self.extractIncomingData(packet)
195 | if packet.isFlagActive(Flags.PUSH):
196 | data_decrypt = self.decryptWrapperData()
197 | self.overlay.inbox.put(Message("serverworker",self.id,'overlay',0,MessageType.STREAM,data_decrypt))
198 | if self.someOverlayData():
199 | packettosend = self.makeTransferPacket(packet)
200 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
201 | else:
202 | packettosend = self.generatePollResponse(packet)
203 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
204 | else:
205 | if self.someOverlayData():
206 | packettosend = self.makeTransferPacket(packet)
207 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
208 | else:
209 | packettosend = self.generatePollResponse(packet)
210 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
211 | else:
212 | if self.someOverlayData():
213 | packettosend = self.makeTransferPacket(packet)
214 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
215 | else:
216 | packettosend = self.generatePollResponse(packet)
217 | response = Message("serverworker",self.id,"router",0,MessageType.STREAM,packettosend.toBytes(),wsrvinbox)
218 | self.storePackets(packet,packettosend)
219 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Header Sent: {response.printHeader()}")
220 | return response
221 |
222 | # Method that responds to a client termination request
223 | def doTermination(self,packet,wsrvinbox):
224 | self._LOGGING_ and self.logger.debug(f"[ServerWorker {self.id}] initializing Termination process")
225 | pollpacket = self.generatePollResponse(packet)
226 | self.st = Status.TERMINATING
227 | self.storePackets(packet,pollpacket)
228 | self.overlay.inbox.put(Message("serverworker",self.id,'overlay',0,MessageType.SIGNAL,SignalType.COMMS_FINISHED))
229 | return Message("serverworker",self.id,"router",0,MessageType.STREAM,pollpacket.toBytes(),wsrvinbox)
230 |
231 | # Method that performs the session reinitialization process
232 | def doReinitialization(self, packet):
233 | reinitpacket = self.generateReinitResponse(packet)
234 | self.storePackets(packet, reinitpacket)
235 | return reinitpacket.toBytes()
236 |
237 | # Trigger method that performs the checks associated with each function and then invokes it
238 | # by returning a response.
239 | def initialChecks(self, msg, checkerFunc, nextFunc):
240 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Header Recv: {msg.printHeader()}")
241 | p = self.transformToPacket(msg.content)
242 | if p is None:
243 | self._LOGGING_ and self.logger.error(f"[ServerWorker {self.id}] cannot convert data to sotp packet, re-sending...")
244 | return Message("serverworker",self.id,"router",0,MessageType.STREAM,self.lostPacket().toBytes(),msg.wrapServerQ)
245 | if self.checkReinitialization(p):
246 | return Message("serverworker",self.id,"router",0,MessageType.STREAM,self.doReinitialization(p),msg.wrapServerQ)
247 | if self.checkTermination(p):
248 | self._LOGGING_ and self.logger.info(f"[ServerWorker {self.id}] termination packet detection")
249 | return self.doTermination(p,msg.wrapServerQ)
250 | if checkerFunc(p) is False:
251 | self._LOGGING_ and self.logger.error(f"[ServerWorker {self.id}] {checkerFunc} has failed, re-sending...")
252 | return Message("serverworker",self.id,"router",0,MessageType.STREAM,self.lostPacket().toBytes(),msg.wrapServerQ)
253 | if self.checkConfirmation(p) is False:
254 | self._LOGGING_ and self.logger.error(f"[ServerWorker {self.id}] cannot confirm our last sent packet, re-sending...")
255 | return Message("serverworker",self.id,"router",0,MessageType.STREAM,self.lostPacket().toBytes(),msg.wrapServerQ)
256 | return nextFunc(p,msg.wrapServerQ)
257 |
258 | # Method that processes the data from the overlay and stores it in its buffer.
259 | def overlayProcessing(self, data):
260 | self._LOGGING_ and self.logger.debug(f"[DataThread] {data.sender} sent {len(data.content)} bytes of data, storing...")
261 | self.storeOverlayContent(data.content)
262 |
263 | # A method (running on a thread) that receives the data from the overlay, encrypts it,
264 | # chunks it and saves it parallel to the mistica server thread.
265 | def dataEntry(self):
266 | while True:
267 | data = self.datainbox.get()
268 | if data.isTerminateMessage():
269 | break
270 | self.overlayProcessing(data)
271 | self._LOGGING_ and self.logger.debug(f"[DataThread] Terminated")
272 |
273 | # Handler for STREAM (data) type messages
274 | def handleStream(self, msg):
275 | if self.st == Status.WORKING:
276 | self.outbox.put(self.initialChecks(msg, self.checkWorkRequest, self.doWork))
277 | elif self.st == Status.TERMINATING:
278 | self.overlay.inbox.put(Message("serverworker",self.id,'overlay', self.overlay.id, MessageType.SIGNAL,SignalType.COMMS_FINISHED))
279 |
280 | # Handler for SIGNAL type messages.
281 | def handleSignal(self, msg):
282 | if msg.isTerminateMessage():
283 | self.datainbox.put(Message("serverworker", self.id, "datathread", 0, MessageType.SIGNAL, SignalType.TERMINATE))
284 | self.exit = True
285 |
286 | # Entry point of the associated Worker when creating a new session with a client.
287 | def run(self):
288 | self._LOGGING_ and self.logger.info(f"[ServerWorker {self.id}] associated with {self.overlay.name} started!")
289 | dataThread = Thread(target=self.dataEntry)
290 | dataThread.start()
291 | while (not self.exit):
292 | msg = self.inbox.get()
293 | if (msg.isSignalMessage()):
294 | self.handleSignal(msg)
295 | continue
296 | self.handleStream(msg)
297 | self._LOGGING_ and self.logger.debug(f"[ServerWorker {self.id}] Terminated")
298 |
--------------------------------------------------------------------------------
/sotp/misticathread.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2020 Carlos Fernández Sánchez and Raúl Caro Teixidó.
3 | #
4 | # This file is part of Mística
5 | # (see https://github.com/IncideDigital/Mistica).
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | from threading import Thread
21 | from queue import Queue
22 | from utils.messaging import Message, MessageType, SignalType
23 | from argparse import ArgumentParser
24 |
25 | class MisticaMode:
26 | SINGLE = 0
27 | MULTI = 1
28 |
29 |
30 | class MisticaThread(Thread):
31 | def __init__(self, name, logger):
32 | Thread.__init__(self)
33 | self.name = name
34 | self.inbox = Queue()
35 | self.exit = False
36 | # Logger parameters
37 | self.logger = logger
38 | self._LOGGING_ = False if logger is None else True
39 |
40 | def generateArgParser(self):
41 | config = self.CONFIG
42 |
43 | parser = ArgumentParser(prog=config["prog"],description=config["description"])
44 | for arg in config["args"]:
45 | for name,field in arg.items():
46 | opts = {}
47 | for key,value in field.items():
48 | opts[key] = value
49 | parser.add_argument(name, **opts)
50 | return parser
51 |
52 | def handleMessage(self, msg):
53 | answer = None
54 | if (msg.isSignalMessage()):
55 | answer = self.handleSignal(msg)
56 | elif (msg.isStreamMessage()):
57 | answer = self.handleStream(msg)
58 | else:
59 | raise Exception('Invalid Message')
60 | return answer
61 |
62 | # OVERRIDE ME
63 | def handleSignal(self, msg):
64 | pass
65 |
66 | # OVERRIDE ME
67 | def handleStream(self, msg):
68 | pass
69 |
70 | # OVERRIDE ME
71 | def processAnswer(self, msg):
72 | pass
73 |
74 | def run(self):
75 | while True:
76 | try:
77 | message = self.inbox.get()
78 | answer = self.handleMessage(message) # Answer can be None
79 | self.processAnswer(answer)
80 | if self.exit:
81 | self._LOGGING_ and self.logger.debug(f"[{self.name}] MisticaThread detect Exit Flag.")
82 | break
83 | except Exception as e:
84 | self._LOGGING_ and self.logger.exception(f"[{self.name}] MisticaThread Exception: {e}")
85 | self._LOGGING_ and self.logger.debug(f"[{self.name}] Terminated")
86 |
87 |
88 | class ClientOverlay(MisticaThread):
89 |
90 | def __init__(self, name, qsotp, qdata, args, logger):
91 | MisticaThread.__init__(self, name, logger)
92 | self.qsotp = qsotp
93 | self.qdata = qdata
94 | self.hasInput = False
95 | # Generate argparse and parse args
96 | self.argparser = self.generateArgParser()
97 | self.args = self.argparser.parse_args(args.split())
98 | self.parseArguments(self.args)
99 | # Get tag, by default, from args
100 | self.tag = self.args.tag[0]
101 | # Logger parameters
102 | self.logger = logger
103 | self._LOGGING_ = False if logger is None else True
104 | self.initCommunication()
105 |
106 | # OVERRIDE ME
107 | def parseArguments(self, args):
108 | pass
109 |
110 | # First of all, Overlay send a Signal Start Msg to Sotp.
111 | def initCommunication(self):
112 | m = Message(self.name, 0, "clientworker", 0, MessageType.SIGNAL, SignalType.START)
113 | self.qsotp.put(m)
114 |
115 | def handleSignal(self, msg):
116 | if msg.isTerminateMessage() or msg.isCommunicationEndedMessage() or msg.isCommunicationBrokenMessage():
117 | self.exit = True
118 | self.qsotp.put(Message(self.name, 0, "clientworker", 0, MessageType.SIGNAL, SignalType.TERMINATE))
119 | pass
120 |
121 | def handleStream(self, msg):
122 | answer = None
123 | if (msg.sender == "input"):
124 | answer = self.handleInputStream(msg)
125 | elif (msg.sender == "clientworker"):
126 | answer = self.handleSOTPStream(msg)
127 | return answer
128 |
129 | def handleInputStream(self, msg):
130 | content = self.processInputStream(msg.content)
131 | if content is None:
132 | return None
133 | return Message(self.name, 0, "datathread", 0, MessageType.STREAM, content)
134 |
135 | def handleSOTPStream(self, msg):
136 | content = self.processSOTPStream(msg.content)
137 | if content is None:
138 | return None
139 | return Message(self.name, 0, "datathread", 0, MessageType.STREAM, content)
140 |
141 | # Route answer to sotp or datathread queue.
142 | def processAnswer(self, answer):
143 | if answer is not None:
144 | if answer.receiver == "clientworker":
145 | self.qsotp.put(answer)
146 | elif answer.receiver == "datathread":
147 | self.qdata.put(answer)
148 | pass
149 |
150 | # OVERRIDE ME
151 | def processInputStream(self, content):
152 | pass
153 |
154 | # OVERRIDE ME
155 | def processSOTPStream(self, content):
156 | pass
157 |
158 |
159 | class ClientWrapper(MisticaThread):
160 |
161 | def __init__(self, name, qsotp, logger):
162 | MisticaThread.__init__(self,name, logger)
163 | self.qsotp = qsotp
164 | self.exit = False
165 | # Logger parameters
166 | self.logger = logger
167 | self._LOGGING_ = False if logger is None else True
168 | # Generate argparse
169 | self.argparser = self.generateArgParser()
170 |
171 | def handleSignal(self, msg):
172 | if msg.isTerminateMessage():
173 | self.exit = True
174 | pass
175 |
176 | def handleStream(self, msg):
177 | try:
178 | if (msg.sender == self.name):
179 | return self.unwrap(msg.content)
180 | elif (msg.sender == "clientworker"):
181 | self.wrap(msg.content)
182 | except Exception as e:
183 | m = Message(self.name,0,"clientworker",0,MessageType.SIGNAL,SignalType.COMMS_BROKEN)
184 | self._LOGGING_ and self.logger.exception(f"[{self.name}] Exception at handleStream: {e}")
185 | self.qsotp.put(m)
186 |
187 | # Route answer to sotp queue.
188 | def processAnswer(self, answer):
189 | if answer is not None:
190 | m = self.messageToSOTP(answer)
191 | self.qsotp.put(m)
192 |
193 | def messageToSOTP(self, content):
194 | return Message(self.name, 0, "clientworker", 0, MessageType.STREAM, content)
195 |
196 | def messageToWrapper(self, content):
197 | return Message(self.name, 0, "wrapper", 0, MessageType.STREAM, content)
198 |
199 | # OVERRIDE ME
200 | def wrap(self, content):
201 | pass
202 |
203 | # OVERRIDE ME
204 | def unwrap(self, content):
205 | pass
206 |
207 |
208 | class ServerOverlay(MisticaThread):
209 |
210 | def __init__(self, name, id, qsotp, mode, args, logger):
211 | MisticaThread.__init__(self, name, logger)
212 | self.workers = []
213 | self.id = id
214 | self.qsotp = qsotp
215 | self.mode = mode
216 | # Generate argparse and parse args
217 | self.argparser = self.generateArgParser()
218 | self.args = self.argparser.parse_args(args.split())
219 | self.parseArguments(self.args)
220 | # Get tag
221 | self.tag = self.args.tag[0]
222 | # Logger parameters
223 | self.logger = logger
224 | self._LOGGING_ = False if logger is None else True
225 |
226 | # override me!
227 | def parseArguments(self, args):
228 | pass
229 |
230 | def handleSignal(self, msg):
231 | answer = None
232 | if (msg.sender == "input"):
233 | answer = self.handleInputSignal(msg)
234 | elif (msg.sender == "serverworker" or msg.sender == "router"):
235 | answer = self.handleSOTPSignal(msg)
236 | return answer
237 |
238 | def handleInputSignal(self, msg):
239 |
240 | if msg.isTerminateMessage():
241 | if self.mode == MisticaMode.SINGLE:
242 | return Message(self.name, self.id, "router", 0,
243 | MessageType.SIGNAL, SignalType.TERMINATE)
244 | else:
245 | return self.handleInterrupt()
246 |
247 | # override this function to alter the behavior on multihandler mode
248 | def handleInterrupt(self):
249 | print("Interrupt ignored.")
250 | return None
251 |
252 | def handleSOTPSignal(self, msg):
253 | answer = None
254 | if msg.isTerminateMessage():
255 | self.exit = True
256 | elif msg.isCommsFinishedMessage() or msg.isCommsBrokenMessage():
257 | if self.mode == MisticaMode.SINGLE:
258 | answer = Message(self.name, self.id, "router", 0,
259 | MessageType.SIGNAL, SignalType.TERMINATE)
260 | else:
261 | self.removeWorker(msg.sender_id) # crashed worker
262 | return answer
263 |
264 | def handleStream(self, msg):
265 | answer = None
266 | if (msg.sender == "input"):
267 | answer = self.handleInputStream(msg)
268 | elif (msg.sender == "serverworker"):
269 | answer = self.handleSOTPStream(msg)
270 | return answer
271 |
272 | def handleInputStream(self, msg):
273 | content = self.processInputStream(msg.content)
274 | # By default, only one worker. Must be overriden for more
275 | # In a multi-worker scenario inputs must be mapped to workers
276 | return self.streamToSOTPWorker(content, self.workers[0].id)
277 |
278 | def handleSOTPStream(self, msg):
279 | for worker in self.workers:
280 | if msg.sender_id == worker.id:
281 | break
282 | else:
283 | return None
284 | content = self.processSOTPStream(msg.content)
285 | if content is None:
286 | return None
287 | return self.streamToSOTPWorker(content, msg.sender_id)
288 |
289 | def processAnswer(self, answer):
290 | if answer is None:
291 | return
292 | elif answer.receiver == "serverworker":
293 | for worker in self.workers:
294 | if answer.receiver_id == worker.id:
295 | worker.inbox.put(answer)
296 | return
297 | elif answer.receiver == "datathread":
298 | for worker in self.workers:
299 | if answer.receiver_id == worker.id:
300 | worker.datainbox.put(answer)
301 | return
302 | elif answer.receiver == "router":
303 | self.qsotp.put(answer)
304 |
305 | # override me
306 | def processInputStream(self, content):
307 | pass
308 |
309 | # override me
310 | def processSOTPStream(self, content):
311 | pass
312 |
313 | def addWorker(self, worker):
314 | # By default only one worker can be added.
315 | # For multi-worker scenarios (e.g. RAT console), this method must be overrriden
316 | if not self.workers: # empty
317 | self.workers.append(worker)
318 | else:
319 | raise(f"Cannot Register worker on overlay module. Module {self.name} only accepts one worker")
320 |
321 | def removeWorker(self, id):
322 | for worker in self.workers:
323 | if id == worker.id:
324 | self.workers.remove(id)
325 | break
326 |
327 | def streamToSOTPWorker(self, content, workerid):
328 | return Message(self.name, self.id, "datathread", workerid, MessageType.STREAM, content)
329 |
330 | def signalToSOTPWorker(self, content, workerid):
331 | return Message(self.name, self.id, "serverworker", workerid, MessageType.SIGNAL, content)
332 |
333 |
334 | class ServerWrapper(MisticaThread):
335 |
336 | def __init__(self, id, name, qsotp, servername, args, logger):
337 | MisticaThread.__init__(self, name, logger)
338 | self.id = id
339 | self.servername = servername
340 | self.qsotp = qsotp
341 | # Logger parameters
342 | self.logger = logger
343 | self._LOGGING_ = False if logger is None else True
344 | # Generate argparse
345 | self.argparser = self.generateArgParser()
346 | self.parseArguments(args)
347 |
348 | def handleSignal(self, msg):
349 | answer = None
350 | if (msg.sender == self.servername):
351 | answer = self.handleServerSignal(msg)
352 | elif (msg.sender == "router"):
353 | answer = self.handleSOTPSignal(msg)
354 | return answer
355 |
356 | def handleSOTPSignal(self, msg):
357 | if msg.isTerminateMessage():
358 | self.exit = True
359 | return None
360 |
361 | def handleServerSignal(self, msg):
362 | # TODO
363 | pass
364 |
365 | def handleStream(self, msg):
366 | answer = None
367 | if (msg.sender == self.servername):
368 | answer = self.messageToRouter(self.unwrap(msg.content), msg.wrapServerQ)
369 | elif (msg.sender == "serverworker" or msg.sender == "router"):
370 | answer = self.messageToWrapServer(self.wrap(msg.content), msg.wrapServerQ)
371 | return answer
372 |
373 | # OVERRIDE ME
374 | def wrap(self, content):
375 | pass
376 |
377 | # OVERRIDE ME
378 | def unwrap(self, content):
379 | pass
380 |
381 | def processAnswer(self, answer):
382 | if answer is None:
383 | return
384 | if answer.receiver == "router":
385 | self.qsotp.put(answer)
386 | else: # For a wrap server
387 | answer.wrapServerQ.put(answer)
388 |
389 | def messageToRouter(self, content, wrapServerQ):
390 | if content is None:
391 | return None
392 | else:
393 | return Message(self.name, self.id, "router", 0, MessageType.STREAM, content, wrapServerQ)
394 |
395 | def messageToWrapServer(self, content, wrapServerQ):
396 | if content is None:
397 | return None
398 | else:
399 | return Message(self.name, self.id, self.servername, 0, MessageType.STREAM, content, wrapServerQ)
400 |
--------------------------------------------------------------------------------