
Il s'agit d'un challenge CTF de type pwn que j'ai écrit en C et qui demande à l'utilisateur d'exploiter la vulnérabilité CVE-2021-4034. Les joueurs reçoivent 2 binaires dans le répertoire challenge de ce dépôt. Le binaire chal implémente le challenge CTF et shelly.so est un binaire auxiliaire.
Au moment de la rédaction de ce writeup, le Dockerfile n'est pas encore terminé. Le Dockerfile est nécessaire pour déployer ce challenge lors d'un CTF en direct, mais pas localement : vous pouvez tout de même émuler ce challenge localement en configurant les permissions utilisateur et en installant les paquets vulnérables comme suit:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt appartenant à root:root dans le répertoire courant.challenge dans le répertoire courant.chal en tant qu'utilisateur non privilégié.L'exécution du binaire chal nous donne une vague idée de ce que fait ce binaire :```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
En jouant avec la taille de l’entrée dans la fonction `Add` et les indices des fonctions `Cancel`, on n’obtient rien de spécial (pas de débordement ni d’erreur de segmentation). Cependant, quelques points intéressants :
- Il semble que les commandes soient placées dans une liste (une liste chaînée peut-être) et qu’elles soient indexées à partir de 0 ?
- Il y a ce `Order number: 0x7ffde93681f0` qui semble afficher un emplacement sur la pile ?
- Aussi, le programme crée un répertoire portant notre nom d’entrée ainsi que 3 exécutables, dont l’un est le fichier binaire d’assistance qui nous est fourni :```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
Les exécutables contiennent notre entrée.
Vous pouvez jouer avec les autres fonctions et espérer tomber sur quelques bugs (ce qui est probable), mais je vais aller droit au but et ouvrir le programme dans Ghidra.
En comparant les chaînes qui apparaissent pendant l'exécution du programme avec celles présentes dans Ghidra, nous pouvons renommer certaines fonctions FUN_* en noms familiers :```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
Le `printf("Order number: %p\n",&local_18);` **affiche l'adresse d'une variable locale sur la pile.**
Une vérification rapide dans gdb nous montre que l'adresse divulguée est l'adresse du pointeur vers la chaîne constante `/recipe`:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
Nous voyons un appel mkdir, qui crée un répertoire avec notre nom d'entrée dans le répertoire courant.
Cela est cohérent avec notre observation lorsque nous avons exécuté le programme. Ensuite, il initialise une variable avant d'appeler la fonction main_menu, qui ressemble à ceci :```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
Si vous n'êtes pas familier avec REV, il s'agit de la décompilation de l'instruction `switch` en C. Il y a une option intéressante, alias `0x539`. Elle permet aux joueurs de deviner `0x10` octets aléatoires. Si tous les octets sont égaux, alors elle appelle `FUN_00101504();` qui appelle `system("cat flag.txt");`. Sinon, le programme se termine.
Cependant, forcer 16 octets aléatoires par force brute équivaut à essayer toutes les 256**16 = 340282366920938463463374607431768211456 possibilités. Bonne chance avec celle-ci lol.
Même si vous passez toutes les possibilités, le programme reste non privilégié, donc il ne peut pas lire le flag. Cette instruction `cat flag.txt` est intentionnelle, non seulement pour piéger les joueurs inexpérimentés en leur faisant choisir cette option de menu `0x539`, mais aussi pour empêcher les joueurs de simplement appeler cette fonction pour lire le flag, ce que nous aborderons plus tard.
***
## Ajouter```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
Si certaines variables ne vous semblent pas aussi lisibles que ça, c'est parce que j'ai pris le temps de les renommer ainsi, ce que vous devriez faire pendant la rétro-ingénierie pour garder le fil. Bon, passons aux points principaux :
puVar2 est un chunk malloc de taille 0x30.puVar3, qui fait 8 octets et pointe vers les notes du chunk courant.puVar2[5] = 0; écrase de toute façon notre débordement d'1 octet avec NULL.DAT_00105140 == 0, on la définit simplement avec le nouveau chunk, donc ce pourrait être un pointeur head pour la liste ?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
Il y a une vérification pour notre index d'entrée, donc nous ne pouvons pas modifier des emplacements arbitraires.
**Cependant, la lecture de 9 octets dans le 4ème champ qui déborde d'un octet dans le 5ème champ est toujours là !!! Nous pouvons écraser 1 octet dans le pointeur `nxt`**
***
## Afficher```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s imprimera jusqu'au caractère NULL, donc si notre 4e champ est long de 8 octets, alors le 4e %s imprimera le 4e champ de notre chunk + la valeur du pointeur nxt dans le 5e champ.
=> Nous obtenons une fuite de heap !!!*
Rien d'intéressant ici.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
Il crée donc les fichiers `recipe` et `notes` dans le répertoire portant le nom de notre entrée. Les instructions `chmod` mettent ces fichiers en mode exécutable : `0x1f8` et `0x1c0` correspondent respectivement à `0700` et `0770` en base octale.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
Cet énorme bloc de code désordonné écrit en gros le contenu de nos morceaux dans ces fichiers, puis exécute la commande cp /tmp/shelly.so dir_name, où dir_name est le nom que nous donnons au programme au début.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
C'est une fonction intéressante. Elle appelle `pkexec` avec un `argv` NULL et le pointeur du tableau `notes` comme variables d'environnement, ce qui est la commande vulnérable dans notre CVE-2021-4034. Cela suggère-t-il que nous devons façonner nos `notes` et rediriger l'exécution vers cette fonction d'une manière ou d'une autre ?
***
## shelly.so
En l'ouvrant dans Ghidra, on comprend qu'il s'agit évidemment d'une bibliothèque `set-UID-root`, semblable à celles utilisées dans l'exploit de la CVE.
***
# Résumé
Quelques points importants :
1. Nous avons une fuite de pile au début du programme (numéro de commande) qui contient l'adresse de la chaîne constante `/recipe` utilisée pour créer le fichier `recipe`.
2. Le champ 0 d'un chunk est son pointeur `notes`.
3. Le champ 5 d'un chunk est le pointeur `nxt` vers le chunk suivant dans la liste chaînée.
4. `Show` peut divulguer une adresse du tas qui est le champ 5 dans un chunk.
5. `Edit` peut écraser 1 octet dans le champ 5. **C'EST LE SEUL BUG D'ÉCRITURE ARBITRAIRE !!**
6. `FUN_00101e84` appelle `pkexec` avec argv NULL et les variables d'environnement que nous contrôlons (le tableau `notes`).
***
# Exploitation
## Disposition des chunks du tas
Une inspection rapide dans gdb peut nous indiquer le décalage entre les différents chunks de notre programme :```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
Ainsi, les chunks sont de taille 0x40 (incluant 0x10 octets de métadonnées), et ils sont séparés (du début du chunk courant au début du chunk suivant) par un décalage de 0x40+0x40 = 0x80. Cela vient du fait que les notes sont allouées à chaque fois que nous allouons un chunk, elles se retrouvent donc toujours entre deux chunks consécutifs.
Avant de passer à l'exploit, comment abuser des bugs de dépassement d'1 octet pour écrire à une adresse arbitraire ? Nous pouvons utiliser la stratégie suivante :
nxt du 2ème chunk (2ème point de la section Summary).
C.B = C-0x80.x l'octet le MOINS significatif de B.x + 8 dans le 5ème champ du chunk courant. Le pointeur nxt résultant sera B + 8.
A = 0x55555555aae0, alors l'adresse fuitée serait B = 0x55555555ab60 et x = 0xe8.A à 0x55555555ab08 avec , le pointeur résultant sera , ce qui n'est pas ce que l'on voulait.Il y a un moyen de sauter vers cette fonction, que je ne vais pas aborder. Mais avant d'essayer, jetez un œil au Dockerfile. Remarquez-vous quelque chose à propos de flag.txt ?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***IL APPARTIENT À ROOT, TANDIS QUE LE PROGRAMME EST EXÉCUTÉ ET APPARTIENT À UN UTILISATEUR NON PRIVILÉGIÉ***.
Ainsi, même si vous exécutez "cat flag.txt", le flag ne s'affichera pas, car le programme n'a pas la permission d'accéder au fichier. Cela implique que nous devons élever nos privilèges à root pour lire le flag.
## 2e méthode - CVE-2021-4034
Pour exploiter CVE-2021-4034, nous avons besoin de la configuration suivante:
1. Un répertoire nommé `GCONV_PATH=.`
2. Dans ce répertoire, nous avons besoin d'un fichier exécutable qui porte le nom du répertoire dans lequel notre fichier de configuration `gconv-modules` sera placé. Appelons-le `recipe` dans notre défi.
3. Dans le répertoire `recipe`, nous avons besoin d'un fichier nommé `gconv-modules` dont nous contrôlons le contenu, et d'une bibliothèque dynamique qui nous ouvrira un shell (peut-être s'agit-il du binaire fourni `shelly.so`)?
4. Ensuite, dans notre `gconv-modules`, nous devons avoir le même CHARSET qu'à l'étape 5 ci-dessous, et le nom de notre bibliothèque dynamique `shelly.so` comme ceci: `module UTF-8// SHELLY// shelly 2`.
5. Après tout cela, nous appelons `pkexec` avec argv NULL et le tableau de variables d'environnement élaboré = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Maintenant, nous exploitons ce défi pour obtenir la configuration ci-dessus.
***
### Objectif 1-2
Les étapes 1-2 sont faciles: il suffit de se connecter avec un nom d'utilisateur `GCONV_PATH=.` pour créer ce répertoire. Ensuite, nous effectuons un `checkout` pour créer l'exécutable `recipe` dans ce répertoire. Puis nous revenons au premier menu.
***
### Objectif 3-4
Nous nous connectons avec le nom d'utilisateur `recipe` pour créer ce répertoire. Maintenant, nous *devons* créer un fichier nommé `gconv-modules`, mais `checkout` ne crée que les fichiers `recipe` et `notes`.
Et si nous écrasions l'emplacement mémoire de la chaîne `/notes` pour qu'elle devienne `/gconv-modules`? Cela pourrait fonctionner, mais cela nécessite que cette mémoire soit accessible en écriture. Vérifions cela dans gdb:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
Et elle est inscriptible !
Comment réaliser l'écriture ?
/recipe./recipe et /notes est 0x10. Soustrayez cette valeur de l'adresse de /recipe obtenue à l'étape 1 pour obtenir l'adresse de /notes./notes par /gconv-modules.En ce qui concerne l'écriture du contenu correct dans le fichier gconv-modules, nous savons que lors du checkout, le contenu des notes des chunks sera écrit dans /notes, qui est maintenant /gconv-modules. Pour être sûrs, nous pouvons simplement fournir la chaîne module UTF-8// SHELLY// shelly 2 à chaque notes.
Ainsi, dès que nous appelons checkout, cela créera 2 fichiers recipe et gconv-modules et écrira quelques lignes module UTF-8// SHELLY// shelly 2 (nous utilisons shelly car c'est le nom de la bibliothèque .so set-uid-root fournie) dans le fichier gconv-modules. Il copiera également un shelly.so dans le même répertoire.
notes sont les variables d'environnement correspondantes. Le tableau de pointeurs notes contiendra 4 pointeurs vers ces 4 notes et un pointeur NULL final, ce qui est exactement ce que nous voulions.RIP de la fonction main par notre fonction secrète FUN_00101e84.
RIP ? Vous vous souvenez de notre adresse de pile divulguée au début du programme ? Lancez gdb pour déterminer son décalage par rapport au RIP, puis ajoutez ce décalage à l'adresse divulguée réelle pour obtenir le RIP.main ? Vous vous souvenez de notre adresse divulguée de /recipe à l'étape 2 de l'objectif 3-4 ? Faites de même ici !Le script de résolution avec commentaires est fourni dans ce dépôt. N'hésitez pas à me contacter si vous avez des questions :)
xnxt0x55555555**ab**e8 != 0x55555555**aa**e8B + 8 au lieu de C. Éditez le chunk B afin que son 1er champ (bread) contienne l'adresse target à laquelle nous voulons écrire.B + 8. Le pointeur notes de ce 3ème chunk est target ! Donc tout ce que nous écrirons dans notes sera écrit dans target !