Signals
Signals are a feature of POSIX systems like (e.g., the Linux kernel) that provide a mechanism for asynchronous communication between processes and the operating system. When certain events occur (e.g., hardware interrupts, illegal operations, or termination requests) the kernel can send a signal to a process to notify it of the event. Each signal is identified by a unique integer and corresponds to a specific type of event. For example, SIGINT
(usually triggered by pressing Ctrl+C
) is used to interrupt a process, while SIGKILL
forcefully terminates a process without cleanup.
Processes can handle these signals in different ways: they may catch and define custom behavior for certain signals, ignore them, or allow the default action to occur.
Restrictions on Signal Catching
libdebug does not support catching SIGTRAP
, SIGSTOP
, and SIGKILL
. While the first is used internally for debugging purposes, the other two cannot be blocked as a result of kernel limitations.
libdebug allows you to intercept signals sent to the tracee. Specifically, you can choose to catch or hijack a specific signal (read more on hijacking).
Signal Catchers
Signal catchers can be created to register stopping events for when a signal is received.
Multiple catchers for the same signal
Please note that there can be at most one user-defined catcher or hijack for each signal. If a new catcher is defined for a signal that is already caught or hijacked, the new catcher will replace the old one, and a warning will be printed.
libdebug API for Signal Catching
The catch_signal()
function in the Debugger object registers a catcher for the specified signal.
Parameters:
Argument | Type | Description |
---|---|---|
signal |
int | str |
The signal number or name to catch. If set to "*" or "all" , all signals will be caught. |
callback |
Callable | bool (see callback signature here) |
The callback function to be executed when the signal is received. |
recursive |
bool |
If set to True , the catcher's callback will be executed even if the signal was triggered by a hijack. |
Returns:
Return | Type | Description |
---|---|---|
SignalCatcher |
SignalCatcher | The catcher object created. |
Callback Signature
Parameters:
Argument | Type | Description |
---|---|---|
t |
ThreadContext | The thread that received the signal. |
catcher |
SignalCatcher | The SignalCatcher object that triggered the callback. |
Signals in multi-threaded applications
In the Linux kernel, an incoming signal could be delivered to any thread in the process. Please do not assume that the signal will be delivered to a specific thread in your scripts.
Example usage of asynchronous signal catchers
from libdebug import debugger
d = debugger("./test_program")
d.run()
# Define the callback function
def catcher_SIGUSR1(t, catcher):
t.signal = 0x0
print("Look mum, I'm catching a signal")
def catcher_SIGINT(t, catcher):
print("Look mum, I'm catching another signal")
# Register the signal catchers
catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
catcher2 = d.catch_signal('SIGINT', callback=catcher_SIGINT)
d.cont()
d.wait()
Example of synchronous signal catching
from libdebug import debugger
d = debugger("./test_program")
d.run()
catcher = d.catch_signal(10)
d.cont()
if catcher.hit_on(d):
print("Signal 10 was caught")
The script above will print "Signal 10 was entered".
Hijacking
When hijacking a signal, the user can provide an alternative signal to be executed in place of the original one. Internally, the hijack is implemented by registering a catcher for the signal and replacing the signal number with the new one.
Parameters:
Argument | Type | Description |
---|---|---|
original_signal |
int | str |
The signal number or name to be hijacked. If set to "*" or "all" , all signals except the restricted ones will be hijacked. |
new_signal |
int | str |
The signal number or name to be delivered instead. |
recursive |
bool |
If set to True , the catcher's callback will be executed even if the signal was dispached by a hijack. |
Returns:
Return | Type | Description |
---|---|---|
SignalCatcher |
SignalCatcher | The catcher object created. |
Example of hijacking a signal
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// Handler for SIGALRM
void handle_sigalrm(int sig) {
printf("You failed. Better luck next time\n");
exit(1);
}
// Handler for SIGUSR1
void handle_sigusr1(int sig) {
printf("Congrats: flag{pr1nt_pr0vol4_1s_th3_w4y}\n");
exit(0);
}
int main() {
// Set up the SIGALRM handler
struct sigaction sa_alrm;
sa_alrm.sa_handler = handle_sigalrm;
sigemptyset(&sa_alrm.sa_mask);
sa_alrm.sa_flags = 0;
sigaction(SIGALRM, &sa_alrm, NULL);
// Set up the SIGUSR1 handler
struct sigaction sa_usr1;
sa_usr1.sa_handler = handle_sigusr1;
sigemptyset(&sa_usr1.sa_mask);
sa_usr1.sa_flags = 0;
sigaction(SIGUSR1, &sa_usr1, NULL);
// Set an alarm to go off after 10 seconds
alarm(10);
printf("Waiting for a signal...\n");
// Infinite loop, waiting for signals
while (1) {
pause(); // Suspend the program until a signal is caught
}
return 0;
}
Signal Filtering
Instead of setting a catcher on signals, you might want to filter which signals are not to be forwarded to the debugged process during execution.
Arbitrary Signals
You can also send an arbitrary signal to the process. The signal will be forwarded upon resuming execution. As always, you can specify the signal number or name.
In multithreaded applications, the same syntax applies when using a ThreadContext object instead of the Debugger object.