Interception de fonctions dans le noyau Linux Ă  l'aide de ftrace

Pingouin Ninja, En3l Dans un projet lié à la sécurité des systÚmes Linux, nous devions intercepter les appels à des fonctions importantes à l'intérieur du noyau (telles que l'ouverture de fichiers et l'exécution de processus) pour permettre de surveiller l'activité dans le systÚme et bloquer préventivement l'activité des processus suspects.

Au cours du processus de dĂ©veloppement, nous avons rĂ©ussi Ă  inventer une assez bonne approche, qui nous permet d'intercepter facilement toute fonction du noyau par son nom et d'exĂ©cuter notre code autour de ses appels. L'intercepteur peut ĂȘtre installĂ© Ă  partir d'un module GPL chargeable, sans reconstruire le noyau. L'approche prend en charge les noyaux version 3.19+ pour l'architecture x86_64.

(Image de pingouin juste au-dessus: © En3l avec DeviantArt .)

Approches connues


API de sécurité Linux


Le plus correct serait d'utiliser l' API de sécurité Linux - une interface spéciale créée spécifiquement à ces fins. Dans les endroits critiques du code du noyau, les appels aux fonctions de sécurité sont localisés, ce qui, à son tour, appelle les rappels définis par le module de sécurité. Le module de sécurité peut examiner le contexte d'une opération et décider si elle est autorisée ou refusée.

Malheureusement, l'API de sécurité Linux présente quelques limitations importantes:

  • les modules de sĂ©curitĂ© ne peuvent pas ĂȘtre chargĂ©s dynamiquement, font partie du noyau et nĂ©cessitent une reconstruction
  • il ne peut y avoir qu'un seul module de sĂ©curitĂ© dans le systĂšme (Ă  quelques exceptions prĂšs)

Si la position des développeurs du noyau est ambiguë quant à la multiplicité des modules, alors l'interdiction du chargement dynamique est fondamentale: le module de sécurité doit faire partie du noyau pour assurer une sécurité constante, dÚs le chargement.

Ainsi, pour utiliser l'API de sécurité, vous devez fournir votre propre assemblage de noyau, ainsi qu'intégrer le module complémentaire avec SELinux ou AppArmor, qui sont utilisés par les distributions populaires. Le client ne souhaitant pas souscrire à de telles obligations, cet itinéraire a donc été fermé.

Pour ces raisons, l'API de sécurité ne nous convenait pas, sinon ce serait une option idéale.

Modification de la table d'appels systĂšme


La surveillance Ă©tait nĂ©cessaire principalement pour les actions effectuĂ©es par les applications utilisateur, de sorte qu'elle pouvait en principe ĂȘtre mise en Ɠuvre au niveau des appels systĂšme. Comme vous le savez, Linux stocke tous les gestionnaires d'appels systĂšme dans la table sys_call_table . La substitution de valeurs dans ce tableau entraĂźne une modification du comportement de l'ensemble du systĂšme. Ainsi, en conservant les anciennes valeurs du gestionnaire et en substituant notre propre gestionnaire dans la table, nous pouvons intercepter tout appel systĂšme.

Cette approche présente certains avantages:

  • ContrĂŽle total sur tous les appels systĂšme - la seule interface vers le noyau pour les applications utilisateur. En l'utilisant, nous pouvons ĂȘtre sĂ»rs que nous ne manquerons aucune action importante effectuĂ©e par le processus utilisateur.
  • Frais gĂ©nĂ©raux minimaux. Il y a un investissement en capital unique lors de la mise Ă  jour de la table d'appels systĂšme. Outre l'inĂ©vitable surveillance de la charge utile, la seule dĂ©pense est un appel de fonction supplĂ©mentaire (pour appeler le gestionnaire d'appels systĂšme d'origine).
  • Configuration minimale du noyau. Si vous le souhaitez, cette approche ne nĂ©cessite aucune option de configuration supplĂ©mentaire dans le noyau, donc en thĂ©orie, elle prend en charge la gamme de systĂšmes la plus large possible.

Cependant, il souffre également de quelques défauts:

  • La complexitĂ© technique de la mise en Ɠuvre. En soi, le remplacement des pointeurs dans une table n'est pas difficile. Mais les tĂąches connexes nĂ©cessitent des solutions non Ă©videntes et une certaine qualification:
    • table d'appel systĂšme de recherche
    • bypass de protection de modification de table
    • remplacement atomique et sĂ»r

    Ce sont toutes des choses intĂ©ressantes, mais elles nĂ©cessitent un temps de dĂ©veloppement prĂ©cieux, d'abord pour la mise en Ɠuvre, puis pour le support et la comprĂ©hension.
  • ImpossibilitĂ© d'intercepter certains gestionnaires. Dans les noyaux antĂ©rieurs Ă  la version 4.16, la gestion des appels systĂšme pour l'architecture x86_64 contenait un certain nombre d'optimisations. Certains d'entre eux ont exigĂ© que le gestionnaire d'appels systĂšme soit un adaptateur spĂ©cial implĂ©mentĂ© dans l'assembleur. En consĂ©quence, de tels gestionnaires sont parfois difficiles, et parfois mĂȘme impossibles Ă  remplacer par les vĂŽtres, Ă©crits en C. De plus, diffĂ©rentes optimisations sont utilisĂ©es dans diffĂ©rentes versions du noyau, ce qui ajoute aux difficultĂ©s techniques de la tirelire.
  • Seuls les appels systĂšme sont interceptĂ©s. Cette approche vous permet de remplacer les gestionnaires d'appels systĂšme, ce qui limite les points d'entrĂ©e Ă  eux uniquement. Toutes les vĂ©rifications supplĂ©mentaires sont effectuĂ©es au dĂ©but ou Ă  la fin, et nous n'avons que les arguments de l'appel systĂšme et sa valeur de retour. Parfois, cela conduit Ă  la nĂ©cessitĂ© de dupliquer les contrĂŽles de l'adĂ©quation des arguments et des contrĂŽles d'accĂšs. Parfois, cela entraĂźne une surcharge inutile lorsque vous devez copier la mĂ©moire du processus utilisateur deux fois: si l'argument est passĂ© par un pointeur, nous devons d'abord le copier nous-mĂȘmes, puis le gestionnaire d'origine copiera Ă  nouveau l'argument pour lui-mĂȘme. De plus, dans certains cas, les appels systĂšme fournissent une granularitĂ© trop faible des Ă©vĂ©nements qui doivent en outre ĂȘtre filtrĂ©s du bruit.

Au dĂ©part, nous avons choisi et mis en Ɠuvre avec succĂšs cette approche, poursuivant les avantages de la prise en charge du plus grand nombre de systĂšmes. Cependant, Ă  cette Ă©poque, nous ne connaissions toujours pas les fonctionnalitĂ©s de x86_64 et les restrictions sur les appels interceptĂ©s. Plus tard, il s'est avĂ©rĂ© essentiel pour nous de prendre en charge les appels systĂšme liĂ©s au dĂ©marrage de nouveaux processus - clone () et execve () - qui sont tout simplement spĂ©ciaux. C'est ce qui nous a conduit Ă  rechercher de nouvelles options.

Utilisation de kprobes


L'une des options envisagées était l'utilisation de kprobes : une API spécialisée principalement conçue pour le débogage et le traçage du noyau. Cette interface vous permet de définir des pré-et post-gestionnaires pour toute instruction dans le noyau, ainsi que des gestionnaires pour entrer et revenir d'une fonction. Les gestionnaires ont accÚs aux registres et peuvent les modifier. Ainsi, nous pourrions obtenir à la fois une surveillance et la capacité d'influencer la suite des travaux.

Avantages de l'utilisation de kprobes pour intercepter:

  • API mature. Les kprobes existent et se sont amĂ©liorĂ©s depuis des temps immĂ©moriaux (2002). Ils ont une interface bien documentĂ©e, la plupart des Ă©cueils ont dĂ©jĂ  Ă©tĂ© trouvĂ©s, leur travail a Ă©tĂ© optimisĂ© autant que possible, etc. En gĂ©nĂ©ral, toute une montagne d'avantages par rapport aux vĂ©los auto-fabriquĂ©s expĂ©rimentaux.
  • Interception de n'importe quel endroit dans le noyau. Les kprobes sont implĂ©mentĂ©es Ă  l'aide de points d'arrĂȘt (instructions int3) intĂ©grĂ©s dans le code exĂ©cutable du noyau. Cela vous permet d'installer kprobes littĂ©ralement n'importe oĂč dans n'importe quelle fonction, si elle est connue. De mĂȘme, les kretprobes sont implĂ©mentĂ©es en usurpant l'adresse de retour sur la pile et vous permettent d'intercepter le retour de n'importe quelle fonction (Ă  l'exception de celles qui en principe ne renvoient pas le contrĂŽle).

Inconvénients des kprobes:

  • DifficultĂ© technique. Kprobes est juste un moyen de dĂ©finir un point d'arrĂȘt n'importe oĂč dans le noyau. Pour obtenir les arguments d'une fonction ou les valeurs des variables locales, vous devez savoir dans quels registres ou oĂč sur la pile ils se trouvent, et les extraire indĂ©pendamment de lĂ . Pour bloquer un appel de fonction, vous devez modifier manuellement l'Ă©tat du processus afin que le processeur pense qu'il a dĂ©jĂ  renvoyĂ© le contrĂŽle de la fonction.
  • Les Jprobes sont obsolĂštes. Jprobes est un module complĂ©mentaire pour kprobes qui vous permet d'intercepter facilement les appels de fonction. Il extraira indĂ©pendamment les arguments de la fonction des registres ou de la pile et appellera votre gestionnaire, qui devrait avoir la mĂȘme signature que la fonction hookĂ©e. Le hic, c'est que les jprobes sont obsolĂštes et coupĂ©s dans les noyaux modernes.
  • Frais gĂ©nĂ©raux non triviaux. Les points d'arrĂȘt sont chers, mais ponctuels. Les points d'arrĂȘt n'affectent pas les autres fonctions, mais leur traitement est relativement coĂ»teux. Heureusement, l'optimisation des sauts est implĂ©mentĂ©e pour l'architecture x86_64, ce qui rĂ©duit considĂ©rablement le coĂ»t des kprobes, mais elle reste plus que, par exemple, lors de la modification de la table d'appels systĂšme.
  • Limitations des kretprobes. Les kretprobes sont implĂ©mentĂ©es en usurpant l'adresse de retour sur la pile. En consĂ©quence, ils doivent stocker l'adresse d'origine quelque part afin de pouvoir y revenir aprĂšs le traitement de kretprobe. Les adresses sont stockĂ©es dans un tampon de taille fixe. En cas de dĂ©passement, lorsque trop d'appels simultanĂ©s de la fonction interceptĂ©e sont exĂ©cutĂ©s dans le systĂšme, kretprobes sautera les opĂ©rations.
  • Extrusion dĂ©sactivĂ©e. Puisque kprobes est basĂ© sur des interruptions et jongle avec les registres du processeur, pour la synchronisation, tous les gestionnaires sont exĂ©cutĂ©s avec prĂ©emption dĂ©sactivĂ©e. Cela impose certaines restrictions aux gestionnaires: vous ne pouvez pas attendre dedans - allouer beaucoup de mĂ©moire, faire des E / S, dormir dans des temporisateurs et des sĂ©maphores, et d'autres choses connues.

Dans le processus de recherche sur le sujet, nos yeux sont tombĂ©s sur le framework ftrace , qui peut remplacer les jprobes. Il s'est avĂ©rĂ© que cela fonctionne mieux pour nos besoins d'interception d'appels de fonction. Cependant, si vous devez suivre des instructions spĂ©cifiques dans les fonctions, les kprobes ne doivent pas ĂȘtre actualisĂ©s.

Épissage


Par souci d'exhaustivitĂ©, il convient Ă©galement de dĂ©crire la mĂ©thode classique d'interception de fonctions, qui consiste Ă  remplacer les instructions au dĂ©but de la fonction par une transition inconditionnelle conduisant Ă  notre gestionnaire. Les instructions d'origine sont transfĂ©rĂ©es Ă  un autre endroit et exĂ©cutĂ©es avant de revenir Ă  la fonction interceptĂ©e. À l'aide de deux transitions, nous intĂ©grons (Ă©pissons) notre code supplĂ©mentaire dans la fonction, par consĂ©quent, cette approche est appelĂ©e Ă©pissage .

C'est ainsi que l'optimisation des sauts pour kprobes est implĂ©mentĂ©e. En utilisant l'Ă©pissage, vous pouvez obtenir les mĂȘmes rĂ©sultats, mais sans coĂ»ts supplĂ©mentaires pour les kprobes et avec un contrĂŽle complet de la situation.

Les avantages de l'épissage sont évidents:

  • Configuration minimale du noyau. L'Ă©pissage ne nĂ©cessite aucune option spĂ©ciale dans le noyau et fonctionne au dĂ©but de toute fonction. Vous avez juste besoin de connaĂźtre son adresse.
  • Frais gĂ©nĂ©raux minimaux. Deux transitions inconditionnelles - c'est toutes les actions que le code interceptĂ© doit effectuer pour transfĂ©rer le contrĂŽle au gestionnaire et vice versa. De telles transitions sont parfaitement prĂ©dites par le processeur et sont trĂšs bon marchĂ©.

Cependant, le principal inconvénient de cette approche obscurcit sérieusement l'image:

  • DifficultĂ© technique. Elle se retourne. Vous ne pouvez pas simplement prendre et réécrire le code machine. Voici une liste courte et incomplĂšte des tĂąches Ă  rĂ©soudre:
    • synchronisation de l'installation et suppression de l'interception (que faire si la fonction est appelĂ©e directement dans le processus de remplacement de ses instructions?)
    • contournement de la protection lors de la modification des rĂ©gions mĂ©moire avec un code
    • Invalidation du cache du processeur aprĂšs le remplacement des instructions
    • dĂ©montage des instructions remplaçables pour les copier en entier
    • vĂ©rification de l'absence de transitions Ă  l'intĂ©rieur de la piĂšce remplacĂ©e
    • vĂ©rifier la possibilitĂ© de dĂ©placer la piĂšce remplacĂ©e vers un autre emplacement

    Oui, vous pouvez espionner les kprobes et utiliser le framework intranucléaire livepatch, mais la solution finale est encore assez compliquée. Il est effrayant d'imaginer le nombre de problÚmes de sommeil dans chaque nouvelle implémentation.

En gĂ©nĂ©ral, si vous ĂȘtes capable d'appeler ce dĂ©mon, subordonnĂ© uniquement aux initiĂ©s, et que vous ĂȘtes prĂȘt Ă  le supporter dans votre code, l'Ă©pissage est une approche complĂštement fonctionnelle pour intercepter les appels de fonction. J'avais une attitude nĂ©gative Ă  l'Ă©gard de l'Ă©criture de vĂ©los, donc cette option est restĂ©e une sauvegarde pour nous au cas oĂč il n'y aurait aucun progrĂšs avec des solutions toutes faites plus faciles.

Nouvelle approche avec ftrace


Ftrace est un framework de traçage du noyau au niveau de la fonction. Il a Ă©tĂ© dĂ©veloppĂ© depuis 2008 et possĂšde une interface fantastique pour les programmes utilisateur. Ftrace vous permet de suivre la frĂ©quence et la durĂ©e des appels de fonction, d'afficher les graphiques des appels, de filtrer les fonctions d'intĂ©rĂȘt par modĂšle, etc. Vous pouvez commencer Ă  lire sur les fonctionnalitĂ©s de ftrace Ă  partir d'ici , puis suivre les liens et la documentation officielle.

Il implémente ftrace basé sur les clés de compilateur -pg et -mfentry , qui insÚrent l'appel à la fonction de trace spéciale mcount () ou __fentry __ () au début de chaque fonction. En général, dans les programmes utilisateur, cette fonction de compilation est utilisée par les profileurs pour suivre les appels à toutes les fonctions. Le noyau utilise ces fonctions pour implémenter le framework ftrace.

Bien sûr, appeler ftrace à partir de chaque fonction n'est pas bon marché, donc l'optimisation est disponible pour les architectures populaires: ftrace dynamique . L'essentiel est que le noyau connaisse l'emplacement de tous les appels à mcount () ou __fentry __ () et dans les premiÚres étapes du chargement remplace leur code machine par nop - une instruction spéciale qui ne fait rien. Lorsque le traçage est inclus dans les fonctions requises, les appels ftrace sont rajoutés. Ainsi, si ftrace n'est pas utilisé, son impact sur le systÚme est minime.

Description des fonctions requises


Chaque fonction interceptĂ©e peut ĂȘtre dĂ©crite par la structure suivante:

 /** * struct ftrace_hook -    * * @name:    * * @function:  -,     *   * * @original:   ,     *  ,    * * @address:   ,    * * @ops:   ftrace,  , *      */ struct ftrace_hook { const char *name; void *function; void *original; unsigned long address; struct ftrace_ops ops; }; 

L'utilisateur doit remplir uniquement les trois premiers champs: nom, fonction, original. Les champs restants sont considĂ©rĂ©s comme un dĂ©tail d'implĂ©mentation. La description de toutes les fonctions interceptĂ©es peut ĂȘtre assemblĂ©e dans un tableau et des macros peuvent ĂȘtre utilisĂ©es pour augmenter la compacitĂ© du code:

 #define HOOK(_name, _function, _original) \ { \ .name = (_name), \ .function = (_function), \ .original = (_original), \ } static struct ftrace_hook hooked_functions[] = { HOOK("sys_clone", fh_sys_clone, &real_sys_clone), HOOK("sys_execve", fh_sys_execve, &real_sys_execve), }; 

Les wrappers sur les fonctions interceptées sont les suivants:

 /* *        execve(). *     .      *  :       , *    ABI (  "asmlinkage"). */ static asmlinkage long (*real_sys_execve)(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); /* *      .   —  *   .      *  .      ,  *    . */ static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_debug("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_debug("execve() returns: %ld\n", ret); return ret; } 

Comme vous pouvez le voir, les fonctions interceptĂ©es avec un minimum de code supplĂ©mentaire. La seule chose nĂ©cessitant une attention particuliĂšre est la signature des fonctions. Ils doivent correspondre un Ă  un. Sans cela, Ă©videmment, les arguments seront mal passĂ©s et tout ira en descendant. Pour intercepter les appels systĂšme, cela est moins important, car leurs gestionnaires sont trĂšs stables et, pour plus d'efficacitĂ©, prennent les arguments dans le mĂȘme ordre que le systĂšme les appelle eux-mĂȘmes. Cependant, si vous prĂ©voyez d'intercepter d'autres fonctions, vous devez vous rappeler qu'il n'y a pas d'interfaces stables Ă  l'intĂ©rieur du noyau .

Initialisation de Ftrace


Tout d'abord, nous devons trouver et enregistrer l'adresse de la fonction que nous allons intercepter. Ftrace vous permet de tracer les fonctions par leur nom, mais nous avons encore besoin de connaĂźtre l'adresse de la fonction d'origine pour l'appeler.

Vous pouvez obtenir l'adresse en utilisant kallsymes - une liste de tous les caractÚres du noyau. Cette liste comprend tous les caractÚres, non seulement exportés pour les modules. Obtenir l'adresse de la fonction hookée ressemble à ceci:

 static int resolve_hook_address(struct ftrace_hook *hook) { hook->address = kallsyms_lookup_name(hook->name); if (!hook->address) { pr_debug("unresolved symbol: %s\n", hook->name); return -ENOENT; } *((unsigned long*) hook->original) = hook->address; return 0; } 

Ensuite, vous devez initialiser la structure ftrace_ops . C'est contraignant
le champ est juste func , indiquant un rappel, mais nous avons aussi besoin
définir des indicateurs importants:

 int fh_install_hook(struct ftrace_hook *hook) { int err; err = resolve_hook_address(hook); if (err) return err; hook->ops.func = fh_ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_IPMODIFY; /* ... */ } 

fh_ftrace_thunk () est notre rappel que ftrace appellera lors du traçage d'une fonction. À propos de lui plus tard. Les drapeaux que nous avons mis seront nĂ©cessaires pour terminer l'interception. Ils demandent Ă  ftrace de sauvegarder et de restaurer les registres du processeur, dont nous pouvons changer le contenu lors du rappel.

Nous sommes maintenant prĂȘts Ă  activer l'interception. Pour ce faire, vous devez d'abord activer ftrace pour la fonction qui nous intĂ©resse en utilisant ftrace_set_filter_ip (), puis autoriser ftrace Ă  appeler notre rappel en utilisant register_ftrace_function ():

 int fh_install_hook(struct ftrace_hook *hook) { /* ... */ err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); if (err) { pr_debug("ftrace_set_filter_ip() failed: %d\n", err); return err; } err = register_ftrace_function(&hook->ops); if (err) { pr_debug("register_ftrace_function() failed: %d\n", err); /*    ftrace   . */ ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); return err; } return 0; } 

L'interception est dĂ©sactivĂ©e de la mĂȘme maniĂšre, uniquement dans l'ordre inverse:

 void fh_remove_hook(struct ftrace_hook *hook) { int err; err = unregister_ftrace_function(&hook->ops); if (err) { pr_debug("unregister_ftrace_function() failed: %d\n", err); } err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); if (err) { pr_debug("ftrace_set_filter_ip() failed: %d\n", err); } } 

Une fois l'appel à unregister_ftrace_function () terminé, l'absence d'activation du rappel installé dans le systÚme (et avec lui nos wrappers) est garantie. Par conséquent, nous pouvons, par exemple, décharger le module d'intercepteur en toute sécurité, sans craindre que quelque part dans le systÚme nos fonctions soient toujours exécutées (car si elles disparaissent, le processeur sera bouleversé).

Exécution d'un hook de fonction


Comment l'interception est-elle réellement réalisée? TrÚs simple. Ftrace vous permet de changer l'état des registres aprÚs avoir quitté un rappel. En modifiant le registre% rip - un pointeur sur la prochaine instruction exécutable - nous modifions les instructions que le processeur exécute - c'est-à-dire que nous pouvons le forcer à exécuter une transition inconditionnelle de la fonction actuelle vers la nÎtre. Ainsi nous prenons le contrÎle.

Le rappel pour ftrace est le suivant:

 static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) { struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); regs->ip = (unsigned long) hook->function; } 

En utilisant la macro container_of (), nous obtenons l'adresse de notre struct ftrace_hook Ă  l'adresse de la struct ftrace_hook incorporĂ©e, aprĂšs quoi nous remplaçons la valeur du registre% rip dans la struct pt_regs par l'adresse de notre gestionnaire. C’est tout. Pour les architectures autres que x86_64, ce registre peut ĂȘtre appelĂ© diffĂ©remment (comme IP ou PC), mais l'idĂ©e leur est en principe applicable.

Notez le qualificatif notrace ajoutĂ© pour le rappel. Ils peuvent signaler les entitĂ©s dont le suivi n'est pas autorisĂ© Ă  l'aide de ftrace. Par exemple, c'est ainsi que les fonctions de ftrace elle-mĂȘme qui sont impliquĂ©es dans le processus de trace sont marquĂ©es. Cela aide Ă  empĂȘcher le systĂšme de geler dans une boucle sans fin lors du traçage de toutes les fonctions dans le noyau (ftrace peut le faire).

Le rappel ftback appelle généralement avec l'extrusion désactivée (comme kprobes). Il peut y avoir des exceptions, mais vous ne devez pas vous y fier. Dans notre cas, cependant, cette restriction n'est pas importante, nous remplaçons donc seulement huit octets dans la structure.

La fonction wrapper, qui sera appelĂ©e ultĂ©rieurement, s'exĂ©cutera dans le mĂȘme contexte que la fonction d'origine. Par consĂ©quent, lĂ , vous pouvez faire ce qui est autorisĂ© Ă  ĂȘtre fait dans la fonction interceptĂ©e. Par exemple, si vous interceptez un gestionnaire d'interruption, vous ne pouvez toujours pas dormir dans un wrapper.

Protection d'appel récursive


: , ftrace, , . - .

, — parent_ip — ftrace-, , . . , .

, parent_ip , — - . , .

, ( ). , . .

, ftrace- :

 static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs) { struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); /*      . */ if (!within_module(parent_ip, THIS_MODULE)) regs->ip = (unsigned long) hook->function; } 

/ :

  • . . , , .
  • . . , .
  • . kretprobes , ( ). , .


: ls , . (, Bash) fork () + execve () . clone() execve() . , execve(), .

- :

sequence-

, ( ) ( ), ftrace ( ) ( ).

  1. SYSCALL. — entry_SYSCALL_64 (). 64- 64- .
  2. . , , do_syscall_64 (), . sys_call_table — sys_execve ().
  3. ftrace. __fentry__ (), ftrace. , , nop , sys_execve() .
  4. Ftrace . ftrace , . , %rip, .
  5. . parent_ip , do_syscall_64() — sys_execve() — , %rip pt_regs .
  6. Ftrace . FTRACE_SAVE_REGS, ftrace pt_regs . ftrace . %rip — — .
  7. -. - sys_execve() . fh_sys_execve (). , do_syscall_64().
  8. . . fh_sys_execve() ( ) . . — sys_execve() , real_sys_execve , .
  9. . sys_execve(), ftrace . , -

  10. . sys_execve() fh_sys_execve(), do_syscall_64(). sys_execve() . : ftrace sys_execve() .
  11. . sys_execve() fh_sys_execve(). . , execve() , , , . .
  12. . fh_sys_execve() do_syscall_64(), , . .
  13. . IRET ( SYSRET, execve() — IRET), . ( ) .


, :

  • API . . , , . — -, .
  • . . - , , , - . ( ), .
  • . , ftrace, . kprobes ftrace.

?

  • . ftrace :
    • kallsyms
    • ftrace
    • ftrace,

    . , , , , . , - , .
  • ftrace , kprobes ( ftrace ), , , . , ftrace — , «» ftrace .
  • . , . , , ftrace . , , .
  • ftrace. parent_ip ftrace . , . , : ftrace , 5 ( call), ftrace .

.


, ftrace kallsyms. :

  • CONFIG_FTRACE
  • CONFIG_KALLSYMS

, ftrace .

  • CONFIG_DYNAMIC_FTRACE_WITH_REGS

, 3.19 , FTRACE_OPS_FL_IPMODIFY. %rip, 3.19 . , — .

, ftrace : , ( ).

  • CONFIG_HAVE_FENTRY

x86_64 , i386 — . - i386 ftrace , ftrace . %eip — , , .

ftrace 32- x86. , ( «»), , ftrace.


: . , , . , .

, . - ftrace- parent_ip . - , ftrace , - .

, , , . , -.

:

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_debug("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_debug("execve() returns: %ld\n", ret); return ret; } 

— :

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { long ret; pr_devel("execve() called: filename=%p argv=%p envp=%p\n", filename, argv, envp); ret = real_sys_execve(filename, argv, envp); pr_devel("execve() returns: %ld\n", ret); return ret; } 

, ? , . - , .

, , , pr_devel() . printk- . , , DEBUG. :

 static asmlinkage long fh_sys_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { return real_sys_execve(filename, argv, envp); } 

. (tail call optimization). , . :

 0000000000000000 <fh_sys_execve>: 0: e8 00 00 00 00 callq 5 <fh_sys_execve+0x5> 5: ff 15 00 00 00 00 callq *0x0(%rip) b: f3 c3 repz retq 

— :

 0000000000000000 <fh_sys_execve>: 0: e8 00 00 00 00 callq 5 <fh_sys_execve+0x5> 5: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax c: ff e0 jmpq *%rax 

CALL — __fentry__(), . real_sys_execve ( ) CALL fh_sys_execve() RET. real_sys_execve() JMP.

«» , , CALL. , — parent_ip . fh_sys_execve() , — . parent_ip , .

, . . .

-:

 #pragma GCC optimize("-fno-optimize-sibling-calls") 

Conclusion



 Linux — . , - , .

, Github .

Source: https://habr.com/ru/post/fr413241/


All Articles