我可以捕获 SIGSEGV(在 Linux 上)吗?让它工作的条件是什么?(对于快手)

逆向工程 linux 快手
2021-06-15 22:26:52

我正在写一些 crackmes.one 挑战,我想写一个挑战,解决方案出现在分段错误上。(而且你必须反汇编代码才能找到段错误的方法。应该很有趣吧?)

我在这里和那里找到了一些难以理解的理论答案,但我找不到实际的解决方案。因为大多数问题是“如何从 sigsegv 中恢复”而大多数答案是“你不能,让你的代码正确,这样它就不会出现段错误”。

什么是最“容易捕获”的段错误?对空函数指针的调用?双免?……?

我可以在我的信号处理程序中做什么?似乎有一些苛刻的条件(可重入,异步信号安全功能等......)。

如果有人可以给我一些安全的指针(双关语)指向某种文档、博客……或者比“只阅读 POSIX 圣经”更有用的解释。这将不胜感激。

我的代码不需要是可移植的。如果它适用于中等标准的 Linux(Debian、Redhat、Ubuntu、CentOS),那就没问题了。

2个回答

我的问题中给出了在 Windows 中工作的解决方案- 这样,您可以使程序在收到SIGSEGV.

你可以在 linux 上做类似的事情(尽管我建议使用sigaction而不推荐使用signal它(见链接))。也就是说,为该信号注册信号处理程序,并以您喜欢的方式启动它 - 执行将传递给处理程序,您可以在那里继续。

下面给出了一个示例代码:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void handler(int sig)
{
    write(1, "success", strlen("success")); // printf is not recommended here, but should work as well
    exit(0);
}

int main()
{
    struct sigaction sa;
    memset (&sa, '\0', sizeof(sa));
    sa.sa_sigaction = &handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL); // register handler for SIGSEGV
    int *a; // a will contain some garbage value
    int b = *a; // trigger segmentation fault; transfer control to handler
}

当然,您可以使用与我在 Windows 上的示例相同的技巧,并在handler.

对于此类问题,可以参考的优秀参考资料(可能是最好的参考资料)是The Linux Programming Interface,其中包括 3 个关于信号主题的完整章节:

  • 信号:基本概念
  • 信号:信号处理程序
  • 信号:高级功能

这些章节包括示例代码以及图表和清晰的解释。(可以在网上轻松找到该书的免费 pdf。)

另请参阅:使用信号的二进制混淆

对于您的问题:

什么是最“可陷阱”的段错误?对空函数指针的调用?双免?……?

信号处理程序(如果存在)将由内核根据信号(特别是在 中定义的编号<signal.h>)而不是触发信号的特定事件来调用这意味着您可以自由决定使用哪种无效内存引用来触发分段错误。取消引用空指针可能是最可靠的,因为它保证程序行为是确定性的,因为无论堆栈内容或进程在内存中的布局如何,段错误总是会发生。


我可以在我的信号处理程序中做什么?似乎有一些苛刻的条件(可重入,异步信号安全功能,...)。

您有多种选择。但首先,关于不可重入库函数的一些信息:

如果函数使用静态数据结构进行内部簿记,则它们也可以是不可重入的此类函数最明显的示例是stdio的成员printf()scanf()等),它们更新缓冲 I/O 的内部数据结构。因此,当printf()在信号处理程序中使用时,我们有时可能会看到奇怪的输出——甚至程序崩溃或数据损坏——如果处理程序在执行调用printf()或另一个stdio函数的过程中中断主程序

重要的部分是最后一句话。SIGSEGV如果在异常处理程序中调用不可重入库函数,则实现异常处理程序可能会导致意外的信号处理程序行为,因为可能会在您预期的特定条件之外触发分段错误。例如,当用户输入通过 读入缓冲区时scanf,除非正确实施边界检查,否则由于输入 1000 个 'A 导致的缓冲区溢出可能导致SIGSEGV被发送到进程,然后在执行过程中触发异常处理程序一个stdio函数。如果信号处理程序也调用stdio函数,则可能会发生未定义的行为。

  • 对于您的情况,最有趣的方法可能是将uc_mcontext.gregs[REG_RIP]信号处理程序context结构中(这里RIP 是 x86-64 指令指针)值更改为指向程序中其他位置的函数。信号处理程序完成后,程序将在该函数处继续执行。或者,该uc_mcontext.gregs[REG_RIP]值可以递增以跳过/跳过导致信号处理程序首先执行的指令。这意味着可以将信号处理程序设计为在接收时简单地跳转到程序中的不同位置SIGSEGV这种方法(或者,执行非本地 goto)消除了信号处理程序执行任何 I/O 的需要(不需要printf(), 等等。)。缺点是这种方法依赖于体系结构。可以在以下文章中找到说明此技术的示例:Linux - 编写故障处理程序一些相关的示例代码也可以在这里找到:在信号处理程序中,如何知道程序在哪里中断?

  • 注意,由于进程可以向自身发送信号,因此不必选择SIGSEGV作为处理信号;getpid()并且kill()可用于向进程发送一些其他信号以触发信号处理程序。

  • 此外,可以通过函数sigsetjmp()从异常处理程序本身执行非本地转到siglongjmp()uc_mcontext.gregs[XXX]用于修改指令指针不同的是,这种方法似乎是可移植的:

    • 通常,最好编写简单的信号处理程序。这样做的一个重要原因是降低产生竞争条件的风险。信号处理程序的两种常见设计如下:

      • 信号处理程序设置一个全局标志并退出。主程序会定期检查此标志,如果已设置,则采取适当的措施。(如果主程序因为需要监视一个或多个文件描述符以查看是否可以进行 I/O 而无法执行此类定期检查,则信号处理程序也可以将单个字节写入专用管道,该管道的读端包含在主程序监视的文件描述符。我们在第 63.5.2 节中展示了这种技术的一个例子。)
      • 信号处理程序执行某种类型的清理,然后终止进程或使用非本地 goto(第 21.2.1 节)来展开堆栈并将控制权返回到主程序中的预定位置。
    • [Executing a nonlocal goto] 提供了一种在传递由硬件异常(例如,内存访问错误)引起的信号后恢复的方法,还允许我们捕获信号并将控制权返回到程序中的特定点。例如,在收到 SIGINT 信号(通常通过键入 Control-C 生成)后,shell 执行非本地 goto 以将控制返回到其主输入循环(从而读取新命令)。


补充资料:

信号传递和处理程序执行

信号是对进程发生事件的通知。信号有时被描述为软件中断。信号类似于硬件中断,因为它们会中断程序的正常执行流程;在大多数情况下,无法准确预测信号何时到达。一个进程可以(如果它有合适的权限)向另一个进程发送信号。在这种用途中,信号可以用作同步技术,甚至可以用作进程间通信 (IPC) 的原始形式。进程也可以向自身发送信号。然而,发送到进程的许多信号的通常来源是内核。


参考:Linux 编程接口,第 20 和 21 章