RDTSC hooking sous Linux : théorie et pratique

14 juillet 2009 – 14:30

L’architecture x86 possède des subtilités parfois méconnues de beaucoup de développeurs. En effet, il existe une instruction assez spéciale, RDTSC, qui renvoie le nombre de cycles d’horloge depuis le démarrage du processeur. En 2007, un chercheur d’IBM présente au Black Hat une technique de hook basée sur cette instruction. En effet, il se trouve qu’il existe un flag dans le registre de contrôle CR4 permettant de désactiver cette instruction en ring 3, et de déclencher une exception #GP (int 13) lors de son appel. Via un hook de l’IDT par un driver codé maison, il devient donc possible de détourner les appels ring 3 à RDTSC, de filtrer les résultats et imaginer toutes sortes de choses. D’autant plus que RDTSC est couramment utilisée dans des application ayant trait à la sécurité, comme les méthodes d’anti-debugging ou de génération de nombres aléatoires…

Pour cet article, on se propose d’écrire un driver qui effectuera ce hook et qui détournera RDTSC afin de rendre les valeurs 11223344 et 55667788 respectivement dans EAX et EDX lorsqu’on l’appelle. Je présente en premier lieu la théorie nécessaire pour l’attaque, puis décris comment l’implémenter sous Linux. Enfin, je détaille une difficulté majeure à laquelle on peut faire face sur les distributions récentes telles qu’ArchLinux : le flag TIF_NOTSC.

L’instruction RDTSC et le flag TSD

RDTSC signifie « ReaD TimeStamp Counter », autrement dit elle permet de lire le compteur de temps du processeur, incrémenté à chaque cycle d’horloge. Ce compteur n’est autre que le MSR IA32_TIME_STAMP_COUNTER (cf manuel 3B d’Intel, section 18.11). Celui-ci fait 64 bits et est retourné dans EDX et EAX lors de l’appel à RDTSC. Les applications classiques s’en servent généralement pour :

  • Effectuer des mesures de performance (benchmark) sans passer par les fonctions du noyau
  • Générer des nombres pseudo-aléatoires, à cause du caractère à priori non prévisible de ce compteur (surtout des bits de poids faible)
  • Détecter des débogueur en mesurant des deltas entre deux instructions fixes ; si un débogueur est présent et qu’un breakpoint a été posé (ou que le mode step-by-step a été utilisé), le temps écoulé sera beaucoup plus long donc il est facile à l’application de quitter.

Cependant, ce n’est pas exactement comme cela qu’est décrit l’instruction RDTSC dans le manuel 2B d’Intel. En effet, on peut y lire le pseudo-code suivant :

IF (CR4.TSD = 0) or (CPL = 0) or (CR0.PE = 0)
THEN EDX:EAX ? TimeStampCounter;
ELSE (* CR4.TSD = 1 and (CPL = 1, 2, or 3) and CR0.PE = 1 *)
#GP(0);
FI;

On y apprend que le registre CR4 possède un flag TSD qui, s’il est activé, provoquerait une exception lorsque RDTSC est appelé dans un ring supérieur à 0 (mode protégé). Cette exception est la General Protection Fault, notée #GP et définie à l’index 13 dans l’IDT (table des interruptions). L’OS traite cette exception par une routine du noyau qui n’est généralement pas prévue pour gérer ce cas, donc on aura par défaut droit à un crash du programme ayant appelé RDTSC. Sous Linux, cela se traduit par l’envoi d’un signal SIGSEGV au processus, causant une segmentation fault.

IDT hooking

Pour hooker RDTSC, il faut donc dans un premier temps mettre à 1 le flag TSD (bit 2) de CR4 pour déclencher une #GP. Mais ce que nous voulons, c’est appeler notre fonction et non celle du noyau lors de l’exception. Il va donc falloir patcher l’IDT en remplaçant l’adresse du handler 13 par le notre ; autrement dit, faire du IDT hooking.

L’IDT recense des descripteurs explicités à la section 5.11 du manuel 3A d’Intel. Les descripteurs suivant plus ou moins le même format :

Descripteur d'interruption

Descripteur d'interruption

Comme d’habitude dans la doc Intel, le schéma se lit de bas en haut et de droite à gauche (little endian, quand tu nous tiens…). A la mode des autres descripteurs propres à l’architecture x86, on constate que le champ Offset est découpé en deux parties : poids forts et poids faibles. C’est ce champ qui pointe vers le handler à exécuter lors de l’exception. Il suffit de remplacer sa valeur par l’adresse d’une de nos fonctions, et nous pourrons alors détourner le flux d’exécution lors d’un appel ring 3 à RDTSC.

Trouver l’IDT

Pour pouvoir faire un hook de l’IDT, il faut d’abord savoir la trouver. En fait, il est nécessaire de préciser que pour les processeurs multi-coeur, il n’y a pas une seule IDT mais plusieurs :  une par cœur. Il est donc en théorie nécessaire de hooker toutes les IDT pour éviter les problèmes. Pour connaître l’IDT référencée par un cœur, il suffit d’utiliser l’instruction SIDT sur ce cœur. Cette instruction est accessible en ring 3 ; voici un code qui l’illustre. Cependant, si vous utilisez Linux dans une machine virtuelle telle que VirtualBox, il se peut que vous rencontriez des problèmes en fonction de vos options de virtualisation. En effet, l’instruction SIDT n’est pas toujours bien émulée par l’hyperviseur et il se peut que la valeur qu’elle retourne soit erronée. Préférez-donc la solution suivante si vous tenez à faire vos tests dans une VM.

Même si un procceseur peut avoir plusieurs IDT, Linux n’en utilise qu’une car chaque cœur référence la même. Celle-ci est définie dans le noyau par le symbole idt_table. Pour connaître son adresse, tentez :

grep idt_table /proc/kallsyms

Le premier champ retourné est l’addresse de l’IDT. Si cela ne vous renvoie rien, il vous faudra à la place utiliser le fichier /boot/Symbol.map, généré à la compilation du noyau. Il se peut que son nom soit quelque peu différent ; par exemple sous Ubuntu il suit le format /boot/System.map-$(uname -r) alors que sous ArchLinux il s’appelle /boot/System.map26.

Conception du handler

Une bonne conception du nouveau handler d’interruption est cruciale pour éviter de rendre instable tout le système. En effet, #GP est utilisée non seulement pour RDTSC mais aussi à chaque fois qu’un check de privilèges échoue (pour une bonne ou une mauvaise raison) dans l’OS, autrement dit un sacré paquet de fois… Autant dire qu’il est préférable de laisser l’OS gérer ces cas là tout seul.

Pour cela, il va falloir filtrer dans un premier temps les #GP dues à RDTSC et celles dues à une autre instruction. Détecter l’instruction fautive est facile vu que l’EIP a été empilé ; il suffit de le regarder, d’examiner ce qu’il pointe et de comparer cette valeur à l’opcode de RDTSC : 0F 31, soit 0x310F en mot de 16 bits little endian. Si cela ne correspond pas, on saute sur le handler de base de l’OS pour ne pas tout crasher.

Ce n’est pas tout : les programmes ring 3 de l’OS aussi utilisent RDTSC. Si nous leur rendons des valeurs comme 0×11223344, ils risquent d’avoir un comportement plutôt imprévisible, surtout s’ils s’en servent comme base de temps. J’ai d’ailleurs testé sous Linux ; Cron a segfaulté instantanément et la machine est devenue inutilisable en quelques secondes.

Bref, il faut se débrouiller pour rendre la bonne valeur à ces programmes. La solution est d’émuler RDTSC dans le driver, et de transmettre les résultats dans EAX et EDX au ring 3. Mais comment savoir quand retourner les bonnes et valeurs et les fakes ? La solution la plus simple qui m’est venue à l’esprit est d’utiliser le PIDs du processus courant, en supposant que l’on connaisse le PID à hooker. Pour transmettre au driver le PID du processus en question, on peut utiliser des IOCTLs, justement prévues pour la communication ring 3 – ring 0. Une fois que l’on a le PID, il suffit de consulter le PID courant et on peut savoir si on doit émuler RDTSC ou forger les valeurs.

Récupération du PID courant

Nous avons quasiment tout ce qu’il faut pour implémenter cette attaque sous Linux. La seule chose qu’il nous manque, c’est un moyen dé récupérer le PID du processus courant lorsque l’on est dans un handler d’interruption. Après lecture en diagonale du chapitre 7 d’Understanding The Linux Kernel 3rd edition, on constate qu’il existe une macro nommée current qui permet de récupérer un pointeur vers le descripteur de processus courant. Après avoir testé cette macro, je me suis rendu compte qu’elle ne marche en fait pas dans le contexte d’un handler d’interruption. Il faut utiliser à la place la fonction current_thread_info() qui marche à tous les coups. A partir de là, récupérer le PID est très simple, via l’expression suivante : current_thread_info()->task->pid.

Implémentation 1

Nous pouvons maintenant implémenter l’attaque. Je l’ai réalisé sans problèmes particulier sur une Ubuntu 9.04 avec un noyau 2.6.28, sur processeur AMD dualcore. Les sources sont disponibles plus bas ; voici les points principaux.

//Typedefs
typedef unsigned char u_int8;
typedef unsigned short u_int16;
typedef unsigned int u_int32;
typedef unsigned long long int u_int64;

/**
 * An IDT entry. Cf Intel SDM 3A
 */
typedef struct {
 u_int16 low_offset;
 u_int16 selector;
 u_int8 unused_lo;
 u_int8 segment_type:4;
 u_int8 system_segment_flag:1;
 u_int8 DPL:2;
 u_int8 P:1;
 u_int16 hi_offset;
} __attribute__((packed)) IDTENTRY_ST, *P_IDTENTRY_ST;

Dans un premier temps, on déclare la structure d’un descripteur d’interruption. On fera particulièrement attention à bien spécifier __attribute__((packed)) pour spécifier au compilateur de ne pas faire de padding entre les champs. La fonction effectuant le hook est ci-après :

//Interrupt handlers
u_int32 old_int_handler, new_int_handler2;

void HookOneIDT (P_IDTENTRY_ST _p_IDT, u_int32 _interrupt_number,
                 u_int32* _old_address, u_int32 _new_address)
{
 asm("cli\n\t");

 *_old_address =  ((_p_IDT[_interrupt_number].hi_offset << 16)
                | (_p_IDT[_interrupt_number].low_offset));
 _p_IDT[_interrupt_number].hi_offset = (_new_address >> 16) & 0xFFFF;
 _p_IDT[_interrupt_number].low_offset = (_new_address & 0xFFFF);

 asm("sti\n\t");
}

Rien de particulier ici, à part une désactivation temporaires des interruptions. D’ailleurs, pour être plus rigoureux, il aurait fallu les désactiver sur tous les cœurs, mais comme cette fonction sera appelée avec interrupt_number = 13, qui n’est de toutes façon pas masquable, il n’y a pas de risque.

Le nouveau handler d’interruption est codé à part dans un fichier assembleur. Il s’agit en fait d’un squelette qui sauvegarde le contexte et appelle une fonction C, pour des raisons de commodité :

.globl interrupt_handler

//The interrupt handler.
//This function must be naked. Since it's not possible with gcc on x86 platforms, we put it in a separate asm file.
interrupt_handler: 

 //Save registers
 pusha
 pushf

 //Call our hook function and  the parameter
 //Since convention call of my_func_handler is fastcall, parameter has to be in %ecx
 mov %esp, %ecx
 call my_func_handler

 //Check the return value
 cmp $1, %eax

 //If 1, throw the exception away
 je  my_exit

 //Otherwise, restore registers
 popf
 popa

 //Jump to the original handler
 jmpl * old_int_handler

my_exit:

 //Restore registers
 popf
 popa

 //Pop interrupt error code
 add $4, %esp

 //Return from interrupt
 iret

La fonction appelée, my_func_handler, doit déterminer la nature de l’exception et la traiter si besoin en détournant RDTSC. On utilise son code de retour pour savoir si l’on repasse la main au handler par défaut de Linux, ou si on se contente de retourner en userland.

//Opcode for RDTSC : 0F 31 => 31 OF in little endian
#define RDTSC_OPCODE 0x310F

//Size of RDTSC instruction
#define RDTSC_SIZE   2

/**
 * Interrupt stack structures
 */
typedef struct
{
 u_int32 edi;
 u_int32 esi;
 u_int32 ebp;
 u_int32 esp;
 u_int32 ebx;
 u_int32 edx;
 u_int32 ecx;
 u_int32 eax;
} PUSHA_ST, *P_PUSHA_ST;

typedef struct
{
 u_int32 error_code;  // !! Check Intel Manuals to see if the error code is present or not
 u_int32 eip;
 u_int32 cs;
 u_int32 eflags;
 u_int32 esp;
 u_int32 sp;
} INT_STACK_HARD_ST, P_INT_STACK_HARD_ST;

typedef struct
{
 u_int32              eflags;

 PUSHA_ST             pusha_st;
 INT_STACK_HARD_ST    int_stack_hard_st;

} MY_INT_STACK_ST, *P_MY_INT_STACK_ST;

/**
 * Return current PID
 */
unsigned int GetCurrentPID (void)
{
 // !!! The 'current' macro doesn't work in interrupt context !
 // !!! We have to use current_thread_info()->task instead
 return current_thread_info()->task->pid;
}

/**
 * Function called by the interrupt handler.
 *  !! WARNING !! Don't call printk() inside, or the kernel will freeze !
 *
 * @param stack pointer to the stack
 * @return 0 if this is a normal #GP exception,
 * 1 if it is due to our RDTSC hook
 */
u_int32  __attribute__((__fastcall__))
         my_func_handler (P_MY_INT_STACK_ST stack)
{
 //nb_interrupts++;+
 asm volatile("lock incl nb_interrupts\n\t");

 //Detect if the instruction that triggered the exception is RDTSC
 if(* (u_int16*) stack->int_stack_hard_st.eip == (u_int16) RDTSC_OPCODE)
 {
 //Check who is executing RDTSC
 if(GetCurrentPID() == pid_to_hook)
 {
 //Change EAX and EDX with magic values
 stack->pusha_st.eax = 0x11223344;
 stack->pusha_st.edx = 0x55667788;
 }
 else
 {
 //Perform a normal call to RDTSC
 RDTSC_ST rdtsc;
 RDTSC(&rdtsc);

 stack->pusha_st.eax = rdtsc.eax;
 stack->pusha_st.edx = rdtsc.edx;
 }

 //Increment EIP
 stack->int_stack_hard_st.eip += RDTSC_SIZE;

 return 1;
 }
 else
 {
 return 0;
 }

}

Il y a plusieurs détails qui ont leur importance. D’une part,on définit des structures correspondant à l’état de la pile lors de l’appel à cette fonction. Cela inclut les registres généraux pushés par PUSHA ainsi que les valeurs pushés automatiquement par le processeur. Il faut faire attention à bien inverser leur ordre relativement aux specifications d’Intel, vu que la pile croît des addresses hautes vers les basses. On récupère l’EIP empilé, on déréférence ce pointeur et on compare le mot de 16 bits avec l’opcode de RDTSC renversé (vu qu’il se trouve en mémoire, donc en little-endian). On émule RDTSC su besoin, et on n’oublie pas d’incrémenter EIP afin de sauter par dessus l’instruction lors du retour. On notera que le debug de cette fonction n’est pas trivial, car il est impossible d’utiliser des fonctions comme printk() à l’intérieur.

Voici désormais la partie relative aux IOCTLs. Je n’ai pas détaillé cette partie précédemment car elle fait plutôt partie d’un choix d’implémentation.

#include <linux/ioctl.h>

//The device name in /proc/devices
#define DEVICE_NAME        "rdtsc_exploit"

//The name of the device file in /dev
#define DEVICE_FILE_NAME   "/dev/rdtsc_exploit"

//IOCTL command codes
#define IOCTL_SET_PID    _IOWR(0, 0, unsigned int)

//Device major and minor numbers
static dev_t g_device_num;

//Count the number of hooked interrupts
extern volatile unsigned int nb_interrupts;

//The file_operation structure, to link the device
//to the appropriate handlers
static struct file_operations g_fops = {
 .owner   = THIS_MODULE,
 .ioctl   = my_ioctl,
};

//Char device structure
static struct cdev g_device;

Sous Linux, pour pouvoir communiquer avec un module en utilisant des IOCTLs, il faut créer un périphérique virtuel en mode caractère (char device) et lui assigner un handler l’ioctl. Ce device possèdera un numéro majeur dynamiquement alloué par le noyau. Pour le numéro mineur, nous choisissons simplement 0. Une fois ces ressources allouées, nous enregistrons le device ce qui a pour effet de le faire apparaître dans /proc/devices. Tout ce procédé est fort bien décrit aux chapitres 3 et 6 de Linux Device Drivers, 3rd edition, livre libre que je vous conseille vivement.

/**
 * Create the device
 */
int create_device (void)
{
 //Allocate the device major and minor
 if(alloc_chrdev_region(&g_device_num, 0, 1, DEVICE_NAME))
 {
 printk(KERN_INFO "ERROR: alloc_chrdev_region FAILED\n");
 return -1;
 }

 //Initialise the device
 cdev_init(&g_device, &g_fops);

 //Fill in some fields (optional)
 g_device.owner = THIS_MODULE;
 g_device.ops = &g_fops;

 //Register the device into the kernel
 if(cdev_add(&g_device, g_device_num, 1))
 {
 printk(KERN_INFO "ERROR: cdev_add FAILED\n");
 return -1;
 }

 printk(KERN_INFO "Device registrated successfully - name = %s, "
                  "major = %d, minor = %d\n", DEVICE_NAME,
                  MAJOR(g_device_num), MINOR(g_device_num));

 return 0;
}

/**
 * Delete the device
 */
void delete_device (void)
{
 //Unregister the device
 cdev_del(&g_device);

 //Unregister the device number
 unregister_chrdev_region(g_device_num, 1);
}

Ces deux fonctions réalisent la création et la suppression du device.

Pour manipuler le flag TSD de CR4, on cree les fonctions suivantes :

//Flag of CR4 that disable RDTSC in userland
#define FLAG_DISABLE_USER_RDTSC 0x4

/**
 * Get CR4 value
 */
u_int32 GetCR4 (void)
{
 u_int32 res = 0;

 asm volatile (
 "push %%eax\t\n"
 "mov %%cr4, %%eax\t\n"
 "mov %%eax, %0\t\n"
 "pop %%eax\t\n"
 : "=m"(res));

 return res;
}

/**
 * Set CR4 value
 */
void SetCR4 (u_int32 _new_cr4)
{
 asm volatile(
 "push %%eax\t\n"
 "mov %0, %%eax\t\n"
 "mov %%eax, %%cr4\t\n"
 "pop %%eax\t\n"
 : : "m" (_new_cr4));
}

/**
 * Enable userland calls to RDTSC
 */
void EnableUserRDTSC (void)
{
 SetCR4(GetCR4() & ~FLAG_DISABLE_USER_RDTSC);
}

/**
 * Disable userland calls to RDTSC
 */
void DisableUserRDTSC (void)
{
 SetCR4(GetCR4() | FLAG_DISABLE_USER_RDTSC);
}

On notera au passage la syntaxe assez inhabituelle de l’assembleur inline de GCC, notemment les doubles % nécessaires puisque l’on utilise des références (%0), ainsi que les \n\t en fin de ligne. Et bien entendu, les arguments inversés par rapport à la syntaxe officielle d’Intel.

Lors du chargement du driver, il suffira de hooker l’IDT et de positionner le flag CR4.TSD. Cependant, cette dernière opération doit être faite sur tous les coeurs. On utilisera donc la macro on_each_cpu().

//Hook the General Protection Fault handler (0x0D)
#define INTERRUPT_VECTOR_TO_HOOK 0x0D

#include <linux/module.h>  /* Needed by all modules */
#include <linux/kernel.h>  /* Needed for KERN_ALERT */
#include <linux/init.h>     // Needed for the macros

#include "../include/defines.h"
#include "hook.h"
#include "device.h"

static int module_load(void)
{
 Hook();
 create_device();

 //Must return 0, otherwise the module is not loaded
 return 0;
}

static void module_unload(void)
{
 delete_device();
 UnHook();
}  

module_init(module_load);
module_exit(module_unload);

/**
 * Hook
 */
void Hook ()
{
 //Get the IDT address (all CPUS use the same)
 P_IDTENTRY_ST pIDT = GetIDTSoft();

 printk(KERN_INFO "interrupt_handler = %08x\n", (u_int32) interrupt_handler);

 //Hook interrupt handler
 HookOneIDT(pIDT, INTERRUPT_VECTOR_TO_HOOK,
            &old_int_handler, (u_int32) interrupt_handler);

 //Hook RDTSC
 on_each_cpu(DisableUserRDTSC, 0, 0);
}

/**
 * Unhook
 */
void UnHook ()
{
 //Unhook RDTSC
 on_each_cpu(EnableUserRDTSC, 0, 0);

 //Unhook interrupt handler
 HookOneIDT(GetIDTSoft(), INTERRUPT_VECTOR_TO_HOOK,
            &new_int_handler2, old_int_handler);
}

Dans mon prototype, je récupère l’adresse de l’IDT en userland dans le Makefile…

IDT_ADDRESS = "0x`grep idt_table /boot/System.map-2.6.28-11-generic
               | cut -d ' ' -f 1`"

… que je passe en paramètre à GCC lors de la compilation avec le flag -D. Le module la récupère comme une constante pré-processeur :

/**
 * Get a pointer to the IDT - the soft way.
 * Works perfectly in VMs, but we either have to hardcode the IDT offset,
 * or read it from userland ('grep idt_table /proc/kallsyms'
 * or 'grep idt_table /boot/System.map').
 */
P_IDTENTRY_ST GetIDTSoft (void)
{
 P_IDTENTRY_ST pIDT = 0;

 pIDT = (P_IDTENTRY_ST) IDT_ADDRESS;

 return pIDT;
}

En userland, il faudra transmettre le PID à hooker au device, ce qui se fait par le code suivant :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <fcntl.h>
#include <sys/ioctl.h>

#include "defines.h"

int main(int ac, char **av)
{

 //int i;
 int fd;
 int pid_to_hook;

 if(ac != 2)
 {
 printf("Usage: set_pid pid\n");
 printf("Set the pid to hook.\n\n");
 exit(0);
 }

 pid_to_hook = atoi(av[1]);

 if(pid_to_hook <= 0)
 {
 fprintf(stderr, "Error, pid must be > 0.\n");
 exit(1);
 }

 //Open the device in order to communicate with the driver
 fd = open(DEVICE_FILE_NAME, O_RDONLY);

 if(fd == -1)
 {
 printf("Error: %s does not exist!\n", DEVICE_FILE_NAME);
 exit(1);
 }

 //Send IOCTLs to the driver to set the pid do hook
 if(ioctl(fd, IOCTL_SET_PID, pid_to_hook))
 {
 fprintf(stderr, "Error setting the pid.\n");
 }
 else
 {
 printf("pid set successfully.\n");
 }

 //Close the device
 close(fd);

}

Enfin, la routine de traitement de l’IOCTL qui sert à récupérer le PID dans le module est relativement simple :

/**
 * IOCTL handler
 */
int my_ioctl (struct inode * _inode, struct file * _file,
              unsigned int _ioctl_num, unsigned long _ioctl_param)
{
 struct task_struct *task;

 switch(_ioctl_num)
 {
 //Set the PID
 case (IOCTL_SET_PID):

 pid_to_hook = (unsigned int) _ioctl_param;

 break;

 default:
 printk(KERN_INFO "rdtsc_exploit: ERROR: Unsupported ioctl code: "
                  "%08x.\n", _ioctl_num);
 }
 return 0;
}

Test

Après avoir compilé le tout, on charge le module :

# insmod module/rootkit.ko
# grep rdtsc_exploit /proc/devices
250 rdtsc_exploit
# mknod /dev/rdtsc_exploit c 250 0

On lance le programme exécutant RDTSC dans un shell à part :

$ exe/rdtsc/rdtsc
Press <Enter> to call rdtsc. Press q to quit.

RDTSC result (edx : eax) = (0000126d : 8c1cc9a2)

RDTSC result (edx : eax) = (0000126d : a38e75be)

Puis on envoie son PID au module avec :

# exe/set_pid/set_pid $(pidof rdtsc)
pid set successfully.

Et on revient au terminal précédent, en constatant que le hook fonctionne bien :

RDTSC result (edx : eax) = (55667788 : 11223344)

RDTSC result (edx : eax) = (55667788 : 11223344)

RDTSC result (edx : eax) = (55667788 : 11223344)

On n’oubliera pas de décharger le module avec :

# rmmod rootkit
# rm /dev/rdtsc_exploit

Problème avec ArchLinux

En testant l’implémentation précédente avec deux distributions ArchLinux de noyaux 2.6.29 et 2.6.30, j’ai constaté qu’ell ne marchait tout simplement pas. En faisant plusieurs tests, je constate que le handler de #GP est bien hooké, mais RDTSC ne l’est pas du tout car le programme de test affiche toujours des valeurs normales. J’affiche la valeur de CR4.TSD à plusieurs reprises, et je vois que de temps en temps, il repasse à 0, ce qui expliquerait pourquoi RDTSC n’est pas détournée.

Après plusieurs recherches, je tombe sur ce blog, qui pointe du doigt quelques bizarreries du noyau Linux concernant justement le flag TSD. Apparemment, il serait possible de l’activer ou non pour certains processus seulement. Il s’agit du Thread Information Flag TIF_NOTSC définit dans le fichier arch/x86/include/asm/thread_info.h du noyau. Ce flag est plus ou moins l’équivalent du flag TSD, mais dans le contexte de chaque processus. Il est possible de le définir avec l’appel système prctl en utilisant l’option PR_SET_TSC. La valeur PR_TSC_ENABLE revient à positionner TSD = 0, tandis que PR_TSC_SIGSEGV est équivalent à TSD = 1.

Ces flags existent déjà dans les noyaux 2.6.28 d’Ubuntu 9.04 ; je n’ai pas encore bien saisi pourquoi ceuxi-ci sont effectivement appliqués sur ArchLinux. Le blog cité précédemment parle de l’option de configuration CONFIG_SECCOMP du noyau, présente sur ArchLinux, mais visiblement désactivée ia le flag TIF_SECCOMP qui vaut 0 pour tous les processus. Je vais continuer mes recherches de ce côté… Si toutefois vous avez des explications, je suis preveur :) .

Implémentation 2

En attendant, il reste tout de même effectuer le hook de RDTSC. Il suffit de positionner le flag TIF_NOTSC du processus en question à PR_TSC_SIGSEGV. Cela peut se faire en appelant prctl, mais cette technique n’est pas vraiment convenable car un hook se doit d’être extérieur au processus. La technique consiste donc à émuler le fonctionnement de cet appel système au sein de notre module. Il nous suffit de parcourir la liste chainée des processus, d’isoler celui qui a le bon PID, et à positionner son flag. Cela revient à modifier la fonction my_ioctl() comme ceci :

/**
 * IOCTL handler
 */
int my_ioctl (struct inode * _inode, struct file * _file,
              unsigned int _ioctl_num, unsigned long _ioctl_param)
{
   struct task_struct *task;

   switch(_ioctl_num)
   {
      //Set the PID
      case (IOCTL_SET_PID):

         pid_to_hook = (unsigned int) _ioctl_param;
         printk(KERN_INFO "rdtsc_exploit: pid_to_hook = "
                          "%d.\n", pid_to_hook);

         for_each_process(task) {
            if(task->pid == pid_to_hook){
               test_and_set_ti_thread_flag(task_thread_info(task), TIF_NOTSC);
               printk("TIF_NOTSC set for process %d\n", task->pid);
            }
         }

      break;

      default:
         printk(KERN_INFO "rdtsc_exploit: ERROR: Unsupported ioctl code: "
                          "%08x.\n", _ioctl_num);
   }
   return 0;
}

La macro for_each_process() définie dans linux/sched.h permet d’itérer très simplement sur les threads du système. On utilise la fonction test_and_set_ti_thread_flag() afin de positionner le flag TIF_NOTSC du thread en question. On notera qu’il n’y a même plus besoin de modifier à la main CR4 à l’initialisation.

Sources

Téléchargez les sources

Les sources incluent l’implémentation 2, sachant que celle-ci fonctionne aussi bien sur les deux distributions que j’ai testées (Ubuntu et ArchLinux). Les lignes spécifiques à la 1ère implémentation sont commentées, donc vous pouvez toujours jouer avec et voir le résultat que vous obtenez.

Pour compiler, invoquez simplement make à la racine de rdtsc_exploit. Si jamais cela ne compile pas, éditez le fichier module/Makefile, et indiquez le bon chemin vers votre fichier /boot/System.map. Vérifiez également que le fichier module/handler.S a bien un S majuscule concernant son extension.

Les fichiers fournis sont organisés comme ceci :

  • module/ contient les sources du module
  • exe/ contient deux sources d’exécutables :
    • rdtsc : programme de test exécutant RDTSC à chaque appui sur une touche. Il contient aussi un fichier de test du noyau, disable-tsc-test.c, que j’ai jugé intéressant de garder pour des tests. A compiler séparément.
    • set_pid : programme prenant en paramètre le PID de rdtsc et l’envoyant au module par ioctl
  • scripts/ contient trois scripts permettant d’automatiser le chargement du module et la création du device. load_hook.sh et unload_hook.sh appellent en réalité load.sh, capable de charger/décharger un module et créer/détruire son device.

Applications

Pour terminer, voici quelques possibilités offertes par le hook de RDTSC :

  • Empoisonnement des générateurs de nombres pseudo-aléatoires : Certaines applications utilisent RDTSC comme source d’aléa, pour générer des valeurs pseudo-aléatoires qui peuvent par exemple être utilisées pour la génération de clé de chiffrement. En forçant à RDTSC à renvoyer des valeurs bien précise, on peut injecter des valeurs bien précises dans l’algorithme de génération et pouvoir prédire plus facilement son résultat.
  • Anti-anti-debuging : Comme dit au premier paragraphe, une technique d’anti-debug consiste à utiliser RDTSC pour estimer le temps passé entre deux instructions et le comparer à une valeur seuil. Une technique d’anti-anti-debug peut donc être de hooker RDTSC et de retourner des valeurs plausibles à l’application, en masquant le fait que celle-ci est en train de se faire déboguer. C’est précisément ce que fait le plugin Olly Advanced d’OllyDbg.
  • Communication offusquée entre une application et un driver : Puisqu’avec cette technique RDTSC est exécutée en ring 3 et provoque une exception #GP en ring 0, c’est un moyen de donner la main à un driver afin qu’il effectue des opérations « ni vu ni connu », dans le sens ou il n’y a aucun appel explicite vers fonction noyau dans l’application ring 3.

Conclusion

Cette technique n’est pas nouvelle, mais encore assez peu connue (enfin sans doute pas des reversers :p). Cependant, elle peut se révéler très intéressantes dans de multiples occasions. Si je devais donner un conseil, ce serait d’éviter de l’utiliser en ring 3, pour deux raisons principales de sécurité :

  • Il existe des générateurs aléatoires reconnus comme fiables, il est donc préférable de les utiliser plutôt que de se faire son propre algorithme.
  • L’OS fournit généralement des appels systèmes permettant d’appeler RDTSC en ring 0 et de retourner sa valeur (cf NtQueryPerformanceCounter() sous Windows). Comme l’appel est en ring 0, la méthode de hook décrite précédemment ne marche plus.

Références

PR_SET_TSC
  1. 4 réponses à “RDTSC hooking sous Linux : théorie et pratique”

  2. Yo,

    Très bon article, très détaillé !
    T’aurais du utiliser un hyperviseur pour faire ça ;)

    +
    Ivan

    Par Ivanlef0u le 14 juillet 2009

  3. Ouais, ça sera la prochaine étape… Mais coder un hyperviseur demande énormément de temps et je me suis pas encore penché sur les specs Intel/AMD. Je mets ça dans ma todo list ;)

    Par Emilien Girault le 14 juillet 2009

  4. Super article bravo ! \o/

    Par wluce0` le 15 juillet 2009

  5. J’ai compris la moitié du truc, mais c’est intéressant. Je devrais m’attaquer à moins gros, en fait.

    Bon courage pour la suite.

    _Geo_

    Par Geo le 17 juillet 2009

Désolé, les commentaires sont fermés pour le moment.