TLBs are your friends

février 4th, 2008 at 12:30 admin

Un drame m’est arrivé, la pire chose au monde. Ce matin au réveil, je m’aperçois que je n’ai plus de chocapicz … Tel un drogué je comprends que mon besoin primaire de cette nourriture ne pourra être satisfait rapidement, c’est dans ce genre de moment qu’on réalise que nos petites habitudes ont parfois prit une ampleur sur notre existence insoupçonnée, comment faire pour ne pas y penser ? Comment faire pour que mon ventre creux arrête de gargouiller, il est 13h (wiwi c’est l’heure ou je me réveil) tout est fermé, je me dis « Ivan, soit fort, combat cette drogue, fait autre chose, chasse cette envie de ton esprit ! » et j’ai fait ce que je sais faire le mieux, j’ai codé …

Ca faisait pas mal de temps que j’avais envie de jouer avec la pagination de OS. Bidouiller les PDEs, les PTEs, jouer avec mon cr3, courir avec des TLB près de la mer, bref, m’éclater avec ces mécanismes. J’avais toujours en tête les différents papers plus au moins vieux sur le sujet mais je voulais faire quelque chose d’un peu plus original sous Windows. Un pote m’a montré la documentation de PAX, vous savez le patch kernel unix qui sert à prévenir pas mal d’exploitations de binaires. En fait PAX émule une protection en exécution des pages mémoire, comme le NX bit dispo sur les processeurs récents. Le NX bit à besoin du PAE activé ou un mode 64 tapz pour fonctionner ce qui est un peu dommage par rapport à ce qu’il apporte en terme de sécurité.

Pour comprendre comment fonctionne la protection en non-execute des pages, il faut d’abord savoir comment fonctionne la translation d’adresses virtuelles en adresses physiques, pour cela je vous invite à relire ce post.

Si vous avez lu (ou que vous connaissez déjà un peu le sujet) vous remarquerez que le processus de translation d’adresses virtuelles (virtual addresses, VA) en adresses physiques (physical addresses, PA) est un peu long et demande au moins 3 lectures en mémoire. Pour pouvoir optimiser cela les constructeurs de CPU ont ajouté un cache permettant de faire le lien entre les VA et PA plus rapidement, on appel ces caches des TLB (Translation Lookaside Buffers). Ils contiennent des cellules qui font correspondre les VA au PA du process courant afin de pouvoir optimiser la translation d’adresses.

Après il faut savoir que les TLP sont splités en 2 parties, l’ITLB et le DTLB. L’ITLB (Instruction TLB) va contenir les translations d’adresses pour le code exécutable. Le DTLB (Data TLB) quand a lui contient les translations pour le reste des données. L’utilisation des TLB est donc résumer par le schéma suivant :

TLB

Tout d’abord la MMU va check si une entré est présente dans les TLB pour faire la translation, si oui on un un TLB Hit, sinon on a un TLB Miss et on doit perdre du temps 0 parcourir les PDE/PTE.

Les TLB sont assez connu dans le domaine de la sécu, notamment pour 2 faits. Premièrement, pour avoir été décrit dans phrack par Sherri Sparks et Jamie Butler dans l’implémentation du shadow walker. Le concept du shadow walker permet à un rootkit kernel-land de caché du code ou des données au processeur lui même. Le principe réside évidemment sur la translation d’adresses. Prenons une gentille page mémoire présente dans la RAM, tranquillement elle vit sa vie, se faire lire, écrire voir même exécute. Elle est forcément décrite par un PTE (Page Table Entry) qui, sous architecture x86, est de la forme :
PTE

Lorsque l’OS trouve que la page n’est pas assez souvent utilisée, il peut, pour gagner de la mémoire, la swapper sur le disque dans le fameux fichier pagefile.sys. Vous comprenez bien que si un programme décide d’aller chercher cette page en mémoire, il va se sentir un peu seul quand il va voir qu’elle n’y est plus, il faut donc que l’OS la recharge. Cependant il doit savoir, quand la recharger, vous me direz « bah quand le programme en a besoin sale ruskoff communiste buveur de vodka », certes … Pour cela, lorsque l’OS outswap la page sur le disque, il met la 0 le premier bit du PTE (Present Bit), ainsi lorsque la MMU va aller chercher la page, en voyant que le present-bit est 0, va lancer une exception de type page-fault (0x0E) alertant ainsi le memory-manager qu’il faut inswapper la page en mémoire.

Jusqu’ici rien de nouveau, le shadow walker va utiliser a son escient le present-bit. Prenons une autre gentille page mémoire et mettons son present-bit à 0, sans l’outswapper. Si un accès est fait sur cette page, une exception est générée, normalement le memory-manager ne sait pas gérer ce genre d’exceptions, c’est pourquoi nous avons notre propre page-fault handler placé avant. Celui-vi va servir à vérifier d’ou provient l’accès à notre page, dans le cas d’une demande d’exécution, si l’accès ne provient pas d’une source valide on remplace le PTE par un fake PTE représentant une page contenant du junk code. Cet à ce moment qu’interviennent les TLB, au lieu de remettre la present-bit de la page à 1, ce qui pourrait être problématique car nous ne pourrions intercepter les prochaines exécutions sur notre page, on va tout simplement ajouter une entré dans l’ITLB en faisant correspondre à notre VA en PA sur une page contenant du junk code, c’est de la balle, car avec cette technique, on est en mesure d’intercepter les prochaines demandes d’exécution de notre page.

Dans le cas d’une demande de lecture/écriture, on regarde aussi d’ou provient l’appel, si la demande est autorisée on charge la translation d’adresse dans le DTLB.

Les TLB sont aussi connus pour servir dans la détection des VM, l’idée peut-être résumé par le post de Thomas Patcek de chez Matasano. Le but est de désynchroniser les TLB et les PTE, c’est à dire de laisser dans les TLB des translations d’adresses de pages dont le present-bit est à 0. L’attaque consiste à se place dans la VM, on désynchronise les TLB et les PTE et on fait en sorte de saturer le TLB sur un gros bloque de mémoire qu’on a alloué et dont on a mit les present-bits de ses pages à 0. Il faut savoir que pour différences les TLB entries de l’host et de la VM, les entries sont taguées par des ASID (Address Space Identifiers), donc un flush des TLB dans l’host ou la VM n’affectera pas les entries taguées par les autres ASID. Maintenant qu’on a floodé le TLB, que ce passerait-il si on devait revenir dans l’environnement de l’hôte ? Naturellement ce dernier irait flusher une parie des entries du TLB, celui-ci étant plein, pour pouvoir gérer la VM, après retour dans la vm, on aurait donc une translation d’adresse fausse avec les entries du TLB taguées avec les ASID de la VM. Il suffit donc de prendre une instruction demandant une sortie de l’hyperviseur mais qui dans un contexte normal n’effectue aucun accès mémoire, cette instruction c’est CPUID. Sous Intel VT-x et AMD-V, elle requiert une sortie de l’hyperviseur pour être géré, de ce fait on est capable de vider des entries de la VM du TLB et de fucker les translations de notre big bloque de mémoire et DONC par effet de bord de détecter la VM, ouf …

Bon évidemment je vous prends la tête avec des notions par forcément évidentes à comprendre. Pour la suite retenez 4 choses par coeur sur les TLB :

- Reloading cr3 causes all TLB entries except global entries to be
flushed. This typically occurs on a context switch.

- The invlpg causes a specific TLB entry to be flushed.

- Executing a data access instruction causes the DTLB to be loaded with
the mapping for the data page that was accessed.

- Executing a call causes the ITLB to be loaded with the mapping for the
page containing the code executed in response to the call.

Revenons un peu à notre patch noyau, PAX, celui-ci tire avantage des TLB pour émuler une protection en non-execute. La question est comment ? Simple il suffit de lire la doc, extrait :

« The above described TLB behaviour means that software has explicit control
over ITLB/DTLB loading: it can get notified on hardware TLB load attempts
if it sets up the page tables so that such attempts will fail and trigger
a page fault exception, and it can also initiate a TLB load by making the
appropriate memory access to have the CPU walk the page tables and load one
of the TLBs. This in turn is the key to implement non-executable pages:
such pages can be marked either as non-present or requiring supervisor level
access in the page tables hence userland memory accesses would raise a page
fault. The page fault handler can then decide whether it was an instruction
fetch attempt (by comparing the fault address to that of the instruction
that raised the fault) or a legitimate data access. In the former case we
will have detected an execution attempt in a non-executable page and can
act accordingly (terminate the task), in the latter case we can just change
the affected page table entry temporarily to allow user level access and
have the CPU load it into the DTLB (we will of course have to restore the
page table entry to the old state so that further page table walks will
again raise a page fault). »

Pour résumer, l’idée consiste à marquer les present-bits des pages à protéger à 0, ainsi lorsqu’une demande est faite sur ces pages, elle génère une exception de type page-fault. On vérifie le type de demande, dans le cas d’une requête en read ou write on charge une entrée dans le DTLB avec le code suivant :

cli //disable interruptions
mov ebx, ptePage //ebx : Pointeur sur le PTE de notre page filtré
or dword ptr [ebx], 0x01 //marque la page présente
mov eax, dword ptr [eax] //acces en lecture qui va remplir le DTLB
and dword ptr [ebx], 0xFFFFFFFE //page non présente
sti //enale interruptions

On oublie évidemment pas de flusher notre page de l’ITLB avec l’instruction invplg. Maintenant que notre DTLB est remplit, on peut sans problème travailler sur nos pages. Lors du prochain changement de contexte, les entries sera flushée mais du fait que les present-bits des PTE décrivant les pages à protéger sont à 0, on aura de nouveau une interruption et de nouveau on rechargera le DTLB. Ce qui est cool c’est qu’on à besoin que d’un page-fault pour ajouter une entrée au DTLB, donc tant qu’on reste dans le contexte de notre process (même cr3) tout se passe bien.

Si un programme demande un accès en exécution sur une page protégée, du fait que l’ITLB est vite, on aura un ITLB miss qui va appeler le page-fault handler, notre page-fault handler même, ce dernier aura pour rôle de dire au programme qu’il est impossible d’exécuter cette page.

Je me suis amusé à réaliser un couple programme user-land, driver r0, mettant en application ce concept. Le driver va recevoir des IOCTL du programme lui demandant de protéger une seule page mémoire, il va aussi installer son propre page-fault handler dans l’IDT afin de filtrer les exceptions. Voici le code du nouveau page-fault handler :

//Our KiTrap0E handler
VOID __declspec(naked) NewInt0EHandler(void)
{
    __asm
    {
        // sur la stack on a
        // [page fault error code]
        // [faulting eip]
        // ...
        pushad
        mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode
        test edx, 0x04 //verif si le core etait en usermode lors de l'except
        je PassDown  //sinon on se casse                              

        //verif si l'except s'est produite dans notre proces space
        mov eax, cr3
        mov ebx, Mycr3
        cmp eax, ebx
        jne PassDown

        mov esi, cr2 //esi : Adresse du page fault
        mov eax, esi
        shr eax, 12 //masque a 0x1000

        mov ebx, vaPage
        shr ebx, 12 //masque a 0x1000

        cmp ebx, eax
        //si ce n'est pas notre page
        jne PassDown
        //un acces a ete fait sur notre page
        //on flush le TLB
        invlpg vaPage

        //esp+0x24=eip fautif, si eip=adresse du page fault, alors on a une demande d'exec
        //et on leave
        cmp [esp+0x24], esi
        je Execution

        //
        // DTLB
        //
        mov eax, vaPage
        //on ajoute une entrée dans le DTLB
        cli //disable interruptions
        mov ebx, ptePage //ebx : Pointeur sur le PTE de notre page filtré
        or dword ptr [ebx], 0x01 //marque la page présente
        mov eax, dword ptr [eax] //acces en lecture qui va remplir le DTLB
        and dword ptr [ebx], 0xFFFFFFFE //page non présente
        sti //enale interruptions
        jmp ReturnWitoutPassDown

        //
        //ITLB
        //
Execution:
        int 3
        //hack de la mort qui tue ....
        // !!! NE LE REFAITE PAS CHEZ VOUS !!!
        //en esp+0x20+4 on a l'adresse du page-fault, c'est à dire le début de notre page (esi)
        //pointé par esp+0x20+16 on a le user-land esp qui pointe sur l'adresse de retour pushé par le "call Page"
        //le iretd va retourner sur l'adress pointé par esp+0x20+4, pour eviter un nouveau page-fault on le fait
        //retourner sur l'adresse pointé par esp+0x20+16 en userland.

        mov eax, dword ptr [esp+0x20+0x10] //eax=userland esp
        mov eax, [eax] //eax=adresse de retour
        mov [esp+0x24], eax //change le retour de iretd

ReturnWitoutPassDown:
        popad
        add esp, 4
        iretd       

PassDown:
        popad
        jmp OldInt0EHandler
    }//end asm
}//end NewInt0E

Dans mon code, dans le cas d’une demande d’exécution j’ai du tricker comme un péruvien élevant des lamas … On sait que lors d’une exception on se retrouve sur la stack kernel-land avec :
esp+00 Error code
esp+04 Return EIP
esp+08 Return CS
esp+12 Return EFLAGS
esp+16 Return ESP
esp+20 Return SS
esp+24 Return ES
esp+28 Return DS
esp+32 Return FS
esp+36 Return GS

Cette stack sert pour le retour en user-land avec l’instruction iretd. Sachant que j’ai appelé ma page à protéger avec un call, donc sur la stack user-land, pointé par esp, j’ai mon adresse de retour. Pour retrouver le esp user-land, il suffit de regarder le esp+16 kernel-land, on remplace l’adresse de retour du iretd (esp+4) et hop ! On retombe sur nos pattes, bon j’avoue c’est tricky, mais bon, c’est de l’info, tout est permit :]
MAIS pour une raison inconnue lors du retour en userland par iretd, le segment fs est mit à 0 alors que dans le esp+32 kernel-land il vaut sa valeur normale (0x3B), donc lorsque le soft qui call le driver veut lire son TEB au moment de quitter il fuck :’( Pourtant quand le retour se fait avec le jmp sur PassDown tout est OK, j’avoue être dans le brouillard :(

Bref, mise à part ce bug, ca fonctionne. J’espère que vous avez à peu prés comprit le principe pour émuler une protection en non-execute grâce aux TLBs, j’attends avec impatience vos impressions. En attendant vous pouvez jouer avec les sources+binaires dispo ici :
http://ivanlef0u.fr/repo/TLB.rar

Sinon, allez lire blog de mon jeune padawan c’est du contenu moins hardcore mais tout aussi instructif :)
http://0vercl0k.blogspot.com/

ref :
http://book.itzero.com/read/microsoft/0507/microsoft.press.microsoft.windows.internals.fourth.edition.dec.2004.internal.fixed.ebook-ddu_html/0735619174/ch07.html
http://bluepillproject.org/stuff/IsGameOver.ppt
http://www.matasano.com/log/930/side-channel-detection-attacks-against-unauthorized-hypervisors/
http://en.wikipedia.org/wiki/Translation_Lookaside_Buffer
http://pax.grsecurity.net/docs/pageexec.txt
http://phrack.org/issues.html?issue=63&id=8&mode=txt

Les précédentes recherches de b0l0k
http://www.c0ding.fr/blog/?p=6
http://www.c0ding.fr/blog/?p=7
Ses liens :
An Architectural Approach to Preventing Code Injection Attacks
http://cairo.cs.purdue.edu/pubs/dsn07-codeinj.pdf
Hardware-assisted circumvention of self-hashing software tamper resistance
http://www.scs.carleton.ca/~paulv/papers/IEEE-extended.7april05.pdf
Rootkits ‘n Stuff
http://www.acm.uiuc.edu/sigmil/talks/shadowwalker/Shadow+Walker+Talk.pdf

Entry Filed under: RE

24 Comments

  • 1. Taron  |  février 4th, 2008 at 22:21

    Salut, j’ai pas lu l’article mais j’ai juste aperçu ton pb avec FS au retour en userland. En fait déjà je crois que la stack que tu montres est fausse : INT push sur la pile kernel ss, esp, eflags, cs et eip. Les regsitres fs, ed, ds, gs sont normalement pushés plus tard dans le prologue du service, t’es pas d’accord ?


  • 2. admin  |  février 4th, 2008 at 22:53

    Hum oui bien vu mon cher Taron. On a bien sur la stack
    error code
    eip
    cs
    eflags
    esp
    ss
    Pour s’en assurer il suffit de mettre un BP sur la fonction NewInt0EHandler et de regarder esp

    Breakpoint 0 hit
    tlbdrv!NewInt0EHandler:
    f9ffb510 60              pushad
    kd> dd esp
    f8247dc8  00000004 040013c9 0000001b 00010202
    f8247dd8  0006ff68 00000023
    

    J’ai mal lu les man intel, désolé. Tu as encore raison quand tu dis que c’est le prologue de la fonction qui gère l’exception qui push fs sur la stack :

    kd> u KiTrap0E l 20
    nt!KiTrap0E:
    804e0877 66c74424020000  mov     word ptr [esp+2],0
    804e087e 55              push    ebp
    804e087f 53              push    ebx
    804e0880 56              push    esi
    804e0881 57              push    edi
    804e0882 0fa0            push    fs
    804e0884 bb30000000      mov     ebx,30h
    804e0889 8ee3            mov     fs,bx
    804e088b 648b1d00000000  mov     ebx,dword ptr fs:[0]
    804e0892 53              push    ebx
    804e0893 83ec04          sub     esp,4
    804e0896 50              push    eax
    804e0897 51              push    ecx
    804e0898 52              push    edx
    804e0899 1e              push    ds
    804e089a 06              push    es
    804e089b 0fa8            push    gs
    804e089d 66b82300        mov     ax,23h
    

    Comme on peut le voir, fs est pushé sur la stack par le prologue de KiTrap0E et mit à la valeur 0×30 qui correspond bien au descripteur de segment kernel-land. La vrai question qui vaut très cher c’est pourquoi, lorsqu’il y une exception, la valeur de fs vaut déja 0×30 alors que le prologue ne s’est pas exécuté ? Par exemple

    Breakpoint 0 hit
    nt!KiTrap0E:
    804e0877 66c74424020000  mov     word ptr [esp+2],0
    kd> r
    eax=f9c8aaa0 ebx=00000000 ecx=00000000 edx=00000000 esi=e15c12f8 edi=00012c5a
    eip=804e0877 esp=f9c8aa7c ebp=f9c8aad4 iopl=0         nv up di pl nz na po nc
    cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000002
    nt!KiTrap0E:
    804e0877 66c74424020000  mov     word ptr [esp+2],0       ss:0010:f9c8aa7e=0000
    

    Fs est déjà mit à la valeur 0×30 alors qu’en user-land il vaut 0x3B. Celon le « AMD64 Architecture Programmer’s Manual Volume 2 System Programming » section 8.7.3 Interrupt To Higher Privilege on a :

    When a control transfer to an exception or interrupt handler running at a higher privilege occurs
    (numerically lower CPL value), the processor performs a stack switch using the following steps:

    1. The target CPL is read by the processor from the target code-segment DPL and used as an index
    into the TSS for selecting the new stack pointer (SS:ESP). For example, if the target CPL is 1, the
    processor selects the SS:ESP for privilege-level 1 from the TSS.

    2. Pushes the return stack pointer (old SS:ESP) onto the new stack. The SS value is padded with two
    bytes to form a doubleword.

    3. Pushes the EFLAGS register onto the new stack.

    4. Clears the following EFLAGS bits to 0: TF, NT, RF, and VM.

    5. The processor handles the EFLAGS.IF bit based on the gate-descriptor type:
    - If the gate descriptor is an interrupt gate, EFLAGS.IF is cleared to 0.
    - If the gate descriptor is a trap gate, EFLAGS.IF is not modified.

    6. Saves the return-address pointer (CS:EIP) by pushing it onto the stack. The CS value is padded
    with two bytes to form a doubleword.

    7. If the interrupt-vector number has an error code associated with it, the error code is pushed onto
    the stack.

    8. The CS register is loaded from the segment-selector field in the gate descriptor, and the EIP is
    loaded from the offset field in the gate descriptor.

    9. The interrupt handler begins executing with the instruction referenced by new CS:EIP.

    Ou qu’on parle de FS ici ? :)


  • 3. ...  |  février 5th, 2008 at 03:13

    DTC :)


  • 4. Taron  |  février 5th, 2008 at 18:26

    Sinon tu peux pas tester de le restaurer à l’épilogue de ton KiTtrap ?


  • 5. admin  |  février 5th, 2008 at 19:17

    Wai, j’ai testé et ca marche, j’en ai profité pour maj les sources sur le repo. Mais bon c’est quand même bizarre cette affaire. Merci de ton aide en tout cas :)


  • 6. question  |  février 9th, 2008 at 04:04

    J’ai pas trop compris.
    En gros il sert a quoi ton code ?
    Merci.


  • 7. admin  |  février 9th, 2008 at 15:07

    Lol k, le code que je file consiste en 2 parties un prog userland et un driver. Le soft userland va allouer une page en mémoire avec tous les droits et demander au driver de la protéger en exécution en lui envoyant un IOCTL. Dans le code user-land j’ai mit :

    //No exception with read/write access
    	printf("First page dword : 0x%p\n", *(PULONG)Page);
    	
    	//Exception occurs
    	__asm
    	{
    		call Page	
    	}
    

    Les 2 accès à la page mémoire seront gérer par mon page fault-handler, le premier, une lecture sur la page sera autorisé, le second, une demande d’exécution sera renvoyer comme si rien ne s’était passé. Voilà, j’espère que c’est plus clair.


  • 8. martin  |  février 12th, 2008 at 07:53

    iretd ne ferait pas un tss switch ?

    la pagination se complique avec les nouveaux systemes, le PAE: extension 36bits comme xp sp2 ou le 64bits avec 4 indirections au lieu de 3…
    Ca peut faire l’objet d’un autre post?

    Le pb majeur de la TLB comme methode de detection VM est de trouver la geometrie du cache pour tout les CPUs, pas evident. Si la taille tlb est mal evaluee la methode ne marche pas.
    Un outil free comme codeanalyze d’amd aide bien il permet de tracer les tlb miss.


  • 9. Helpless  |  février 13th, 2008 at 01:43

    Salut à tous !
    Je me posais une question sur l’adressage mémoire sur un système 32 Bits. Un process n’a alors accès qu’à 4Go de mémoire, répartis par défaut entre 2Go de user-space, et 2 Go de kernel space. Est-ce que qq’un pourrait m’expliquer à quoi servent les 2Go du noyau ? J’ai par exemple tester de lancer un prog chargeant l’adresse mémoire de kernel32.dll, et celle-ci se trouve dans le user-space… (0x7c800000)


  • 10. admin  |  février 13th, 2008 at 11:46

    @martin
    J’ai matté le manuel, il n’y a rien qui stipule un changement de TSS …
    « IRET, Less Privilege. If an IRET changes privilege levels, the return program must be at a lower
    privilege than the interrupt handler. The IRET in this case causes a stack switch to occur:
    1. The return pointer is popped off of the stack, loading both the CS register and EIP register
    (RIP[31:0]) with the saved values. The return code-segment RPL is read by the processor from the
    CS value stored on the stack to determine that a lower-privilege control transfer is occurring.
    2. The saved EFLAGS image is popped off of the stack and loaded into the EFLAGS register.
    3. The return-program stack pointer is popped off of the stack, loading both the SS register and ESP
    register (RSP[31:0]) with the saved values.
    4. Control is transferred to the return program at the target CS:EIP. »

    Le mystère du segment fs reste donc entier :)

    Sinon je vais voir pour faire un sur la pagination sur systèmes avec PAE et 64 bits plus tard. Je note l’idée.

    Merci pour le nom du soft « codeanalyze » je vais faire mumuse avec.

    @Helpless
    Kernel32 n’est qu’une DLL servant d’interface avec les appels système situés dans ntdll.dll, c’est normal que ces 2 DLL soient chargées en user-space. Ntdll se charge de faire l’appel système au noyau qui est par défaut ntoskrnl.exe dans ton c:\windows\system32 et qui se situe bien à une adresse > 0×80000000. Dans les 2GO > 0×80000000 on retrouve le kernel et les drivers.


  • 11. YoLeJedi  |  février 16th, 2008 at 21:37

    Salut,

    Pour le registre FS, il n’y a pas de mystère. En fait, vous cherchez dans la mauvaise direction car le phénomène ne vient pas du hardware mais bel et bien du système d’exploitation lui-même.
    Vous oubliez que vos observations se font par l’intermédiaire d’un debugger et qu’une « Trap Frame » est construite par le système d’exploitation et non pas par le cpu !!!

    Plusieurs indices indiquent la piste à suivre.

    1- Le registre FS est un registre de segment qui n’a pas de rôle prédéfini. Il est là en option au même titre que le registre GS. Et ils sont libres d’utilisation par le système d’exploitation. Contrairement aux registres de segment CS, DS ou bien SS qui ont un rôle bien précis et définissent des segments précis. Ils sont alors susceptibles d’être modifiés directement par le cpu lors d’évènement hardware.

    2- Tous les prologues des routines KiTrapXX s’occupent eux-mêmes de modifier le registre FS. Ceci prouve bien que cela doit être fait « à la main » et qu’il ne faut pas compter sur le hardware.

    Pour vérifier cela, il suffit simplement de placer le breakpoint un tout petit peu plus loin.
    En plaçant le breakpoint juste après le « push fs » dans le prologue de la routine nt!KiTrap0E, on peut vérifier ce que j’avance : Lorsque la faute de page vient du mode user, on trouve bien la valeur 0x3B sur la pile.

    Donc c’est bien le système qui modifie FS avant de passer la main au debugger.

    Pour trouver le code qui place FS à 0×30, il faut aller voir dans le dispatcher d’exception (KiDispatchException) qui est appelé lors d’un évènement de type break point.

    Le Dispatcher sauve le CONTEXT en fonction du KTRAP_FRAME avant de passer la main au debugger. Tout se passe dans la routine KeContextFromKframes.

    Et dans cette routine, on y trouve ceci :

    Dans le KTRAP_FRAME,

    SI les interruptions sont masquées (KTRAP_FRAME.EFlags.IF == 0)
    ET SI le registre de segment de code CS est égal à 8 (KTRAP_FRAME.SegCs == 8)
    ALORS placer les registres de segments :
    - KTRAP_FRAME.SegGs à 0
    - KTRAP_FRAME.SegFs à 0×30
    - KTRAP_FRAME.SegEs à 0×23
    - et KTRAP_FRAME.SegDs à 0×23
    AVANT de construire le CONTEXT à partir de KTRAP_FRAME.

    Dans le code :

    .text:00410163 test byte ptr [edi+(KTRAP_FRAME.EFlags+2)], 2 ;EFlags.IF
    .text:00410167 jnz loc_44ACB0
    .text:0041016D cmp [edi+KTRAP_FRAME.SegCs], 8
    .text:00410171 jz loc_42AB2D
    .
    .
    .
    .text:0042AB2D loc_42AB2D: ; CODE XREF: KeContextFromKframes(x,x,x)+AB j
    .text:0042AB2D and [edi+_KTRAP_FRAME.SegGs], 0
    .text:0042AB31 push 23h
    .text:0042AB33 pop eax
    .text:0042AB34 mov [edi+_KTRAP_FRAME.SegFs], 30h
    .text:0042AB3B mov [edi+_KTRAP_FRAME.SegEs], eax
    .text:0042AB3E mov [edi+_KTRAP_FRAME.SegDs], eax
    .text:0042AB41 jmp loc_410177 -> Construire la structure CONTEXT à partir du Kframes

    Et pour un break point à l’entrée de la routine nt!KiTrap0E, on a bien toutes ces conditions de réunies :)

    Je pense que le truc important à retenir est que :
    Il ne faut jamais oublier que le système d’exploitation agit TOUJOURS entre un évènement hardware et un résultat obtenu avec le debugger. ;)

    Voilà :)

    En espérant que mon petit message amènera une petite pierre à l’édifice.

    Bonne continuation Ivan.

    Et bon week-end,
    Lionel


  • 12. lol  |  février 17th, 2008 at 00:08

    ce beau bordel :’(


  • 13. admin  |  février 17th, 2008 at 11:26

    HAN ! Bien vu Yolejedi ! J’avais complètement oublier que le debugger pouvait avoir une influence . Merci beaucoup.


  • 14. Taron  |  février 17th, 2008 at 12:06

    c’etait donc ça.. :)


  • 15. Taron  |  février 17th, 2008 at 16:58

    Yolejedi >> pourquoi KiDispatchException a besoin de créer une CONTEXT ? normalement ce serait pour appeler KiUserDispatchException, mais vu qu’on vient du kernel mode, je vois pas l’intérêt ..


  • 16. YoLeJedi  |  février 18th, 2008 at 18:34

    Salut,

    Taron : Je suis désolé mais je ne comprends pas très bien ta question.

    > pourquoi KiDispatchException a besoin de créer une CONTEXT ?

    Windows essaye toujours en premier lieu de passer une exception à un debugger s’il y en a un d’actif. Pour cela, une structure CONTEXT est crée à partir du TrapFrame. C’est comme ça. Windows est pensé comme ça. Ça permet au debugger de modifier les infos proposées dans le CONTEXT et éventuellement de rattraper l’exception.

    > normalement ce serait pour appeler KiUserDispatchException,

    Pourquoi normalement ?

    > vu qu’on vient du kernel mode, je vois pas l’intérêt ..

    Le « kernel mode » c’est tout simplement un environnement avec des privilèges hardware en plus. Mais il existe les mêmes mécanismes de debuggage et de SEH en user et en kernel mode. Il ne faut pas oublier qu’un thread user passe très régulièrement en mode kernel par le biais des syscall ou des interruptions.

    Dans notre cas, nous avons un beakpoint sur l’entrée de nt!KiTrap0E. Ce breakpoint n’a pu que être placé à partir d’un debugger kernel. Et lors de l’interruption 3, le système formate le TrapFrame en CONTEXT avant de passer l’exception au debugger kernel.

    Une interruption est toujours interceptée en kernel mode. Ensuite le système traite l’exception suivant sa provenance. Il filtre quoi ;)
    Le passage par KiUserExceptionDispatcher de ntdll ne se fait que si l’exception à eu lieu à partir du user mode. Tout ceci me semble logique. :)
    Et même lorsque l’exception passe par KiUserExceptionDispatcher, le retour se fait par du kernel mode avec NtContinue. D’ailleurs, le fait que l’on puisse modifier les registres de debug Dr0-Dr3, Dr6 et Dr7 dans le CONTEXT à partir du user mode ne fait qu’appuyer ceci.

    Voilà :)

    Je ne sais pas si j’ai répondu à ta question Taron. Mais pour bien comprendre, il suffit de suivre tout cela en static avec IDA et en regardant dans ntdll.dll et ntoskrnl.exe.


  • 17. Taron  |  février 18th, 2008 at 19:04

    Si si, t’as bien répondu à ma question, juste un dernier détail, pourquoi dans le cas où l’exception parvient du kernel land, on place FS à KCPR, et ES/DS à des segments user-land avant de construire le CONTEXT ?

    Merci


  • 18. YoLeJedi  |  février 19th, 2008 at 21:58

    Sous Windows, lorsqu’un thead passe du user au kernel mode, le registre FS doit OBLIGATOIREMENT être placé sur le KPCR. De plus, pour pouvoir changer FS de 0x3B à 0×30 il faut OBLIGATOIREMENT que les interruptions soit masquées et ceci dés la première instruction assembleur exécutée en kernel mode.
    En passant par un system call (sysenter) ou par un interrupt gate, les interruptions sont automatiquement masquées par le cpu avant l’exécution de la première instruction en kernel mode…donc pas de souci. Ceci explique pourquoi il n’y a pas d’instruction « cli » à l’entrée des routines nt!KiTrapXX avant de modifier le registre FS.
    Mais (et voilà où je voulais en venir) en utilisant une Call Gate pour changer de privilège, les interruptions ne sont pas automatiquement masquées par le cpu ! Et on a beau placer en toute première instruction exécutée en kernel mode un « cli », il y a toujours un risque que le thread soit switché avant que les interruptions aient été masquées et donc avant que le registre FS ait été modifié. Et dans ce cas là, c’est le crash assuré. J’avais un peu parlé de cela (tardivement) dans un commentaire sur le post d’Ivan parlant des CallGate.
    C’est pourquoi je pense que la technique des CallGate est une possibilité offerte par le cpu mais qui n’est pas adaptée et fiable dans un environnement tel que Windows.
    Je me permets d’insister là dessus car je pense que c’est un point très important à comprendre qui n’est pas du tout abordé en général lorsque l’on parle des CallGate sous Windows.

    Et dans notre cas d’un breakpoint sur le début de la routine nt!KiTrap0E, le système vérifie avant tout l’environnement. Et s’il y a eu interruption en kernel mode (registre CS = 8) et SI les interruptions sont masquées (Eflags.IF = 0), le système assure les arrières en modifiant le registre FS à 0×30 directement dans le KTRAP_FRAME car suivant où le breakpoint a été placé, FS peut ne pas encore avoir été modifié.

    Mais bon, je m’égare un peu ;)

    Pour ce qui est des registres DS et ES placé à 0×23, il semble que le système soit pensé comme cela. Seul le segment de pile en kernel mode est un segment uniquement accessible au Kernel mode (SS = 0×10). Les autres (DS et ES) reste à 0×23.
    Mais ce ne sont pas des « segments user-land » comme tu dis. Ceux sont des segments accessibles aussi à partir du user-land. Ce qui n’est pas tout à fait la même chose.
    La différence est qu’en user-land (pour un système 32bit) seul les 2 premiers Go sont accessible. A partir du kernel-land, la totalité du segment est accessible.

    Mais ta remarque est pertinente Taron. Et je t’avoue que je ne sais pas vraiment pourquoi ces registres de segments ne sont pas modifiés pour être uniquement accessibles à partir du kernel mode. Il existe dans la GDT qu’un segment de DATA32 RING0 de 4Go, c’est le selecteur 0×10. Et celui-ci semble être exclusivement utilisé pour la pile kernel (SS = 0×10).

    Si quelqu’un croise Bill… ça serait cool de lui en glisser 2 mots ;)

    Voilà Taron. J’ai été un peu bavard. :P
    Et désolé, Ivan, de m’étendre sur ton blog.
    Je me calme là…promis… ;)

    En tout cas, merci Taron pour cette remarque. Je vais corriger cette info dans mes scripts pour windbg. Car je donnais finalement de mauvaises infos pour les GDT :

    Off.  Sel.  Type      Sel.:Base      Limit     Present  DPL  AVL  Informations
    ----  ----  --------  -------------  --------  -------  ---  ---  ------------
    
    0000  0000  NullDesc  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
    0008  0008  Code32         00000000  FFFFFFFF  YES      0    0    Execute/Read, accessed  (Ring 0)CS=0008
    0010  0010  Data32         00000000  FFFFFFFF  YES      0    0    Read/Write, accessed  (Ring 0)DS/SS/ES=0010
    0018  001B  Code32         00000000  FFFFFFFF  YES      3    0    Execute/Read, accessed  (Ring 3)CS=001B
    0020  0023  Data32         00000000  FFFFFFFF  YES      3    0    Read/Write, accessed  (Ring 3)DS/SS/ES=0023
    0028  0028  TSS32          80042000  000020AB  YES      0    0    (Busy) Eip = 0c4d8b51
    0030  0030  Data32         FFDFF000  00001FFF  YES      0    0    Read/Write, accessed  (Ring 0)FS=0030  FS:0->(KPCR*)FFDFF000
    0038  003B  Data32         00000000  00000FFF  YES      3    0    Read/Write, accessed  (Ring 3)FS=003B  FS:0->(TEB*) 00000000
    
    
    Alors que je devrais mettre :
    
    0010  0010  Data32         00000000  FFFFFFFF  YES      0    0    Read/Write, accessed  (Ring 0)SS=0010
    
    Et 
    
    0020  0023  Data32         00000000  FFFFFFFF  YES      3    0    Read/Write, accessed  (Ring 0-3)DS/ES=0023 (Ring 3)SS=0023
    

  • 19. admin  |  février 19th, 2008 at 22:20

    Plop YoLeJedi, nan tk ya pas de problème tu peux squatter autant que tu veux, ca fait plaisir d’avoir des bons commentaires. Je t’avouerais que ca m’intriguais un peu aussi, mais si on regarde bien, au final les segments ES et DS ne sont jamais modifiés et cela quelque soit le CPL courant, je dirais que les dev MS on du s’en foutent un peu dans le sens que les segments sont defs en flat model et que donc ils représentent tout l’espace mémoire sur 32 bits, après qu’on soit en r0 ou r3 ça ne change rien, le système reste valable et fonctionne bien.

    Merci quand même d’avoir éclaircit le sujet.


  • 20. Taron  |  février 20th, 2008 at 00:39

    C’est moi qui te remercie YoLeJedi, géniaux tes posts.


  • 21. newsoft  |  février 22nd, 2008 at 09:38

    Un commentaire du « vieux » (que je suis) : « dans le temps », Windows NT4 tournait sur processeurs x86, MIPS R3000 et Alpha.

    Donc le kernel a été conçu pour être indépendant des fonctionnalités matérielles disponibles sur le processeur.

    Tous les processeurs n’ont pas forcément de support de la segmentation, c’est p/e ce qui explique pourquoi la gestion des segments est minimaliste …


  • 22. Hooking IDT. | Nibbles mi&hellip  |  juin 7th, 2008 at 15:15

    [...] donc d’installer son propre handler afin de lancer du code r0 par exemple, deux exemples : un article de ivan, et un crackme de [...]


  • 23. Ivanlef0u’s Blog &r&hellip  |  juillet 2nd, 2008 at 18:43

    [...] réaliser de driver pas comme dans l’exemple que j’ai fait avec la pagination dans ce post. Il me fallait donc une solution implémentable depuis le user-land qui ne va défoncer tout mon [...]


  • 24. Null  |  mai 22nd, 2012 at 13:08

    Plop, nice article !

    Je voulais savoir s’il est possible de jouer avec toutes ces structures pour déterminer si une adresse mémoire correspond à quelque chose dans le kernel.

    En gros, l’idée ce serait d’émuler un IsBadReadPtr() dans le kernel, histoire de pas faire crasher le système en tentant de lire à une adresse qui ne contient rien. Apparemment c’est faisable pour un process, mais pour le kernel, je sais pas trop, et je ne trouve pas grand chose sur le sujet…

    Qu’est-ce que tu en penses ?


Trackback this post


Calendar

novembre 2024
L Ma Me J V S D
« fév    
 123
45678910
11121314151617
18192021222324
252627282930  

Most Recent Posts