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 :
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

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 :D


  • 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


Calendar

mai 2025
L Ma Me J V S D
« fév    
 1234
567891011
12131415161718
19202122232425
262728293031  

Most Recent Posts