├── 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 Timeout

408 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 | --------------------------------------------------------------------------------