Archive for juillet, 2008

Task Scheduler credentials dumper

Maître newsoft à un problème, parfois durant ses pentests de Windows (qu’il réussit toujours, bien évidemment) il voit que l’admin a programmé des taches planifiées, il sait grâce à son immense sagesse que le task manager de Windows a demandé à l’admin son login/pass pour pouvoir lancer la tâche. Donc en toute logique maître newsoft se demande ou sont stocké les credentials de l’admin par le système et surtout si il est possible de les récupérer, le pire c’est qu’il ne connaît aucun tool qui fait cela pour le moment. Il faut dire que dumper la base SAM avec pwdump puis péter les hash NTLM avec John c’est long et chiant, ça serait donc bien d’avoir une technique plus smooth pour obtenir les credentials. C’est à ce moment que moi, simple padawan, intervient, mon but ? Regarder comment fonctionne le Task Manager sous Windows et comprendre comment sont stockés les credentials des tâches planifiées.

C’est partit, première étage, jouer avec l’application et observer son comportement, direction le Panneau de configuration, icône « Tâches planifiées », une GUI se lance qui me demande le nom de la tâche, quelle appli je veux planifiée, à quelle date puis me demande mon login/pass. En parallèle je surveille avec Process Explorer le processus explorer.exe, la Dll mstask.dll (Fichier DLL d’interface du Planificateur de tâches) a été chargée dans explorer.dll. Un coup de IDA suffit à comprendre que c’est juste une Dll qui utilise des RPC pour communiquer avec le provider du Task Scheduler, reste plus qu’a trouver avec quels RPC elle communique.

Pour cela, on peut regarder le paramètre EndPoint passé à la fonction RpcStringBindingCompose car c’est lui qui contient le nom pipe ou du port RPC sur lequel vont se faire les requêtes. Dans mstask.dll on retrouve la string «  »\PIPE\atsvc », un coup de Process Explorer nous indique que ce pique appartient à un processus svchost, il nous manque cependant le nom de la Dll qui crée le serveur RPC. En regardant les descriptions des Dll on peut voir pour schedsvc.dll « Moteur du Planificateur de tâches », un coup d’oeil dans les strings de la Dll (toujours avec Process Explorer) et on retrouve notre « \PIPE\atsvc », ok c’est celle la !

On va utiliser un plugin très pratique pour IDA de Tenable Network Security, Mida pour obtenir l’ensemble des interfaces RPC crée par schedsvc.dll à travers leurs structures MIDL :

---------------------------------------------------
mIDA Plugin v1.0.8
Copyright (C) 2006, Tenable Network Security
---------------------------------------------------

Scanning database for MIDL structures ...
Found MIDL structure at address 0x76B12D48 : 378e52b0-c0a9-11cf-822d-00aa0051e40f v1.0
Opcode : 0x00 , address : 0x76B24F60, name : _SASetAccountInformation
Opcode : 0x01 , address : 0x76B23905, name : _SASetNSAccountInformation
Opcode : 0x02 , address : 0x76B23C32, name : _SAGetNSAccountInformation
Opcode : 0x03 , address : 0x76B24DAF, name : _SAGetAccountInformation
Found MIDL structure at address 0x76B12F90 : 1ff70682-0a51-30e8-076d-740be8cee98b v1.0
Opcode : 0x00 , address : 0x76B262A0, name : _NetrJobAdd
Opcode : 0x01 , address : 0x76B269FD, name : _NetrJobDel
Opcode : 0x02 , address : 0x76B26502, name : _NetrJobEnum
Opcode : 0x03 , address : 0x76B26B0F, name : _NetrJobGetInfo
Found MIDL structure at address 0x76B136C0 : 0a74ef1c-41a4-4e06-83ae-dc74fb1cdd53 v1.0
Opcode : 0x00 , address : 0x76B33493, name : _ItSrvRegisterIdleTask
Opcode : 0x01 , address : 0x76B32D83, name : _ItSrvUnregisterIdleTask
Opcode : 0x02 , address : 0x76B3215C, name : _ItSrvProcessIdleTasks
Opcode : 0x03 , address : 0x76B32151, name : _ItSrvSetDetectionParameters
Number of MIDL structures found: 3

On peut voir que schedsvc fournit 3 types d’interfaces les Sa*, NetrJob* et ItSrv*. Un peu de google et on tombe sur ça mais aussi sur « [MS-TSCH]: AT Service Remote Protocol Specification« , w0ot ces protocoles sont documentés depuis que Microsoft a été obligé de les mettre en ligne. Dans notre cas on a les protocoles SASec et ATSvc pour le dernier on s’en fou. Petite description:

1.3 Protocol Overview (Synopsis)
The Task Scheduler Remoting Protocol is used to register and configure tasks or to query the statu
of running tasks on a remote server. The Task Scheduler Remoting Protocol primarily consists of
three separate remote procedure call (RPC) interfaces:
- Net Schedule (ATSvc)
- Task Scheduler Agent (SASec)
- Windows Vista Task Remote Protocol (ITaskSchedulerService)
All three interfaces use RPC as their transport to configure and manage tasks remotely, as shown
Figure 1. The three interfaces represent a continuum of increasing functionality, with ATSvc providing
rudimentary functionality and ITaskSchedulerService providing the most functionality.
Historically, the ATSvc interface is the oldest. The SASec interface was introduced in Windows 2000
and the ITaskSchedulerService interface made its debut in Windows Vista. The three interfaces
are not independent—they operate on the task store, shared persistent storage for tasks.

Pour le moment avec les interfaces disponibles on ne peut que ajouter/supprimer des jobs, énumérer/définir des infos dessus, il n’y a rien concernant les credentials. Pour info, la commande at.exe utilise la lib netapi32 (Schedule Functions) qui fonctionne aussi avec l’interface RPC ATSvc.

Arrivé à ce point on sait donc comment communiquer avec le Task Scheduler, c’est un service qui fournit des interfaces RPC avec une interface de base ATSvc et une plus aboutie SASec. Maintenant il faut creuser dans la Dll schedsvc pour trouver la fonction qui lancer les tâches puis voir comment elle récupère les credentials, IDA est notre ami en plus on accès aux symbols. Après quelques recherches on trouve une fonction intéressante nommée DecryptCredentials de proto :

__int32 __stdcall DecryptCredentials(const struct _RC2_KEY_INFO *, unsigned __int32, unsigned __int8 *, struct _JOB_CREDENTIALS *, int)

Avec au dessus des appels à ReadLsaData, GetCSPHandle (le provider crypto) et ComputeCredentialKey. A partir de là on peut donc penser que la Dll va récup les credentials cryptés dans le provider LSA, demander d’utiliser un custom cryptographic service provider avec les APIS CryptAcquireContext et CryptGenKey sur du RC2 http://en.wikipedia.org/wiki/RC2 puis calculer la clé de chiffrement qui à servit a crypté les credentials. D’après ce que j’ai pu voir cette clé dépend du nom de la machine de son SID et de d’autres valeurs, je ne me suis pas amusé à rentrer dans les détails, la flemme ! En regardant le graph de crossref à DecryptCredentials on peut voir :

Regardez la chaines de calls MainServiceLoop->RunLogonJobs->RunJobs->RunNtJobs->LogonAccount->GetAccountInformation->DecryptCredentials, cette série de calls montre que pour lancer une tâche, le Task Scheduler va récupérer les credentials du user qui la enregistré, normal :] Pour vérifier cela on se crée un tâche bidon on sort le kernel debugger puis mettre un BP sur DecryptCredentials dans schedsvc.dll. Svchost n’aimant pas du tout se faire debug par OllyDbg. Ensuite, on lance cette tâche à la main on espérant que ca break puis on examine la stack avant et après en espérant trouver des infos intéressantes, surtout on regarde le 4 ème paramètre de DecryptCredentials qui un pointeur sur un structure non documentée _JOB_CREDENTIALS.

1: kd> bp schedsvc!DecryptCredentials
breakpoint 0 redefined
1: kd> g
Breakpoint 0 hit
schedsvc!DecryptCredentials:
001b:77312787 8bff            mov     edi,edi
1: kd> kv
ChildEBP RetAddr  Args to Child
0120ee30 77313c65 0120ee84 00000038 037724c4 schedsvc!DecryptCredentials (FPO: [Non-Fpo])
0120efd0 77317ce1 01ef35f0 0120f00c 0000001c schedsvc!GetAccountInformation+0x174 (FPO: [Non-Fpo])
0120f448 773182c9 01ef35f0 01ef3320 0120f974 schedsvc!LogonAccount+0x1ca (FPO: [Non-Fpo])
0120f938 7730ff5b 01ef3320 00359b80 0120f994 schedsvc!CSchedWorker::RunNTJob+0xfa (FPO: [Non-Fpo])
0120fbe8 773106c6 00359ee8 530995a0 00000102 schedsvc!CSchedWorker::RunJobs+0x304 (FPO: [Non-Fpo])
0120fe74 773109c5 7c80a6a9 00000000 00000000 schedsvc!CSchedWorker::CheckDir+0x36a (FPO: [Non-Fpo])
0120ff28 77310e9c 7730b532 00000000 000c979c schedsvc!CSchedWorker::MainServiceLoop+0x2b1 (FPO: [Non-Fpo])
0120ff2c 7730b532 00000000 000c979c 00099894 schedsvc!SchedMain+0xb (FPO: [1,0,0])
0120ff5c 7730b63f 00000001 000c9798 0120ffa0 schedsvc!SchedStart+0x266 (FPO: [Non-Fpo])
0120ff6c 010011cc 00000001 000c9798 00000000 schedsvc!SchedServiceMain+0x33 (FPO: [Non-Fpo])
0120ffa0 77deb48b 00000001 000c9798 0007e898 svchost!ServiceStarter+0x9e (FPO: [Non-Fpo])
0120ffb4 7c80b683 000c9790 00000000 0007e898 ADVAPI32!ScSvcctrlThreadA+0x12 (FPO: [Non-Fpo])
0120ffec 00000000 77deb479 000c9790 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])

1: kd> dd esp
0120ee34  77313c65 0120ee84 00000038 037724c4
0120ee44  0120f00c 00000001 0120f994 0120f974
0120ee54  00000000 00000000 00000078 00000084
0120ee64  03772484 0000004c 00000000 0120ef7c
0120ee74  037a6480 03772478 037c7580 00000000
0120ee84  00000000 00000000 29852de1 9a45f9f4
0120ee94  b347543e 4007221f 263c713a 7bafc308
0120eea4  d1d64357 90b82461 25100c36 eefab642

1: kd> gu
schedsvc!GetAccountInformation+0x174:
001b:77313c65 85c0            test    eax,eax
1: kd> db 0120f00c
0120f00c  04 00 00 00 69 00 76 00-61 00 6e 00 00 00 1d 00  ....i.v.a.n.....
0120f01c  a2 3f 83 7c 68 f5 20 01-02 00 80 00 04 f3 20 01  .?.|h. ....... .
0120f02c  00 00 00 00 d8 f0 20 01-a0 10 08 00 52 d6 6c 53  ...... .....R.lS
0120f03c  8c f0 20 01 c9 55 91 7c-04 00 80 00 04 f4 20 01  .. ..U.|...... .
0120f04c  02 00 80 00 45 09 91 7c-4e 09 91 7c c4 f3 20 01  ....E..|N..|.. .
0120f05c  24 00 02 00 b0 f1 20 01-02 00 00 00 90 41 91 7c  $..... ......A.|
0120f06c  00 d0 fd 7f 05 10 90 7c-58 f0 20 01 00 00 00 00  .......|X. .....
0120f07c  28 f1 20 01 18 ee 90 7c-70 09 91 7c c0 e4 97 7c  (. ....|p..|...|

1: kd> db 0120f00c+22C
0120f238  03 00 00 00 66 00 66 00-73 00 00 00 f8 00 00 00  ....f.f.s.......
0120f248  20 33 ef 01 00 00 00 00-00 00 00 00 00 00 00 00   3..............
0120f258  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0120f268  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0120f278  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0120f288  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0120f298  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0120f2a8  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Alors, « ivan » est mon login et « ffs » mon pass sur ma machine de test. Vous allez me demandez comment j’ai trouvé l’offset 0x22C ? Tout simplement en mattant le dissas de DecryptCredentials qui faisait des memcpy sur ces offsets. Dans tout les cas, on le login et le pass en clair qui apparaissent à la sortie de DecryptCredentials, cool ! On peut donc reconstituer la structure JOB_CREDENTIALS par :

typedef struct _JOB_CREDENTIALS
{
	ULONG LoginLen; //+0
	WCHAR Login[256]; //+4

	UCHAR Padding[40];

	ULONG PassLen; //+0x22C
	WCHAR Pass[256];
}JOB_CRENDENTIALS, *PJOB_CREDENTIALS;

Si on regarde plus loin dans le binaire on voit que la tâche est lancée avec CreateProcessAsUser avec un token fournit par LogonUser.

Maintenant on va prendre un café et on pense. On sait que les credentials sont gérés par le provider LSA et qu’ils sont chiffrés par un algo RC2 avec une clé dépendant de la machine. Une première tentative d’attaque pourrait consister à reproduire le fonctionnement de schedsvc en allant obtenir les credentials dans LSA, recalculer la clé de chiffrement et recoder DecryptCredentials, bref un travail super long qui en plus requiert d’avoir le token « SYSTEM » pour aller taper les credentials de LSA, c’est mort ! Une approche plus smooth serait de tout simplement hooker la fonction DecryptCredentials dans schedsvc.dll en injectant un Dll dedans, même si cela requiert le SeDebugPrivilege on en revient à un pwdump qui fait lui aussi une injection de Dll sauf que c’est dans lsass.exe. Problème c’est que l’appel à la fonction DecryptCredentials se fera uniquement lorsque la tâche sera lancée c’est à dire aussi bien dans 1 minute que dans 1 mois, pas super pour un pentest surtout que le newsoft c’est un nerveux. Par contre si vous regardez attentivement le graph de crossrefs à DecryptCredentials vous pouvez voir quelle est appelée par GetNSAccountInformation et GetAccountInformation, intéressant car ces APIs font partie de celles fournies par l’interface RPC.

On pourrait donc monter une attaque qui consisterait à injecter une dll dans le svchost qui héberge schedsvc.dll, hooker la fonction DecryptCredentials, faire un appel RPC sur SaGetAccountInformation ou SaGetNSAccountInformation puis lire la sortie de DecryptCredentials, cela nous éviterait d’attendre que la tâche se lance pour obtenir les credentials, parfait !

Reste un gros souci que je n’ai toujours pas réussi à corriger, la fonction DecryptCredentials n’est pas exportée par schedsvc.dll et ne se trouve pas non plus près d’une fonction exportée. Le problème c’est que pour procéder à notre hook nous devons connaître son adresse précise dans le process svchost.exe. Pour le moment j’utilise donc un offset hardcodé relatif à l’ImageBase de schedsvc.dll pour calculer l’adresse de cette fonction, ouais je sais c’est moche, je vais voir si il est possible de faire une recherche par pattern matching dans le binaire pour avoir quelque chose de plus générique.

Arrivé à ce niveau on peut designer proprement l’attaque :

  1. Vérifier qu’on peut obtenir le SeDebugPrivilege sur la machine pour pouvoir jouer avec les processes des autres accounts.
  2. Vérifier que le service du Task Scheduler tourne sur la machine, pour cela on utilise les api du SCM (Service Control Manager) en regardant l’état du service nommé « Schedule », si il n’est pas actif on arrête.
  3. Vérifier qu’il y a des tâches dans le gestionnaire. On peut utiliser la méthode Enum de la classe IEnumWorkItems, si il n’y pas de tâche on stop.
  4. Retrouver le processus svchost.exe qui a chargé la Dll schedsvc.dll, pour cela on énumère les processus avec CreateToolHelp32Snapshot puis leurs modules avec EnumProcessModules.
  5. Pour pouvoir monitorer les messages provenant de la Dll qui va être injectée j’ai décidé de surveiller le buffer fournit sous forme de mapped section servant aux debug strings. La Dll va donc écrire dedans avec l’API OuputDebugString et notre process va crée un thread chargé de monitorer ce buffer, ce thread va filtrer la provenance des messages afin de ne conserver que ceux qui proviennent du process svchost contentant la Dll schedsvc.dll.
  6. On inject la Dll dans le bon svchost, elle va hooker la fonction DecryptCredentials avec un inline hook. Pour réaliser le hook j’ai utilisé la lib detours de Miscrosoft superbement bien foutue. La fonction qui remplace DecryptCredentials ne fait juste qu’appeler la vraie fonction DecryptCredentials puis dumper les champs Login et pass de la structure JOB_CRENDENTIALS avec des OutputDebugString qui seront filtré par la suite avec le thread du process injecteur.
  7. Pour trigger la fonction DecryptCredentials on demande d’obtenir les noms des users ayant crée les taches. J’ai choisit arbitrairement d’utiliser l’interface SAGetNSAccountInformation qui peut être appelé avec la méthode GetAccountInformation de la classe IScheduledWorkItem.
  8. Le hook nous renvoie les credentials, on les affiche, newsoft est content, il peut faire un beau rapport!
  9. On n’oublie pas bien sur de virer le hook et la Dll injectée afin de rendre quelque chose de propre après l’attaque.

Plusieurs utilisateurs peuvent en enregistrer des tâches planifiées, ce qui cool c’est que chaque user peut voir les tâches planifiées des autres. Cela veut dire que l’attaque va aussi dumper les credentials des tâches installés par les autres utilisateurs, et ça, c’est priceless ! Dans l’exemple suivant j’ai crée 2 tâches, chacune avec un user différent, le premier ivan:ffs, le second barp:rofl. Je crois que l’output de l’attaque parle de lui même :

Windows Task Scheduler credentials dumper
By Ivanlef0u, thanks to Nicolas Ruff
BE M4D!
Works on Windows XP SP3 and schedsvc.dll version 5.1.2600.5512
    /|
\`O.o'
=(_|_)=
    U
There is 2 tasks in queue
Current tasks are :
Command Prompt
barp
Error with EnumProcessModules : 299
schedsvc.dll found in process [svchost.exe:332]
Task Scheduler svchost pid is : 332
Dll to inject : C:\Test\tasksched\TaskPwdDmpDll.dll
Injecting DLL in process : 332
[Msg from PID 332]-> Dll injected in process 332

[Msg from PID 332]-> schedsvc dll is at : 0x77300000

[Msg from PID 332]-> Hooking DecryptCredentials at 0x77312787

[Msg from PID 332]-> Credentials barp:rofl

[Msg from PID 332]-> Credentials ivan:ffs

Dll successfully unloaded

Au final, on a une attaque qui permet de récup les credentials stockés en local par les users de la machine à travers les tâches planifiées. Je pense que cela est toujours pratique dans le cadre d’une intrusion même si 2 fortes contraintes interviennent, la première est le besoin du tool du SeDebugPrivilege, la seconde la difficulté de retrouver l’adresse de DecryptCredentials. Je vais essayer de travailler sur le second point.

En attendant je vous livre les sources de l’outil TaskPwdDump ainsi que les binaires. Pour l’utilisation il vous suffit de mettre le binaire TaskPwdDump.exe et la Dll TaskPwdDumpDll.dll dans le même dossier. Le tout ce DL ici :
http://ivanlef0u.fr/repo/TaskPwdDmp.rar

N’hésitez pas à me contacter pour tout problème rencontré.
Enjoy !
Au final je tient à remercier maître newsoft pour son aide sur le dev de cet outil !

Sinon Converge en concert, ca défonce !

22 comments juillet 25th, 2008

SMM

Prochainement à BlackHat aura lieu une conf abordant le thème du SMM (System Management Mode) ce fameux mode du CPU qui a fait tant parlé de lui depuis que Loic Duflot a présenté une conf à CanSecWest 2k6. Au point même qu’on en retrouve une colonne sur SecurityFocus et un article dans le dernier Phrack, bref tout cela a provoqué un bon petit buzz tout comme celui de Kris Kaspersky sur sa future conf de HITB sur l’exploitation de bugs dans les CPU en remote, miam ! Sur le papier le SMM à l’air vraiment cool, ca déchire sa race et ca poutre des loutres comme on dit nous les jeunes cependant je vais vous expliquer pourquoi je suis sceptique à propos de la conf de Sherri Sparks et Shawn Embleton à BlackHat.

Tout d’abord le SMM c’est quoi ? Le SMM est un mode existant sur les CPU x86 qui vient s’ajouter à ceux déjà existant, qui à la particularité d’être exécuté sans que l’OS en soit conscient, contrairement aux autres ou l’OS peut contrôler l’activation ou l’arrêt. C’est justement tout l’intérêt du SMM, pourvoir agir sur la mémoire et les périphériques sans que l’OS le sache, lorsque le CPU en SMM toutes les interruptions, exceptions et même les NMIs sont masquées à noter que le SMM ne peut pas être réentrant ce qui signifie donc que les autres SMIs sont masquées aussi. Le SMM est utilisé par des routines critiques comme celle qui est appelée lorsque le CPU chauffe trop pour l’éteindre ou bien pour gérer les erreurs sur le bus processeur, le fameux FSB.

Lorsque le CPU passe en SMM il se retrouve en real-mode, le bit 0 du cr0 (PE, Protection Enable) est à 0. Le real-mode exécute du code 16 bits en ayant une gestion de la segmentation et des exceptions réalisée de façon plus basique. Le calcul de l’adresse virtuelle (qui est en fait l’adresse physique car il n’y à pas de pagination) est effectué en décalant le segment selector courant (CS, DS, ES ou SS) de 4 bits puis en l’ajoutant à l’offset, ce qui au final donne une adresse sur 20 bits, soit un espace d’adressage allant de 0h à 10FFEFh (1Mo+64Ko). A noter que le support des segments FS et GS ainsi que la possibilité d’exécuter des instructions 32 bits est arrivé plus tard, pour plus d’info vous pouvez lire le « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide« .

Pour les exceptions on retrouve aussi une table des interruptions sauf que celle-ci porte le nom d’IVT (Interrupt Vector Table). C’est une table de 256 entrées dont les champs font 4 bytes, 2 pour l’offset du handler et 2 bytes pour le nouveau segment selector CS.

C’est donc dans ce contexte que s’exécute le SMM. Le passage du processeur en SMM se fait à travers une SMI (System Management Interrupt), ce n’est pas une interruption comme les autres dans le sens ou elle n’est pas gérée par un handler de l’IDT, c’est un signal envoyé au CPU sur le pin in SMI# depuis le bus APIC. Remarquez que le lorsque le CPU est en SMM il notifie le northbrige avec le pin SMIACT# ceci aura de l’importance pour la suite. Enfin, la sortie du SMM se fait en appelant l’instruction RSM, celle ci va juste restaurer le contexte du CPU qui a été sauvegardé en mémoire lors du passage en SMM.

Le code et des données SMM sont stocké dans une zone mémoire spéciale appelée la SMRAM, cette zone mémoire de 64 Ko commençant par défaut à l’adresse physique 0×30000 (SMBASE) est uniquement accessible lorsque le CPU est en SMM. Lors du passage en SMM le CPU sauvegarde son contexte dans la zone allant de [SMBASE+0xFE00] jusqu’à [SMBASE+0xFFFF] il va ensuite exécuter les instructions situées à partir de [SMBASE+0x8000] le reste de la SMRAM est libre et dépend de votre BIOS car c’est lui qui initialise cette zone mémoire lors du démarrage. En temps normal, la SMRAM pointe vers la mémoire vidéo.

Il est possible de modifier la SMBASE en changeant le registre SMBASE (offset SMBASE+0x7EF8) qui est sauvegardé lors du passage en SMM, le retour avec l’instruction RSM mettra à jour ce registre interne du CPU et les prochaines SMIs prendront en compte la nouvelle SMBASE. D’après ce que j’ai pu voir sur les systèmes actuels la SMRAM est relogée en 0xA0000. Il est même possible d’accéder à un espace mémoire de 4Go depuis le SMM avec l’aide d’un préfixe sur les instructions et adresses, le Operand-size override prefix (0×66) et le Address-size override prefix (0×67).

Finit l’introduction, passons aux choses sérieuses. Un attaquant aurait ainsi 2 raisons d’utiliser le SMM :

  1. Avoir son code placé dans une zone mémoire inaccessible en temps normal par les HIPS et qui récemment encore était inconnu du public.
  2. Travailler avec un niveau de privilège élevé sur la machine en ayant avec un accès total au CPU, à la mémoire et aux périphériques permettant de contourner toutes les protections mises en place par l’OS.

C’est donc sur ces points qu’insistent les conférences de Loic Dufloc et très certainement celle de BlackHat aussi. En fait le vrai problème si situe au niveau de l’accès à cette fameuse mémoire SMRAM, si un attaquant veut exécuter du code en il va devoir y écrire mais comme je l’ai dit précdemment la SMRAM est uniquement accessible lorsque le CPU est en SMM, donc pour le moment est bloqué. Serait-ce la fin de notre voyage ? Pas forcément, il est quand même possible de taper dans la SMRAM, cependant pour cela il faut descendre un peu plus bas sur la carte mère en allant jouer avec le nortbridge.

Le northbridge ce chipset faisant le lien entre votre CPU, votre bus PCI Express (ou AGP), votre SDRAM et le southbridge, contrôle en effet l’accès à la SMRAM à travers la notification par le pin out du CPU SMIACT#. En temps normal les adresses physiques de 0xA0000 à 0xBFFFF sont des memory mapped I/O gérées par le bus PCI. Sur l’image suivante on voit que ces adresses sont routées vers le chipset Intel Q965/Q963 qui est mon northbridge.
pci

Hop on récupère datasheet du chipset sur le site de Intel et on passe en mode RTFM. A l’aide de la doc et des papiers sur le sujet on apprend qu’il existe un registre dans le northbridge qui sert à contrôler l’accès à la SMRAM, le SMRAMC (System Management RAM Control). Pour accéder au registre de quelconque périphérique sur un bus PCI on doit connaître l’indice du bus, l’indice du device, l’indice de la fonction du device et l’index du registre à lire/écrire, dans mon cas les devices accessibles depuis le northbridge peuvent être représenté par le schéma suivant :
bus

Pour pouvoir accéder à la configuration de ces devices le mécanisme est assez simple : Il existe 2 registres d’I/O, le CONF_ADDR (4 bytes en 0xCF8)et le CONF_DATA (4 bytes en 0xCFC). Le premier sert à choisir le bus, le device, la fonction et le registre du device à accéder, le second sert à effectuer l’opération d’I/O. On définir ces registres à l’aide des instructions IN et OUT. Sachant que le SMRAMC est sur le bus 0, device 0, function 0, registre 0x9D, on définit le CONF_ADDR de la façon suivante :

#define MAKE_CONF_ADDR(Reg, Fct, Dev, Bus, Enable) (((Reg)&0xfc) | (((Fct)&7)<<8) | (((Dev)&31)<<11) | ((Bus&0xff)<<16) | ((Enable)<<31))
#define SMRAM 0x9D
ConfReg=MAKE_CONF_ADDR(SMRAM&0xFFFC, 0, 0, 0, 1); // On aligne notre I/O sur une adress multiple de 4

Problème, l’I/O ne fonctionne pas en user-land. Faire une I/O demande que le CPL (Current Privilege Level) soit inférieur ou egal à l’IOPL (I/O Privilege level, bits 12 et 13 de l’EFlags), comme par hasard on a par défautl un IOPL à 0 et un CPL de 3 en user-land. Une solution simple serait de coder un driver pour les I/O mais la flemme. Après quelques recherches sur le net j’ai trouvé une solution plus élégante : L’api native ZwSetInformationProcess permet avec l’InformationClass ProcessUserModeIOPL (16) de modifier l’IOPL du thread courant. On code un sample et là bim ! Echec ! La fonction nous renvoie le numéro d’erreur 0xC0000061 (STATUS_PRIVILEGE_NOT_HELD), en effet comme le spécifie l’article de Michael Wookey il faut que le token du process possède le privilège SeTcbPrivilege. Celui-ci peut être attribuer en allant dans Panneau de configuration -> Outils d’administration -> Stratégie de sécurité locale -> Stratégies locales -> Attribution des droits utilisateurs et en ajoutant votre utilisateur dans la stratégie « Agir en tant que partie du système d’exploitation » (après avoir ajouter votre user il faut vous relancer votre session). Voilà le bout de code :

BOOL EnablePrivilege(PTCHAR Privilege)
{
	BOOL rc=FALSE;
	HANDLE hToken;
	LUID luid;
	TOKEN_PRIVILEGES tokenPrivilege;

	//
	//  Open the current process' token.
	//
	rc=OpenProcessToken(
		GetCurrentProcess(),
		TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
		&hToken);
	if(rc)
	{
		rc=LookupPrivilegeValue(NULL, Privilege, &luid);
		if(rc)
		{
			tokenPrivilege.PrivilegeCount = 1;
			tokenPrivilege.Privileges[0].Luid = luid;
			tokenPrivilege.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

			//
			//  Assign the given privilege.
			//
			rc=AdjustTokenPrivileges(
			hToken,
			FALSE,
			&tokenPrivilege,
			sizeof(tokenPrivilege),
			NULL,
			NULL);
		}
	}

	if(hToken)
		CloseHandle(hToken);

	return rc;
}

BOOL EnableUserModeHardwareIO()
{
	NTSTATUS Status;
    ULONG IOPL=3;

	//
	//  Enable SeTcbPrivilege
	//
	if(!EnablePrivilege(SE_TCB_NAME))
		return FALSE;

	//
	//  Grant user mode hardware IO access.
	//
	Status=ZwSetInformationProcess(
		GetCurrentProcess(),
		ProcessUserModeIOPL,
		&IOPL,
		sizeof(IOPL));
	if(!NT_SUCCESS(Status))
	{
		printf("Error with ZwSetInformationProcess : 0x%x\n", Status);
		return FALSE;
	}

	return TRUE;
}

Dorénavant on sait comment faire des I/O depuis le user-land, c’est beaucoup plus smooth que de coder un driver. Revenons donc à ce registre SMRAMC, ce qui nous intéresse c’est la valeur des bits D_OPEN et D_LCK, ceux qui contrôlent l’accès à la SMRAM. Par défaut le D_OPEN est à 0, signifiant que tout accès à une adresse comprise entre 0xA0000 et 0xBFFFF sera renvoyé vers la mémoire vidéo, par contre lorsque ce bit est à 1, le northbridge reroute les accès vers la SMRAM, c’est ce qui se passe lorsque le CPU est en SMM. Pour info voici à quoi ressemble le registre SMRAMC :

La question de-la-mort-fatale-atomique-qui-tue-avec-des-chocapicz est : Peut-on modifier ce bit librement pour accéder à la SMRAM sans être en SMM ?
La réponse est oui ! On peut avoir accès à la SMRAM ! Une fois le droit d’écriture obtenu il est possible d’ajouter notre propre handler en [SMBASE+0x8000] et hop le tour est joué ! Evidemment il faut prendre quelques précautions en écrivant un handler de SMI mais ca se fait. Je ne vais rentrer dans les détails de ce qu’il est possible de faire en SMM, vous avez les papers de phrack et de Duflot pour ça, nan je vais plutôt commencer à pleurer. En effet après avoir codé un tool qui dump le registre SMRAMC j’obtiens les résultats suivants :

Windows SMRAMC and ESMRAMC registers dumper
Only works on Intel northbridge chipsets
By Ivanlef0u
BE M4D!

Reading VID-Vendor Identification Register
Writing CONF_ADDR register with 0x80000000
CONF_DATA register 0x8086
Reading SMRAM and ESMRAMC register
Writing CONF_ADDR register with 0x8000009c
CONF_DATA register 0xb81a
Dumping SMRAM register ...
RESERVED0_BIT : 0
D_OPEN_BIT : 0
D_CLS_BIT : 0
D_LCK_BIT : 1
G_SMRAME_BIT : 1
C_BASE_SEG2_BIT : 0
C_BASE_SEG1_BIT : 1
C_BASE_SEG0_BIT : 0
Writing CONF_ADDR register with 0x8000009c
CONF_DATA register 0xb81a
Dumping ESMRAMC register ...
T_EN_BIT : 0
TSEG_SIZE_BIT0 : 0
TSEG_SIZE_BIT1 : 0
SM_L2_BIT : 1
SM_L1_BIT : 1
SM_CACHE_BIT : 1
E_SMERR_BIT : 0
H_SMRAME_BIT : 1

Regardez bien le bit D_LCK, il est à 1 ! Il verrouille donc le bit D_OPEN qui ne peut plus être modifié par l’utilisateur … YOU FAILED ! Ce qui est drôle c’est que tous les chipsets que j’ai pu tester avaient ce bit de lock à 1, m’empêchant ainsi de progresser dans mon ambition de conquête du monde. Juste un mot sur le registre ESMRAMC (Extended System Management RAM Control), pour faire court c’est une extension du registre SMRAMC, il permet de gérer le caching de la SMRAM ou bien un remapping de la SMRAM dans les adresses hautes.

Pour le moment on est au point mort, pas moyen de dumper cette foutue SMRAM. Loic Duflot propose dans sa thèse de contourner le bit D_OPEN en utilisant une feature du chipset AGP, l’ouverture graphique ou en anglais « aperture« , disponible sur les chipsets plus anciens. L’aperture permet au chipset AGP de faire croire à la carte graphique que le FrameBuffer est une zone contigue dans la mémoire physique alors qu’en réalité elle ne l’est pas, pour cela le chipset AGP agit comme un TLB (Translation Lookaside Buffer) mais au niveau des adresses physiques en les remappant vers d’autres adresses physiques. L’attaque pour contourner le bit D_LCK consiste en gros à crée une table de translation représentant la fonction identité, c’est à dire qui mappe chaque page sur elle-même, puis, comme si de rien n’était faire une écriture ou une lecture dans la SMRAM. Le chipset croyant qu’on tente un accès dans la mémoire vidéo va translater les adresses en fonction des TLB du chipset, en sachant que l’aperture est prioritaire sur la SMRAM la notre translation s’effectue sans problème nous laissant ainsi libre accès à la SMRAM. Dans la réalité cette technique est un peu plus compliqué que ça, d’ailleurs je pense que je n’ai pas tout parfaitement compris mais l’idée est là :p Au passage je fais de la pub pour les posts d’un pote qui à fait un travail intéressant sur le PCI et bootvid.dll.

Alors, de mon coté sur ma carte-mère je n’ai pas d’AGP, j’ai du PCI-Express, en lisant la doc on apprend que le mécanisme d’aperture n’existe plus avec le PCI-Express. Par contre le chipset Q963/Q965 possède un Integrated Graphics Device (IGD) une sorte de carte graphique de base qu’on peut configurer à travers le bus 0, device 2, function 0 (voir le schéma du bus PCI plus haut). Cet IGD offre la possibilité de mettre en place une Graphics Translation Table (GTT), un mécanisme d’aperture, W0ot ! Il ne reste plus qu’a faire le même type d’attaque qu’avec l’aperture AGP et c’est gagné. Sauf que la doc dit :

3.8.7 SMM Access Through GTT TLB (Intel 82Q965, 82Q963, 82G965 GMCH Only)

Accesses through GTT TLB address translation to enabled SMM DRAM space are no
allowed. Writes will be routed to memory address 000C_0000h with byte enables d
asserted and reads will be routed to memory address 000C_0000h. If a GTT TLB
translated address hits enabled SMM DRAM space, an error is recorded.

PCI Express and DMI Interface originated accesses are never allowed to access SM
space directly or through the GTT TLB address translation. If a GTT TLB translated
address hits enabled SMM DRAM space, an error is recorded.

PCI Express and DMI Interface write accesses through GMADR range will be snoop
Assesses to GMADR linear range are supported. PCI Express and DMI interface tile
and tileX writes to GMADR are not supported. If, when translated, the resulting
physical address is to enabled SMM DRAM space, the request will be remapped to
address 000C_0000h with de-asserted byte enables.

PCI Express and DMI Interface read accesses to the GMADR range are not support
therefore, will have no address translation concerns. PCI Express and DMI interfac
reads to GMADR will be remapped to address 000C_0000h. The read will complete
with UR (unsupported request) completion status.

Pan ! Dans les dents ! L’accès à la SMRAM avec la GTT est vérifié, YOU FAILED AGAIN ! Y’en a marre … j’abandonne.

Après toutes ces tentatives je me demande comment actuellement avec les derniers chipsets il est possible d’accéder à la SMRAM sans être en SMM. C’est pourquoi je suis si sceptique sur la prochaine conf de Sherri Sparks et Shawn Embleton parce que le bit D_LCK est à 1 sur tous les chipsets récents et que les accès à la SMRAM sont sévèrement contrôlés. On peut très bien supposer qu’ils aient trouvé une technique géniale qui permet d’accéder à la SMRAM même si ces protections sont activées et là c’est de la balle mais j’avoue en douter fortement. Je dirais plus qu’ils ont fait leurs expériences sur des b0x débridées leurs offrant un accès sans problèmes à la SMRAM. Au final on se retrouvait avec une technique de furtivité de code impressionnante mais qui marche uniquement sur des machines datant de la guerre des druides. Bref wait and see …

En attendant voici le code qui permet de dumper les registres SMRAC et ESMRAMC :
http://ivanlef0u.fr/repo/smram.rar

Si quelqu’un connaît une technique permettant d’accéder à la SMRAM, je suis très intéressé et prêt à payer en chocapicz !!!

Réf :
http://www.rcollins.org/ddj/Jan97/Jan97.html

6 comments juillet 21st, 2008

Sudami KillMe

Toujours en quête de nouvelles techniques de rootkit pour Windows, je suis tombé en lisant mes RSS sur un POC chinois réalisé par Sudami qui permet de rendre un process immortel. Intéressé par la chose j’ai voulu prendre le code source mais manque de bol d’après le google translate il faut être inscrit et faire partie d’une communauté underground aux yeux bridés pour y avoir accès. Cependant on peut download le binaire librement. Après quelques heures de reverse je vous propose de plonger au coeur de ce nouveau bébé très innovant qui permet de bien faire chier n’importe quel utilisateur et AV. Le must pour un rootkit :]

Dans le .rar, on ne trouve qu’un simple binaire nommé « sudami.exe » qui fait 67,5 Ko, dès le départ on se doute qu’il va devoir charger un driver dans le noyau au vu du titre du post « DKOM to Protect EXE self,without any hook », DKOM signifiant Direct Kernel Object Manipulation on sait que le programme va opérer au niveau noyau. Le binaire est packé avec PECompact 2.x, pour l’unpack rien de très difficile, on trace le programme de SEH en SEH pour tomber sur celui qui saute sur l’OEP (Original Entry Point). Pour info ce saut est en 0x42BF8A (jmp eax). Une fois qu’on est sur l’OEP on peut tranquillement dumper le binaire avec OllyDump ou LordPE+Imprec pour retrouver le binaire d’origine. L’orignal pèse 180 Ko, un coup d’oeil aux ressources avec LordPe nous permet de voir une ressource nommée « SYS » qui, en regardant le dump hexa, commence avec « MZ » (0x4D 0x5A 0×90), ca nous suffit largement pour dire que cette ressource est un binaire, après extraction ce binaire fait 9 Ko. Le champ Subsystem du PE header du binaire extrait vaut 1, ce qui veut dire que celui-ci est un driver. En regardant le plus près le binaire celui-ci va chargé le driver en utilisant le Service Manager avec les APIs OpenSCManager, OpenService et ControlService puis va spawner une window avec plein de mots chinois qu’on comprend pas dedans. Il va ensuite envoyer une série d’IOCTL au driver (0×80002000, 0×80002008, 0×80002010 et 0×80002018) avec l’API DeviceIoControl pour rendre le process immortel. Le mieux est que vous regardiez par vous même le binaire sous IDA ou sous Olly pour voir son fonctionnement. Bref au final on se retrouve avec une popup en chinois qui reste ouverte sans qu’on puisse la killer. Tout cela n’est pas la partie visible de l’iceberg, le vrai stuff intéressant se trouve bien évidemment dans le driver.

Commence donc l’étude du driver, premièrement je le charge en VM histoire de regarder ce qu’il définit comme MajorFunctions et ses Devices :

0: kd> !drvobj \driver\sudami 3
Driver object (815c87e0) is for:
*** ERROR: Module load completed but symbols could not be loaded for sudami.sys
 \Driver\sudami
Driver Extension List: (id , addr)

Device Object list:
813b1490  

DriverEntry:   f9e08005	sudami
DriverStartIo: 00000000	
DriverUnload:  f9e04250	sudami
AddDevice:     00000000	

Dispatch routines:
[00] IRP_MJ_CREATE                      f9e042a0	sudami+0x12a0
[01] IRP_MJ_CREATE_NAMED_PIPE           804f9709	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       f9e042a0	sudami+0x12a0
[03] IRP_MJ_READ                        804f9709	nt!IopInvalidDeviceRequest
[04] IRP_MJ_WRITE                       804f9709	nt!IopInvalidDeviceRequest
[05] IRP_MJ_QUERY_INFORMATION           804f9709	nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION             804f9709	nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                    804f9709	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      804f9709	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               804f9709	nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    804f9709	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      804f9709	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           804f9709	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         804f9709	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              f9e043e0	sudami+0x13e0
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     804f9709	nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    804f9709	nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL                804f9709	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     804f9709	nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT             804f9709	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              804f9709	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                804f9709	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       804f9709	nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              804f9709	nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               804f9709	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 804f9709	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   804f9709	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         804f9709	nt!IopInvalidDeviceRequest


0: kd> !devobj 813b1490 
Device object (813b1490) is for:
 devsudami \Driver\sudami DriverObject 815c87e0
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00000040
Dacl e12a719c DevExt 00000000 DevObjExt 813b1548 
ExtensionFlags (0000000000)  
Device queue is not busy.


0: kd> !object \Global??
Object: e1002898  Type: (817ef418) Directory
    ObjectHeader: e1002880 (old version)
    HandleCount: 1  PointerCount: 126
    Directory Object: e1001be0  Name: GLOBAL??

    Hash Address  Type          Name
    ---- -------  ----          ----
     00  e147efe0 SymbolicLink  D:
         e13173c8 SymbolicLink  NDIS
         e138fbd0 SymbolicLink  DISPLAY1

	 [...................................]
     31  e17b5fe0 SymbolicLink  sudami
     [...................................]

Le driver va donc créer un device nommé sudami avec un SymbolicLink (qui est donc mit dans l’ObjectDirectory \Global??) autorisant la communication depuis le user-land qui s’appelle aussi sudami. On retrouve seulement 2 MajorFunctions, la même pour les IRP_MJ_CREATE et IRP_MJ_CLOSE qui sert juste à complêter l’IRP avec un STATUS_SUCCESS.

0: kd> uf f9e042a0
sudami+0x12a0:
f9e042a0 8bff            mov     edi,edi
f9e042a2 55              push    ebp
f9e042a3 8bec            mov     ebp,esp
f9e042a5 8b450c          mov     eax,dword ptr [ebp+0Ch] ; IRP
f9e042a8 c7401800000000  mov     dword ptr [eax+18h],0 ; IRP.IoStatus.Status=0=STATUS_SUCCESS
f9e042af 8b4d0c          mov     ecx,dword ptr [ebp+0Ch] ; IRP
f9e042b2 c7411c00000000  mov     dword ptr [ecx+1Ch],0 ; IRP.IoStatus.Information=0
f9e042b9 32d2            xor     dl,dl
f9e042bb 8b4d0c          mov     ecx,dword ptr [ebp+0Ch]
f9e042be ff151860e0f9    call    dword ptr [sudami+0x3018 (f9e06018)] (IofCompleteRequest)
f9e042c4 33c0            xor     eax,eax
f9e042c6 5d              pop     ebp
f9e042c7 c20800          ret     8

Enfin on a la MajorFunction qui gère les IOCTL en 0xf9e043e0. Maintenant il est temps de sortir IDA pour analyser la routine DispatchDeviceControl.

Première chose qui saute aux yeux et nous fait pissé du sang, le code du driver est offusqué, il contient plein de junk code posé dans les fonctions et IDA n’apprécie pas du tout pour l’analyse. Juste pour déconner voici la gueule du DriverEntry du driver :

.text:000110B0
.text:000110B0                ; Attributes: bp-based frame
.text:000110B0
.text:000110B0                sub_110B0       proc near               
.text:000110B0
.text:000110B0                var_18          = dword ptr -18h
.text:000110B0
.text:000110B0 8B FF                          mov     edi, edi
.text:000110B2 55                             push    ebp
.text:000110B3 8B EC                          mov     ebp, esp
.text:000110B5 83 EC 18                       sub     esp, 18h
.text:000110B8 C7 45 E8 00 00+                mov     [ebp+var_18], 0
.text:000110BF
.text:000110BF                loc_110BF:                              
.text:000110BF 8D 05 C5 10 01+                lea     eax, loc_110C5
.text:000110C5
.text:000110C5                loc_110C5:                             
.text:000110C5 83 C0 0E                       add     eax, 0Eh
.text:000110C8 EB 05                          jmp     short loc_110CF
.text:000110CA                ; ---------------------------------------------------------------------------
.text:000110CA EB F3                          jmp     short loc_110BF
.text:000110CA                ; ---------------------------------------------------------------------------
.text:000110CC DB 30 57                       db 0DBh, 30h, 57h
.text:000110CF                ; ---------------------------------------------------------------------------
.text:000110CF
.text:000110CF                loc_110CF:                             
.text:000110CF FF E0                          jmp     eax
.text:000110CF                sub_110B0       endp
.text:000110CF
.text:000110CF                ; ---------------------------------------------------------------------------
.text:000110D1 E9 21 74                       db 0E9h, 21h, 74h
.text:000110D4 03 75 01 E8 8B+                dd 0E8017503h, 0C708458Bh
.text:000110DC                ; ---------------------------------------------------------------------------
.text:000110DC 40                             inc     eax

On retrouve ce schéma de junk dans quasiment toutes les fonctions du driver, l’auteur s’est dit qu’il y aurait bien des gars capables de reverser son driver pour piquer sa technique donc il ajouter du junk dans le code à la main en insérant des macros toutes les 2 lignes de code C pour rendre la tâche du reverser plus longue mais pas impossible ! En effet, en regardant de plus près on a un « lea eax, loc_110C5″ suivit d’un « add eax, 0EH », eax vaut donc loc_110C5+0EH. Ensuite nous avons un « jmp short loc_110CF » qui saute sur un « jmp eax », nous devons donc normalement arriver en loc_110C5+0EH et la IDA n’arrive pas analyser correctement, c’est l’échec :] En 0x110D3 (0x110C5+0xE) on peut voir l’opcode 0×74 qui correspond à un JZ et en 0x110D5 on peut voir l’opcode d’un JNZ (0×75), le plus drôle c’est que le JZ saute 3 bytes plus loin et le JNZ 1 byte plus loin, donc tous les deux sautent au même endroit, c’est à dire en 0x110D8. Hop on nettoie ca sous IDA pour obtenir le code clarifié :

.text:000110B8 C7 45 E8 00 00+    mov dword ptr [ebp-18h], 0
.text:000110BF 8D 05 C5 10 01+                lea     eax, loc_110C5
.text:000110C5
.text:000110C5                loc_110C5:                              
.text:000110C5 83 C0 0E                       add     eax, 0Eh
.text:000110C8 EB 05                          jmp     short loc_110CF
.text:000110C8                ; ---------------------------------------------------------------------------
.text:000110CA EB                             db 0EBh ; Ù
.text:000110CB F3                             db 0F3h ; ¾
.text:000110CC DB                             db 0DBh ; ¦
.text:000110CD 30                             db  30h ; 0
.text:000110CE 57                             db  57h ; W
.text:000110CF                ; ---------------------------------------------------------------------------
.text:000110CF
.text:000110CF                loc_110CF:                             
.text:000110CF FF E0                          jmp     eax
.text:000110CF                ; ---------------------------------------------------------------------------
.text:000110D1 E9                             db 0E9h
.text:000110D2 21                             db  21h ; !
.text:000110D3                ; ---------------------------------------------------------------------------
.text:000110D3 74 03                          jz      short loc_110D8
.text:000110D5 75 01                          jnz     short loc_110D8
.text:000110D5                sub_110B0       endp
.text:000110D5
.text:000110D5                ; ---------------------------------------------------------------------------
.text:000110D7 E8                             db 0E8h ; Þ
.text:000110D8                ; ---------------------------------------------------------------------------
.text:000110D8
.text:000110D8                loc_110D8:                              
.text:000110D8                                                        
.text:000110D8 8B 45 08                       mov     eax, [ebp+arg_0]
.text:000110DB C7 40 38 A0 12+                mov     dword ptr [eax+38h], offset sub_112A0
.text:000110E2 8D 05 E8 10 01+                lea     eax, loc_110E8

Le junk consiste donc à un jmp sur 2 jumps conditionnels qui ensemble forment le même saut, jump if zero (JZ) qui saute si le ZeroFlag de l’EFlags est à 1 et jump if not zero (JNZ) qui fait l’inverse. On peut donc remplacer tout ce code par des NOP vu qu’il est inutile et regrouper les NOPs dans un tableau pour obtenir un disass clair. Après avoir fait le ménage on obtient :

.text:000110B8 C7 45 E8 00 00+    mov dword ptr [ebp-18h], 0
.text:000110B8 00 00          ; ---------------------------------------------------------------------------
.text:000110BF 90 90 90 90 90+    db 19h dup(90h)
.text:000110D8                ; ---------------------------------------------------------------------------
.text:000110D8 8B 45 08           mov eax, [ebp+8]

Ce qui est clairement plus lisible ! Il ne reste plus qu’a automatiser la tâche avec un script IDC. Pour le script j’ai fait simple, j’ai recherché tout les « jmp eax » dans le binaire avec la function FindBinary, j’ai noppé les instructions autour qui ne servaient à rien puis regrouper les NOPs dans un tableau. Voici le script très simple :

//
// Cleaner for driver http://hi.baidu.com/sudami/blog/item/1fe5b203005f45e909fa9368.html
// Ivanlef0u
//

#include <idc.idc>

static main()
{
	auto i;
	auto addr;
	
	Message("Starting clean script ...\n");

	addr=0;

	do
	{
		//
		// jmp eax= 0xFF 0xE0
		//
		addr=FindBinary(addr, SEARCH_DOWN, "FF E0");
		Message("jmp eax at : 0x%x\n", addr);
		
		//
		// 16 bytes avant le "jmp eax" commence le junk et se termine 9 bytes plus loin
		//
		for (i=0; i<16+9; i++)
			PatchByte(addr-16+i, 0x90);
	
		//
		// Instrution suivante 
		//	
		addr=addr+2;
	}while(addr!=(-1+2)); //((addr=-1 lorsque plus rien)

	addr=0;
	
	//
	// Met les nop dans des tableaux
	//
	do
	{
		addr=FindBinary(addr, SEARCH_DOWN, "90");
		Message("nop at : 0x%x\n", addr);
		
		for(i=0; i<25; i++)
		{	
			MakeUnknown(addr+i, 1, DOUNK_SIMPLE);
		}	
		MakeArray(addr, 25);
		
		//
		// Instrution suivante 
		//	
		addr=addr+26;
	}while(addr!=(-1+26)); //((addr=-1 lorsque plus rien)
}

Une fois qu'on a nettoyé le driver on peut enfin commencer à analyser son fonctionnement, nous allons juste nous intéresser à la fonction DispatchDeviceControl. On sait que le driver sert à rendre le process immortel, il va donc devoir agir sur la structure EPROCESS et très certainement sur les structures ETHREADs correspondant aux threads du process. Pour analyser la routine de dispatch des IOCTLs nous allons juste suivre les fonctions appelées en fonction des IOCTLs passé au driver.

Commençons par le premier IOCTL (0x80002000) celui-ci nous va utliser IoGetCurrentProcess pour récupérer l'EPROCESS du process courant, donc celle du programme à protéger et à partir du pointeur sur la double liste chainée ThreadListHead de la KPROCESS (offset KPROCESS+0x50) va parcourir la liste de structures ETHREAD représentant les threads du process pour changer le champ KernelApcDisable (offset 0xD4 de la KTRHEAD), si ce champ est à 0 alors il le passe à la valeur 0xA98AC7 (grut?!). En regardant les symbols on peut voir que ce champ fait 2 bytes, alors d'après ce que j'ai compris ce champ agirait comme un compteur. Je m’explique lorsqu'on envoie un APC (Asynchronous Procedure Call) à un thread 3 routines sont mises en place, une KernelRoutine qui est appelée directement en préemptant le thread en kernel-land à un IRQL de APC_LEVEL, une NormalRoutine qui est généralement appelée en user-land lorsque le thread est en state Waiting puis une RundownRoutine qui est la routine qui sera appelée lorsque le thread sera killé et qui à pour rôle de faire le ménage sur les APC qui n'auront pas été exécutés. La fonction KiInsertQueueApc va vérifier la valeur du champ KernelApcDisable si celui-ci est à 0 alors la fonction queue un APC kernel pour le thread. Lorsque le thread appel une des APIs KeEnterCriticalRegion ou KeEnterGuardedRegion le champ KernelApcDisable est décrémenté inversement pour les APIS KeLeaveCriticalRegion et KeLeaveGuardedRegion, ce qui faut retenir c'est que la valeur de KernelApcDisable est différente de 0 dès que le thread rentre dans une région critique. Du fait qu'il est possible d'avoir des plusieurs appels à KeEnterCriticalRegione et KeEnterGuardedRegion un compteur est mis en place afin de connaître le nombre d'appels à ces APIs, après je n'arrive pas à comprendre pourquoi ce dernier compte à l'envers ... Je reviendrais sur rôle des APCs kernel plus loin dans ce post, d'abord voyons les actions des autres IOCTLs.

Continuons avec le second IOCTL envoyé au driver (0x80002008), la fonction associée va aussi parcourir la liste des threads du process mais cette fois ci pour modifier le champ State (offset 0x2D de la KTHREAD) à la valeur 4 qui correspond à l'état Terminated, celui la même qui est définit lorsqu'on appel TerminateThread. Juste pour rappel voici les différents états possibles pour un thread sous Windows :

//
// Thread scheduling states.
//

typedef enum _KTHREAD_STATE {
    Initialized,
    Ready,
    Running,
    Standby,
    Terminated, //=4
    Waiting,
    Transition,
    DeferredReady,
    GateWait
} KTHREAD_STATE;

Le troisième IOCTL (0x80002010) est beaucoup plus freestyle, la fonction chargée de gérer cet IOCTL va commencer par récupérer l'adresse de la fonction PsTerminateSystemThread et scanner ses instructions une par une en utilisant un LDE (Length Disassembly Engine) qui permet d'obtenir la taille des instructions en fonction de leurs opcodes, ca se voit sous IDA vu la bouillie que crée la fonction LDE. La fonction de scan recherche l'instruction commençant par (0x80F6) puis lit le word situé 2 bytes plus loin par rapport au début de l'instruction. Pour savoir à quoi ça correspond il suffit de désassembler la fonction PsTerminateSystemThread :

lkd> uf PsTerminateSystemThread
nt!PsTerminateSystemThread:
805d2c2a 8bff            mov     edi,edi
805d2c2c 55              push    ebp
805d2c2d 8bec            mov     ebp,esp
805d2c2f 64a124010000    mov     eax,dword ptr fs:[00000124h]
805d2c35 f6804802000010  test    byte ptr [eax+248h],10h
805d2c3c 7507            jne     nt!PsTerminateSystemThread+0x1b (805d2c45)

nt!PsTerminateSystemThread+0x14:
805d2c3e b80d0000c0      mov     eax,0C000000Dh
805d2c43 eb09            jmp     nt!PsTerminateSystemThread+0x24 (805d2c4e)

nt!PsTerminateSystemThread+0x1b:
805d2c45 ff7508          push    dword ptr [ebp+8]
805d2c48 50              push    eax
805d2c49 e828fcffff      call    nt!PspTerminateThreadByPointer (805d2876)

nt!PsTerminateSystemThread+0x24:
805d2c4e 5d              pop     ebp
805d2c4f c20400          ret     4

L'instruction qui commence par 0x80F6 est "test byte ptr [eax+248h],10h", 2 bytes après le début des opcodes on retrouve la valeur 0x0248 qui tout l'offset d'un champ dans la structure ETHREAD. L'auteur à tout simplement implémenter une méthode générique pour retrouver l'offset de ce champ en se basant sur le code de la fonction noyau PsTerminateSystemThread, c'est pas mal, je dirais même bien mais je ne pense pas que ca soit nécessaire, un coup de PsGetVersion avec un switch pour définir les offsets suffit aussi et à l'avantage d'être beaucoup moins lourd. Quoiqu'il en soit à l'offset 0x248 de la structure on retrouve le champ CrossThreadFlags, un byte servant à définir différents flags pour le thread :

lkd> dt nt!_ETHREAD
[...]
+0x248 CrossThreadFlags : Uint4B
+0x248 Terminated       : Pos 0, 1 Bit
+0x248 DeadThread       : Pos 1, 1 Bit
+0x248 HideFromDebugger : Pos 2, 1 Bit
+0x248 ActiveImpersonationInfo : Pos 3, 1 Bit
+0x248 SystemThread     : Pos 4, 1 Bit
+0x248 HardErrorsAreDisabled : Pos 5, 1 Bit
+0x248 BreakOnTermination : Pos 6, 1 Bit
+0x248 SkipCreationMsg  : Pos 7, 1 Bit
+0x248 SkipTerminationMsg : Pos 8, 1 Bit
[...]

La fonction va parcourir la liste des threads du process et mettre le flag Terminated à 1 pour tous les threads.

Enfin le dernier IOCTL (0x80002018) va modifier le PID du process au niveau du champ UniqueProcessId à l'offset 0x84 de la structure EPROCESS à la valeur de 7 et le propager aux ETHREADS en modifiant le champ UniqueProcess de la structure CLIENT_ID située à l'offset 0x1Ec de la structure ETHREAD. Je suppose que cette modification est faite pour tromper la PspCidTable, cette table de handles qui correspond aux PID et qui permet de faire la translation PID->EPROCESS afin de tromper les APIs kernel qui normalement vérifient que les 2 valeurs de PID, celle envoyée et celle trouvée, correspondent, à vérifier quand même.

Ouf, finit le reverse d'IOCTLs, résumons à peu ce que nous avons 4 IOCTLS qui activent 4 protections différentes :

  • Le premier sert à désactiver les APC kernel sur tout les threads du process.
  • Le second va changer l'état du thread (State) à Terminated.
  • Le troisième modifie les flags des différentes threads du process en activant le flag Terminated. La différence avec le point précédent c'est que le champ State sert uniquement au thread scheduler alors que le flag est utilisé par les APIs noyau pour vérifier l'état du thread.
  • Enfin le dernier IOCTL change le PID du process et celui de ses threads à la valeur 7, une valeur qui n'existe pas dans la PspCidTable.

Je reviens sur la désactivation des APC noyau, le point qui me gênait le plus. En fait lorsqu'on appel l'API NtTerminateThread celle-ci appelle PspTerminateThreadByPointer après avoir référencé l'objet ETHREAD avec ObReferenceObjectByHandle et va envoyer un APC sur le thread avec la fonction KeInsertQueueApc. Remarquez que même la fonction PsTerminateSystemThread qui sert à killer un thread kernel en étant appelé par le thread lui même va appeler PspTerminateThreadByPointer. En fait en y pensant un peu cela est normal, un APC est une routine qui est exécutée dans le context d'un thread, il est normal qu'un thread ne puisse pas s'auto détruise, ca serait comme couper la branche sur laquelle il est, c'est donc une routine externe qui s'en charge. En l'occurrence cette routine s'appelle PsExitSpecialApc et lance PspExitThread. Donc comme le driver désactive les APC noyau en faisant croire que celui-ci est une région critique, l'APC chargé de killé le thread n'est pas lancé et ainsi le thread n'est pas détruit, tout s'explique ! (ou bien j’ai rien compris :p)

Pour conclure, je trouve le taff de Sudami vraiment très intéressant, ca innove et c'est basé sur des choses simples, on voit qu'il connaît très bien l'architecture du noyau Windows. En fait son code revient juste à faire croire au kernel que les threads du process sont déjà killés, le noyau ne va donc pas essayer de les détruire dans ce cas là et ils se verront toujours runner. La partie déobffuscation de code était aussi marrante, ce n’est pas très méchant, moi qui n'est pas vraiment l'habitude de voir des binaires comme ça j'ai apprit quelques trucs, cependant vivement IDA 5.3 qui permettra de faire du script avec des languages comme python ou ruby :]

Voilà, c'est fini, j'espère que vous avez appris pas mal de trucs, maintenant si quelqu'un est motivé pour recoder les fonctionnalités du driver, je suis preneur ;) Je vous file une archive avec le binaire original, le binaire unpacké, le driver extrait et l'ouput html du reverse sous IDA de ce dernier. Vous trouverez tout ça ici : http://ivanlef0u.fr/repo/sudami_KillMe.rar

Enjoy !

18 comments juillet 15th, 2008

Win userland Pax-like with segmentation

Je viens de retrouver les pass de mon blog, je les avais laissés trainer sous forme alphabétique dans un .txt crypté avec un double rot13 sur une dedib0x … Je vais pouvoir enfin vous parler d’un projet que j’ai commencé récemment. Je mattais la doc de PAX quand je suis tombé sur la protection d’exécution utilisant la segmentation. L’idée consiste à utiliser une feature CPU datant des débuts de l’i386, la segmentation, pour séparer le code des datas dans des zones possédants des droits différents, une zone en lecture+exécution et l’autre en lecture+écriture. Ok l’idée est cool, c’est mignon, ca brille et dans le noir on peut le voir mais pour l’instant c’est sous l’OS des barbus et ce n’est pas vraiment mon kiff de faire mumuse avec. Je me suis donc lancé dans l’implémentation d’un équivalent pour Windows et pour être franc, j’en ai chié :]

Ok, je ne suis pas complètement partit de rien, l’implémentation de PAX consiste à utiliser un patch noyau, clairement je ne voulais pas de cela, trop compliqué à réaliser sous Windows. D’ailleurs je ne voulais même pas 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 système en patchant/hookant/changeant la couleur des features de mon noyau.

Entrons un peu dans les détails de l’implémentation de PAX :

2. Implementation

The core of SEGMEXEC is vma mirroring which is discussed in a separate
document. The mirrors for executable file mappings are set up in do_mmap()
(an inline function defined in include/linux/mm.h) except for a special
case with RANDEXEC (see separate document). do_mmap() is the one common
function called by both userland and kernel originated mapping requests.

The special code and data segment descriptors are placed into a new GDT
called gdt_table2 in arch/i386/kernel/head.S. The separate GDT is needed
for two reasons: first it simplifies the implementation in that the CS/SS
selectors used for userland do not have to change, and second, this setup
prevents a simple attack that a single GDT setup would be subject to (the
retf and other instructions could be abused to break out of the restricted
code segment used for SEGMEXEC tasks). Since the GDT stores the userland
code/data descriptors which are different for SEGMEXEC tasks, we have
to modify the low-level context switching code called __switch_to() in
arch/i386/kernel/process.c and the last steps of load_elf_binary() in
fs/binfmt_elf.c (where the task is first prepared to execute in userland).

The GDT also has APM specific descriptors which are set up at runtime and
must be propagated to the second GDT as well (in arch/i386/kernel/apm.c).
Finally the GDT stores also the per CPU TSS and LDT descriptors whose
content must be synchronized between the two GDTs (in set_tss_desc() and
set_ldt_desc() in arch/i386/kernel/traps.c).

Since the kernel allows userland to define its own code segment descriptors
in the LDT, we have to disallow it since it could be used to break out of
the SEGMEXEC specific restricted code segment (the extra checks are in
write_ldt() in arch/i386/kernel/ldt.c).

Pour faire simple, PAX va créer une nouvelle GDT dans laquelle il va définir de nouveaux segment descriptors auxquels seront associés donc des segment selectors. En temps normal les segment selectors que sont CS, DS, ES, FS, GS et SS sont positionnés en flat model, c’est à dire que les segment descriptors associés représentent tout l’espace mémoire, 4 Gb sur un système 32 bits que ce soit sous Win ou nux (exception faite à FS et GS qui peuvent différentes valeurs en fonction de l’implémentation de l’OS). Ce qui signifie que l’EIP qui est utilisé dans l’espace représenté par le segment CS peut se promener ou il veut en mémoire, donc dans le cas d’une exploitation d’un buffer overflow dans la pile avec le shellcode placé dans le stack, EIP pouvant prendre n’importe quelle adresse, il pourra être redirigé sur le shellcode dans la stack sans problème puisque le segment CS l’autorise à aller partout. Attention cependant, même si le segment descriptor est en flat model, la notion de ring intervient toujours et vous empêche d’aller exécuter du code situer dans une page ring0 si votre CPL est à 3. Check this out !
Si vous voulez plus d’infos sur ce sujet allez lire le chapitre 3 « Protected-mode memory management » du man Intel « Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3A System Programming Guide, Part 1« .

Pour mémoire, un segment selector possède la forme :
Segment selector

L’index permet au core de retrouver le segment descriptor dans la GDT, sachant qu’un segment descriptor est définit par :
Segment descriptor

En flat model, tous les champs Base sont à 0 et le champ Limit vaut 0xFFFFFF, le champ Granularity étant à 1, il faut multiplier Limit par 4Kb et lui ajouter 0xFFF pour obtenir la taille du segment. Donc dans le cas flat on à une Base à 0 et une Limit à 0xFFFFFFFF, tout l’espace mémoire sur 32 bits. En fait le calcul de l’adresse virtuelle est réalisé en additionnant la base du segment avec l’adresse logique, comme en flat model la base vaut 0, on se retrouve avec adresse logique=adresse virtuelle. Dans la GDT on va donc retrouver 2 descriptors de code, un pour le code ring 3, l’autre pour le code ring 0, pareil pour les descripteurs de data. On se retrouve donc avec une GDT de la forme :

kd> !!display_all_gdt

#################################
# Global Descriptor Table (GDT) #
#################################

Processor 00
Base : 8003F000    Limit : 03FF

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
0040  0043  Data16         00000400  0000FFFF  YES      3    0    Read/Write
0048  0048  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
0050  0050  TSS32          80550880  00000068  YES      0    0    (Available) Eip = nt!KiTrap08 (804e069d)
0058  0058  TSS32          805508E8  00000068  YES      0    0    (Available) Eip = nt!KiTrap02 (804df5b6)
0060  0060  Data16         00022F30  0000FFFF  YES      0    0    Read/Write, accessed
0068  0068  Data16         000B8000  00003FFF  YES      0    0    Read/Write
0070  0070  Data16         FFFF7000  000003FF  YES      0    0    Read/Write
0078  0078  Code16         80400000  0000FFFF  YES      0    0    Execute/Read
0080  0080  Data16         80400000  0000FFFF  YES      0    0    Read/Write
0088  0088  Data16         00000000  00000000  YES      0    0    Read/Write
0090  0090  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
0098  0098  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00A0  00A0  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00A8  00A8  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00B0  00B0  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00B8  00B8  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00C0  00C0  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00C8  00C8  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00D0  00D0  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00D8  00D8  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 00000000 00000000
00E0  00E0  Code16         FA7B5000  0000FFFF  YES      0    0    Execute/Read, accessed
00E8  00E8  Data16         00000000  0000FFFF  YES      0    0    Read/Write
00F0  00F0  Code16         804D8B28  0003B337  YES      0    0    Execute-Only
00F8  00F8  Data16         00000000  0000FFFF  YES      0    0    Read/Write
0100  0100  Data32         FA7C5000  0000FFFF  YES      0    0    Read/Write, accessed
0108  0108  Data32         FA7C5000  0000FFFF  YES      0    0    Read/Write, accessed
0110  0110  Data32         FA7C5000  0000FFFF  YES      0    0    Read/Write, accessed
0118  0118  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 8003F120 00000000
0120  0120  Reserved  ....:........  ........  NO       0    0    Raw Descriptor : 8003F128 00000000
[...]

Pour en revenir à PAX, ce dernier va définir une nouvelle GDT en modifiant les segment descriptors de code et data ring3 pour découper l’espace user-land en 2. Sous linux le user-space faisant 3 Go, on a 1,5 Go pour le code et le reste pour les datas. PAX va ainsi effectuer une séparation des segments de code et data, en laissant CS dans la partie mémoire basse l’empêchant d’aller trainer dans la partie haute de la mémoire user-land la ou seront contenu les data, la stack et le heap.

Un peu plus tard un pote m’a filé un lien provenant de felinemenace.org, un article d’Andrew Griffiths (il a un gros zizi le monsieur…) Pseudo-PaX-in-userland. Cette fois-ci à la place de créer une nouvelle GDT, Andrew va utiliser une LDT, l’équivalent d’une GDT sauf que celle-ci est spécifique à un thread ou process (en fonction de la manière dont l’OS définit la notion de tâche). Cela lui permet ainsi d’éviter de crée une nouvelle GDT et surtout d’appliquer le concept uniquement à une tâche précise. En effet, un segment selector permet de choisir dans quelle table récupérer les segments descriptors, si le champ Table est à 1 alors le CPU va prendre les segment descriptors de la GDT. L’adresse de base de la GDT est stockée dans le registre GDTR (32 bits), lorsqu’il y a une LDT, le LDTR (16 bits) correspond à un un segment selector sur un segment descriptor system de la GDT indiquant la base de la LDT pour le LDTR courant. On peut donc de cette manière avoir plusieurs LDT existant en parallèle sur le système. Pour info une GDT/LDT peut contenir 8192 entrées, ca laisse de la marge pour en ajouter des LDT :] Pour résumer voici un petit schéma tiré du man Intel.
Gdt Ldt

Maintenant, je passe à l’étape Windows, je veux développer l’équivalent d’un PAX utilisant la segmentation depuis une LDT pour un process précis. Je veux pouvoir le faire tourner pour n’importe quel type de process tout en évitant d’être trop envahissant et sans avoir à recompiler celui-ci (gnii#@!). Concrètement si on prend n’importe quel binaire Windows, on se retrouve avec les sections de code suivies des sections de datas et ce pour chaque module chargé en mémoire, chaque module étant chargé en mémoire en fonction de son ImageBase (rellocation si nécessaire), en gros si on prend un dump de la mémoire d’un process on me retrouve avec (exemple avec un helloworld) :

Memory map
Address    Size       Owner      Section    Contains      Type   Access    Initial
[...]
00400000   00001000   BofMe                 PE header     Imag   R         RWE
00401000   00001000   BofMe      .text      code          Imag   R         RWE
00402000   00001000   BofMe      .rdata     imports       Imag   R         RWE
00403000   00001000   BofMe      .data      data          Imag   R         RWE
77BE0000   00001000   MSVCRT                PE header     Imag   R         RWE
77BE1000   0004C000   MSVCRT     .text      code,imports  Imag   R         RWE
77C2D000   00007000   MSVCRT     .data      data          Imag   R         RWE
77C34000   00001000   MSVCRT     .rsrc      resources     Imag   R         RWE
77C35000   00003000   MSVCRT     .reloc     relocations   Imag   R         RWE
7C800000   00001000   kernel32              PE header     Imag   R         RWE
7C801000   00084000   kernel32   .text      code,imports  Imag   R         RWE
7C885000   00005000   kernel32   .data      data          Imag   R         RWE
7C88A000   00076000   kernel32   .rsrc      resources     Imag   R         RWE
7C900000   00006000   kernel32   .reloc     relocations   Imag   R         RWE
7C910000   00001000   ntdll                 PE header     Imag   R         RWE
7C911000   0007A000   ntdll      .text      code,exports  Imag   R         RWE
7C98B000   00005000   ntdll      .data      data          Imag   R         RWE
7C990000   00033000   ntdll      .rsrc      resources     Imag   R         RWE
7C9C3000   00003000   ntdll      .reloc     relocations   Imag   R         RWE
[...]

Les sections .text étant celles qui contiennent le code, si on veut reprendre le même principe que PAX il faudrait pouvoir dissocier les sections .text dans une espace mémoire réservé. Cela implique donc de modifier le loader de Windows et ca, il n’en est pas question, trop compliqué, pas forcément portable et trop long à faire. De plus, même si on arrivait à le faire se poserait le problème des références aux variables situées dans les sections de data. En effet, ces variables sont référencées à travers leurs adresses virtuelles, donc si on change l’adresse de chargement de ces sections, il faudra mettre à jour ces références. Dans le cas d’une DLL les relocations sont la pour ça, mais pour le binaire principal celui-ci ne possède pas de section .reloc, il faudrait donc mettre à jour ces adresses à la main … c’est mort je suis f0u mais pas à ce point !

Pour simplifier la chose, j’ai donc décidé de ne pas toucher au chargement des modules en mémoire et surtout de ne pas définir une seule région de code mais plusieurs régions de code, plus précisément une pour chaque section .text. Ainsi nous n’avons pas besoin de modifier l’agencement des modules en mémoire mais de bien définir notre LDT pour quelle contienne un segment descriptor de code pour chaque section .text. Concernant les segments de datas, ont les laisse en flat model avec les valeurs par défaut (donc celles de la GDT), on veut juste contrôler EIP pour qu’il reste dans nos segments de code.

Pour chaque .text on va donc avoir un segment descriptor dans la LDT, ce segment descritptor aura pour Base le début de la section, la Limit sera la taille de la section, on désactive aussi la Granularity dans le seg descriptor sachant qu’on peut représenter un espacé mémoire de 1Mo sans, cela devrait suffir pour la majorité des sections .text. On va donc avoir 0 <= Eip logique <= Limit qui donne lorsqu’on passe en virtuel, Base <= Eip virtuel <= Base+Limit.

Pour l’instant c’est l’idée, maintenant vient le passage à l’implémentation. J’ai décidé d’utiliser l’API de debug de Windows pour pouvoir surveiller le comportement du processus visé. Ainsi en debuggant le process nous somme capable de surveiller les chargements de modules, la création de thread et les exceptions. On va donc créer le process avec CreateProcess en activant les flags DEBUG_PROCESS et DEBUG_ONLY_THIS_PROCESS puis on rentre dans notre boucle de debug avec les APIs WaitForDebugEvent et ContinueDebugEvent comme dans la doc.

Il nous faut aussi être capable de définir une LDT sous Windows depuis le user-land. Pour cela nous allons faire appel aux APIs ZwSetInformationProcess et ZwQueryInformationProcess en les utilisant avec l’InformationClass ProcessLdtInformation (10). Il faut savoir que si ZwSetInformationProcess est appelé avec ProcessLdtInformation, celle-ci va automatiquement définir un segment descriptor de LDT dans le GDT avec les fonctions PspCreateLdt et Ke386SetLdtProcess. Dans les 2 cas la structure à passer à l’API est de type PROCESS_LDT_INFORMATION :

typedef struct _LDT_INFORMATION {
    ULONG Start;
    ULONG Length;
    LDT_ENTRY LdtEntries[1];
} PROCESS_LDT_INFORMATION, *PPROCESS_LDT_INFORMATION;

Start étant l’endroit ou commence la copie/lecture des segment descriptors de la LDT, Length est la taille de la copie/lecture et LdtEntries est un tableau de structures LDT_ENTRY qui est tout simplement la définition d’un segment descriptor :

typedef struct _LDT_ENTRY
{
    USHORT LimitLow;
    USHORT BaseLow;
    union
    {
        struct
        {
            UCHAR BaseMid;
            UCHAR Flags1;
            UCHAR Flags2;
            UCHAR BaseHi;
        } Bytes;
        struct
        {
            ULONG BaseMid:8;
            ULONG Type:5;
            ULONG Dpl:2;
            ULONG Pres:1;
            ULONG LimitHi:4;
            ULONG Sys:1;
            ULONG Reserved_0:1;
            ULONG Default_Big:1;
            ULONG Granularity:1;
            ULONG BaseHi:8;
        } Bits;
    } HighWord;
} LDT_ENTRY, *PLDT_ENTRY, *LPLDT_ENTRY;

Il existe aussi l’API native ZwSetLdtEntries qui permet de définir des segments descriptors par paires mais je n’avais pas envie de l’utiliser. Je vous donne quand même son prototype si vous avez envie de faire mumuse avec :

NTSTATUS
ZwtSetLdtEntries(
    __in ULONG Selector0,
    __in ULONG Entry0Low,
    __in ULONG Entry0Hi,
    __in ULONG Selector1,
    __in ULONG Entry1Low,
    __in ULONG Entry1Hi
    )
/*++

Routine Description:

    This routine sets up to two selectors in the current process's LDT.
    The LDT will be grown as necessary.  A selector value of 0 indicates
    that the specified selector was not passed (allowing the setting of
    a single selector).

Arguments:

    Selector0 -- Supplies the number of the first descriptor to set
    Entry0Low -- Supplies the low 32 bits of the descriptor
    Entry0Hi -- Supplies the high 32 bits of the descriptor
    Selector1 -- Supplies the number of the first descriptor to set
    Entry1Low -- Supplies the low 32 bits of the descriptor
    Entry1Hi -- Supplies the high 32 bits of the descriptor

Return Value:

    NTSTATUS.

--*/

Cool, on est maintenant capable de définir une LDT, à noter que sous Windows, la notion de LDT est process specific, le LDTR est mit à jour lors de chaque changement de context à partir du champ LdtDescriptor (offset 0×20) de la structure KPROCESS.

Lorsque notre process va être lancé, le debugger va voir les tentatives de chargement de modules par le loader de Windows et ainsi à chaque nouveau module mettre à jour la LDT du debuggee pour créer un nouveau segment de code pour la section .text. Voici la routine qui va initialiser le nouveau segment descriptor :

VOID InitCodeDescriptor(PLDT_ENTRY LdtCodeSegDecriptor, ULONG Base, ULONG Limit)
{
	ULONG GranLimit=0;

	if((Base<0x1000) && (Base<=0x7ffeffff)) //MmHighestUserAddress
	{
		printf("Base is invalid\n");
		return;
	}

	if((Base+Limit)>0x7ffeffff)
	{
		printf("Limit is invalid\n");
		return;
	}

	RtlZeroMemory(LdtCodeSegDecriptor, sizeof(*LdtCodeSegDecriptor));

	//
	// Segment code descriptor
	//
	LdtCodeSegDecriptor->LimitLow=(USHORT)(Limit&0xFFFF);
	LdtCodeSegDecriptor->BaseLow=(USHORT)(Base&0xFFFF);
	LdtCodeSegDecriptor->HighWord.Bits.BaseMid=(UCHAR)((Base&0xFF0000)>>16);
	LdtCodeSegDecriptor->HighWord.Bits.Type=0x1A; //type=1, code segment
	LdtCodeSegDecriptor->HighWord.Bits.Dpl=3;
	LdtCodeSegDecriptor->HighWord.Bits.Pres=1;
	LdtCodeSegDecriptor->HighWord.Bits.LimitHi=((Limit&0xFF0000)>>16);
	LdtCodeSegDecriptor->HighWord.Bits.Sys=0;
	LdtCodeSegDecriptor->HighWord.Bits.Default_Big=1;
	LdtCodeSegDecriptor->HighWord.Bits.Granularity=0;
	LdtCodeSegDecriptor->HighWord.Bits.BaseHi=(UCHAR)((Base&0xFF000000)>>24);

	return;
}

Reste le problème des changements d’EIP inter-segments, en effet notre binaire va forcément vouloir utiliser des APIs exportées par les autres modules et donc va vouloir sortir du segment de code courrant pour aller voir ailleur. Une tentative de définir un Eip à une valeur dépassant la Limit du segment génère une #GP ce qui correspont sous Win à STATUS_ACCESS_VIOLATION (0xC0000005L). Deux solutions à ce problème :

  1. L’émulation : On regarde ou veut sauter l’Eip puis on le met à jour à la main, sauf que dans le cas d’un call/jmp [reg32+XXh] c’est vraiment lourd à faire, il faut de plus penser à pusher l’adresse de retour sur la stack si c’est un call et à la fin mettre à jour le segment CS du fait que nous avons changé de module. Bref pas forcément simple à faire.
  2. La tracing : On repasse notre segment de code en flat model, on laisse faire le branchement en activant le TrapFlag dans l’Eflag, l’EIP arrive donc dans le module de destination, génère une exception (EXCEPTION_SINGLE_STEP) qui nous permet de remettre à jour le segment CS pour qu’il soit celui associé au nouveau module.

J’ai choisit la deuxième solution, j’ai donc décidé d’avoir dans ma LDT 1 segment de code flat dans ma LDT pour pouvoir géré les branchements inter-segments. Ce qui est cool avec cette méthode c’est que l’Eip qui est pushé sur la stack lors d’un call est un Eip virtuel ce qui signifie que lors du Ret inter-segment va être mit dans Eip une adresse virtuelle qui forcément sera plus grande que la Limit du segment et donc génèrera une exception. On gère donc cette exception de la même manière que le call en repassant sur un segment de code en flat puis en activant le TrapFlag et voilà nous sommes revenu dans le module de départ suivit d’une exception après le Ret, il ne reste plus qu’a redéfinir le segment de code sur la valeur du segment de code associé au segment descriptor du module de départ et ca roule ! w00t !

Reste le cas uber-particulier que je me suis prit dans la face. Par exemple avec une fonction de kernel32.dll on retrouve un schéma d’appel à import de la forme :

7C870DDD  |.  E8 41FB0000   CALL kernel32.NtTerminateProcess         ;  JMP to ntdll.ZwTerminateProcess
7C880923 > $- FF25 0814807C JMP NEAR DWORD PTR DS:[<&ntdll.NtTermina>;  ntdll.ZwTerminateProcess

Un call sur un jmp [ptr de fct], c'est joli mais dans cas ca fout la merde. En effet l'Eip qui est pushé sur la stack lors du call est un Eip logique, valable dans le cadre du segment actuel, c'est le jmp qui va crée l'exception (#GP) parce qu'il va vouloir sortir du segment, or au moment du retour, l'Eip qui va être popé sera un Eip logique mais qui à de force d'être valable dans le contexte du segment du module qui contient la fonction appelée, donc il sera prit en compte mais partira n'importe tout. Ce qu'il faudrait pour corriger ce bug, c'est mettre à jour le saved Eip sur la stack lors d'un jmp inter-segment, pour cela je n'ai pas trouvé que d'utiliser une lib de disass pour vérifier le type d'instruction qui à généré l'exception au moment du branchement inter-segment. Ainsi si c'est un jmp et que le saved-Eip est inférieur à la taille de la plus grande section .text définie alors on met à jour le saved Eip en lui ajoutant la Base du segment decriptor utilisé lors du jmp. Oui je sais c'est un peu compliqué mais j'ai du le prendre le compte, j'en profite au passage pour faire de la pub à la lib de disass que j'ai utilisé celle de Beatrix, BeaEngine qui permet de connaitre de type de l'instruction désassemblée :

enum OPCODE_TYPE
{
  JMP_TYPE = 1,
  JCC_TYPE,
  CALL_TYPE,
  RET_TYPE,
  ILLEGAL_TYPE,
  SUPERFLUOUS_PREFIX,
  INCOMPATIBLE_TYPE
};

Un autre problème aussi m'a été posé à cause des syscalls, en effet sysenter (KiFastSystemCall dans ntdll) et sysexit vont mettre à jour le l'Eip et CS avec des valeurs définies par le noyau avec les MSR SYSENTER_EIP_MSR et SYSENTER_CS_MSR, dans le cas du sysenter on s'en fout mais dans le cas du sysexit ca pose soucis. En effet sysexit renvoie l'Eip sur la routine KiFastSystemCallRet dans ntdll qui va directement effectuer un Ret, pas cool car le saved Eip pushé sur la stack est un Eip logique qui va être utilisé avec un CS en flat model, celui de la GDT mit à jour avec sysexit ... Ouinz ! Pour contrer cela, j'ai décidé de mettre des breapoints sur KiFastSystemCall et KiFastSystemCallRet. Lorsque le thread de notre debugge effectue un syscall il passe par KiFastSystemCall, hop breakpoint (EXCEPTION_BREAKPOINT), on repasse en CS flat model en mettant l'Eip à jour (ajout de la Base du segment de code actuel) puis on laisse faire le syscall. Au retour on break sur KiFastSystemCallRet, exception (int 3), on remet à jour le segment de code sur celui associé à la section .text de ntdll dans notre LDT et on realigne eip en redonnant la main au programme.

Bon, évidemment ce ne sont pas les seuls soucis que j'ai eu et il en a bien d'autres, avec ca je suis capable de faire tourner des petits programmes.

Concernant l'implémentation actuelle, celle-ci ne gère que certains process console, des binaires relativement peu complexes c'est à dire avec peu de modules chargés en mémoire et ne générant pas des exceptions bizarres, il ne gère pas non plus les process multithreadés, je pourrais le faire mais pour le moment j'ai la flemme et autre chose à faire. Le code manque de commentaires aussi donc n'hésitez pas à poser des questions. Lorsque le programme détecte une Eip qui sort d'une section .text, il kill directement le debuggee, on fait dans la dentelle.

Pour le moment j'arrive à faire tourner des binaires comme nc.exe ou fport.exe. Ce n’est pas super puissant, ca plante souvent mais ca peut marcher pour des programmes simples :) En fait le plus intéressant pour vous n'est pas savoir quel binaire ou fonctionne mais d'exploiter (avec une vraie exécution de code qui fait quelque chose) le binaire que j'ai mit dans l'archive, il s'agit d'un simple BofMe dont le code est :

#include <stdio.h>

int main(int argc, char *argv[])
{
	char Buff[24];

	if(argc>1)
	{
		strcpy(Buff, argv[1]);
		printf(Buff);
	}
	return 0;
}

Je suis gentil, je vous ai même laissé une format string. Pour le lance faites par exemple : "C:\Code\ldt>UserlandPaxLdt.exe BofMe.exe aaaaaaaaaaaaaa > NUL". Si vous voulez voir les logs du programme je vous conseil de faire "C:\Code\ldt>UserlandPaxLdt.exe BofMe.exe aaaaaaaaaaaaaa > barp.txt", vous verrez bien qu'il aime pas qu'on touche à son Eip :p Un indice, une soluce pour réussir une exploitation est indiqué dans l'article de Andrew Griffith.

Vous trouvez le binaire ici :
http://ivanlef0u.fr/repo/UserlandPaxLdtrar.rar
Allez, j'attends de vos retours sur ce petit projet !

Sinon quelques liens pour déconner (attention il y a un piège) :

http://en.wikipedia.org/wiki/List_of_problems_solved_by_MacGyver
http://seclists.org/dailydave/2008/q3/0000.html
http://pedobear.net
http://blogs.iss.net/archive/TheWebBrowserThreat.html

8 comments juillet 2nd, 2008


Calendar

juillet 2008
L Ma Me J V S D
« juin   sept »
 123456
78910111213
14151617181920
21222324252627
28293031  

Posts by Month

Posts by Category