Tout le monde et sa grand mere a ecrit un billet sur MS12-020, et c'est donc mon tour. Le soucis, c'est que personne ne semble avoir vraiment compris la vulnerabilite, et par consequent la quantite d'inexactitudes, d'approximations et autres contresens me fait grincer des dents.
Les specifications du protocole RDP sont disponibles en ligne: http://msdn.microsoft.com/en-us/library/cc240445(PROT.10).aspx, et je vous invite a les lire si vous avez des problemes a trouver le sommeil, et la description du bug par Luigi est ici: http://aluigi.org/adv/termdd_1-adv.txt.
Je me suis donne 5 jours pour tenter d'exploiter la vulnerabilite, au dela la rentabilite pour une vulnerabilite corrigee devient moindre, et cela donne une idee du temps restant pour obtenir un exploit viable. En voici un condense, visant principalement XP et 2003. Au passage, Microsoft a donne une exploitabilite de 1 au bug - exploit stable sous 30 jours. Je n'ai pas passe 30 jours, gardez cela a l'esprit.
Vulnerabilite
Tout d'abord la vulnerabilite. Comme decouvert plus ou moins correctement par bon nombre de personnes, un flag n'etait pas mis a zero dans deux fonctions, rdpwd!NMAbortConnect et rdpwd!NM_Disconnect apres un appel a rdpwd!NMDetachUserReq. Le soucis, c'est qu'un chemin existe permettant d'appeler rdpwd!NMDetachUserReq deux fois, avec la possibilite d'un free dans le premier et d'un use-after-free dans le second. Le premier appel provient de rdpwd!NMAbortConnect, le second de rdpwd!NMAbortConnect -> rdpwd!SM_OnConnected -> rdpwd!SM_Disconnect -> rdpwd!NM_Disconnect.
La vulnerabilite est trouvee. Comment peut-on generer un appel a rdpwd!NMAbortConnect? rdpwd!NMAbortConnect est appele depuis rdpwd!NM_Connect, et il existe 5 xrefs menant a cette fonction. Les deux premiers surviennent avant que le flag @ +1Ch ne soit mis a 1 et peuvent donc etre ecartes. Les trois suivants impliquent que rdpwd!MCSChannelJoinRequest retourne quelquechose != 0. Il y a plusieurs moyens de faire echouer cette fonction, certains plus pratiques que d'autres. On va ecarter la possibilite que nt!ExAllocatePoolWithTag retourne NULL (plus de pool disponible!), et se concentrer sur rdpwd!GetNewDynamicChannel.
Le code de rdpwd.sys permet de definir jusqu'a 31 canaux dans un PDU Client Core Data (cf: 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE), 2.2.1.3.4 Client Network Data (TS_UD_CS_NET), 2.2.1.3.4.1 Channel Definition Structure (CHANNEL_DEF)), mais permet aussi de definir le nombre maximum de canaux autorises (cf: 3.2.5.3.3 Sending MCS Connect Initial PDU with GCC Conference Create Request, http://www.itu.int/rec/T-REC-T.125-199802-I/en). Si le nombre de canaux crees par defaut plus le nombre de canaux demandes depasse le maximum, la fonction rdpwd!GetNewDynamicChannel va echouer lors de la creation du nieme canal.
Trigger
Testons, avec maxChannelIds de targetParameters a 32, et un channelDefArray contenant 31 canaux (eax contient le code d'erreur):
Breakpoint 0 hit
RDPWD!NMAbortConnect:
f6f13062 8bff mov edi,edi
kd> r eax
eax=0000000f
C'est moche, contrairement a ce que 99.9% de la communaute pense, maxChannelIds peut prendre toute valeur entre 0 et 32 (et meme plus dans certains cas) et toujours declencher l'appel a rdpwd!NMAbortConnect. Donc si votre signature d'IDS ou votre "analyse" se fonde sur un maxChannelIds a 0 (ou <= 5), vous brassez du vent.
Maintenant, appel a rdpwd!NMAbortConnect ne signifie pas necessairement use-after-free. Si vous avez un client qui envoie des paquets propres, etc, vous pouvez declencher un abort a coup sur, mais pas de use-after-free. Il faut autre chose, que je vous laisserai decouvrir par vous meme.
Une fois la sequence de paquets correctement construite, observons les consequences:
Breakpoint 1 hit
RDPWD!MCSDetachUserRequest+0x24:
f6f2bc82 e8a7ffffff call RDPWD!StackBufferAlloc (f6f2bc2e)
kd> !pool @esi 2
Pool page e1582998 region is Paged pool
*e1582990 size: 58 previous size: 18 (Allocated) *TSmc
Pooltag TSmc : PDMCS - Hydra MCS Protocol Driver
kd> g
Breakpoint 1 hit
RDPWD!MCSDetachUserRequest+0x24:
f6f2bc82 e8a7ffffff call RDPWD!StackBufferAlloc (f6f2bc2e)
kd> !pool @esi 2
Pool page e1582998 region is Paged pool
*e1582990 size: 58 previous size: 18 (Free ) *TSmc
Pooltag TSmc : PDMCS - Hydra MCS Protocol Driver
Deuxieme appel a rdpwd!MCSDetachUserRequest, sauf que cette fois ci @esi pointe sur un chunk libere. Et la, c'est le drame. Voici le code de la fonction en question:
Comme vous pouvez le voir, @edi est lu du contenu de @esi plus precisement le premier PVOID. Sous achitecture 32 bits, le chunk en question est de taille 0x58, sous 64 bits 0xa0, et c'est important. Pourquoi? Parceque la taille du chunk entrainera une liberation vers un des lookasides (sauf s'ils sont tous plein), et cela signifie que les 4 premiers octets du chunk libre (8 sous 64) constituent un pointeur vers un autre chunk libre de meme taille. Cela donne:
kd> !pool @edi 2
Pool page e126afb0 region is Paged pool
*e126afa8 size: 58 previous size: 20 (Free ) *Sect (Protected)
Pooltag Sect : Section objects
kd> !pool poi(@edi) 2
Pool page e12a9330 region is Paged pool
*e12a9328 size: 58 previous size: 100 (Free ) *NtFd
Pooltag NtFd : DirCtrl.c, Binary : ntfs.sys
Bien entendu cela peut etre different en fonction du nombre d'entrees dans le lookaside, etc. Ce dernier pointeur P sera utilise dans termdd!IcaBufferAlloc, et passe a termdd!IcaGetPreviousSdLink, ou un pointeur sera lu *(P-14h+0Ch) et retourne, -8. Le probleme ici c'est que P-14h+0Ch pointe sur le nt!_POOL_HEADER du chunk libre, et donc le pointeur retourne sera les 4 premiers octets de l'entete du chunk libre (moins 8). Sous 64 bits *(P-28h+18h), et on a le meme probleme, sauf que cette fois-ci le tag du chunk constitue les 32 bits de poids fort du pointeur retourne.
kd> dd poi(@edi)-8 L 2
e12a9328 040b0420 6446744e
kd> dt nt!_POOL_HEADER e12a9328
+0x000 PreviousSize : 0y000100000 (0x20)
+0x000 PoolIndex : 0y0000010 (0x2)
+0x002 BlockSize : 0y000001011 (0xb)
+0x002 PoolType : 0y0000010 (0x2)
+0x000 Ulong1 : 0x40b0420
+0x004 PoolTag : 0x6446744e
+0x004 AllocatorBackTraceIndex : 0x744e
+0x006 PoolTagHash : 0x6446
Comme vous le voyez ici, on a un BlockSize de 0xb (*8=0x58 soit 0x50 octets plus 8 octets d'entete) et un PoolType de 0x2, ce qui explique les differents crashes 0x040bXXXX postes partout. Bien evidemment, les 16 bits de poids faible dependent de la taille du chunk precedent et varient d'un crash a un autre. Le pointeur lu sera utilise dans la sequence d'instruction devenue celebre:
.text:0001188C 8B 46 18 mov eax, [esi+18h]
.text:0001188F 83 38 00 cmp dword ptr [eax], 0
.text:00011892 75 27 jnz short loc_118BB
et un potentiel call [eax] un peu plus loin. Cette situation est la situation "ideale", et en fonction de la disposition du kernel pool, du contenu du lookaside, tout peut changer. Controler cela avec une connection RDP pre-auth, c'est loin d'etre gagne.
Exploitation
A ce point, j'ai pense a deux possibilites d'exploitation distinctes: remplir la memoire usermode du svchost.exe dans le contexte duquel tourne Terminal Services/RDP, ou racer l'allocation du chunk de 0x50 bytes entre sa liberation et son utilisation.
Il s'avere que la course n'est pas trop compliquee a gagner sur un 2003, principalement parceque Terminal Services supporte plus de connexions concurrentes que RDP sous XP. Cela donne le resultat poste sur Twitter. Les quatres premiers octets (ou 8 pour 64 bits) du chunk precedemment libre sont maintenant sous notre controle, car le chunk a ete realloue avant son utilisation. Le probleme maintenant est qu'il va se produire quatre dereferencements successifs du pointeur initial avant le call [eax]. Et je n'ai pas trouve de moyen de "survivre" a ces quatre dereferencements dans le temps passe. Cela necessiterait soit de leaker un pointeur, soit de copier des donnees sous notre controle a un endroit fixe ou connu.
Une autre idee toujours fondee sur le controle des 4 premiers octets du chunk est de reussir a passer l'appel a termdd!IcaBufferalloc, et d'obtenir une autre erreur dans les fonctions appelees ulterieurement (rdpwd!CreateDetachUserInd par exemple). Je ne suis arrive a rien ici non plus.
Au niveau usermode, l'instance de svchost.exe en question abrite des services RPC qui peuvent s'averer utiles mais requierent authentification ou ne sont pas accessibles par defaut a distance. Il existe d'autres methodes mais je me suis heurte a des problemes d'alignements, de timing, qui m'ont semble redhibitoires au final.
Conclusion
Evidemment, il me faudrait passer 25 jours supplementaires a travailler sur cette vulnerabilite pour tenter d'en epuiser les possibilites, mais MS12-020 ne me semble pas super exploitable a distance. Localement c'est une autre histoire... Quand j'aurais un peu plus de temps je regarderai Vista/7/2008.
Il est dommage de voir la quantite d'horreurs publiees sur le sujet, de diffs approximatifs, d'erreurs d'interpretations.