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ímn
počet znaků zapisovaného řetězcestrstr
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řeswrite
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 nafflush
.
Poznámky:
$bpnum
díky Tromeymu na:https://sourceware.org/bugzilla/show_bug.cgi?id=18727rdi
:registr, který obsahuje číslo systémového volání Linuxu v x86_64,1
je prowrite
rsi
:první argument systémového volání prowrite
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 RIPsetarch -R
deaktivuje ASLR pro proces spersonality
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:
-
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. ) -
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ů:
- Nastavte bod přerušení v
_write_nolock
. -
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:
-
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.
-
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.