Wiki
Clone wikipymoult / 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);
void um_end (um_data* dbg);
Inspection de la pile
void um_destroy_stack(um_frame* list);
um_frame* um_unwind (um_data* dbg, const char* target, um_frame** cache, int flags);
um_frame* um_get_next_frame (um_frame* cur);
int um_get_stack_size (um_data* dbg, um_frame* stack);
const char* um_get_function (um_data *dbg, um_frame* context);
void um_generate_trace (um_data* dbg, um_frame* stack, char** functions, uint64_t* addresses);
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);
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);
-
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);
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);
Fonctions particulières
uint64_t um_get_function_addr (um_data *dbg, char* name);
uint64_t um_get_local_var_addr(um_data* dbg, const char* name, const char* scope);
size_t um_get_var_size (um_data *dbg, const char* var_name, const char* scope_name);
Attachage/détachage à un processus
int um_attach (pid_t pid);
int um_cont (pid_t pid);
int um_detach (pid_t 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);
int um_write_registers(um_data* dbg, struct user_regs_struct* regs);
uint64_t um_read_addr (um_data* dbg, uint64_t addr, size_t size);
int um_read_registers(um_data* dbg, struct user_regs_struct* regs);
Injection de code
int um_load_code (um_data* dbg, const char* file_name);
-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);
int um_redefine(um_data* dbg, char* name1, char* name2);
int um_set_variable (um_data* dbg, char* name, bool is_local, char* scope, uint64_t val, size_t size);
Updated