diff --git a/TODO b/TODO new file mode 100644 index 0000000..ae50ed4 --- /dev/null +++ b/TODO @@ -0,0 +1,7 @@ +TODO + - 00 80 at beginning of every packet - what is this? + - writing random data to dev generates OSError - handle this + +FEATURES + - implement configurable packet aggregation + - if possible and useful: helper to replace ethernet diff --git a/tunnel/spam/.gitignore b/tunnel/spam/.gitignore new file mode 100644 index 0000000..860ad38 --- /dev/null +++ b/tunnel/spam/.gitignore @@ -0,0 +1 @@ +conf.py diff --git a/tunnel/spam/conf.sample.py b/tunnel/spam/conf.sample.py new file mode 100755 index 0000000..ca81689 --- /dev/null +++ b/tunnel/spam/conf.sample.py @@ -0,0 +1,80 @@ +import os + +ENCRYPTION_NONE, ENCRYPTION_STARTTLS, ENCRYPTION_SSL = range(3) + +Conf = { + # ======== network settings ======== + # select the kind of tunnel + # True: tap-device, tunneling ethernet + # False: tun-device, tunneling ip + 'tunnelEthernet': True, + + # ipsettings for the device + 'network': + { + 'address': '10.10.10.3', + 'netmask': '255.255.255.0', + #gateway: '', + 'mtu': 1400, + }, + + # ======== mail settings ======== + # mail adress to which all packets are sent + 'mailTo': '', + + # mail address to send mail from (only for the mailheader / display) + 'mailFrom': '', + + # extra address to handle broadcast packets + # set to None if you want broadcasts also handled by mailTo addr + # ATM this is not implemented + 'broadcastTo': None, + + # smtp data for outgoing packets + 'smtp': + { + 'server': '', + 'crypto': ENCRYPTION_SSL, + 'authentication': True, + 'user': '', + 'password': '', + }, + + # smtpd - a small mailserver to handle incoming packets + 'smtpd': + { + 'enabled': False, + 'listen': ('0.0.0.0', 25), + }, + + # imap - get incoming data from an imap server + 'imap': + { + 'enabled': False, + 'server': '', + 'crypto': ENCRYPTION_SSL, + 'authentication': True, + 'user': '', + 'password': '', + 'folder': 'INBOX', + 'deleteAfterwards': True, + # intervals to wait when fetching mail fails + # afterwards the fetcher will go into IMAPv4 IDLE mode + 'mailWait': [0.25, 0.5, 0.75], + 'useIDLE': True + }, + + # mail handler configuration + # this is the part which handles incoming mail delivered by imap or smtpd + 'handler': + { + # list of all allowed senders, None for everyone + # e.g. ["foo@somedomain.de", "bar@someserver.de"] + 'allowFrom': None, + + # list of all allowed recipients, None for everyone + # e.g. ["foo@somedomain.de", "bar@someserver.de"] + 'allowTo': None, + }, +} + diff --git a/tunnel/spam/spam.py b/tunnel/spam/spam.py new file mode 100755 index 0000000..76342e9 --- /dev/null +++ b/tunnel/spam/spam.py @@ -0,0 +1,234 @@ +#!/usr/bin/python + +import time +import asyncore +import select +import smtpd +import smtplib +import imaplib +import email +from email.mime.text import MIMEText +import threading +import sys +sys.path.append("../../../") + +from wtbase import SpamGenerator, DecodingException +from ether2any import Ether2Any +from ether2any.helper import getDstMacFromPkt, isBroadcast, binToHexStr +from conf import Conf, ENCRYPTION_NONE, ENCRYPTION_STARTTLS, ENCRYPTION_SSL + +# Todo +# Error checking at some places +# Check for closed imap/smtp connections + +class NetMailHandler(): + devWriteMutex = threading.Lock() + """ Parse, decode and write incoming mails to output. """ + def __init__(self, dev, allowFrom, allowTo): + self.dev = dev + self.allowFrom = allowFrom + self.allowTo = allowTo + self.generator = SpamGenerator() + + def receiveMail(self, mailfrom, mailto, data): + # FIXME: mailfrom can be None + mail = email.email.message_from_string(data) + + # try to harvest text/plain part of mail + if mail.get_content_type() == "text/plain": + self._handleText(mail.get_payload()) + elif mail.get_content_type() == "multipart/alternative": + for msg in mail.get_payload(): + if msg.get_content_type() == "text/plain": + self.handleText(msg.get_payload()) + elif self.allowFrom == None and self.allowTo == None: + self._handleText(data) + else: + pass + + def _handleText(self, text): + data = None + # FIXME: Where do these "\n " or 0a 20 come from? + # Seem to occure only when smtplib sends (long) mails to smtpd + text = text.replace("\n ", "") + try: + data = self.generator.decode(text) + except DecodingException, e: + print "Error: Could not decode text! See error below" + print " < ----------- 8< ----------- > " + print e + print " < ----------- 8< ----------- > " + if data: + self.devWriteMutex.acquire() + self.dev.write(data) + self.devWriteMutex.release() + +class SimpleSMTPServer(smtpd.SMTPServer): + """ Simple small SMTP Server, gives mails to a handler. """ + def __init__(self, handler, *args, **kwargs): + smtpd.SMTPServer.__init__(self, *args, **kwargs) + self._handler = handler + def process_message(self, peer, mailfrom, mailto, data): + # give mail to handler + self._handler.receiveMail(mailfrom, mailto, data) + +class SMTPServerThread(threading.Thread): + def __init__(self, listen, handler): + threading.Thread.__init__(self) + self.server = SimpleSMTPServer(handler, listen, None) + + def run(self): + asyncore.loop() + +class SimpleIMAPClient(threading.Thread): + def __init__(self, imapConf, mailTo, handler): + threading.Thread.__init__(self) + self.imapConf = imapConf + self.imap = None + self.quit = False + self.mailTo = mailTo + self.handler = handler + + self.idleTagNum = 0 + + def connect(self): + if self.imapConf['crypto'] == ENCRYPTION_SSL: + self.imap = imaplib.IMAP4_SSL(self.imapConf['server'], 993) + else: + self.smtp = imaplib.IMAP4(self.imapConf['server']) + + if self.imapConf['crypto'] == ENCRYPTION_STARTTLS: + self.imap.starttls() + + if self.imapConf['authentication']: + self.imap.login(self.imapConf['user'], self.imapConf['password']) + + ret = self.imap.select(self.imapConf['folder']) + if ret[0] != 'OK': + print "Error!" + + def fetchNewMailToDev(self): + l = self.imap.search(None, 'UNSEEN') + newMsgIds = l[1][0].replace(" ", ",") + if newMsgIds == '': + return False + msgs = self.imap.fetch(newMsgIds, '(RFC822)') + for msg in msgs[1]: + if msg == ")": + # where does this come from...? + continue + if len(msg) != 2: + print "Warning: Message broken, %d values in list, text '%s'" % (len(msg), msg) + continue + (flags, data) = msg + self.handler.receiveMail(None, self.mailTo, data) + return (len(msgs) > 0) + + def run(self): + self.connect() + + while not self.quit: + print "New IMAP loop" + tries = 0 + while tries < len(self.imapConf['mailWait']): + # get new mail + if self.fetchNewMailToDev(): + tries = 0 + else: + time.sleep(self.imapConf['mailWait'][tries]) + tries += 1 + # go into idle mode + # NOT IMPLEMENTED + if self.imapConf['useIDLE']: + print "Going into IDLE mode..." + self.idleTagNum += 1 + idleTag = "a%04d" % self.idleTagNum + self.imap.send("%s IDLE\r\n" % (idleTag,)) + quitLoop = False + while not quitLoop: + (r, w, e) = select.select([self.imap.socket()], [], []) + msg = self.imap.readline() + # TODO: Check if this filters out all idle "no new message" status msgs + if not msg.startswith("+") and not msg.startswith("* OK"): + #if msg.find("RECENT") >= 0 + quitLoop = True + self.imap.send("DONE\r\n") + # clear away ack msg + msg = self.imap.readline() + while msg.find(idleTag) < 0: + msg = self.imap.readline() + +class MailTunnel(Ether2Any): + def __init__(self): + Ether2Any.__init__(self, tap=Conf.get('tunnelEthernet', True)) + + handlerConf = Conf.get('handler', {'allowFrom': None, 'allowTo': None}) + self.mailHandler = NetMailHandler(self.dev, **handlerConf) + self.mailTo = Conf.get('mailTo', None) + self.mailFrom = Conf.get('mailFrom', None) + + self.smtpConf = Conf.get('smtp') + smtpd = Conf.get("smtpd", {'enabled': False}) + if smtpd['enabled']: + self.smtpd = SMTPServerThread(smtpd['listen'], self.mailHandler) + else: + self.smtpd = None + + imapConf = Conf.get("imap", {'enabled': False}) + if imapConf['enabled']: + self.imap = SimpleIMAPClient(imapConf, self.mailTo, self.mailHandler) + else: + self.imap = None + + self.generator = SpamGenerator() + + network = Conf.get('network', {'mtu': 1400}) + self.dev.ifconfig(**network) + + def connectSMTP(self): + if self.smtpConf['crypto'] == ENCRYPTION_SSL: + self.smtp = smtplib.SMTP_SSL(self.smtpConf['server'], 465) + else: + self.smtp = smtplib.SMTP(self.smtpConf['server']) + + if self.smtpConf['crypto'] == ENCRYPTION_STARTTLS: + self.smtp.starttls() + + if self.smtpConf['authentication']: + self.smtp.login(self.smtpConf['user'], self.smtpConf['password']) + + def sendMail(self, fromAddr, toAddrs, subject, msg): + e = MIMEText(msg) + e['Subject'] = subject + e['From'] = fromAddr + e['To'] = ",\n".join(toAddrs) + try: + self.smtp.sendmail(fromAddr, toAddrs, e.as_string()) + except smtplib.SMTPServerDisconnected: + self.connectSMTP() + self.smtp.sendmail(fromAddr, toAddrs, e.as_string()) + + def sendToNet(self, packet): + data = self.generator.encode(packet) + self.sendMail(self.mailFrom, [self.mailTo], "Ohai!", data) + + def sendToDev(self, socket): + pass + + def run(self): + # start threads / connections + self.connectSMTP() + + if self.imap: + self.imap.start() + + if self.smtpd: + self.smtpd.start() + + # call super method + Ether2Any.run(self) + +if __name__ == '__main__': + mailtun = MailTunnel() + mailtun.run() + diff --git a/tunnel/spam/wtbase.py b/tunnel/spam/wtbase.py new file mode 100755 index 0000000..6d2aa5e --- /dev/null +++ b/tunnel/spam/wtbase.py @@ -0,0 +1,441 @@ +#!/usr/bin/python + +""" wtbase - what the base +Provides classes to encode text to arbitrary bases (base 2^n supported) and +then to textual forms. """ + +import math +import random + +class DecodingException(Exception): + pass + +class Token(): + """ A Token contains a word/sentence and a list of tokenlists which can follow the word. """ + def __init__(self, word, nextLists): + self.word = word + self.nextLists = nextLists + + def __str__(self): + return "Token: word \"%s\"" % self.word + +class TextGenerator(): + """ Basis generator to en- and decode text-bits. """ + def __init__(self, base=16): + self.lists = {} + self.base = base + self.baseBit = math.log(base, 2) + if self.baseBit != int(self.baseBit): + raise ValueError("base must be a power of 2") + self.baseBit = int(self.baseBit) + self.startList = "initial" + + def addList(self, identifier, newList): + self.lists[identifier] = newList + + def getList(self, identifier): + return self.lists.get(identifier, None) + + def convToBits(self, data): + """ Converts a data string into n-bit parts defined by self.base. + + Returns a list of integers. """ + l = [] + bit = 0 + rest = 0 + for c in data: + n = ord(c) + if bit != 0: + nc = rest | ((n & ((1 << bit) - 1 << 8-bit)) >> 8-bit) + l.append(nc) + while 8-bit >= self.baseBit: + nc = (n & (((1 << self.baseBit)-1) << 8-bit-self.baseBit)) >> (8-bit-self.baseBit) + l.append(nc) + bit += self.baseBit + rest = (n & (1 << 8-bit) - 1) << self.baseBit-(8-bit) + bit = (bit+self.baseBit) % 8 % self.baseBit + if bit != 0: + l.append(rest) + return l + + def convToNums(self, data): + """ Reassemble a list of bits back to the original data bytestring. """ + l = "" + w = 0 + rest = 0 + bit = 0 + for n in data: + if bit+self.baseBit >= 8: + w |= n >> self.baseBit - (8-bit) + bit = self.baseBit - (8-bit) + l += chr(w) + if bit != 0: + w = (n & (1 << bit) - 1) << 8 - bit + else: + w = 0 + else: + w |= n << 8-bit-self.baseBit + bit = (bit + self.baseBit) % 8 + if bit == 0: + l += chr(w) + w = 0 + return l + + #def convTo4Bits(self, data): + # l = [] + # for c in data: + # n = ord(c) + # lo = n & ((1 << 4)-1) + # hi = (n & (((1 << 4)-1) << 4)) >> 4 + # l.extend([hi, lo]) + # return l + + +class SpamGenerator(TextGenerator): + """ De- and encode data in base8 spam text. """ + def __init__(self): + TextGenerator.__init__(self, base=8) + self.startList = "greeting" + self.addList("greeting", + { + 0: Token("Hi,\n\n", ["start"]), + 1: Token("Hey,\n\n", ["start"]), + 2: Token("Greetings,\n\n", ["start"]), + 3: Token("Dear Mr. or Mrs.,,\n\n", ["start"]), + 4: Token("SPECIAL OFFER!\n", ["start"]), + 5: Token("High Quality! Read on!\n", ["start"]), + 6: Token("Best buy!\n\n", ["start"]), + 7: Token("Dear Valued Customer,\n\n", ["start"]), + 8: Token("Well, uhm, ", ["start"]), + }) + self.addList("start", + { + 0: Token("we are happy to ", ["inform_them"]), + 1: Token("we are glad to ", ["inform_them"]), + 2: Token("we gladly ", ["inform_them"]), + 3: Token("it happens that we can ",["inform_them"]), + 4: Token("we want to ", ["inform_them"]), + 5: Token("today ", ["you_have"]), + 6: Token("ITS TRUE! ", ["you_have"]), + 7: Token("you won! ", ["you_have"]), + 8: Token("awesome for you, buddy! ",["leaving"]), + }) + self.addList("inform_them", + { + 0: Token("inform you, that ", ["you_have"]), + 1: Token("make a remarkt, that ", ["you_have"]), + 2: Token("announce, that ", ["you_have"]), + 3: Token("celebrate with you! ", ["you_have"]), + 4: Token("congratulate you, because ", ["you_have"]), + 5: Token("take the extra step: ", ["you_have"]), + 6: Token("tell you, that ", ["you_have"]), + 7: Token("don't forget about you, ", ["you_have"]), + 8: Token("move property! ", ["you_have"]), + }) + + self.addList("you_have", + { + 0: Token("you won ", ["won_item"]), + 1: Token("you have won ", ["won_item"]), + 2: Token("you aqcuired ", ["won_item"]), + 3: Token("one time offer only: ", ["won_item"]), + 4: Token("at your account we found ", ["won_item"]), + 5: Token("the prince of nigeria offers to you ", ["won_item"]), + 6: Token("off shore accounts brought you ", ["won_item"]), + 7: Token("insider traging brought you ", ["won_item"]), + 8: Token("you managed to get", ["won_item"]), + }) + + self.addList("won_item", + { + 0: Token("a sum of ", ["money_sum"]), + 1: Token("the priceless diamond of Zalanda. " , ["claim"]), + 2: Token("free viagra! ", ["claim"]), + 3: Token("an inheritance of ", ["money_sum"]), + 4: Token("the opportunity to make money online! ", ["claim"]), + 5: Token("a part of an oil pipe line, worth ", ["money_sum"]), + 6: Token("free money - ", ["money_sum"]), + 7: Token("a rare antique item worth", ["money_sum"]), + 8: Token("quiet a bit o' stuff. ", ["claim"]), + }) + + + self.addList("money_sum", + { + 0: Token( "5,000,000 USD. ", ["claim"]), + 1: Token("10,000,000 USD. ", ["claim"]), + 2: Token( "300,000 USD. ", ["claim"]), + 3: Token("13,412,573 USD. ", ["claim"]), + 4: Token( "7,555,530 USD. ", ["claim"]), + 5: Token( "50,000 USD. ", ["claim"]), + 6: Token( "4,500,000 USD. ", ["claim"]), + 7: Token("42,000,000 USD. ", ["claim"]), + 8: Token("87,000,000 USD. ", ["claim"]), + }) + + self.addList("claim", + { + 0: Token("To claim ", ["claimable_item"]), + 1: Token("To get hold ", ["claimable_item"]), + 2: Token("To acquire ", ["claimable_item"]), + 3: Token("To receive ", ["claimable_item"]), + 4: Token("To obtain ", ["claimable_item"]), + 5: Token("To gatherh ", ["claimable_item"]), + 6: Token("To take ownership ", ["claimable_item"]), + 7: Token("To collect ", ["claimable_item"]), + 8: Token("To finally get ", ["claimable_item"]), + }) + + self.addList("claimable_item", + { + 0: Token("this item, please send ", ["sendables"]), + 1: Token("this stuff, please send ", ["sendables"]), + 2: Token("your profit, please send ", ["sendables"]), + 3: Token("these assets, please send ", ["sendables"]), + 4: Token("this price, please send ", ["sendables"]), + 5: Token("your earnings, please send ", ["sendables"]), + 6: Token("this top-line profit, please send ", ["sendables"]), + 7: Token("this treasure, please send ", ["sendables"]), + 8: Token("this your winnings, please send ", ["sendables"]), + }) + + self.addList("sendables", + { + 0: Token("us all your information.\n\n", ["more_stuff", "jibberjabber_start"]), + 1: Token("us your account data.\n\n", ["more_stuff", "jibberjabber_start"]), + 2: Token("us a transfer-free of 50 USD.\n\n", ["more_stuff", "jibberjabber_start"]), + 3: Token("us a list of your passwords.\n\n", ["more_stuff", "jibberjabber_start"]), + 4: Token("10 valid TAN Numbers.\n\n", ["more_stuff", "jibberjabber_start"]), + 5: Token("us your mothers maiden name.\n\n", ["more_stuff", "jibberjabber_start"]), + 6: Token("your birth certificate.\n\n", ["more_stuff", "jibberjabber_start"]), + 7: Token("a listing of your incomes.\n\n", ["more_stuff", "jibberjabber_start"]), + 8: Token("us your personal information.\n\n", ["jibberjabber_start", "leaving"]), + }) + + self.addList("more_stuff", + { + 0: Token("But wait, there is more! ", ["you_have"]), + 1: Token("But that is not all! ", ["you_have"]), + 2: Token("And there is even more! ", ["you_have"]), + 3: Token("Also ", ["you_have"]), + 4: Token("And because you seem to be the luckiest person alive: ", ["you_have"]), + 5: Token("And how does this sound: ", ["you_have"]), + 6: Token("In addition ", ["you_have"]), + 7: Token("But... what is this? ", ["you_have"]), + 8: Token("AND! ", ["you_have"]), + }) + + # loop this. random conversation starter + self.addList("jibberjabber_start", + { + 0: Token("Would you ", ["jj_consider"]), # have you + 1: Token("Will you ", ["jj_consider"]), + 2: Token("Did you ever ", ["jj_consider"]), + 3: Token("Maybe you ", ["jj_consider"]), + 4: Token("In ", ["jj_times"]), # in