Tout Operating System possède un système de gestion mémoire. Les Operating Systems récents travaillent avec de la mémoire protégée. Un processus x ne peut pas accéder à la mémoire d’un processus y. Pour que le processus x dialogue avec le processus y, ils vont devoir employer d’autres méthodes (Shared Memory, Message Queues, Pipes, Network Connections, …). Ces méthodes sécurisent les processus entre-euxs mais ralentissent néanmoins leurs fonctionnements. L’IOS ne gère pas la mémoire partagée, tous les processus ont accès à la mémoire sans restrictions. Un processus est donc libre de dialoguer avec un autre en écrivant dans la mémoire de ce dernier (Buffer Overflow = Crash, j’y reviendrai avec la notion de bloc). Il existe néanmoins une notion de mémoire R/O et R/W. L’IOS travaille avec des pools de mémoire, c’est le Pool Manager qui en est responsable :
#show memory
Head       Total(b)     Used(b)    Free(b)    Lowest(b)   Largest(b)
Processor
653B8C20   155481056    86243592   69237464   68168948    67670028
I/O
EE800000    25165824     5269012   19896812   19819968    19871932
Ici, un pool réservé au processor et un pool réservé aux I/O:
  • Head : début du pool
  • Total : taille du pool en bytes
  • Used : utilisation actuelle du pool en bytes
  • Free : mémoire libre actuelle du pool en bytes
  • Lowest : mémoire libre historiquement la plus basse en bytes
  • Largest : le plus grand bloc de mémoire libre contigüe
Ces mêmes pools appartiennent à des régions de mémoires gérées par le Region Manager :
#show region
Region Manager:
Start       End            Size(b)  Class  Media  Name
0x0E800000  0x0FFFFFFF    25165824  Iomem  R/W    iomem:(iomem)
0x60000000  0x6E7FFFFF   243269632  Local  R/W    main
0x6000F000  0x632FFFFF    53415936  IText  R/O    main:text
0x63300000  0x64DFFCFF    28310784  IData  R/W    main:data
0x64DFFD00  0x653B8C1F     6000416  IBss   R/W    main:bss
0x653B8C20  0x6E7FFFFF   155481056  Local  R/W    main:heap
0x80000000  0x8E7FFFFF   243269632  Local  R/W    main:(main_k0)
0xA0000000  0xAE7FFFFF   243269632  Local  R/W    main:(main_k1)
0xEE800000  0xEFFFFFFF    25165824  Iomem  R/W    iomem
Le pool de mémoire Processor est donc compris dans la région main:heap. Cette région fait partie de la région main qui commence en 0x60000000 et finit en 0x6E7FFFFF. Le pool de mémoire I/O représente la région iomem de 0xEE800000 à 0xEFFFFFFF. Dans la région main, on trouve :
  • main:text : contient le code de l’IOS en R/O (IText)
  • main:data : contient les variables initialisées R/W (IData)
  • main:bss : contient les variables non initialisées R/W (IBss)
  • main:heap : contient les structures de mémoire standard locales R/W
  • iomem : contient la mémoire des périphériques (mémoire pour le bus I/O)
On peut remarquer que certaines régions sont redondantes: main:(main_k0) ; main:(main_k1) ; Elles correspondent toutes à la même région main. On peut aussi retrouver iomem à deux endroits différents : 0x0E800000->0x0FFFFFFF et 0xEE800000->0xEFFFFFFF, il s’agit pourtant de la même zone mémoire. D’un périphérique CISCO à un autre, l’endroit où est stocké tel ou tel type de mémoire diffère. Sur un type de routeur on peut trouver de la mémoire SRAM pour iomem, tandis que dans d’autres pour la même zone on trouvera de la mémoire DRAM. Le Pool Manager définit des zones mémoires indépendamment du type de mémoire utilisé (hardware abstraction). Revenons au Pool Manager. En utilisant la commande « show memory processor », on peut remarquer que la mémoire est subdivisée en blocs :
#show memory processor
Processor memory
Address      Bytes     Prev     Next Ref     PrevF    NextF  Alloc PC  what
65A817E0 0000000084 65A8175C 65A81864 001  -------- -------- 628215E8  Init
65A81864 0000001372 65A817E0 65A81DF0 001  -------- -------- 608E3218  Skinny Socket Server
65A81DF0 0000001156 65A81864 65A822A4 001  -------- -------- 608E3218  Skinny Socket Server
  • Address : début du bloc
  • Bytes : taille du bloc
  • Prev : adresse du bloc précédent (chaînage)
  • Next : adresse du bloc suivant (chaînage)
  • Ref : par combien de processus ce bloc est-il utilisé ?
  • PrevF : bloc libre précédent
  • NextF : bloc libre suivant
  • Alloc PC : processus ayant alloué ce bloc
  • What : nom du processus détenant le bloc
Inspectons de plus près un bloc mémoire :
#show memory 0x65A81864
65A81860:          AB1234CD 010B0000 66B4EBEC      +.4M....f4kl
65A81870: 6395634C 608E3218 65A81DF0 65A817F4  c.cL`.2.e(.pe(.t
65A81880: 800002AE 00000001 605C3DD4 00000112  ........`=T.... ...
65A81DE0:                            FD0110DF              }.._
Après quelques recherches on devine déjà les champs du header de bloc :
65A81860:            [MAGIC  ]  [PID   ] [?       ]
65A81870: [PTR_NAME] [ALLOCPC]  [NEXT  ] [PREV+20d]
65A81880: [SIZE*] [REF ] [?  ] [ DATA ->] ...
65A81DE0:                     [<- DATA] [MAGIC   ]
image
  • MAGIC_START est toujours 0xAB1234CD
  • PID : l’id du processus
  • [ PLACEHOLDER ]
  • PTR PS_ NAME : pointeur vers le nom du processus détenant le bloc
  • ALLOC_PC : même valeur que dans la commande « show memory processor »
  • NEXT : pointeur vers le prochain bloc
  • PREV : pointeur vers le précédent bloc + 20d (directement sur le champ next du précédent)
  • SIZE : renferme une petite subtilité, le MSB (Most Significant Bit) de ce champ est un flag qui positionné à 1 indique que ce bloc est utilisé. De plus la valeur lue dans ce champ diffère de la valeur affichée avec la commande « show memory processor ». Pour obtenir la valeur correcte je dois faire : (valeur lue)*2+4 ????
  • REF : combien de processus utilisent ce bloc
  • [ PLACEHOLDER ]
  • DATA : les données …
  • MAGIC_END est toujours 0xFD0110DF (palindrome en hexa)
Par exemple en partant de ce bloc, si je veux atteindre le suivant sans utiliser le pointeur Next, je dois faire : Adresse du bloc + sizeof(magic_start) + sizeof(header) + (2*[valeur du champ size]+4)+sizeof(magic_end) Soit: 0x65A81864+0x4+0x24+0x560+0x4=0x65A81DF0 Vérification du contenu du champ PID:
#show process 0x0000010B [010B <> 0000]
Process ID 267 [Skinny Socket Server],
TTY 0
Memory usage [in bytes] Holding: 89644, Maximum: 109468, Allocated: 6601380, Freed: 6514500
Getbufs: 0,
Retbufs: 0,
Stack: 8908/12000
CPU usage PC: 60846DE4, Invoked: 26, Giveups: 1, uSec: 9384 5Sec: 0.00%, 1Min: 0.00%, 5Min: 0.00%, Average: 0.00% Age: 5007208 msec,
Runtime: 244 msec
State: Waiting for Event, Priority: Normal
Vérification du contenu du champ PTR PS_NAME:
#show memory 0x6395634C
63956340:                            536B696E              Skin
63956350: 6E792053 6F636B65 74205365 72766572  ny Socket Server
63956360: 00000000 0A496E76 616C6964 20736B69  .....Invalid ski
->Skinny Socket Server
Vérification que nous arrivons bien à la fin du bloc (présence d’un magic number FD0110DF) :
0x65A81864 + 2*[SIZE]+4 + SIZEOF(MAGICSTART) + [HEADERSIZE] = 0x65A81864 + 0X560 + 0x4 + 0x24
#show memory 0x65A81DEC
65A81DE0:                            FD0110DF              }.._
S'il nous venait à l’idée de réaliser un buffer overflow dans une zone mémoire, donc écrire plus de bytes prévu que ceux alloués pour cette zone mémoire, on finirait par écraser le header de la prochaine zone mémoire et donc rompre le chaînage mémoire IOS. Hélas, ou pas, l’IOS vérifie en permanence la structure de sa mémoire et à la moindre incohérence, force un crash. Donc pour réussir un buffer overflow sans crash, il faut s’arranger pour réécrire un header cohérant sur la prochaine zone mémoire. Ne pas oublier non plus de faire un « jmp » pour sauter à la suite de son code juste après le header ! image Le post suivant parlera du « chunk manager » de l’IOS.