In this post, we’re going to dissect a very simple challenge from Hack the Box, “Behind the Scenes”. We’ll also look at how to work with Unix signals and how to skip illegal instructions in executables. Buckle up!

Cracking the challenge

First of all, let’s try running the challenge executable.


The program expects a single argument with a password. Neat. Running strings over the executable haven’t yielded anything interesting, so let’s look at its internals with Ghidra.

Looking at the main function, we can see some Unix signals trickery at the beginning, and the endless loop with the invalidInstructionException(); in the end.


Let’s begin with the latter. While selecting the invalidInstructionException(); instruction in the decompilation, Ghidra highlights the following assembly instruction:

001012e6	0f 0b	UD2

Referring to IntelĀ® 64 and IA-32 Architectures Software Developer Manual, Vol. 2B, UD2 “generates an invalid opcode exception. This instruction is provided for software testing to explicitly generate an invalid opcode exception. The opcodes for this instruction are reserved for this purpose.”
Ghidra stops disassembling the file after the first occurrence of UD2. It makes total sense, by default the whole program will be terminated due to receiving a SIGILL Unix signal. However, there’s clearly more code after that instruction:


So, first of all, let’s select the rest of the code that hasn’t been disassembled and disassemble it by pressing “D”. As we can see, it’s indeed the executable code:


Next, let’s replace all occurrences of the UD2 instruction with a NOP (there are a few dozens of them scattered across the main function). After doing so, we can immediately see that there are 4 strcmps comparing the user input with string constants. And the flag seems to be a concatenation of these constants:


By inputting the concatenated value into the program, we can verify that it’s the correct flag. Easy, huh?


But how does it work?

That’s a good question, let’s figure it out.

As I’ve mentioned earlier, the operating system sends a SIGILL signal to the program trying to execute an illegal instruction. But can we skip the illegal instruction and continue the execution? The answer is yes, and that’s what the program does by registering a custom signal handler.

Installing a signal handler

Let’s take a look at the decompiled fragment of the main once again:


Let’s break it down:

  • Line 6, a property of type sigaction is defined. For convenience, I renamed it to sa;
  • Line 10, sa gets filled with zeroes to clear the allocated memory. We can make an educated guess that initially, it looked like memset(&sa, 0, sizeof(struct sigaction)), but the compiler was able to replace the sizeof call with a constant value;
  • Line 11, sa.sa_mask is cleared out by calling the sigemptyset function;
  • Line 12, a pointer to the local signal handler function segill_sigaction is assigned to the sa.sa_sigaction (named by Ghidra as sa.__sigaction_handler);
  • Line 13, the value of SA_SIGINFO flag is assigned to the sa.sa_flags (#define SA_SIGINFO 0x00000004 in signal.h);
  • Line 14, the constructed sa structure is assigned as a handler for a SIGILL signal (#define SIGILL 4 in signal.h) by calling the sigaction function (yes, the structure and the function are named identically).

Handling the received signal

The next piece of the puzzle is the signal handler function, segill_sigaction in our case.
The signature of the handler function is void handler(int sig, siginfo_t *info, void *ucontext). As sigaction(2) page states, these three arguments are as follows:

  • int sig: The number of the signal that caused invocation of the handler;
  • siginfo_t *info: A pointer to a siginfo_t, which is a structure containing further information about the signal;
  • void *ucontext: This is a pointer to a ucontext_t structure, cast to void *. The structure pointed to by this field contains signal context information that was saved on the user-space stack by the kernel.

Page sigaction(3p) also states that new applications should explicitly cast the third argument of the signal handling function to ucontext_t *.

Let’s retype the 3rd parameter and take a look at the decompilation:


First of all, let’s take a look at the ucontext_t structure contents. As The GNU C Library Reference Manual states, it should include at least the following fields:

  • ucontext_t *uc_link: This is a pointer to the next context structure which is used if the context described in the current structure returns;
  • sigset_t uc_sigmask: Set of signals which are blocked when this context is used;
  • stack_t uc_stack: Stack used for this context. The value need not be (and normally is not) the stack pointer;
  • mcontext_t uc_mcontext: This element contains the actual state of the process. The mcontext_t type is also defined in this header but the definition should be treated as opaque. Any use of knowledge of the type makes applications less portable.

Aha, we’re getting closer. Let’s take a look at the excerpt from the glibc sources for the x86-64 architecture containing the mcontext_t definition and some of its nested data types:

/* Context to describe the whole processor state.  */
typedef struct
    gregset_t __ctx(gregs);
    fpregset_t __ctx(fpregs);
    __extension__ unsigned long long __reserved1 [8];
} mcontext_t;

/* Container for all general registers.  */
typedef greg_t gregset_t[__NGREG];

/* Number of each register in the `gregset_t' array.  */
  REG_R8 = 0,
# define REG_R8		REG_R8
# define REG_R9		REG_R9
# define REG_R10	REG_R10
# define REG_R11	REG_R11
# define REG_R12	REG_R12
# define REG_R13	REG_R13
# define REG_R14	REG_R14
# define REG_R15	REG_R15
# define REG_RDI	REG_RDI
# define REG_RSI	REG_RSI
# define REG_RBP	REG_RBP
# define REG_RBX	REG_RBX
# define REG_RDX	REG_RDX
# define REG_RAX	REG_RAX
# define REG_RCX	REG_RCX
# define REG_RSP	REG_RSP
# define REG_RIP	REG_RIP
# define REG_EFL	REG_EFL
# define REG_ERR	REG_ERR
# define REG_CR2	REG_CR2

As we can see, the field gregs contains all the saved CPU registers.

At this point, let’s refer back to sigaction(3p) that states that “when the signal handler returns, the receiving thread resumes execution at the point it was interrupted unless the signal handler makes other arrangements”. In our case, we deliberately want to resume the execution after skipping the invalid instruction. Since the UD2 operation is 2 bytes long, we can achieve that by incrementing the RIP register by 2.

In C, it would look like this:

ucontext->uc_mcontext.gregs[REG_RIP] += 2;

Important note: this solution is quite hacky and tied up to run strictly on x86-64 POSIX-compliant systems. Don’t do this in any production code.
The only possible exception is if you deliberately trying to do some bizarre obfuscation, this can be one of your tools - since you have the access to all the saved context, it’s possible to change the values of the general-purpose registers, jump to another address by modifying the instruction pointer, and do another weird stuff (:

Compare it with what the Ghidra decompiled. Looks quite similar, isn’t it? :)

The only difference is that Ghidra wasn’t able to figure out the name of the register and just provided its index in the array, 0x10. We can manually verify that it’s the correct index of the REG_RIP. Alternatively, we can print the index of REG_RIP by running this snippet of code, which in turn prints 0x10:

#define __USE_GNU
#define _GNU_SOURCE
#include <stddef.h>
#include <stdio.h>
#include <ucontext.h>

void main() {
    printf("0x%x\n", REG_RIP);

Another way of doing that is to find the offset in the whole ucontext_t structure. Imagine that we only have the assembly listing of the function, which looks like this:


After some argument shuffling, the address of the ucontext gets stored in the local_ucontext variable. Its value at the offset 0xa8 is loaded into the RAX, incremented by 2, and written back into the structure.

For the reference, function decompilation looks like this:


By making an educated guess we assume that the function increments the RIP register. We can find it’s offset in the structure by running the following snippet of code:

#define __USE_GNU
#define _GNU_SOURCE
#include <stddef.h>
#include <stdio.h>
#include <ucontext.h>

void main() {
   size_t rip_offset = offsetof(ucontext_t, uc_mcontext.gregs[REG_RIP]);
   printf("0x%x\n", rip_offset);

It prints 0xa8, which matches the offset in both the assembly listing and the decompilation.

Combining it all together

Now let’s combine all the pieces of the puzzle together. Here’s a simple program which conceptually works the same as the challenge binary:

#include <stdio.h>
#define __USE_GNU
#include <signal.h>
#include <string.h>

void sighandler (int signo, siginfo_t *info, void *context) {
    ucontext_t *uc = (ucontext_t *)context;

    int instruction_length = 2; // The length of the "instruction" to skip

    uc->uc_mcontext.gregs[REG_RIP] += instruction_length;

void main() {
	struct sigaction sa;
	memset(&sa, 0, sizeof(struct sigaction));
	sa.sa_flags = SA_SIGINFO;
	sa.sa_sigaction = sighandler;
	sigaction(SIGILL, &sa, NULL);

	printf("%s", "Reachable\n");

	asm("ud2"); // Adding an illegal opcode

	printf("%s", "Unreachable\n");

All in all, the resulting code is quite similar to the accepted answer to this question on Stack Overflow.

After compiling and executing it prints the following:


Comparing the binaries

In conclusion, let’s compare the decompilation of our recreated binary (on the left, compiled with gcc (GCC) 12.1.0) and the one from the challenge (on the right):

12 13

It’s surprising how similar they are, and how close the decompilation is to the source code!

That’s it for today. Thanks for reading, and happy hacking!

Further reading