GNU/Linux >> Linux Esercitazione >  >> Linux

Come posso monitorare ciò che viene inserito nel buffer di uscita standard e rompersi quando una stringa specifica viene depositata nella pipe?

Questa domanda potrebbe essere un buon punto di partenza:come posso inserire un punto di interruzione su "qualcosa viene stampato sul terminale" in gdb?

Quindi potresti almeno interrompere ogni volta che qualcosa viene scritto su stdout. Il metodo prevede sostanzialmente l'impostazione di un punto di interruzione su write syscall con una condizione che il primo argomento sia 1 (es. STDOUT). Nei commenti c'è anche un suggerimento su come ispezionare il parametro stringa del write chiama pure.

modalità x86 a 32 bit

Ho trovato quanto segue e l'ho testato con gdb 7.0.1-debian. Sembra funzionare abbastanza bene. $esp + 8 contiene un puntatore alla locazione di memoria della stringa passata a write , quindi prima lo trasformi in un integrale, quindi in un puntatore a char . $esp + 4 contiene il descrittore di file su cui scrivere (1 per STDOUT).

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

modalità x86 a 64 bit

Se il tuo processo è in esecuzione in modalità x86-64, i parametri vengono passati attraverso i registri di lavoro %rdi e %rsi

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

Nota che un livello di riferimento indiretto è stato rimosso poiché stiamo utilizzando i registri di lavoro anziché le variabili nello stack.

Varianti

Funzioni diverse da strcmp può essere utilizzato negli snippet precedenti:

  • strncmp è utile se vuoi abbinare il primo n numero di caratteri della stringa da scrivere
  • strstr può essere utilizzato per trovare corrispondenze all'interno di una stringa, poiché non puoi sempre essere certo che la stringa che stai cercando sia all'inizio della stringa scritta attraverso il write funzione.

Modifica: Mi è piaciuta questa domanda e ho trovato la risposta successiva. Ho deciso di scrivere un post sul blog.


catch + strstr condizione

La cosa bella di questo metodo è che non dipende da glibc write in uso:traccia l'effettiva chiamata di sistema.

Inoltre, è più resistente a printf() buffering, in quanto potrebbe persino rilevare stringhe stampate su più printf() chiamate.

versione 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

Programma di prova:

#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;
}

Risultato:pause in:

  • qwer1
  • qwer2
  • fflush . Il precedente printf in realtà non hanno stampato nulla, sono stati bufferizzati! Il write syacall è avvenuto solo sul fflush .

Note:

  • $bpnum grazie a Tromey su:https://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi :registro che contiene il numero della chiamata di sistema Linux in x86_64, 1 sta per write
  • rsi :primo argomento della chiamata di sistema, per write punta al buffer
  • strstr :chiamata di funzione C standard, cerca submatch, restituisce NULL se non trovata

Testato in Ubuntu 17.10, gdb 8.0.1.

strace

Un'altra opzione se ti senti interattivo:

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

Esempio di output:

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

Ora copia quell'indirizzo e incollalo in:

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

Il vantaggio di questo metodo è che puoi usare i soliti strumenti UNIX per manipolare strace output e non richiede GDB-fu profondo.

Spiegazione:

  • -i rende l'output di strace RIP
  • setarch -R disabilita ASLR per un processo con personality chiamata di sistema:come eseguire il debug con strace -i quando ogni volta che l'indirizzo è diverso GDB lo fa già per impostazione predefinita, quindi non è necessario farlo di nuovo.

La risposta di Antonio è fantastica. Dopo la sua risposta, ho provato un'altra soluzione su Windows (Windows x86-64 bit). So che questa domanda è per GDB su Linux, tuttavia, penso che questa soluzione sia un supplemento per questo tipo di domande. Potrebbe essere utile per gli altri.

Soluzione su Windows

In Linux una chiamata a printf risulterebbe in una chiamata all'API write . E poiché Linux è un sistema operativo open source, potremmo eseguire il debug all'interno dell'API. Tuttavia, l'API è diversa su Windows, ha fornito il proprio API WriteFile. Poiché Windows è un sistema operativo commerciale non open source, non è stato possibile aggiungere punti di interruzione nelle API.

Ma parte del codice sorgente di VC è pubblicato insieme a Visual Studio, quindi abbiamo potuto scoprire nel codice sorgente dove finalmente si chiamava WriteFile API e impostare un punto di interruzione lì. Dopo aver eseguito il debug sul codice di esempio, ho trovato il printf potrebbe risultare in una chiamata a _write_nolock in cui WriteFile è chiamato. La funzione si trova in:

your_VS_folder\VC\crt\src\write.c

Il prototipo è:

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

Rispetto al write API su Linux:

#include <unistd.h>

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

Hanno totalmente gli stessi parametri. Quindi potremmo semplicemente impostare un condition breakpoint in _write_nolock basta fare riferimento alle soluzioni di cui sopra, con solo alcune differenze in dettaglio.

Soluzione portatile per Win32 e x64

È molto fortunato poter utilizzare direttamente il nome dei parametri in Visual Studio quando si imposta una condizione per i punti di interruzione sia in Win32 che in x64. Quindi diventa molto facile scrivere la condizione:

  1. Aggiungi un punto di interruzione in _write_nolock

    AVVISO :Ci sono poche differenze su Win32 e x64. Potremmo semplicemente usare il nome della funzione per impostare la posizione dei punti di interruzione su Win32. Tuttavia, non funzionerà su x64 perché all'ingresso della funzione i parametri non vengono inizializzati. Pertanto, non è stato possibile utilizzare il nome del parametro per impostare la condizione dei punti di interruzione.

    Ma fortunatamente abbiamo qualche soluzione:usa la posizione nella funzione piuttosto che il nome della funzione per impostare i punti di interruzione, ad esempio la prima riga della funzione. I parametri sono già inizializzati lì. (Intendo usare il filename+line number per impostare i punti di interruzione, oppure aprire direttamente il file e impostare un punto di interruzione nella funzione, non l'ingresso ma la prima riga. )

  2. Limita la condizione:

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

AVVISO :c'è ancora un problema qui, ho provato due modi diversi per scrivere qualcosa in stdout:printf e std::cout . printf scriverebbe tutte le stringhe nel _write_nolock funzione in una volta. Comunque std::cout passerebbe solo carattere per carattere a _write_nolock , il che significa che l'API si chiamerà strlen("your string") volte. In questo caso, la condizione non potrebbe essere attivata per sempre.

Soluzione Win32

Ovviamente potremmo usare gli stessi metodi di Anthony fornito:imposta la condizione dei punti di interruzione dei registri.

Per un programma Win32, la soluzione è quasi la stessa con GDB su Linux. Potresti notare che c'è una decorazione __cdecl nel prototipo di _write_nolock . Questa convenzione di chiamata significa:

  • L'ordine di passaggio degli argomenti è Da destra a sinistra.
  • La funzione chiamante estrae gli argomenti dallo stack.
  • Convenzione per la decorazione dei nomi:il carattere di sottolineatura (_) è preceduto dai nomi.
  • Nessuna traduzione maiuscola eseguita.

C'è una descrizione qui. E c'è un esempio che viene utilizzato per mostrare i registri e gli stack sul sito Web di Microsoft. Il risultato potrebbe essere trovato qui.

Quindi è molto facile impostare la condizione dei punti di interruzione:

  1. Imposta un punto di interruzione in _write_nolock .
  2. Limita la condizione:

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

È lo stesso metodo di Linux. La prima condizione è assicurarsi che la stringa sia scritta in stdout . Il secondo è quello di abbinare la stringa specificata.

Soluzione x64

Due importanti modifiche da x86 a x64 sono la capacità di indirizzamento a 64 bit e un set piatto di 16 registri a 64 bit per uso generale. Con l'aumentare dei registri, x64 usa solo __fastcall come convenzione di chiamata. I primi quattro argomenti interi vengono passati nei registri. Gli argomenti dal quinto in su vengono passati in pila.

Puoi fare riferimento alla pagina Parameter Passing sul sito Web di Microsoft. I quattro registri (in ordine da sinistra a destra) sono RCX , RDX , R8 e R9 . Quindi è molto facile restringere la condizione:

  1. Imposta un punto di interruzione in _write_nolock .

    AVVISO :è diverso dalla soluzione portatile sopra, potremmo semplicemente impostare la posizione del punto di interruzione sulla funzione piuttosto che sulla prima riga della funzione. Il motivo è che tutti i registri sono già inizializzati all'ingresso.

  2. Condizione di restrizione:

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

Il motivo per cui abbiamo bisogno di cast e dereference su esp è quel $esp accede al ESP register, ea tutti gli effetti è un void* . Mentre i registri qui memorizzano direttamente i valori dei parametri. Quindi non è più necessario un altro livello di indiretto.

Pubblica

Anche questa domanda mi piace molto, quindi ho tradotto il post di Anthony in cinese e ho inserito la mia risposta come supplemento. Il post potrebbe essere trovato qui. Grazie per il permesso di @anthony-arnold.


Linux
  1. Guida per principianti a DNSSEC e come può proteggere Internet

  2. Quanto è grande il buffer del tubo?

  3. Come impostare le impostazioni internazionali e quali sono le implicazioni di farlo?

  4. Come monitorare il traffico TCP tra l'host locale e l'indirizzo IP?

  5. Come scoprire gli eseguibili delle librerie dinamiche caricati durante l'esecuzione?

Come trovare una stringa o una parola specifica in file e directory

Come scoprire chi ha riavviato il sistema Linux e quando

Come monitorare l'avanzamento dei dati attraverso un tubo utilizzando il comando "pv".

Come monitorare l'utilizzo della memoria dedicata al kernel?

Come posso modificare gli indirizzi IP e gateway in modo permanente?

Come posso nascondere l'output di un'applicazione shell in Linux?