6

I'm writing some crackmes.one challenge and I want to write a challenge where the solution appears on a segmentation fault. (And you have to disassemble the code to find a way to segfault. It should be fun, right?)

I found some hard-to-understand theoretical answer here and there but I can't find a practical solution. Because most of the question are "how to recover from a sigsegv" and most of the answer are "you can't, make your code right so it doesn't segfault".

What would be the most "trap-able" segfault? a call to a null function pointer? a double-free? ...?

What can I do in my signal handler? It seems there are some harsh condition (reentrancy, async-signal-safe function, etc...).

If someone can give me some safe pointer (pun intended) to some kind of documentation, blog, ... or an explanation a bit more useful than "just read the POSIX bible". It would be greatly appreciated.

My code doesn't need to be portable. If it works on a moderately standard Linux (Debian, Redhat, Ubuntu, CentOS) it will be fine.

2 Answers 2

5

A solution working in Windows was given in my question - this way, you can cause program to continue after it receives SIGSEGV.

You can do similar thing on linux (though I would recommend using sigaction instead of signal which is deprecated (see link)). That is, register signal handler for this signal and cause it whichever way you prefer to - execution will be passed to the handler, where you can continue.

An example code is given below:

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

Of course, you can use the same trick as in my example on Windows and continue the execution outside handler.

2
  • 1
    Thank you ! i bookmarked the post you linked and will read it too. I don't have enough reputation to upvote you but your answer is greatly appreciated.
    – ker2x
    Commented Jul 1, 2019 at 18:10
  • You can accept it if you feel it answered your question. And if not, let me know about any doubts, so I can improve it.
    – bart1e
    Commented Jul 1, 2019 at 18:11
2

An excellent reference (probably the best one) that can be consulted for this type of problem is The Linux Programming Interface, which includes 3 full chapters on the topic of signals:

  • Signals: Fundamental Concepts
  • Signals: Signal Handlers
  • Signals: Advanced features

These chapters include example code as well as diagrams and clear explanations. (A free pdf of the book can easily be found online.)

See also: Binary Obfuscation Using Signals

To your questions:

What would be the most "trap-able" segfault ? a call to a null function pointer ? a double-free ? ... ?

A signal handler, should it exist, will be called by the kernel on the basis of the signal (specifically, its number as defined in <signal.h>), rather than the specific event that triggered the signal. This means that you are free to decide what kind of invalid memory reference to make in order to trigger a segmentation fault. Dereferencing a null pointer is probably the most reliable, since it guarantees program behavior will be deterministic, as a segmentation fault will always occur irrespective of stack contents or the layout of the process in memory.


What can i do in my signal handler ? It seems there are some harsh condition (reentrancy, async-signal-safe function,...).

You have several options. But first, a bit about non-reentrant library functions:

Functions can also be nonreentrant if they use static data structures for their internal bookkeeping. The most obvious examples of such functions are the members of the stdio library (printf(), scanf(), and so on), which update internal data structures for buffered I/O. Thus, when using printf() from within a signal handler, we may sometimes see strange output—or even a program crash or data corruption—if the handler interrupts the main program in the middle of executing a call to printf() or another stdio function.

The important part is the last sentence. Implementing an exception handler for SIGSEGV may result in unintended signal handler behavior if non-reentrant library functions are called within the exception handler, since it may be possible to trigger a segmentation fault outside of the specific conditions you anticipate. For example, when user input is read into a buffer via scanf, unless bounds checking is properly implemented, a buffer overflow due to 1000 'A's being entered can result in SIGSEGV being sent to the process, which will then trigger the exception handler during the execution of an stdio function. If the signal handler also calls an stdio function, undefined behavior may occur.

  • The most interesting approach for your case would probably be changing the uc_mcontext.gregs[REG_RIP] (RIP here is the x86-64 instruction pointer) value in the signal handler's context struct to point to a function somewhere else in the program. After the signal handler finishes, program execution would resume at that function. Alternatively, the uc_mcontext.gregs[REG_RIP] value can incremented to skip/jump over the instructions that caused the signal handler to execute in the first place. This means that the signal handler could be designed to simply jump to a different location in the program upon receiving SIGSEGV. This approach, (or alternatively, executing a nonlocal goto), eliminates the need for any I/O to be performed by the signal handler (no need for printf(), etc.). The drawback is that this approach is architecture-dependent. Examples illustrating this technique can be found in the following article: Linux - Writing Fault Handlers. Some related example code can also be found here: In a signal handler, how to know where the program is interrupted?

  • Note that since it is possible for a process to send a signal to itself, it is not necessary to choose SIGSEGV as the handled signal; getpid() and kill() can be used to send some other signal to the process in order to trigger the signal handler.

  • Additionally, it is possible to execute a non-local goto from within the exception handler itself via functions sigsetjmp() and siglongjmp(). Unlike using uc_mcontext.gregs[XXX] to modify the instruction pointer, this approach appears to be portable:

    • In general, it is preferable to write simple signal handlers. One important reason for this is to reduce the risk of creating race conditions. Two common designs for signal handlers are the following:

      • The signal handler sets a global flag and exits. The main program periodically checks this flag and, if it is set, takes appropriate action. (If the main program cannot perform such periodic checks because it needs to monitor one or more file descriptors to see if I/O is possible, then the signal handler can also write a single byte to a dedicated pipe whose read end is included among the file descriptors monitored by the main program. We show an example of this technique in Section 63.5.2.)
      • The signal handler performs some type of cleanup and then either terminates the process or uses a nonlocal goto (Section 21.2.1) to unwind the stack and return control to a predetermined location in the main program.
    • [Executing a nonlocal goto] provides a way to recover after delivery of a signal caused by a hardware exception (e.g., a memory access error), and also allows us to catch a signal and return control to a particular point in a program. For example, upon receipt of a SIGINT signal (normally generated by typing Control-C), the shell performs a nonlocal goto to return control to its main input loop (and thus read a new command).


Supplementary info:

Signal delivery and handler execution

A signal is a notification to a process that an event has occurred. Signals are sometimes described as software interrupts. Signals are analogous to hardware interrupts in that they interrupt the normal flow of execution of a program; in most cases, it is not possible to predict exactly when a signal will arrive. One process can (if it has suitable permissions) send a signal to another process. In this use, signals can be employed as a synchronization technique, or even as a primitive form of interprocess communication (IPC). It is also possible for a process to send a signal to itself. However, the usual source of many signals sent to a process is the kernel.


Reference: The Linux Programming Interface, chapters 20 and 21

2
  • 1
    Thank you <3 I just finished my latest crackme (guild hall adventure) and will implement this trick in chapter 4 ! :)
    – ker2x
    Commented Jul 5, 2019 at 20:11
  • @ker2x you’re welcome and I look forward to trying to solve your new crackme programs :)
    – julian
    Commented Jul 6, 2019 at 1:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.