Archive for avril 8th, 2007

MS07-017 Kernel Vuln Exploited

L’histoire de ce post commence dans le train, youpi c’est les vacances mais avant je dois me payer 6h d’ennui à mater les vaches, au lieu de cela je vais plutôt faire un peu de RE. En début de semaine on m’avait fait remarqué que les white papers des confs Blackhat était dispo, l’un d’eux, celui de Joel Eriksson intitulé « Kernel Wars » attira mon attention, il présentait diverses exploitations de vulnérabilités kernel sur des noyaux unix mais aussi et surtout démontrait qu’il était possible d’exploiter la vulnérabilité GDI paru dans le Month Of Kernel Bugs (MOKB). W00T. Mauvais chose pour nous il décrit sans trop de détail comment il à exploité ce bug, fournissant juste une méthodologie et même pas de sploit, OUIN ! Moi après avoir rigoler avec le pauvre BOF de la vuln au niveau des icônes animées (fichiers .ani) je me suis dit que l’écriture d’un sploit pour la vuln GDI ferait un zoli post pour mon blog.

La première dont nous parle Jojo concerne la récupération d’un handle sur un truc appelé la GdiSharedHandleTable. Dans la 1ère version du POC de MOKB, la récupération du handle était fait par brute-force (lil), Jojo lui est plus malin et préfère utiliser des propriétés de l’objet qu’il veut pour obtenir son handle. Sachant qu’un handle correspond juste à l’indice dans la HANDLE_TABLE d’une strucure HANDLE_TABLE_ENTRY, on va regarder cela sous avec notre ami kernel debugger.
Après avoir le code du POC du MKB je note l’indice que me renvoie le brute-force :

HANDLE hMapFile=(HANDLE)0x10;
while(!lpMapAddress){
hMapFile=(void*)((int)hMapFile+1);
lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
}

L’objet en question est de type Section, normal car MapViewOfFile (NtMapViewOfSection en fait) ne marche que sur ce type d’objet. Il est donc intéressant de savoir d’ou provient ce handle, hop je lance le POC dans Olly, et je m’aperçois que le handle est déjà présent dans la HANDLE_TABLE alors que je ne suis que sur l’EntryPoint de mon .exe, je décide de dire à Olly de breaker avant le lancement du loader de Windows (Options->Debugging Options->Event, Make first pause at : system breakpoint), je regarde les handles, aucun de type section. C’est donc le loader de win qui l’ajoute dans la table, reste plus qu’à trouver par quelle fonction, hop je trace gentiment en regardant particulièrement les appels au fonction systèmes (seules capable d’ajouter un handle dans la HANDLE_TABLE du process) et je remarque qu’après l’appel à une fonction de GDI32.dll, le nombre de handle est modif et passe de 7 à 11:

Avant l'appel à NtGdiInit :
PROCESS 838b3020  SessionId: 0  Cid: 076c    Peb: 7ffde000  ParentCid: 0744
DirBase: 07af6000  ObjectTable: e1f88168  HandleCount:   7.
Image: GDI.exe

Après :
PROCESS 838b3020  SessionId: 0  Cid: 076c    Peb: 7ffde000  ParentCid: 0744
DirBase: 07af6000  ObjectTable: e1f88168  HandleCount:  11.
Image: GDI.exe

lkd> !handle 0 3 76C Section
processor number 0, process 0000076c
Searching for Process with Cid == 76c
Searching for handles of type Section
PROCESS 838b3020  SessionId: 0  Cid: 076c    Peb: 7ffde000  ParentCid: 0744
DirBase: 07af6000  ObjectTable: e1f88168  HandleCount:  11.
Image: GDI.exe

Notre objet de type section :
Handle table at e109f000 with 11 Entries in use
001c: Object: e1531bf8  GrantedAccess: 000f001f Entry: e109f038
Object: e1531bf8  Type: (843c43b8) Section
ObjectHeader: e1531be0
HandleCount: 19  PointerCount: 20

La call stack :
Thread ID : 1656
0x77ef67eb : GDI32!NtGdiInit+0xc
0x77d1f54e : USER32!_UserClientDllInitialize+0x315
0x7c911193 : ntdll!LdrpCallInitRoutine+0x14
0x7c92c9e4 : ntdll!LdrpRunInitializeRoutines+0x344
0x7c931abc : ntdll!LdrpInitializeProcess+0x1131
0x7c928d66 : ntdll!_LdrpInitialize+0x183

La DLL GDI32 fait appel système sur la fonction NtGdiInit du driver win32k.sys. Hop je regarde ce que NtGdiInit à dans le ventre :

lkd> uf win32k!NtGdiInit
win32k!NtGdiInit:
bf8c1f3c 33c0            xor     eax,eax
bf8c1f3e 40              inc     eax
bf8c1f3f c3              ret

Grut?? Comme vous le voyez la foncion NtGdiInit n’est pas très consistante, à ce moment un gros doute s’empare de moi, d’ou qu’il sort se put1 de handle FFS. Bon calmons nous et reprenons, l’appel système se réalise de la manière suivante.
NtGdiInit dans GDI32.dll -> KiFastSystemCall -> sysenter | KiFastCallEntry -> KiSystemService -> NtGdiInit

Forcément le handle est ajouté par KiFastCallEntry ou bien KiSystemService, reste plus qu’à regarder.

Dans KiSystemService, je tombe sur le disass suivant :

mov     ecx, ds:0FFDFF018h
xor     ebx, ebx
or      ebx, [ecx+0F70h]
jz      short loc_4077C0
push    edx
push    eax
call    ds:_KeGdiFlushUserBatch ; Grut ?
pop     eax
pop     edx

En fait en 0xFFDFF000 on est dans la structure KPCR (Kernel Process Control Region) dont le premier champ est une structure NT_TIB, et en 0×18 de cette struct on trouve un pointeur sur le TEB du thread courant :

lkd> dt nt!_NT_TIB FFDFF000
+0x000 ExceptionList    : 0xf2914c7c _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase        : 0xf2914df0
+0x008 StackLimit       : 0xf2912000
+0x00c SubSystemTib     : (null)
+0x010 FiberData        : (null)
+0x010 Version          : 0
+0x014 ArbitraryUserPointer : (null)
+0x018 Self             : 0x7ffde000 _NT_TIB

Et a l’offset 0xF70 du TEP on tombe sur :

dt nt!_TEB
[...]
+0xf70 GdiBatchCount    : Uint4B
[...]

Ho le joli compteur, ce que je comprend pour l’instant c’est que la fonction KiSystemService vérifie si le champ GdiBatchCount du TEB du thread courant vaut 0, si oui alors on appel la fonction pointé par la var globale KeGdiFlushUserBatch.

lkd> dd nt!KeGdiFlushUserBatch l1
80561548  bf80db87
lkd> ln bf80db87
(bf80db87)   win32k!NtGdiFlushUserBatch   |  (bf80e078)   win32k!BRUSHMEMOBJ::pbrAllocBrush
Exact matches:
win32k!NtGdiFlushUserBatch = 

Comme on peut le voir, cette var pointe sur la fonction NtGdiFlushUserBatch de win32k.sys. hop je cours, je saute, je zoom et je fait chauffer IDA sur cette API. Arf grosseuh fonction codé avec un switch à 9 ‘case’ (IDA l’analyse très bien), bon là j’ai pas envie de RE pour savoir quel case du switch est prit en compte, va falloir tracer cela sous VM et puis j’ai plus de batterie :p
De retour chez moi, je lance ma VM en mettant un BP sur NtGdiFlushUserBatch, après 1h de recherche, j’arrive à RIEN ! Apparement le handle ne provient pas de cette fonction …. ….. (TILT!) Je me suis souvenu que eeye avait pondu un advisorie sur cette faille . Tient il parle de la fonction GdiProcessCallout, hop BP dessus et je relance. BIM TOUCHAY!

Voici la call stack :

kd> k
ChildEBP RetAddr
f7f09cd4 bf8465af win32k!GdiProcessCallout+0xb8
f7f09cf0 8057ed06 win32k!W32pProcessCallout+0x5c
f7f09d54 804ded5e nt!PsConvertToGuiThread+0x123
f7f09da0 7c92de0f nt!KiBBTUnexpectedRange+0xc
7ffe0300 7c91eb93 ntdll!LdrDisableThreadCalloutsForDll+0x82
7ffe0308 00000000 ntdll!KiFastSystemCallRet+0x4

Le plus drôle c’est que l’appel à KiBBTUnexpectedRange provient de KiSystemService, il se faisait juste avant celui de NtGdiFlushUserBatch, ouinnn !
Dans GdiProcessCallout je vois un magnifique ObOpenObjectByPointer, qui permet de récupérer un handle sur un objet à partir d’un pointeur.

NTSTATUS
ObOpenObjectByPointer (
__in PVOID Object,
__in ULONG HandleAttributes,
__in_opt PACCESS_STATE PassedAccessState,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_TYPE ObjectType,
__in KPROCESSOR_MODE AccessMode,
__out PHANDLE Handle
)

lea     eax, [ebp+SectionHandle] ; Handle renvoyé sur l'objet
push    eax
push    ebx             ; AccessMode = ebx =0
push    ebx             ; ObjectType
push    0F001Fh         ; DesiredAccess  : SECTION_ALL_ACCESS
push    ebx             ; PassedAccessState
push    ebx             ; HandleAttributes
push    _gpHmgrSharedHandleSection ; Object : pointeur sur l'objet
call    ds:__imp__ObOpenObjectByPointer@28 ; ObOpenObjectByPointer(x,x,x,x,x,x,x)

Regardons la table des handles de notre process à présent.

kd> !process

PROCESS ffbc86f8  SessionId: 0  Cid: 0418    Peb: 7ffde000  ParentCid: 0530

DirBase: 06673000  ObjectTable: e15c6978  HandleCount:   8.

Image: GDI.exe

VadRoot 80df7d68 Vads 22 Clone 0 Private 27. Modified 0. Locked 0.

DeviceMap e17669a0

Token                             e1028d48

ElapsedTime                       00:00:00.911

UserTime                          00:00:00.020

KernelTime                        00:00:00.821

QuotaPoolUsage[PagedPool]         10532

QuotaPoolUsage[NonPagedPool]      880

Working Set Sizes (now,min,max)  (136, 50, 345) (544KB, 200KB, 1380KB)

PeakWorkingSetSize                136

VirtualSize                       8 Mb

PeakVirtualSize                   8 Mb

PageFaultCount                    132

MemoryPriority                    BACKGROUND

BasePriority                      8

CommitCharge                      42
THREAD 80de8478  Cid 0418.03f0  Teb: 7ffdd000 Win32Thread: 00000000 RUNNING on processor 0
kd> !handle 0 3 0418 Section

processor number 0, process 00000418

Searching for Process with Cid == 418

Searching for handles of type Section

PROCESS ffbc86f8  SessionId: 0  Cid: 0418    Peb: 7ffde000  ParentCid: 0530

DirBase: 06673000  ObjectTable: e15c6978  HandleCount:   8.

Image: GDI.exe
Handle table at e1043000 with 8 Entries in use

001c: Object: e13ad6b0  GrantedAccess: 000f001f Entry: e1043038

Object: e13ad6b0  Type: (80eb2040) Section

ObjectHeader: e13ad698

HandleCount: 16  PointerCount: 17

Youpi, l’objet apparaît dans la table :]

La fonction ObOpenObjectByPointer est suivie d’un ZwMapViewOfSection qui va mapper dans l’espace mémoire du processus la section.

mov     [ebp+BaseAddress], ebx
mov     [ebp+ViewSize], ebx
mov     [ebp+SectionHandle], ebx

push    2               ; Protect : #define PAGE_READONLY 0x02
push    ebx             ; AllocationType
push    2               ; InheritDisposition
lea     eax, [ebp+ViewSize]
push    eax             ; ViewSize
push    ebx             ; SectionOffset
push    ebx             ; CommitSize
push    ebx             ; ZeroBits
lea     eax, [ebp+BaseAddress]
push    eax             ; BaseAddress = 0
push    0FFFFFFFFh      ; ProcessHandle, -1 = CurrentProcess
push    [ebp+SectionHandle] ; SectionHandle
call    ds:__imp__ZwMapViewOfSection@40 ; ZwMapViewOfSection(x,x,x,x,x,x,x,x,x,x)
mov     edi, eax
cmp     edi, ebx

Si l’argument BaseAddress est nul (c’est le cas ici), alors la fonction ZwMapViewOfSection nous donne le premier espace mémoire UserSpace dispo quelle trouve. Après l’appel la fonction renvoie dans [ebp+BaseAddress] 0×410000. La section est donc mappé dans l’espace mémoire du process en lecture seule à l’addresse 0×410000. Jetons un coup d’œil aux VAD.

kd> !vad 80df7d68
VAD     level      start      end    commit
80e22728 ( 1)         10       10         1 Private      READWRITE
ffbc7ef8 ( 2)         20       20         1 Private      READWRITE
80df2648 ( 3)         30      12f         3 Private      READWRITE
80da3660 ( 4)        130      132         0 Mapped       READONLY
ffaf95b0 ( 5)        140      23f         3 Private      READWRITE
ffaeeca8 ( 6)        240      24f         6 Private      READWRITE
ffb94308 ( 7)        250      25f         0 Mapped       READWRITE
80d25958 ( 8 )        260      275         0 Mapped       READONLY
80d2c1e8 ( 9)        280      2bc         0 Mapped       READONLY
80d2c218 (10)        2c0      300         0 Mapped       READONLY
80d2c248 (11)        310      315         0 Mapped       READONLY
80d25928 (12)        320      3e7         0 Mapped       EXECUTE_READ
80d00550 ( 0)        400      406         7 Mapped  Exe  EXECUTE_WRITECOPY
80cec448 ( 4)        410      512         0 Mapped       READONLY  <------------ Notre section
80e1f230 ( 3)      77d10    77d9f         2 Mapped  Exe  EXECUTE_WRITECOPY
80e3a670 ( 4)      77ef0    77f35         1 Mapped  Exe  EXECUTE_WRITECOPY
80d0c5f0 ( 2)      7c800    7c903         5 Mapped  Exe  EXECUTE_WRITECOPY
80d00520 ( 1)      7c910    7c9c6         5 Mapped  Exe  EXECUTE_WRITECOPY
80e71c40 ( 3)      7f6f0    7f7ef         0 Mapped       EXECUTE_READ
80d25898 ( 2)      7ffb0    7ffd3         0 Mapped       READONLY
80d00900 ( 3)      7ffdd    7ffdd         1 Private      READWRITE
80d21228 ( 4)      7ffdf    7ffdf         1 Private      READWRITE

Bon calmons nous, on sait maintenant d’ou provient ce handle. Le loader de windows, lors du chargement de user32.dll va appeler la fonction NtGdiInit de GDI32.dll. Durant l’appel système jusqu’à NtGdiInit, le code passe par la fonction KiSystemService (qui se charge en fait de retrouver la fonction dans la SSDT), celle-ci lance KiBBTUnexpectedRange qui à son tour appel W32pProcessCallout à travers un PsConvertToGuiThread. La fonction W32pProcessCallout, à partir d’un pointeur nommé _gpHmgrSharedHandleSection, obtient un handle sur l’objet et le map dans l’espace mémoire du process qui a appelé NtGdiInit… RIGOLO hein! :p :p :p

D’après ce que j’ai vu, Jojo il avait pas remarqué ça et il n’en pas eu besoin pour réaliser son exploit. Ce qui se passe en fait c’est que le handle ouvert par ObOpenObjectByPointer n’est pas refermé, il réside donc toujours dans la table dans handles du process. Si on peut le retrouver, on peu remapper la section en mémoire en écriture car le handle a été ouvert avec tout les droits (SECTION_ALL_ACCESS). C’est pour cela que dans le POC le brute-force fonctionne, le handle sur un objet de type Section se trouvant dans les premiers indices de la table.

Alors voyons d’ou provient l’objet global _gpHmgrSharedHandleSection, 3 coups de IDA et on voit que la section est crée par la fonction HmgCreate, elle même étant appelé par InitializeGre qui est lancée par GsDriverEntry. En explorant le disass de HmgCreate on peut voir :

push    ebx
push    ebx
push    8400000h ;
push    edi
lea     eax, [ebp+var_C]
push    eax
push    ebx
push    6
push    offset _gpHmgrSharedHandleSection
mov     [ebp+var_8], ebx
mov     dword ptr [ebp-0Ch], 102ADCh
call    ds:__imp__MmCreateSection@32 ; MmCreateSection(x,x,x,x,x,x,x,x)
test    eax, eax
jl      loc_BF89083A ;si NULL on se kass

lea     eax, [ebp+ViewSize]
push    eax             ; ViewSize
push    offset _gpGdiSharedMemory ; MappedBase
push    _gpHmgrSharedHandleSection ; Section
mov     [ebp+ViewSize], ebx
call    ds:__imp__MmMapViewInSessionSpace@12 ; MmMapViewInSessionSpace(x,x,x)
test    eax, eax
jl      loc_BF89083A ;si NULL on se kass

mov     eax, _gpGdiSharedMemory
cmp     eax, ebx
lea     ecx, [eax+100000h]
mov     _gpentHmgr, eax ; _gpentHmgr=_gpGdiSharedMemory

MmCreateSection crée l’objet _gpHmgrSharedHandleSection puis la fonction MmMapViewInSessionSpace va créer la section dans l’espace kernel. Si on regarde le contenu à l’adresse _gpGdiSharedMemory qui se trouve dans la kernel memory et celui qu’on peut voir dans notre process après le MapViewOfFile, on remarque qu’ils sont identiques. Normal, le MapViewOfFile permet de faire « voir » à la partie userland du process une section de la mémoire noyau.

Maintenant que j’ai mieux compris la chose, je continue de lire le paper de Jojo. Apparemment la section contient une liste de structure GDITableEntry, définie par :

typedef struct
{
DWORD pKernelInfo;
WORD  ProcessID;
WORD  _nCount;
WORD  nUpper;
WORD  nType;
DWORD pUserInfo;
} GDITableEntry;

Tout les process ayant cette liste en commun seul le champ ProcessID permet de savoir à quel process appartient une structure. Comme le dit si bien Jojo, c’est en manipulant la valeur pKernelInfo qu’il est possible d’écrire dans le KernelSpace. Il avoue quand même qu’il à chialé sa race pour trouver quelque chose de correct :]
« The methodology used for finding a way to achieve an arbitrary memory overwrite was partially trial and error [...] »

Alors il faut trouvé une fonction système, qu’on peut appeler du userland, qui manipule cette section et en particuler le champ pKernelInfo. Je recherche donc des fonction en Nt**** dans win32k.sys faisant référence à l’espace mémoire pointé par _gpGdiSharedMemory et _gpentHmgr ……

[.............................Traversée du désert .............................]

2 jours de recherches, 2 put1 de jours à tester des centaines de possibilités et à bouffer des chocapics, j’ai faillit craquer plusieurs fois mais j’ai finalement réussi.

Il me fallait trouver un bout de code qui modifiait une adresse définie, je l’ai obtenu (non sans mal) dans la fonction bDeleteBrush du driver win32k.sys. Cette fonction permet de supprimé un objet de type « brush » crée par CreateSolidBrush, la supression se faisant à l’aide de l’API DeleteObject. Après avoir modifié la valeur pKernelInfo de la structure GDITableEntry correspondant à mon « brush » et essayé divers combinaisons, que seule une personne se droguant aux chocapics aurait pu penser, j’ai réussir à écrire à une adresse arbitraire. Le dissass est le suivant :

win32k.sys bDeleteBrush

mov     esi, [edx] ;esi=pKernelInfo
cmp     [esi+4], ebx ; ebx=0, il faut que [esi+4]>0
mov     eax, [edx+0Ch]
mov     [ebp+var_8], eax
ja      short loc_BF80C1E7 ;jump si [esi+4] > 0

loc_BF80C1E7:
mov     eax, [esi+24h]  ; [esi+24] = addr qu'on veut fister
mov     dword ptr [eax], 2

Ainsi si pKernelInfo pointe sur un buffer crafté par nos soins et qu’en 0×24 de ce buffer se trouve une adresse valide, son contenu prendra la valeur 2.

Alors LOL?! qu’est ce qu’on peut foutre avec ca ?! Hé bien le trick de maladouf consiste à modifier l’adresse d’un appel système (contenu dans la SSDT) pour qu’il soit rediriger vers une adresse basse, c’est-à-dire dans le userspace. Par exemple :

lkd> dps bf998300 L 2
bf998300  bf934921 win32k!NtGdiAbortDoc
bf998304  bf94648d win32k!NtGdiAbortPath

Doit devenir :

lkd> dps bf998300 L 2
bf998300  00000002
bf998304  bf94648d win32k!NtGdiAbortPath

Ainsi si on mappe en userspace à l’adresse 0×2 un payload et qu’on appel la fonction native NtGdiAbortDoc, BIM ! COUP DE TETE, BALAYETTE, MANCHETTE !!!!!! le payload sera exécuté. J’ai choisit la SSDT du driver win32k.sys car celle du ntoskrnl est en lecture seule (merci à Jojo pour toutes ces infos).

Pour allouer de la mémoire en 0×2 il suffit d’utiliser NtAllocateVirtualMemory. Ensuite la SSDT du driver win32k.sys n’étant pas chargée à une adresse constante, j’ai du récup l’imageBase du driver avec un NtQuerySystemInformation ayant l’InformationClass mise à SystemModuleInformation (11). Enfin Pour l’appel à l’API native NtGdiAbortDoc, on réutilise direct le code ASM de l’appel natif contenu dans gdi32.dll :

lkd> uf GDI32!NtGdiAbortDoc
GDI32!NtGdiAbortDoc:
77f3073a b800100000      mov     eax,1000h
77f3073f ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
77f30744 ff12            call    dword ptr [edx]
77f30746 c20400          ret     4

Concernant le shellcode, j’ai utilisé un payload générique faisait reboot la machine :] (le même que celui de mon post « kernel BOF »). Je me réserve le droit d’en coder un plus pratique pour owner de la b0x :p

Autrement Immunity a achété les droits du sploit jusqu’à la fin du moi d’Avril, ces mofo devait surement croirent que personne n’oserait codé un sploit sur cette faille. C’est désormais chose faite et j’en ai profité pour le release sur milw0rm.

Ivanlef0u, OMG th4t w4s s0 l33t !

le sploit here :

http://ivanlef0u.fr/repo/GDI-MS07-017.rar

Références :

http://www.microsoft.com/technet/security/bulletin/MS07-017.mspx

http://research.eeye.com/html/alerts/zeroday/20061106.html

http://projects.info-pull.com/mokb/MOKB-06-11-2006.html

https://www.blackhat.com/presentations/bh-eu-07/Eriksson-Janmar/Whitepaper/bh-eu-07-eriksson-WP.pdf

http://www.securityfocus.com/bid/20940/info

18 comments avril 8th, 2007


Calendar

avril 2007
L Ma Me J V S D
« mar   mai »
 1
2345678
9101112131415
16171819202122
23242526272829
30  

Posts by Month

Posts by Category