Nouveaux concepts :
Stockage des données
|
Avant d'aller plus loin, il est nécessaire de comprendre les structures de données, car elles peuvent sembler impressionnantes mais sont importantes dans la programmation et reverse engeneering. Par exemple, les données qui composent les Headers des fichiers exécutables portables sont contenus dans diverses structures, donc pour coder une application qui manipule des fichiers PE, vous devez savoir comment les structures de données sont gérées en masm. Le SEH (Structured Exception Handling),qui est présent dans certains systèmes de protection, utilise également des structures. Lors du dernier tutoriel, nous nous sommes familiarisés avec les structures "startupinfo" et "process_information". Dans le prochain, nous utiliserons la structure "openfilename" et travaillerons avec les PE Headers (en-têtes). Stockage des données en mémoire Bien qu'une unité de mémoire dans les ordinateurs modernes contienne un byte (octet), les unités de données utilisées dans un programme sont souvent beaucoup plus grandes. Avant que des données puissent être stockées en mémoire, vous devez indiquer à l'ordinateur combien d'espace réserver en utilisant un type de données abstrait. Cela spécifie la quantité de mémoire nécessaire pour stocker les données et le type de données qui sera stocké à cet emplacement mémoire. Il existe 2 types de types de données abstraits : les types de données primitifs et les types de données définis par l'utilisateur. Les types de données primitifs sont intégrés dans
le langage de programmation. Par exemple, MASM utilise les types de variables entières
(nombres entiers) suivants : BYTE
8 bits nombres non signés de 0 à 255 Alors que les variables peuvent être placées n'importe où en mémoire, les éléments des types de données définis par l'utilisateur se trouvent les uns à côté des autres en mémoire. Il est plus efficace de pointer vers les éléments d'un tableau que vers des variables, car vous pouvez pointer vers l'élément suivant du tableau en vous déplaçant simplement vers la prochaine adresse mémoire. Nous aborderons les tableaux et les structures plus en détail dans un instant. Indirection et déréférencement Il est important de comprendre la
distinction entre le CONTENU d'une variable (sa
valeur) et l'ADRESSE d'une variable (son emplacement en
mémoire). Avoir l'ADRESSE d'une variable établit un niveau de INDIRECTION "un". Lorsque vous travaillez avec des tableaux, vous pouvez avoir un tableau d'adresses. Si vous utilisez l'ADRESSE de ce tableau pour accéder à la VALEUR d'une variable à partir de son adresse dans le tableau, vous avez un niveau supplémentaire d'indirection. La raison pour laquelle les niveaux d'indirection sont si utiles est qu'une seule adresse DWORD ou POINTEUR peut accéder à un nombre beaucoup plus élevé de variables très rapidement. REMARQUE : Un pointeur est simplement une variable qui contient une adresse en tant que valeur. Il est possible de supprimer un niveau d'indirection à partir d'une adresse en la plaçant entre crochets. Cette technique s'appelle le DÉRÉFÉRENCEMENT. Dans l'exemple suivant, eax est utilisé comme un pointeur vers la variable MyVar et est déréférencé dans ebx pour donner la valeur de la variable : .data Voir ici pour des notes sur la syntaxe [] (crochets).
Comme mentionné précédemment, un tableau est une collection séquentielle de variables, toutes de même taille et de même type, appelées éléments. Vous pouvez accéder à tous les
éléments d'un tableau par rapport au premier élément. Alors
qu'un tableau est identifié par son nom, chaque élément d'un tableau est
référencé par un numéro d'index, en commençant par zéro.
L'indice du tableau apparaît entre crochets après le nom du tableau : array[9] En assembleur, l'indice d'un élément fait référence au nombre de
octets entre l'élément et le début du tableau. Cette
distinction peut être ignorée pour les tableaux d'éléments de taille d'un octet,
puisque le numéro de position d'un élément correspond à son
indice. Par exemple, en définissant un tableau d'octets appelé
"prime": prime BYTE 1, 3, 5, 7, 11, 13, 17 on attribue la valeur 1 à prime[0], la valeur 3 à prime[1],
et ainsi de suite. Cependant, pour les tableaux dont les éléments sont plus grands que 1
octet, les numéros d'index (à l'exception de zéro) ne correspondent pas à la
position d'un élément. Vous devez multiplier la position d'un élément
par sa taille pour déterminer l'index de l'élément. Ainsi, pour le tableau wprime WORD 1, 3, 5, 7, 11, 13, 17 wprime[4] représente le troisième élément (5), qui se trouve à 4 octets
du début du tableau. De même, l'expression wprime[6] représente le quatrième élément (7) et wprime[10]
représente le sixième élément (13). Les structures sont simplement des modèles pour les données à stocker en mémoire de manière organisée. Elles ne sont pas des données en elles-mêmes. Une structure est un groupe de types de données abstraits qui peuvent être accédés en tant qu'unité ou par l'un de ses champs composants. Les champs au sein de la structure peuvent être de différents types de données et de différentes tailles. Ils sont placés séquentiellement en mémoire lorsqu'une instance de la structure est définie dans un programme. Une fois que vous avez déclaré un type de structure, vous pouvez définir autant de variables de ce type que vous le souhaitez. Pour chaque variable définie, de la mémoire est allouée. Les champs individuels peuvent ensuite être référencés directement ou indirectement. Comme d'autres variables, les variables de structure peuvent être accessibles par leur nom. Vous pouvez accéder directement aux champs des variables de structure avec cette syntaxe : variable.champ DATE STRUCT 2. Ensuite, définissez la variable (une instance de la structure DATE appelée "yesterday") soit avec des données non initialisées : .data? ou avec des données initialisées : .data 3. Enfin, vous pouvez accéder à la structure et à ses champs directement par leur nom, comme ceci : mov al, yesterday.month Ou indirectement via un pointeur, comme ceci : mov ebx, offset yesterday
;charge l'adresse de la variable yesterday dans ebx L'utilisation d'un pointeur DWORD vers une structure accélère les appels de fonction, car de grandes quantités de données dans une structure peuvent être accessibles par la fonction, mais seul le pointeur doit être passé sur
la pile. Celui-ci contient 3 éléments, le premier est un DWORD non initialisé contenant la signature exécutable (pour PE, NE, OS/2, etc.), puis 2 structures imbriquées supplémentaires contenant le header de yesterday et le header optionnel. Le header de yesterday est le suivant : Ainsi, les champs Signature et NumberOfSections peuvent être indirectement accessibles assez simplement. Supposons que nous ayons un pointeur vers le début du PE header dans edi, vous pouvez superposer le modèle de structure sur ce bloc de mémoire afin d'adresser les différents champs par leur nom, comme ceci : mov eax,[edi].IMAGE_NT_HEADERS.Signature Masm comprendra également cela si les crochets englobent l'opérande entier : mov eax,[edi.IMAGE_NT_HEADERS.Signature] Nous en verrons plus à ce sujet dans les tutoriels suivants. Il existe également une autre façon d'utiliser les structures de manière indirecte avec la directive "assume". Cela indique à l'assembleur de substituer le registre de votre choix par une expression contenant le pointeur de votre structure. Par exemple : assume edi:ptr
IMAGE_NT_HEADERS Cependant, bien que cela semble légèrement plus rapide, le fait d'assumer un registre puis d'oublier de le "désassumer" produit des résultats imprévisibles. De plus, la directive "assume" peut être si éloignée de la ligne qui en a besoin que sa signification reste floue. Il existe une quatrième syntaxe pour faire cela, mais elle est maladroite et ne fonctionnera pas dans certaines circonstances (par exemple, après une directive ".if"), il est donc conseillé d'utiliser la première méthode. Elle utilise l'opérateur "ptr" et je l'ai incluse ici pour des raisons de complétude :
mov eax,(IMAGE_NT_HEADERS ptr [edi]).Signature Cette section deviendra particulièrement importante lorsque nous aborderons plus tard la programmation pour les fichiers PE. UNIONS Les unions
apparaissent également dans les PE header. Elles sont identiques aux structures, à la différence que les champs d'une union se chevauchent en mémoire, ce qui vous permet de définir différents formats de données pour le même espace mémoire. Les unions peuvent stocker différents types de données en fonction de la situation. Elles peuvent également stocker des données en tant qu'un type de données et les récupérer en tant qu'un autre type de données. Le premier membre peut contenir soit les données Characteristics, soit les données OriginalFirstThunk. Dans ce cas, Characteristics a pu être à un moment donné un ensemble de flags, mais cela a été interrompu par M$ et elle représente maintenant toujours les données OriginalFirstThunk. Au passage, sachez que la plupart des structures nécessaires pour la programmation sous Windows (y compris les éléments PE) sont déjà déclarées pour nous dans le fichier windows.inc.
|
Copyright (C)- xtx Team (2021)