如何监视input到标准输出缓冲区中的内容,并在特定string存储在pipe道中时中断?

在Linux中,用C / C ++代码,使用gdb,如何添加一个gdb断点来扫描传入的string以打破特定的string?

我没有访问特定库的代码,但是只要该库发送一个特定的string到标准输出,我就会中断,这样我就可以返回堆栈并调查我调用库的那部分代码。 当然,我不想等到缓冲区刷新发生。 可以这样做吗? 也许在libstdc++的例程?

Solutions Collecting From Web of "如何监视input到标准输出缓冲区中的内容,并在特定string存储在pipe道中时中断?"

这个问题可能是一个很好的起点: 我怎样才能把一个断点“在gdb中打印到终端”?

所以,只要有东西写入标准输出,至少可以中断。 该方法基本上涉及在write系统调用时设置一个断点,条件是第一个参数是1 (即STDOUT)。 在注释中,还有一个关于如何检查write调用的字符串参数的提示。

x86 32位模式

我想出了以下内容,并使用gdb 7.0.1-debian进行测试。 这似乎工作得很好。 $esp + 8包含一个指向字符串传递给write的内存位置的指针,所以首先将它转换为一个整数,然后转换为一个指向char的指针。 $esp + 4包含要写入的文件描述符(1表示标准输出)。

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

x86 64位模式

如果您的进程在x86-64模式下运行,则参数将通过临时寄存器%rdi%rsi传递

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

请注意,由于我们使用临时寄存器而不是堆栈中的变量,所以删除了一个间接级别。

变种

上面的代码片段中可以使用除strcmp以外的其他函数:

  • 如果要匹配被写入的字符串的前n个字符,则strncmp非常有用
  • 可以使用strstr来查找字符串中的匹配项,因为您不能总是确定要查找的字符串是在通过write函数写入的字符串的开头

编辑:我喜欢这个问题,并找到它的后续答案。 我决定做一个关于它的博客文章 。

安东尼的答案真棒。 在他的回答之后,我尝试了Windows上的另一个解决方案(x86-64位Windows)。 我知道这里的问题是针对Linux上的GDB ,但是我认为这个解决方案是对这种问题的补充。 这可能对其他人有帮助。

Windows上的解决方案

在Linux中,调用printf将导致调用API write 。 而且由于Linux是一个开放源码的操作系统,我们可以在API中进行调试。 但是,这个API在Windows上是不同的,它提供了它自己的API WriteFile 。 由于Windows是一个商业的非开源操作系统,因此无法在API中添加断点。

但是VC的一些源代码与Visual Studio一起发布,所以我们可以在源代码中找到最终调用WriteFile API的地方,并在那里设置一个断点。 在对示例代码进行调试之后,我发现printf方法可能导致调用WriteFile _write_nolock 。 该功能位于:

 your_VS_folder\VC\crt\src\write.c 

原型是:

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

与Linux上的write API相比:

 #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 

他们有完全相同的参数。 所以我们可以在_write_nolock设置一个condition breakpoint ,只是参考上面的解决方案,只是细节上有些不同。

适用于Win32和x64的便携式解决方案

在Win32和x64上设置断点条件时,我们可以直接在Visual Studio上使用参数名称,这是非常幸运的。 所以写出这个条件变得很容易:

  1. _write_nolock添加一个断点

    注意 :Win32和x64没有什么区别。 我们可以使用函数名称来设置Win32上断点的位置。 但是,它不能在x64上工作,因为在函数的入口处,参数未被初始化。 因此,我们不能使用参数名来设置断点的条件。

    但幸运的是,我们有一些解决方法:使用函数中的位置而不是函数名来设置断点,例如函数的第一行。 参数已经在那里初始化了。 (我的意思是使用filename+line number来设置断点,或者直接打开文件并在函数中设置一个断点,而不是第一行的入口。

  2. 限制条件:

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

注意 :这里还有一个问题,我测试了两种不同的方法来写入stdout: printfstd::coutprintf会将所有字符串一次写入_write_nolock函数。 然而, std::cout只能将字符逐个传递给_write_nolock ,这意味着API将被称为strlen("your string")次。 在这种情况下,条件不能永远激活。

Win32解决方案

当然,我们可以使用与Anthony提供的方法相同的方法:通过寄存器设置断点的条件。

对于Win32程序,这个解决方案与Linux上的GDB几乎是一样的。 您可能会注意到_write_nolock的原型中有一个装饰__cdecl 。 这个调用约定意味着:

  • 参数传递顺序是从右到左。
  • 调用函数从堆栈弹出参数。
  • 名称装饰约定:下划线字符(_)以名称为前缀。
  • 没有案件翻译进行。

这里有一个描述。 在微软的网站上有一个用来显示寄存器和堆栈的例子 。 结果可以在这里找到。

那么设置断点的条件是很容易的:

  1. _write_nolock设置一个断点。
  2. 限制条件:

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

这与Linux上的方法相同。 第一个条件是确保字符串被写入stdout 。 第二个是匹配指定的字符串。

x64解决方案

从x86到x64的两个重要修改是64位寻址能力和16个通用的64位寄存器。 随着寄存器的增加,x64只使用__fastcall作为调用约定。 前四个整数参数在寄存器中传递。 参数5和更高在堆栈上传递。

您可以参考Microsoft网站上的参数传递页面。 四个寄存器(从左到右)是RCXRDXR8R9 。 所以限制条件很容易:

  1. _write_nolock设置一个断点。

    注意 :与上述便携式解决方案不同的是,我们可以将断点的位置设置为功能,而不是功能的第一行。 原因是所有的寄存器已经在入口初始化了。

  2. 限制条件:

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

我们需要在esp上进行转换esp引用的原因是$esp访问了ESP寄存器,所有的意图和目的都是void* 。 而这里的寄存器直接存储参数的值。 所以不再需要另一个间接的层面了。

岗位

我也非常喜欢这个问题,所以我把安东尼的文章翻译成中文,并把答案作为补充。 这个帖子可以在这里找到。 感谢@安东诺阿诺德的许可。

抓住

catch + condition是另一种选择。 x86_64的:

 start define stdout catch syscall write commands printf "rsi = %s\n", $rsi backtrace end condition $arg0 $rdi == 1 && strstr((char *)$rsi, "$arg1") != 0 end stdout 2 hello 
  • 2是断点的编号。 1是由start创建的main断点。

    不幸的是,我没有看到自动获取这个数字的方法( if没有catch )。 我已经在https://sourceware.org/bugzilla/show_bug.cgi?id=18727打开了一个请&#x6C42;

    start (或run )是必需的,因为您必须运行程序来加载strcmp

  • 这个方法很酷的地方在于,它不依赖glibc write被使用:它跟踪实际的系统调用。

    所以,即使glibc有另一种不经过write打印方式(我不知道它是否有),它也可以工作。

    不足之处在于它不处理printf缓冲。

strace的

另一种选择,如果你感觉互动:

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

示例输出:

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

现在复制该地址并将其粘贴到:

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

这种方法的优点是可以使用通常的UNIX工具来操作strace输出,而且不需要深度的GDB-fu。

说明:

  • -i使strace输出RIP
  • setarch -R为具有personality系统调用的进程禁用ASLR: 如何在每次地址不同时使用strace -i进行调试 GDB默认已经做了这些操作,所以不需要再次执行。

安东尼的答案是非常有趣的,它肯定会给出一些结果。 但是,我认为它可能会错过printf的缓冲。 事实上, write()和printf()之间的区别在于 ,你可以阅读:“printf不一定每次调用write,而是printf缓冲它的输出。

STDIO包装解决方案

因此,我来​​了另一个解决方案,包括创建一个帮助程序库,您可以预加载包装类似功能的printf。 然后,您可以在这个库源和回溯中设置一些断点,以获得有关您正在调试的程序的信息。

它适用于Linux和目标的libc,我不知道为c + + IOSTREAM,如果程序使用直接写,它会错过它。

这里是劫持printf(io_helper.c)的包装。

 #include<string.h> #include<stdio.h> #include<stdarg.h> #define MAX_SIZE 0xFFFF int printf(const char *format, ...){ char target_str[MAX_SIZE]; int i=0; va_list args1, args2; /* RESOLVE THE STRING FORMATING */ va_start(args1, format); vsprintf(target_str,format, args1); va_end(args1); if (strstr(target_str, "Hello World")){ /* SEARCH FOR YOUR STRING */ i++; /* BREAK HERE */ } /* OUTPUT THE STRING AS THE PROGRAM INTENTED TO */ va_start(args2, format); vprintf(format, args2); va_end(args2); return 0; } int puts(const char *s) { return printf("%s\n",s); } 

我添加了放置,因为海湾合作委员会倾向于取代printf时,它可以。 所以我强迫它回到printf。

接下来,您只需将其编译到共享库。

gcc -shared -fPIC io_helper.c -o libio_helper.so -g

你在运行gdb之前加载它。

LD_PRELOAD=$PWD/libio_helper.so; gdb test

测试是您正在调试的程序。

然后你可以打破break io_helper.c:19因为你用-g编译了库。

说明

我们的运气在这里是printf和其他fprintf,sprintf …只是在这里解决可变参数,并称他们的'v'等价。 (在我们的例子中是vprintf)。 做这个工作很容易,所以我们可以做到这一点,并用“v”函数将实际工作留给libc。 为了得到printf的可变参数,我们只需要使用va_start和va_end。

这个方法的主要优点是你可以确定当你中断的时候,你在程序的那部分输出你的目标字符串,并且这不是一个缓冲区中的剩余部分。 你也不要在硬件上做任何假设。 缺点是你假设程序使用libc stdio函数输出东西。