我正在写一些crammes.one挑战,并且想写一个挑战,其中解决方案出现在细分错误上。 (而且您必须分解代码以找到解决段错误的方法。应该很有趣吧?)因为大多数问题是“如何从sigsegv中恢复”,而大多数答案是“您不能,请正确编写代码,以免它出现段错误”。
什么是最“陷阱,能”段错误?调用空函数指针?一双免费? ...?
我可以在信号处理程序中做什么?似乎存在一些苛刻的条件(可重入,异步信号安全功能等)。或比“只读POSIX圣经”有用的解释。我们将不胜感激。
我的代码不需要可移植。如果它可以在中等标准的Linux(Debian,Redhat,Ubuntu,CentOS)上运行,就可以了。

#1 楼

我的问题给出了一个在Windows中工作的解决方案-这样,您可以在收到SIGSEGV后使程序继续运行。已弃用(请参见链接)。也就是说,为该信号注册信号处理程序并以您喜欢的任何方式导致它-执行将传递到该处理程序,您可以在其中继续。
#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, 'q4312078q', 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上使用与我的示例相同的技巧,然后在sigaction之外继续执行。

评论


谢谢 !我为您链接的帖子添加了书签,并且也会阅读。我没有足够的声誉来支持您,但您的答复深表感谢。

– ker2x
19年7月1日在18:10

如果您认为它回答了您的问题,则可以接受。如果没有,请让我知道任何疑问,以便我改善。

–bart1e
19年7月1日在18:11



#2 楼

可以针对此类问题进行参考的出色参考(也许是最好的参考)是Linux编程接口,其中包括有关信号主题的3个完整章节:概念
信号:信号处理程序
信号:高级功能

这些章节包括示例代码以及图表和清晰的说明。
(本书的免费pdf文件可以可以很容易地在网上找到。)

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

对您的问题:


最多的是“陷阱式” segfault?调用null函数指针?一双免? ...?信号处理程序(如果存在的话)将由内核根据信号(特别是其在<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()函数从异常处理程序自身内部执行非本地goto。与使用uc_mcontext.gregs[XXX]修改指令指针不同,此方法似乎是可移植的:通常,最好编写简单的信号处理程序。这样做的重要原因之一是要减少产生比赛条件的风险。信号处理程序的两种常见设计如下:


信号处理程序设置全局标志并退出。主程序定期
检查此标志,如果已设置,则采取适当的措施。 (如果主程序
无法执行此类定期检查,因为它需要监视一个或多个文件描述符以查看是否可以进行I / O,因此信号处理程序还可以将单个字节写入专用管道,该管道的读取端包括在文件中
描述符由主程序监视。我们在第63.5.2节中展示了此技术的一个示例。)
信号处理程序执行某种类型的清除,然后终止
进程或使用非本地goto(第21.2.1节)来展开该处理。堆栈并
将控制返回到主程序中的预定位置。




[执行非本地goto]提供了一种在传递由硬件异常(例如内存访问错误)引起的信号之后进行恢复的方法,并且还允许捕获信号并将控制权返回到程序中的特定点。例如,在收到SIGINT信号(通常通过键入Control-C生成)后,外壳程序执行非本地goto来将控制权返回到其主输入循环(并因此读取新命令)。



补充信息:




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



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

评论


谢谢<3我刚刚完成了我最新的crackme(行会大厅冒险),并将在第4章中实现此技巧! :)

– ker2x
19年7月5日在20:11

@ ker2x,欢迎您,我期待着尝试解决您的新Crackme程序:)

– julian♦
19年7月6日,1:15