GNU/Linux >> Znalost Linux >  >> Linux

Jak mohu sledovat, co se vkládá do standardního výstupního bufferu, a přeruší se, když je do roury uložen specifický řetězec?

Tato otázka může být dobrým výchozím bodem:jak mohu v gdb umístit bod přerušení na „něco je vytištěno na terminálu“?

Takže byste se mohli alespoň zlomit, kdykoli se něco zapíše do stdout. Metoda v podstatě zahrnuje nastavení bodu přerušení na write syscall s podmínkou, že první argument je 1 (tj. STDOUT). V komentářích je také nápověda, jak byste mohli zkontrolovat řetězec parametru write zavolejte také.

32bitový režim x86

Přišel jsem s následujícím a testoval jsem to s gdb 7.0.1-debian. Zdá se, že to funguje docela dobře. $esp + 8 obsahuje ukazatel na paměťové umístění řetězce předávaného do write , takže jej nejprve přetypujete na integrál a poté na ukazatel na char . $esp + 4 obsahuje deskriptor souboru, do kterého se má zapisovat (1 pro STDOUT).

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

64bitový režim x86

Pokud váš proces běží v režimu x86-64, pak se parametry předávají prostřednictvím registrů škrábanců %rdi a %rsi

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

Upozorňujeme, že je odstraněna jedna úroveň nepřímosti, protože v zásobníku používáme spíše registry škrábanců než proměnné.

Varianty

Funkce jiné než strcmp lze použít ve výše uvedených úryvcích:

  • strncmp je užitečné, pokud chcete najít shodu s prvním n počet znaků zapisovaného řetězce
  • strstr lze použít k nalezení shod v řetězci, protože si nemůžete být vždy jisti, že hledaný řetězec je na začátku řetězce zapisovaného přes write funkce.

Upravit: Tato otázka se mi líbila a našla jsem na ni následnou odpověď. Rozhodl jsem se o tom napsat příspěvek na blog.


catch + strstr stava

Skvělé na této metodě je, že nezávisí na glibc write používá:sleduje skutečné systémové volání.

Navíc je odolnější vůči printf() ukládání do vyrovnávací paměti, protože může dokonce zachytit řetězce, které jsou vytištěny přes více printf() hovory.

x86_64 verze:

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

Testovací program:

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

Výsledek:přestávka v:

  • qwer1
  • qwer2
  • fflush . Předchozí printf ve skutečnosti nic nevytiskly, byly uloženy ve vyrovnávací paměti! write k syacall došlo pouze na fflush .

Poznámky:

  • $bpnum díky Tromeymu na:https://sourceware.org/bugzilla/show_bug.cgi?id=18727
  • rdi :registr, který obsahuje číslo systémového volání Linuxu v x86_64, 1 je pro write
  • rsi :první argument systémového volání pro write ukazuje na vyrovnávací paměť
  • strstr :standardní volání funkce C, hledá dílčí shody, pokud není nalezeno, vrací NULL

Testováno v Ubuntu 17.10, gdb 8.0.1.

strace

Další možnost, pokud se cítíte interaktivní:

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

Ukázkový výstup:

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

Nyní zkopírujte tuto adresu a vložte ji do:

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

Výhodou této metody je, že pro manipulaci s strace můžete použít obvyklé nástroje UNIX výstup a nevyžaduje hluboké GDB-fu.

Vysvětlení:

  • -i dělá výstup trace RIP
  • setarch -R deaktivuje ASLR pro proces s personality systémové volání:Jak ladit pomocí strace -i, když je adresa pokaždé jiná GDB to již dělá ve výchozím nastavení, takže to není třeba opakovat.

Anthonyho odpověď je úžasná. Po jeho odpovědi jsem vyzkoušel jiné řešení na Windows (x86-64 bitů Windows). Vím, že tato otázka je pro GDB na Linuxu si však myslím, že toto řešení je doplňkem pro tento druh otázek. Může to být užitečné pro ostatní.

Řešení v systému Windows

V Linuxu volání na printf by mělo za následek volání rozhraní API write . A protože Linux je open source OS, mohli bychom ladit v rámci API. Rozhraní API se však ve Windows liší, poskytuje vlastní API WriteFile. Vzhledem k tomu, že Windows je komerční neopen source OS, nebylo možné do API přidávat body přerušení.

Některé zdrojové kódy VC jsou však publikovány společně s Visual Studiem, takže ve zdrojovém kódu jsme mohli zjistit, kde se nakonec nazývá WriteFile API a nastavte tam bod přerušení. Po ladění ukázkového kódu jsem našel printf metoda by mohla vést k volání _write_nolock ve kterém WriteFile je nazýván. Funkce se nachází v:

your_VS_folder\VC\crt\src\write.c

Prototyp je:

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

V porovnání s write API v systému Linux:

#include <unistd.h>

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

Mají úplně stejné parametry. Takže bychom mohli nastavit condition breakpoint v _write_nolock stačí se podívat na výše uvedená řešení, pouze s některými rozdíly v detailech.

Přenosné řešení pro Win32 i x64

Je velké štěstí, že jsme mohli použít přímý název parametrů v sadě Visual Studio při nastavování podmínky pro zarážky na Win32 i x64. Takže je velmi snadné napsat podmínku:

  1. Přidejte zarážky do _write_nolock

    UPOZORNĚNÍ :Na Win32 a x64 jsou malé rozdíly. Mohli bychom jen použít název funkce k nastavení umístění bodů přerušení na Win32. Na x64 to však nebude fungovat, protože ve vstupu funkce nejsou parametry inicializovány. Proto jsme nemohli použít název parametru k nastavení podmínky zarážek.

    Ale naštěstí máme nějaké řešení:k nastavení zarážek, např. 1. řádku funkce, použijte místo názvu funkce místo ve funkci. Tam už jsou parametry inicializovány. (Mám na mysli použití filename+line number nastavit zarážky nebo otevřít soubor přímo a nastavit zarážku ve funkci, ne vstupní, ale první řádek. )

  2. Omezte podmínku:

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

UPOZORNĚNÍ :stále je zde problém, testoval jsem dva různé způsoby, jak něco zapsat do stdout:printf a std::cout . printf zapíše všechny řetězce do _write_nolock funkce najednou. Nicméně std::cout by do _write_nolock předával pouze znak po znaku , což znamená, že API by se jmenovalo strlen("your string") časy. V tomto případě nemohla být podmínka aktivována navždy.

Řešení Win32

Samozřejmě bychom mohli použít stejné metody jako Anthony za předpokladu:nastavení podmínky přerušení podle registrů.

Pro program Win32 je řešení téměř stejné s GDB na Linuxu. Můžete si všimnout, že je zde ozdoba __cdecl v prototypu _write_nolock . Tato volací konvence znamená:

  • Pořadí předávání argumentů je zprava doleva.
  • Volání funkce vybere argumenty ze zásobníku.
  • Konvence pro zdobení jmen:Znak podtržítka (_) má před jmény předponu.
  • Nebyl proveden žádný překlad případu.

Zde je popis. A existuje příklad, který se používá k zobrazení registrů a zásobníků na webu společnosti Microsoft. Výsledek lze nalézt zde.

Pak je velmi snadné nastavit podmínku breakpointů:

  1. Nastavte bod přerušení v _write_nolock .
  2. Omezte podmínku:

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

Je to stejná metoda jako na Linuxu. První podmínkou je zajistit, aby byl řetězec zapsán do stdout . Druhým je, aby odpovídal zadanému řetězci.

Řešení x64

Dvě důležité modifikace z x86 na x64 jsou schopnost 64bitového adresování a plochá sada 16 64bitových registrů pro obecné použití. S nárůstem registrů používá x64 pouze __fastcall jako povolávací konvence. První čtyři celočíselné argumenty jsou předány v registrech. Argumenty pět a vyšší se předávají na hromádku.

Můžete se podívat na stránku předávání parametrů na webu společnosti Microsoft. Čtyři registry (v pořadí zleva doprava) jsou RCX , RDX , R8 a R9 . Je tedy velmi snadné omezit podmínku:

  1. Nastavte bod přerušení v _write_nolock .

    UPOZORNĚNÍ :je to odlišné od výše uvedeného přenosného řešení, mohli bychom jen nastavit umístění bodu přerušení na funkci a nikoli na 1. řádek funkce. Důvodem je, že všechny registry jsou již inicializovány u vstupu.

  2. Omezující podmínka:

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

Důvod, proč potřebujeme cast a dereference na esp je to $esp přistupuje k ESP registrovat a pro všechny účely je void* . Zatímco registry zde ukládají přímo hodnoty parametrů. Takže další úroveň nepřímosti již není potřeba.

Příspěvek

Tato otázka mě také velmi baví, proto jsem Anthonyho příspěvek přeložil do čínštiny a vložil do něj svou odpověď jako doplněk. Příspěvek lze nalézt zde. Děkujeme za svolení @anthony-arnold.


Linux
  1. Linux – Jak zjistit, jaké pevné disky jsou v systému?

  2. Jak monitorovat provoz Tcp mezi Localhostem a IP adresou?

  3. Jak zjistit zatížení spustitelných souborů dynamických knihoven při spuštění?

  1. Jaký je význam caddr_t a kdy se používá?

  2. Jak mohu přidat řetězec na začátek každého řádku v souboru?

  3. Jak mohu nechat tcpdump zapisovat do souboru a standardní výstup příslušných dat?

  1. Jak sledovat využití paměti vyhrazené jádru?

  2. Jak mohu trvale změnit IP adresu a adresu brány?

  3. Jak mohu sledovat délku fronty přijetí?