Win userland Pax-like with segmentation
juillet 2nd, 2008 at 06:42 admin
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 :
L’index permet au core de retrouver le segment descriptor dans la GDT, sachant qu’un segment descriptor est définit par :
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.
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 :
- 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.
- 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
Entry Filed under: RE
8 Comments
1. mxatone | juillet 2nd, 2008 at 22:27
Tres bon article, tres interessant. A noter que cette technologie est difficilement (voir impossible) a appliquer a Windows. Le DEP lui même n’est pas appliqué volontairement à certains programme (ie 8 aura le DEP active, ouf !). Sans compter des problèmes des chunks ATL en RWE. Mais merci d’avoir partager avec nous l’approche PaX appliqué à WIndows.
2. Taron | juillet 3rd, 2008 at 03:02
Mamma mia… j’ai été plus effrayé par le troisième lien que par ton post…
3. YoLeJedi | juillet 3rd, 2008 at 16:41
Marrant ton article Ivan
Finalement, tu nous fais une projection vers le passé en transformant ton XP en win98
4. Taron | juillet 4th, 2008 at 02:34
Ivan > y’a pas possibilité avec RETF ??
5. admin | juillet 4th, 2008 at 10:39
Bien Taron, c’est exactement ce que disait Andrew Griffith dans son article :] En effet il suffit de faire un far ret sur un segment de code ring 3 en flat model, soit celui de la GDT, soit celui de m’a LDT pour pouvoir exécuter du code ou l’on veut.T’a plus qu’a coder un exploit pour faire ça
6. jj | juillet 4th, 2008 at 16:09
Le retf c’est le coup classique pour exploiter W^X (openbsd).
cf le challenge scramblito de securitech 2006…
7. Neitsa | juillet 9th, 2008 at 17:56
Salut,
Comme d’hab’ super article. Je viens de remonter les derniers articles et c’est du sacré boulot que tu as fourni, vraiment génial.
Bon, le seul point négatif, je dirais, c’est la musique :p (cf. post « PlayList ». ok, c’est pas de la zik de tapz, mais moi ça me fait peur !). ;p
Ce que je ne comprend pas c’est qu’il y a un paquet de reversers nourri au screamo… (bon, les goûts et les couleurs, ça ne se discute pas… mais ça semble être presque une constante).
bonne continuation !
8. admin | juillet 9th, 2008 at 21:06
Yo Neitsa,
Merci ça fait plaisir de voir des gens qui aiment ce qu’on fait
Concernant la zik, tu préférerais p-e que je mette de la tecktonik ?! Si ça peut te rassurer je n’ai ni le look emo ni tecktotapz :]
Sinon aucune idée pourquoi les reversers tournent au screamo et perso j’en connais pas, en général c’est du black métal satanique qu’ils écoutent en mangeant des chips … Tu pourrais me les présenter ?
+ dude
Trackback this post