
CVE-2020-15368, aka "How to exploit a vulnerable driver"
Exploit et Preuve de concept (PoC) pour CVE-2020-15368. Asrock a réemballé le pilote rweverything pour son outil de configuration du contrôleur RGB et l'a signé. Ils le « protègent » en chiffrant leurs ioctls... lol. Nous avons découvert cette CVE par accident l'été dernier, et à ma connaissance, le pilote n'est toujours pas corrigé. L'impact est bien sûr l'exécution de code arbitraire dans le noyau, etc. Alors profitez de ce « 0day » lol.
Si vous voulez discuter avec moi pour savoir si c'est une VRAIE BONA FIDE CVE, n'hésitez pas à me contacter sur Twitter, on peut avoir une énorme dispute sur les réseaux sociaux publics, ce sera vraiment excitant pour tout le monde ! J'achèterai même un domaine pour ce bug si vous le souhaitez. Tout est une question de marketing !!!!
Bref, ce bug est assez merdique, donc je vais l'utiliser comme tutoriel pour pwner un pilote vuln typique. Ce post s'adresse donc aux débutants. Vous apprendrez à exploiter un pilote vulnérable. Il y a des tonnes d'autres pilotes pourris comme celui-ci. Le monde est à vous. Amusez-vous bien.
AVERTISSEMENT : Cette publication est fournie à des fins éducatives uniquement. Il est de la responsabilité du lecteur de respecter toutes les lois locales, étatiques et fédérales applicables. Le(s) auteur(s) de cette publication déclinent toute responsabilité et ne sont pas responsables des mauvais usages ou dommages causés par le logiciel contenu dans cette publication.
Confinés, mes colocataires (Pear0, Codetector) et moi jouions sur la nouvelle carte mère Asrock de Pear0. Les LED rouges vives étaient extrêmement agaçantes et il était impossible de les configurer sous Linux. Notre plan était donc de rétro-concevoir le pilote Windows qui les contrôlait et de reproduire les opérations d'E/S sous Linux.
Bref, il n'a pas fallu longtemps pour se rendre compte que le pilote n'est littéralement qu'un pilote générique accordant un accès en lecture/écriture arbitraire à tout. Cela inclut les registres de contrôle comme CR3, CR4, la mémoire physique, etc. De tels pilotes sont destinés à être utilisés comme outil de débogage et le site du vendeur le dit clairement.

Nous avons trouvé ça extrêmement drôle. C'est assez excitant la première fois que l'on provoque un triple défaut et un redémarrage brutal depuis l'espace utilisateur. (Peut-être moins la 20e fois.) Bref, nous avons signalé le bug puis l'avons oublié pendant un an.
En tant que noob du noyau, je me demandais comment charger et interagir avec le pilote. Il s'avère que c'est extrêmement facile.
Vous pouvez simplement créer un service pour le pilote dans Process Hacker (évidemment, il faut être admin pour charger des pilotes). Ensuite, vous faites un clic droit et le démarrez. Oui, c'est vraiment aussi simple.

Nous pouvons voir notre objet Device dans WinObjEx64.

On peut même jouer avec le périphérique dans FileTest.


Ces trois outils sont tous fantastiques, surtout PH et FileTest. Ils sont comme un couteau suisse et devraient être dans la boîte à outils de tout rétro-ingénieur Windows. Par exemple, d'après ce que j'ai compris, Jonas L a découvert d'innombrables vulnérabilités LPE Windows en trifouillant simplement dans FileTest. Windows a donc vraiment d'excellents outils pour déconner. J'aimerais qu'il y ait ces trucs sous Linux.
Rweverything a un ioctl qui prend un ioctl en paramètre, lequel contrôle l'opération à effectuer (lire la mémoire, écrire la mémoire, lire le MSR, etc.), et une union de paramètres spécifiques à l'opération comme l'adresse source, l'adresse dest, etc. En comparant le code des deux pilotes :

🤔🤔🤔🤔🤔🤔🤔🤔🤔
Néanmoins, le pilote tente une piètre sécurité par l'obscurité en exigeant que tous les appels ioctl soient correctement chiffrés avec une clé AES codée en dur. Le code (après un peu de nettoyage) ressemble à ceci :
if ( IoControlCode == 0x22EC00 )
{
char enc_key[32];
memset(enc_key, 0, sizeof(enc_key));
memmove(enc_key, "C110DD4FE9434147B92A5A1E3FDBF29A", 32ui64);
memcpy(enc_key + 13, ioctl_args->key, 16);
size_t cb_decrypted = 0;
my_decrypted_cmd* decryptedCmd = NULL;
DWORD iv_size = ioctl_args->iv_size;
DWORD input_size = *(DWORD*)(ioctl_args + bufferLen - 6);
// really just calls BCrypt API to get an AES implementation
if ( (unsigned int)decrypt_ioctl_params(enc_key, 32, ioctl_args->iv, iv_size, ioctl_args + bufferLen - input_size - 6, input_size, &decryptedCmd, &cb_decrypted) )
{
// Decryption failed
if ( decryptedCmd )
ExFreePoolWithTag(decryptedCmd, 0);
irp->IoStatus.Status = 0xC000000D;
goto Fail_Out;
}
IoControlCode = decryptedCmd->opcode;
Rweverything_Args = &decryptedCmd->args;
}
else // not 0x22EC00
{
if ( IoControlCode != 0x22E858 &&
IoControlCode != 0x22E860 &&
IoControlCode != 0x22E800 &&
IoControlCode != 0x22E804 )// whitelisted control codes
IoControlCode = 0; // block everything else
}
Le pilote autorise quelques opérations ennuyeuses qui font du PMIO, mais tous les codes de contrôle « amusants » sont verrouillés derrière cette routine de déchiffrement. Malgré cette liste blanche explicite, il inclut toujours toutes les fonctionnalités dangereuses de Rweverything. Au lieu de cacher ces fonctionnalités dangereuses, ils auraient probablement dû les supprimer complètement.
De plus, il est intéressant de noter qu'il permet également à l'utilisateur de spécifier une partie de la clé (???), pour quelle raison, je n'en ai aucune idée. Le code est tout simplement très mal écrit.
Bref, il est relativement facile d'écrire le code client pour utiliser cette API chiffrée bizarre et lui passer les appels ioctl arbitraires que l'on souhaite. Je ne vais pas vous ennuyer avec les détails.
Nous ouvrons un handle vers le pilote et utilisons DeviceIoControl pour appeler l'ioctl, tout à fait standard.
HANDLE hDevice = CreateFileA("\\\\.\\GlobalRoot\\Device\\AsrDrv104", GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// ... set up the encrypted ioctl data
BOOL result = DeviceIoControl(hDevice, 0x22EC00, ioctl_data, in_buf_size, out_buf, sizeof(out_buf), &bytes_returned, NULL);
Maintenant que nous pouvons parler à la partie cachée de Rweverything du pilote, la première chose que je voulais faire était de déclencher un crash pour savoir si mon client pilote fonctionne.
La façon la plus simple d'y parvenir est de remplacer CR3 par des valeurs aléatoires. Je sais que certains d'entre vous qui lisez ceci sont des noobs et c'est OK, donc je vais expliquer en détail. Je suis stupide aussi, alors peut-être que cela vous aidera à apprendre. Si vous savez ce que vous faites, vous pouvez sauter cette partie.
Sur x86, lorsque la pagination est activée (pratiquement tout le temps dans tout système d'exploitation moderne), CR3 pointe vers l'adresse physique de base du répertoire de tables de pages de plus haut niveau. Si vous ne savez pas ce que cela signifie, allez lire l'article Wikipedia sur la mémoire virtuelle.
Lorsque nous remplaçons CR3 par des valeurs aléatoires, par exemple 0x0000000000000000, le TLB est vidé, et en tentant d'exécuter l'instruction suivante, le processeur (plus précisément l'MMU) essaiera de traduire le pointeur d'instruction en une adresse physique. On peut considérer la traduction d'adresse comme une série de parcours de tables de pages à partir de CR3. CR3 pointe maintenant vers la mémoire physique à l'adresse 0, qui existe et est accessible ; cependant il est extrêmement peu probable que ce soit une table de pages valide. (Les entrées de table de pages, ou PTE en abrégé, ont une structure spécifique à respecter.)
Lorsque cela se produit, nous obtenons un défaut de page sur la traduction d'adresse. Normalement, le CPU nous amènerait à l'adresse du gestionnaire de défaut de page. Mais comment sait-il où se trouve la fonction du gestionnaire de défaut de page ? Elle est stockée dans une structure de données mémoire connue sous le nom de Table des Descripteurs d'Interruption (IDT). Le processeur a un registre (lu/écrit par les instructions sidt et lidt) qui contient l'adresse virtuelle de l'IDT. Voyez-vous le problème maintenant ? Pour gérer le défaut de page, nous devons d'abord effectuer un autre accès mémoire virtuel, et donc une autre traduction d'adresse.
Bien sûr, notre deuxième traduction d'adresse provoquera également un défaut. Nous avons maintenant un Double Défaut : un défaut qui se produit pendant la gestion du premier défaut de page. C'est assez grave mais toujours récupérable — le processeur nous donne une dernière chance de nous en sortir. Bien sûr, cette tentative est elle aussi brutalement interrompue par un troisième et dernier défaut de page, un Triple Défaut. À ce stade, le CPU abandonne et réinitialise la machine. Si vous effectuiez cette procédure sur une machine physique, vous verriez probablement l'écran de démarrage du BIOS à peu près maintenant.
Si vous avez des questions, je vous donnerai la même réponse qu'on m'a toujours donnée : allez lire le Volume 3A du Manuel Intel (alias la Bible).
OK, maintenant comment exploiter réellement le pilote ? En regardant autour, nous voyons une primitive de lecture/écriture libre en mémoire physique. Elle mappe essentiellement n'importe quelle adresse physique que vous voulez avec MmMapIoSpace, copie votre tampon dedans (ou vice versa), puis démape l'adresse.
Petite note : lorsque vous essayez d'appeler
MmMapIoSpaceavec des arguments stupides comme nous avec le débogueur noyau attaché, vous obtiendrez un bugcheck. Vous pouvez contourner cela en écrivant un octet magique dans WinDbg. Cherchez le commentaire faisant référence àMiShowBadMapperdans exploit.cpp pour en voir plus. Je ne sais pas vraiment ce que c'est et je ne veux pas le découvrir.
Comment pouvons-nous exploiter cette primitive pour obtenir une exécution de code dans le noyau ? Le principal problème avec cette primitive est qu'elle opère sur la mémoire physique. En tant que programme en mode utilisateur, nous n'avons pratiquement aucune idée de la disposition de la mémoire physique — le système d'exploitation gère tout cela pour nous. Même si nous pouvons obtenir des adresses virtuelles de certaines structures de données du noyau ou de pointeurs de fonctions du noyau, nous n'avons aucune idée de l'endroit où elles se trouvent dans l'espace d'adressage physique.
Une idée est de lire CR3, de lire les tables de pages et d'effectuer nous-mêmes la traduction d'adresse virtuelle. C'est une excellente idée. Mais ça ne marche pas. Car Windows ne permet plus de mapper les tables de pages avec MmMapIoSpace. Nous devons donc être plus malins.
J'ai utilisé la technique de xeroxz de VDM. C'est assez simple, mais la technique est plutôt ingénieuse. Bien que nous ne connaissions pas la disposition de la mémoire physique, nous pouvons toujours scanner toute la mémoire physique jusqu'à trouver ce que nous cherchons. Une chose que nous pouvons exploiter est que le contenu des pages est toujours le même à la fois physiquement et virtuellement : tous les décalages relatifs aux limites de page sont toujours préservés. Par exemple, si j'ai la page 0x7fff000000000XXX mappée au cadre physique 0x0000000123456XXX, le XXX de toutes les adresses est le même dans l'adresse physique et virtuelle. Toute la structure intra-page est préservée ; nous pouvons donc scanner pour une page intéressante que nous voudrions écraser.
La chose la plus facile à écraser est probablement un gestionnaire de syscall ou d'ioctl facile à atteindre. Sur Windows, il y a une fonction standard Beep() qui fait bipper votre ordinateur. Croyez-le ou non, elle est implémentée dans un pilote, Beep.sys, qui fournit le périphérique Beep. (En fait, vous pouvez le voir dans la capture d'écran WinObjEx64 plus tôt.) Tout le monde peut utiliser le périphérique Beep, et il est rarement appelé. Alors écrasons le gestionnaire d'ioctl de Beep.
Nous pouvons ouvrir Beep.sys dans IDA et regarder le gestionnaire DeviceIoControl.

Au décalage de page 0x270, nous avons ce code avec les octets 40 53 48 .... Aucun de ces octets n'est relocalisé, donc scanner cette fonction est très facile. S'il y avait des octets relocalisés, nous devrions les remplacer par des jokers. C'est la même idée que le scan de signature quand on écrit un hack de jeu.
Donc après avoir scanné la mémoire physique pour localiser ce code, nous pouvons simplement l'écraser avec notre propre shellcode. Vous devez aussi faire attention car il peut y avoir plusieurs copies de cette page en mémoire physique (!), alors allez trouver toutes les copies.
À ce stade, nous pouvons assez facilement élever les privilèges en échangeant le jeton de sécurité de notre processus avec celui d'un processus système pour obtenir les autorisations nt authority\system. Malheureusement, le pilote Asrock nécessite de toute façon des privilèges administrateur pour être ouvert, ce n'est donc pas très intéressant.
Pour nous, nous écrivons un shellcode de base qui alloue et copie une charge utile de deuxième étape, puis génère un nouveau thread noyau. Nous ne pouvons pas tout faire dans notre gestionnaire Beep écrasé car 1) nous sommes limités à une page et 2) nous ferons planter le système lorsque nous essaierons de fermer notre handle vers le périphérique Beep, car nous avons aussi détruit le reste du code dans le périphérique Beep. Quant à obtenir des pointeurs noyau, c'est en fait facile car NtQuerySystemInformation nous les donne gratuitement si nous demandons gentiment.
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp)
{
MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer;
void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao');
user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size);
HANDLE hThread;
user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL);
user_data->nt_IofCompleteRequest(irp, 0);
return 0;
}
Nous patchons donc rapidement Beep, appelons le gestionnaire d'ioctl écrasé, et dépatchons Beep. Maintenant, nous avons créé en toute sécurité un thread noyau exécutant notre code sans rien détruire d'autre sur le système. À ce stade, nous pouvons mapper nos propres pilotes ou autre.
Je suis un mauvais chercheur en sécurité et je ne trouve que des bugs sans valeur, par accident. Merci à tous d'avoir lu. Abonnez-vous à mon OnlyFans69