GNU/Linux >> Tutoriels Linux >  >> Linux

Comment puis-je surveiller ce qui est mis dans le tampon de sortie standard et casser lorsqu'une chaîne spécifique est déposée dans le tuyau?

Cette question pourrait être un bon point de départ :comment puis-je mettre un point d'arrêt sur "quelque chose est imprimé sur le terminal" dans gdb ?

Ainsi, vous pourriez au moins casser chaque fois que quelque chose est écrit sur stdout. La méthode consiste essentiellement à définir un point d'arrêt sur le write syscall avec une condition que le premier argument est 1 (c'est-à-dire STDOUT). Dans les commentaires, il y a aussi un indice sur la façon dont vous pourriez inspecter le paramètre de chaîne du write appeler aussi.

Mode x86 32 bits

J'ai trouvé ce qui suit et je l'ai testé avec gdb 7.0.1-debian. Cela semble fonctionner assez bien. $esp + 8 contient un pointeur vers l'emplacement mémoire de la chaîne passée à write , donc d'abord vous le convertissez en une intégrale, puis en un pointeur vers char . $esp + 4 contient le descripteur de fichier dans lequel écrire (1 pour STDOUT).

$ gdb break write if 1 == *(int*)($esp + 4) && strcmp((char*)*(int*)($esp + 8), "your string") == 0

Mode x86 64 bits

Si votre processus s'exécute en mode x86-64, les paramètres sont transmis via les registres de travail %rdi et %rsi

$ gdb break write if 1 == $rdi && strcmp((char*)($rsi), "your string") == 0

Notez qu'un niveau d'indirection est supprimé puisque nous utilisons des registres de travail plutôt que des variables sur la pile.

Variantes

Fonctions autres que strcmp peut être utilisé dans les extraits ci-dessus :

  • strncmp est utile si vous voulez faire correspondre le premier n nombre de caractères de la chaîne en cours d'écriture
  • strstr peut être utilisé pour trouver des correspondances dans une chaîne, car vous ne pouvez pas toujours être certain que la chaîne que vous recherchez est au début de chaîne en cours d'écriture via le write fonction.

Modifier : J'ai apprécié cette question et trouver sa réponse ultérieure. J'ai décidé de faire un article de blog à ce sujet.


catch + strstr état

L'avantage de cette méthode est qu'elle ne dépend pas de glibc write en cours d'utilisation :il trace l'appel système réel.

De plus, il est plus résistant à printf() mise en mémoire tampon, car il pourrait même intercepter des chaînes qui sont imprimées sur plusieurs printf() appels.

Version x86_64 :

define stdout
    catch syscall write
    commands
        printf "rsi = %s\n", $rsi
        bt
    end
    condition $bpnum $rdi == 1 && strstr((char *)$rsi, "$arg0") != NULL
end
stdout qwer

Programme d'essai :

#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    write(STDOUT_FILENO, "asdf1", 5);
    write(STDOUT_FILENO, "qwer1", 5);
    write(STDOUT_FILENO, "zxcv1", 5);
    write(STDOUT_FILENO, "qwer2", 5);
    printf("as");
    printf("df");
    printf("qw");
    printf("er");
    printf("zx");
    printf("cv");
    fflush(stdout);
    return EXIT_SUCCESS;
}

Résultat :pauses à :

  • qwer1
  • qwer2
  • fflush . Le précédent printf n'ont rien imprimé, ils ont été mis en mémoire tampon ! Le write syacall ne s'est produit que sur le fflush .

Remarques :

  • $bpnum merci à Tromey sur :https://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi :registre qui contient le numéro de l'appel système Linux en x86_64, 1 est pour write
  • rsi :premier argument du syscall, pour write il pointe vers le tampon
  • strstr  :appel de fonction C standard, recherche les sous-correspondances, renvoie NULL si non trouvé

Testé dans Ubuntu 17.10, gdb 8.0.1.

trace

Une autre option si vous vous sentez interactif :

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep '\] write'

Exemple de sortie :

[00007ffff7b00870] write(1, "a\nb\n", 4a

Copiez maintenant cette adresse et collez-la dans :

setarch "$(uname -m)" -R strace -i ./stdout.out |& grep -E '\] write\(1, "a'

L'avantage de cette méthode est que vous pouvez utiliser les outils UNIX habituels pour manipuler strace sortie, et il ne nécessite pas de profondeur GDB-fu.

Explication :

  • -i rend la sortie strace RIP
  • setarch -R désactive ASLR pour un processus avec un personality appel système :comment déboguer avec strace -i lorsque chaque adresse est différente GDB le fait déjà par défaut, donc pas besoin de le refaire.

La réponse d'Anthony est géniale. Suite à sa réponse, j'ai essayé une autre solution sur Windows (Windows x86-64 bits). Je sais que cette question est pour GDB sous Linux, cependant, je pense que cette solution est un complément pour ce genre de question. Cela pourrait être utile pour d'autres.

Solution sous Windows

Sous Linux un appel à printf entraînerait un appel à l'API write . Et parce que Linux est un système d'exploitation open source, nous pourrions déboguer dans l'API. Cependant, l'API est différente sous Windows, elle a fourni sa propre API WriteFile. Étant donné que Windows est un système d'exploitation commercial non open source, les points d'arrêt n'ont pas pu être ajoutés dans les API.

Mais une partie du code source de VC est publiée avec Visual Studio, nous avons donc pu découvrir dans le code source où finalement appelé le WriteFile API et définissez-y un point d'arrêt. Après avoir débogué sur l'exemple de code, j'ai trouvé le printf méthode peut entraîner un appel à _write_nolock dont WriteFile est appelé. La fonction se trouve dans :

your_VS_folder\VC\crt\src\write.c

Le prototype est :

/* now define version that doesn't lock/unlock, validate fh */
int __cdecl _write_nolock (
        int fh,
        const void *buf,
        unsigned cnt
        )

Par rapport au write API sous Linux :

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count); 

Ils ont totalement les mêmes paramètres. Nous pourrions donc simplement définir un condition breakpoint en _write_nolock il suffit de se référer aux solutions ci-dessus, avec seulement quelques différences de détail.

Solution portable pour Win32 et x64

C'est vraiment une chance que nous puissions utiliser le nom des paramètres directement sur Visual Studio lors de la définition d'une condition pour les points d'arrêt sur Win32 et x64. Il devient alors très simple d'écrire la condition :

  1. Ajouter un point d'arrêt dans _write_nolock

    AVIS :Il y a peu de différence sur Win32 et x64. Nous pourrions simplement utiliser le nom de la fonction pour définir l'emplacement des points d'arrêt sur Win32. Cependant, cela ne fonctionnera pas sur x64 car à l'entrée de la fonction, les paramètres ne sont pas initialisés. Par conséquent, nous ne pouvions pas utiliser le nom du paramètre pour définir la condition des points d'arrêt.

    Mais heureusement, nous avons une solution :utilisez l'emplacement dans la fonction plutôt que le nom de la fonction pour définir les points d'arrêt, par exemple, la 1ère ligne de la fonction. Les paramètres y sont déjà initialisés. (Je veux dire utiliser le filename+line number pour définir les points d'arrêt, ou ouvrez le fichier directement et définissez un point d'arrêt dans la fonction, pas l'entrée mais la première ligne. )

  2. Restreindre la condition :

    fh == 1 && strstr((char *)buf, "Hello World") != 0
    

AVIS :il y a toujours un problème ici, j'ai testé deux manières différentes d'écrire quelque chose dans stdout :printf et std::cout . printf écrirait toutes les chaînes dans le _write_nolock fonction à la fois. Cependant std::cout ne passerait que caractère par caractère à _write_nolock , ce qui signifie que l'API s'appellerait strlen("your string") fois. Dans ce cas, la condition n'a pas pu être activée indéfiniment.

Solution Win32

Bien sûr, nous pourrions utiliser les mêmes méthodes que Anthony fourni :définir la condition des points d'arrêt par registres.

Pour un programme Win32, la solution est presque la même avec GDB sur Linux. Vous remarquerez peut-être qu'il y a une décoration __cdecl dans le prototype de _write_nolock . Cette convention d'appel signifie :

  • L'ordre de passage des arguments est de droite à gauche.
  • La fonction d'appel extrait les arguments de la pile.
  • Convention de décoration des noms :le caractère de soulignement (_) est préfixé aux noms.
  • Aucune traduction de cas effectuée.

Il y a une description ici. Et il y a un exemple qui est utilisé pour montrer les registres et les piles sur le site Web de Microsoft. Le résultat est à découvrir ici.

Ensuite, il est très facile de définir la condition des points d'arrêt :

  1. Définir un point d'arrêt dans _write_nolock .
  2. Restreindre la condition :

    *(int *)($esp + 4) == 1 && strstr(*(char **)($esp + 8), "Hello") != 0
    

C'est la même méthode que sur Linux. La première condition est de s'assurer que la chaîne est écrite dans stdout . La seconde consiste à faire correspondre la chaîne spécifiée.

Solution x64

Deux modifications importantes de x86 à x64 sont la capacité d'adressage 64 bits et un ensemble plat de 16 registres 64 bits à usage général. Au fur et à mesure de l'augmentation des registres, x64 n'utilise que __fastcall comme convention d'appel. Les quatre premiers arguments entiers sont passés dans des registres. Les arguments cinq et plus sont passés sur la pile.

Vous pouvez vous référer à la page Parameter Passing sur le site Web de Microsoft. Les quatre registres (dans l'ordre de gauche à droite) sont RCX , RDX , R8 et R9 . Il est donc très facile de restreindre la condition :

  1. Définir un point d'arrêt dans _write_nolock .

    AVIS :c'est différent de la solution portable ci-dessus, nous pourrions simplement définir l'emplacement du point d'arrêt sur la fonction plutôt que sur la 1ère ligne de la fonction. La raison en est que tous les registres sont déjà initialisés à l'entrée.

  2. Condition de restriction :

    $rcx == 1 && strstr((char *)$rdx, "Hello") != 0
    

La raison pour laquelle nous avons besoin de cast et de déréférencement sur esp est-ce $esp accède au ESP enregistrer, et à toutes fins utiles est un void* . Tandis que les registres ici stockent directement les valeurs des paramètres. Ainsi, un autre niveau d'indirection n'est plus nécessaire.

Publier

J'aime aussi beaucoup cette question, alors j'ai traduit le message d'Anthony en chinois et j'y ai mis ma réponse en complément. Le poste peut être trouvé ici. Merci pour la permission de @anthony-arnold.


Linux
  1. Guide du débutant sur DNSSEC et comment il peut sécuriser Internet

  2. Quelle est la taille du tampon de tuyau ?

  3. Linux - Comment savoir quels disques durs sont dans le système ?

  4. Comment définir les paramètres régionaux et quelles sont les implications de le faire ?

  5. Comment surveiller le trafic Tcp entre l'hôte local et l'adresse IP ?

Comment trouver une chaîne ou un mot spécifique dans les fichiers et les répertoires

Comment trouver qui a redémarré le système Linux et quand

Comment surveiller la progression des données dans un tube à l'aide de la commande "pv"

Comment puis-je modifier les adresses IP et de passerelle de manière permanente ?

Comment puis-je masquer la sortie d'une application shell sous Linux ?

En utilisant bash, comment puis-je connaître la moyenne, le maximum et le minimum d'une liste de nombres ?