web-dev-qa-db-fra.com

Provoquer un débordement de pile en C

Je voudrais provoquer un débordement de pile dans une fonction C pour tester les mesures de sécurité de mon système. Je pourrais le faire en utilisant assembleur en ligne. Mais C serait plus portable. Cependant, je ne peux pas penser à un moyen de provoquer un débordement de pile en utilisant C, car la mémoire de pile est gérée en toute sécurité par le langage à cet égard.

Alors, y a-t-il un moyen de provoquer un débordement de pile en utilisant C (sans utiliser inline assembler)?

Comme indiqué dans les commentaires: Le dépassement de capacité de la pile signifie que le pointeur de la pile doit pointer sur une adresse située au-dessous du début de la pile ("ci-dessous" pour les architectures où la pile passe de bas à élevé).

26
Silicomancer

Il y a une bonne raison pour laquelle il est difficile de provoquer un dépassement de capacité de la pile dans C. La raison en est que la conformité aux normes C n'a pas de pile. 

Si vous lisez le standard C11, vous découvrirez qu’il est question d’objectifs, mais pas de piles. La raison en est que la norme essaie, dans la mesure du possible, d'éviter d'imposer des décisions de conception lors de la mise en œuvre. Vous pourrez peut-être trouver un moyen de provoquer un sous-dépassement de pile en C pur pour une implémentation particulière, mais cela reposera sur un comportement non défini ou des extensions spécifiques à l'implémentation et ne sera pas portable.

45
JeremyP

Vous ne pouvez pas faire cela en C, simplement parce que C laisse la gestion de la pile à l'implémentation (compilateur). De même, vous ne pouvez pas écrire un bogue en C où vous placez quelque chose sur la pile sans oublier de le supprimer, ou inversement.

Par conséquent, il est impossible de produire un "dépassement de pile" en C pur. Vous ne pouvez pas sortir de la pile en C ni définir le pointeur de la pile à partir de C. la langue. Afin d'accéder directement au pointeur de pile et de le contrôler, vous devez écrire assembleur.


Ce que vous pouvez faites en C est d’écrire volontairement en dehors des limites de la pile. Supposons que nous sachions que la pile commence à 0x1000 et grandit. Ensuite, nous pouvons faire ceci:

volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;

for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
  *p = garbage; // write outside the stack area, at whatever memory comes next
}

Pourquoi auriez-vous besoin de tester cela dans un programme en C pur qui n'utilise pas assembleur, je n'en ai aucune idée.


Au cas où quelqu'un aurait eu tort de penser que le code ci-dessus invoque un comportement indéfini, voici ce que dit le standard C, texte normatif C11 6.5.3.2/4 (c'est moi qui souligne)

L'opérateur unaire * désigne l'indirection. Si l'opérande pointe vers une fonction, le résultat est un désignateur de fonction; si elle pointe sur un objet, le résultat est une lvalue désignant le objet. Si l’opérande est de type ‘pointeur sur type’, le résultat est de type ‘type’. Si un une valeur non valide a été attribuée au pointeur, le comportement de l'opérateur unaire * est non défini 102)

La question est alors de savoir quelle est la définition d'une "valeur invalide", puisqu'il ne s'agit pas d'un terme formel défini par la norme. La note de bas de page 102 (informative, non normative) fournit quelques exemples:

Parmi les valeurs non valides permettant de déréférencer un pointeur par l'opérateur unary *, il y a un pointeur nul, un adresse incorrectement alignée pour le type d'objet pointé et adresse d'un objet après le fin de vie.

Dans l'exemple ci-dessus, nous ne traitons clairement ni avec un pointeur null, ni avec un objet qui a dépassé sa durée de vie. Le code peut en effet provoquer un accès mal aligné - que ce soit un problème ou non est déterminé par la mise en œuvre, pas par la norme C.

Et le dernier cas de "valeur invalide" serait une adresse qui n'est pas supportée par le système spécifique. Ce n'est évidemment pas quelque chose que le standard C mentionne, car les configurations de mémoire de systèmes spécifiques ne sont pas couvertes par le standard C.

16
Lundin

Il n'est pas possible de provoquer un dépassement de capacité de la pile en C. Afin de provoquer un dépassement de capacité, le code généré devrait avoir plus d'instructions pop que d'instructions Push, ce qui signifierait que le compilateur/interprète n'est pas sain.

Dans les années 1980, il y avait des implémentations de C qui fonctionnaient par interprétation, pas par compilation. En réalité, certains d’entre eux utilisaient des vecteurs dynamiques au lieu de la pile fournie par l’architecture.

la mémoire de pile est gérée en toute sécurité par la langue

La mémoire de pile n'est pas gérée par le langage, mais par l'implémentation. Il est possible d’exécuter du code C et de ne pas utiliser de pile du tout.

Ni la norme ISO 9899 ni K & R ne spécifie quoi que ce soit à propos de l'existence d'une pile dans la langue.

Il est possible de créer des astuces et de détruire la pile, mais cela ne fonctionnera avec aucune implémentation, mais seulement avec certaines implémentations. L'adresse de retour est conservée sur la pile et vous disposez des autorisations en écriture pour la modifier, mais il ne s'agit ni d'un dépassement de capacité, ni d'un transfert.

9
alinsoar

Concernant les réponses existantes: Je ne pense pas que le fait de parler de comportement non défini dans le contexte des techniques de réduction de l’exploitation soit approprié.

Clairement, si une implémentation permet d’atténuer les problèmes de débordement de la pile, une pile est fournie. En pratique, void foo(void) { char crap[100]; ... } finira par avoir le tableau sur la pile.

Une remarque motivée par des commentaires à cette réponse: un comportement indéfini est une chose et en principe tout code qui l’exerce peut être compilé pour n’importe quoi, y compris quelque chose qui ne ressemble en rien au code original. Cependant, le sujet des techniques d’atténuation de l’exploitation est étroitement lié à l’environnement cible et à ce qui se passe dans la pratique. En pratique, le code ci-dessous devrait "fonctionner" parfaitement. Lorsque vous traitez avec ce genre de choses, vous devez toujours vérifier l'assemblage généré pour en être sûr.

Ce qui m'amène à ce qui, dans la pratique, donnera un flux insuffisant (ajout d'une variable volatile pour empêcher le compilateur de l'optimiser):

static void
underflow(void)
{
    volatile char crap[8];
    int i;

    for (i = 0; i != -256; i--)
        crap[i] = 'A';
}

int
main(void)
{
    underflow();
}

Valgrind rapporte joliment le problème.

7

Par définition, un sous-flux de pile est un type de comportement indéfini et, par conséquent, tout code déclenchant une telle condition doit être UB. Par conséquent, vous ne pouvez pas provoquer de manière fiable un dépassement de capacité de la pile.

Cela dit, l’abus suivant de tableaux de longueur variable (VLA) provoquera un dépassement de capacité de la pile contrôlable dans de nombreux environnements (testé avec x86, x86-64, ARM et AArch64 avec Clang et GCC), ce qui définit le pointeur de pile. pointer au-dessus de sa valeur initiale:

#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
    uintptr_t size = -((argc+1) * 0x10000);
    char oops[size];
    strcpy(oops, argv[0]);
    printf("oops: %s\n", oops);
}

Cela alloue un VLA avec une taille "négative" (très très grande), qui enroulera le pointeur de pile et le poussera vers le haut. argc et argv sont utilisés pour empêcher les optimisations d'extraire le tableau. En supposant que la pile diminue (valeur par défaut pour les architectures répertoriées), il s'agira d'un débordement de pile.

strcpy déclenchera une écriture dans une adresse sous-alimentée lorsque l'appel sera effectué ou lors de l'écriture de la chaîne si strcpy est en ligne. Le printf final ne doit pas être accessible.


Bien sûr, tout cela suppose un compilateur qui ne fait pas du VLA une sorte d’allocation de tas temporaire, ce qu’il est tout à fait libre de faire. Vous devez vérifier l'assembly généré pour vérifier que le code ci-dessus correspond à ce que vous attendez réellement de lui. Par exemple, sur ARM (gcc -O):

8428:   e92d4800    Push    {fp, lr}
842c:   e28db004    add fp, sp, #4, 0
8430:   e1e00000    mvn r0, r0 ; -argc
8434:   e1a0300d    mov r3, sp
8438:   e0433800    sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c:   e1a0d003    mov sp, r3 ; sp = r3
8440:   e1a0000d    mov r0, sp
8444:   e5911004    ldr r1, [r1]
8448:   ebffffc6    bl  8368 <strcpy@plt> ; strcpy(sp, argv[0])
6
nneonneo

Cette hypothèse:

C serait plus portable

ce n'est pas vrai. C ne dit rien sur une pile et comment elle est utilisée par la mise en œuvre. Sur votre plate-forme x86 typique, le code suivant ( horriblement non valide ) accèderait à la pile en dehors du cadre de pile valide (jusqu'à ce qu'il soit arrêté par le système d'exploitation), sans pour autant en "exploser":

#include <stdarg.h>
#include <stdio.h>

int underflow(int dummy, ...)
{
    va_list ap;
    va_start(ap, dummy);
    int sum = 0;
    for(;;)
    {
        int x = va_arg(ap, int);
        fprintf(stderr, "%d\n", x);
        sum += x;
    }
    return sum;
}

int main(void)
{
    return underflow(42);
}

Donc, en fonction de ce que vous voulez dire exactement par "stack underflow", ce code fait ce que vous voulez sur la plate-forme some. Mais comme d’un point de vue C, cela expose simplement comportement non défini , je ne recommanderais pas de l’utiliser. C'est pas "portable" du tout.

5
user2371524

Est-il possible de le faire de manière fiable en C conforme à la norme? Non

Est-il possible de le faire sur au moins un compilateur C pratique sans recourir à l'assembleur inline? Oui

void * foo(char * a) {
   return __builtin_return_address(0);
}

void * bar(void) {
   char a[100000];
   return foo(a);
}

typedef void (*baz)(void);

int main() {
    void * a = bar();
    ((baz)a)();
}

Construisez cela sur gcc avec "-O2 -fomit-frame-pointer -fno-inline"

https://godbolt.org/g/GSErDA

Fondamentalement, le flux dans ce programme va comme suit

  • barre d'appels principale.
  • bar alloue un tas d’espace sur la pile (grâce au grand tableau),
  • bar appelle foo. 
  • toto prend une copie de l'adresse de retour (en utilisant une extension gcc). Cette adresse pointe au milieu de la barre, entre "l'allocation" et le "nettoyage".
  • truc retourne l'adresse à barrer.
  • bar nettoie son allocation de pile.
  • bar renvoie l'adresse de retour capturée par foo à main.
  • main appelle l’adresse de retour en sautant au milieu du bar.
  • le code de nettoyage de pile de bar s'exécute, mais bar n'a pas actuellement de cadre de pile (car nous avons sauté au milieu de celui-ci). Ainsi, le code de nettoyage de la pile sous-alimente la pile.

Nous avons besoin de -fno-inline pour arrêter l'optimiseur en ligne et casser notre structure soigneusement préparée. Nous avons également besoin que le compilateur libère l'espace sur la pile par calcul plutôt que par l'utilisation d'un pointeur d'image, -fomit-frame-pointer est la valeur par défaut de la plupart des versions de gcc de nos jours, mais il n'est pas inutile de le spécifier explicitement.

Je crois que cette technique devrait fonctionner pour gcc sur à peu près n'importe quelle architecture de processeur. 

4
plugwash

Il existe un moyen de déborder la pile, mais c'est très compliqué. Le seul moyen auquel je puisse penser est de définir un pointeur sur l'élément du bas, puis de décrémenter sa valeur d'adresse. C'est à dire. * (ptr) -. Mes parenthèses sont peut-être désactivées, mais vous souhaitez décrémenter la valeur du pointeur, puis déréférencer le pointeur.

Généralement, le système d'exploitation ne fait que constater l'erreur et le blocage. Je ne suis pas sûr de ce que vous testez. J'espère que ça aide. C vous permet de faire de mauvaises choses, mais il essaie de s’occuper du programmeur. La plupart des moyens de contourner cette protection consiste à manipuler des pointeurs.

0