Wiki

Clone wiki

pymoult / Cymoult - Libuminati

Objectif du projet

Le but du projet est de fournir une API bas niveau fournissant les mécanismes nécessaires au DSU dans le cadre de programmes en C compilés au format ELF avec des informations de débogage au format DWARF, tous deux standards sous UNIX avec gcc.

Dépendances

Linux sur une architecture x86 64 bits avec les paquets elfutils et binutils complets (certaines distributions se contentent de distribuer les binaires sans les bibliothèques, dans ce cas il faut récupérer les sources)

Compilation

  • Se placer dans le répertoire libuminati

  • $ ./configure

  • $ make lib

Se trouve alors dans le sous-dossier bin des fichiers libuminati.{a,h,so} qui correspondent respectivement à la bibliothèque statique, au fichier d'entête C et à la bibliothèque dynamique.

Résumé du fonctionnement

La bibliothèque repose principalement sur les bibliothèques libdw, libdwfl et libelf du paquet elfutils pour la lecture des informations de débogage, la libbfd présente dans binutils pour la lecture des fichiers contenant le code de la nouvelle version du programme mis à jour, ainsi que l'appel sytème ptrace pour pouvoir accéder à la mémoire et aux registres d'un autre processus.

Hypothèses de fonctionnement et perspectives de relâchement

Les hypothèses faites sont les suivantes :

Au niveau de la plateforme d'exécution :

  • Architecture matérielle : il s'agit de réécrire les codes machines injectés par l'API. Ce code est déjà défini dans des macros pour faciliter ce genre de généralisations.

  • Linux : l'API injecte un appel système à mmap avec des flags définis par l'ABI de Linux mais absents de la norme POSIX. On pourrait imaginer injecter un code plus long qui fasse la même chose en respectant cette norme. Un autre problème à considérer est la dépendance à elfutils, qui n'est pas forcément porté sur toutes les plateformes.

Au niveau de la compilation du programme :

  • Le langage source a peu d'influence en fin de compte, si ce n'est qu'il garantit de ne pas croiser certains symboles de debug qui ne sont par conséquent pas traités. En terminant l'implantation des opérations du standard DWARF, on devrait arriver à traiter les programmes de façon indépendante du langage source (non testé)

  • À la compilation, les contraintes sont d'avoir une option de debug activée (-g), et aucune optimisation. Cette dernière contrainte peut être relâchée en complexifiant le parsing (les informations de débug contiennent alors des indications sur les optimisations faites). De plus, on pourrait imaginer à terme laisser le choix de produire les informations de debug dans un fichier séparé pour ne les fournir qu'au moment de la mise à jour.

  • Au niveau des formats de sortie (ELF et DWARF), étendre les choix possibles nécessiterait de réécrire un parsing pour le nouveau format (et d'introduire une modularité).

  • Le compilateur utilisé n'a pas d'importance tant que celui-ci suit bien les standards définis pour ELF et DWARF. Ont été testés gcc et clang/LLVM.

Au niveau de la mise à jour :

  • La mise à jour de fonctions se fait sous l'hypothèse que la signature soit fixe.

To-Do List

  • Injection de code step 1 : ajout de contraintes sur la compilation (flags -ldl et -rdynamic à l'édition de liens). Ceci permet de faire appel à dlopen pour charger le fichier d'update. Ceci permettra d'avoir une injection de code fonctionnelle bien que très "dirty hack".

  • Relâchement d'hypothèses : gérer des modifications de signature, des optimisations, d'autres plateformes…

  • Ajout d'autres mécanismes.

  • Injection de code step 2 : embarquer un dlopen maison, se passer du recours à nm…

Documentation

Types

L'API définit deux types opaques um_frame et um_data. Le premier permet de décrire une trame présente sur la pile et la valeur des registres dans cette trame, le second embarque toutes les informations générales sur le programme (nom de fichier, PID, informations de debug…).

Généralités

L'API s'efforce de respecter deux conventions : des noms de fonction préfixés par um_, et un argument de type um_data* en premier paramètre des fonctions qui en ont besoin.

Initialisation et destruction

int um_init (um_data** dbg, pid_t pid, const char* fname);
Fonction d'initialisation qui crée une structure um_data.
void um_end (um_data* dbg);
Libère la mémoire occupée par dbg.

Inspection de la pile

void um_destroy_stack(um_frame* list);
Libère de la mémoire toute une liste de um_frame.
um_frame* um_unwind (um_data* dbg, const char* target, um_frame** cache, int flags);
Examine la pile. Peut être utilisée soit pour avoir l'ensemble des trames présentes dans la pile, soit pour rechercher une fonction donnée. Dans le premier cas, target doit être nulle. La fonction place dans cache un pointeur vers la trame au sommet de la pile, et renvoie un pointeur vers la première trame correspondant à la fonction target (0 si absente ou non trouvée). Elle s'arrête quand le bas de la pile est atteint. Les drapeaux à mettre dans flags permettent de changer ce comportement : UM_UNWIND_STOPWHENFOUND permet de s'arrêter quand la fonction est trouvée, UM_UNWIND_RETURNLAST permet de renvoyer la dernière trame (i.e. la trame la plus bas dans la pile) trouvée plutôt que la première.
um_frame* um_get_next_frame (um_frame* cur);
Renvoie la trame ayant appelé la trame cur.
int um_get_stack_size (um_data* dbg, um_frame* stack);
Renvoie le nombre de trames présentes dans la pile en-dessous de la trame stack.
const char* um_get_function (um_data *dbg, um_frame* context);
Renvoie le nom de la fonction correspondant à la trame context.
void um_generate_trace (um_data* dbg, um_frame* stack, char** functions, uint64_t* addresses);
Génère une trace à partir d'une liste de trame stack. La fonction stocke dans le tableau functions des chaines correspondant aux noms de fonctions correspondant à chaque trame, et dans addresses les adresses associées. Ces tableaux doivent être alloués avant l'appel à cette fonction.

Récupération des informations de debug

Parsing général

int um_parse (um_data* dbg, int (*callback) (Dwarf_Die*, const char*, void*, Dwarf_Addr), void* callback_args);
Parcourt les infos de debug, appliquant callback sur chaque élément de celles-ci. Le premier argument du callback est l'élément en question, le deuxième le nom de l'élément parent, le troisième l'argument callback_args de cette fonction, et le dernier un biais qui correspond à la différence entre les adresses réelles et les adresses vues par la libdw.

Recherche générale d'attribut

typedef struct um_count_args{
    const int tag;
    const char* name;
    const char* parent_name;
    const uint64_t address;
    int result;
} um_count_args;
int um_count (Dwarf_Die* die, const char* parent_name, void* vargs, Dwarf_Addr bias);
Compte le nombre d'éléments vérifiant des conditions décrites dans count_args et le place dans le champ result. Ces conditions sont :

  • un tag (DW_TAG_variable pour une variable, DW_TAG_subprogram pour une fonction, macros définies dans dwarf.h)

  • un nom (identificateur utilisé dans le code source), mettre à 0 pour ne pas mettre de condition

  • le nom de l'élément parent (par exemple si l'on cherche une variable locale, le nom de la fonction dans laquelle elle est définie), faire pointer sur la chaîne "*" pour ne pas mettre de condition, mettre à 0 pour dire qu'il ne doit pas y en avoir (par exemple, quand l'on veut des variables globales)

  • l'adresse dans la mémoire virtuelle du programme, mettre à 0 pour ne pas mettre de condition

typedef struct um_search_first_args{
    const int tag;
    const unsigned int wanted_attribute;
    const char* name;
    const char* parent_name;
    const uint64_t address;
    Dwarf_Attribute *result;
} um_search_first_args;
int um_search_first (Dwarf_Die* die, const char* parent_name, void* vargs, Dwarf_Addr bias);
Place dans le champ result l'attribut wanted_attribute du premier élément vérifiant les conditions décrites dans les autres champs (cf. um_count dessus)
typedef struct um_search_all_args{
    const int tag;
    const unsigned int wanted_attribute;
    const char* name;
    const char* parent_name;
    const uint64_t address;
    int n_results;
    Dwarf_Attribute *result;
} um_search_all_args;
int um_search_all (Dwarf_Die* die, const char* parent_name, void* vargs, Dwarf_Addr bias);
Idem que search_first, sauf que result est un tableau de taille n_results

Fonctions particulières

uint64_t um_get_function_addr (um_data *dbg, char* name);
Retourne l'adresse de la fonction name, 0 en cas d'échec.
uint64_t um_get_local_var_addr(um_data* dbg, const char* name, const char* scope);
Retourne l'adresse de la variable name définie dans la fonction scope, 0 en cas d'échec. Si scope est nul, on recherche name à partir de la trame courante.
size_t um_get_var_size (um_data *dbg, const char* var_name, const char* scope_name);
Retourne la taille de la variable locale var_name définie dans la fonction scope_name, 0 en cas d'échec. Si scope_name est nul, on recherche var_name à partir de la trame courante.

Attachage/détachage à un processus

int um_attach (pid_t pid);
S'attache au processus pid.
int um_cont (pid_t pid);
Relance l'exécution du processus pid. Typiquement, il sera stoppé ensuite par un signal.
int um_detach (pid_t pid);
Se détache du processus pid.

Lecture et modification de l'état d'un programme

Lecture/écriture de la mémoire et des registres

int um_write_addr (um_data* dbg, uint64_t addr, uint64_t value, size_t size);
Écrit les size octets de poids faible de la valeur value dans la mémoire du programme à partir de l'adresse addr.
int um_write_registers(um_data* dbg, struct user_regs_struct* regs);
Écrit regs dans les registres du programme.
uint64_t um_read_addr (um_data* dbg, uint64_t addr, size_t size);
Renvoie les size octets de la mémoire du programme à partir de l'adresse addr.
int um_read_registers(um_data* dbg, struct user_regs_struct* regs);
Lit les registres du programme et les place dans regs.

Injection de code

int um_load_code (um_data* dbg, const char* file_name);
Injecte le code contenu dans file_name dans la mémoire du programme. file_name doit être un shared object (.so), i.e. un code compilé en -fPIC -shared. Cette fonction n'est pas encore finie. À l'heure actuelle, elle ne marche que quand le code ne fait pas appel à la moindre librairie dynamique.

Opérations de mise à jour

int um_wait_out_of_stack(um_data* dbg, char* name);
Attend que la fonction name ne soit plus dans la pile. Plus spécifiquement, cette fonction recherche la trame la plus profonde dans la pile correspondant à la fonction name, place un point d'arrêt à l'adresse de retour de cette trame, relance le programme en attendant qu'il s'arrête, une fois que ceci est fait le point d'arrêt est enlevé et la fonction retourne 0. En cas d'échec, la fonction renvoie un code d'erreur <0.
int um_redefine(um_data* dbg, char* name1, char* name2);
Remplace la fonction name1 par la fonction name2. Plus spécifiquement, cette fonction insère à l'adresse de début de name1 un jump vers l'adresse de début de name2.
int um_set_variable (um_data* dbg, char* name, bool is_local, char* scope, uint64_t val, size_t size);
Positionne la variable name à val. is_local est un booléen valant true si la variable est une variable locale, scope est le nom de la fonction dans laquelle elle est définie (l'argument est ignoré si la variable n'est pas locale). size représente la taille de la variable en octets, et est compris entre 1 et 8. Valeur de retour : 0 si tout se passe bien, un code d'erreur <0 sinon.

Updated