Linux下的signal调试
最近逆向时候碰到了好几个关于signal的逆向题,因此来做个总结。
打开gdb输入info handle可以看到gdb对于所有信号量的默认处理方式,我们关注常用的前几个:
注意到圈起来的部分是gdb在调试时自己处理而不会传递给源程序的几个信号量,因此这几个信号量就大量用于逆向题中搞一些骚操作。对这四个信号量进行改写时,如果不更改gdb中的“pass to program”选项,则程序还是会安装默认的处理方式处理该信号。想要进行调试,需要用handle 信号量名 pass
进行更改。
下面我们逐个讲解这四个信号量。
SIGINT
这是linux中的中断信号,当运行程序时按下ctrl+c时就出发了这个中断,正常情况下会结束程序。但是我们可以自己改写对该信号的处理函数。下面就是非常经典的一种用法:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void INThandler(int);
void main(void)
{
signal(SIGINT, INThandler);// install SIGINT的处理函数
while (1)
pause();
}
void INThandler(int sig)
{
char c;
signal(sig, SIG_IGN); // uninstall 当前signal的处理函数
printf("\nDo you really want to quit? [y/n] ");
c =getchar();
if (c == 'y' || c == 'Y')
exit(0);
else
signal(SIGINT, INThandler);
getchar(); // 读取\n
}
注意到我们在INThandler里先uninstall了对SIGINT的处理,之后再reinstall它。这是考虑到如果在处理上一个信号量时又有一个信号量输入进来导致程序发生错误。
SIGTRAP
The SIGTRAP signal is sent to a process when an exception (or trap) occurs: a condition that a debugger has requested to be informed of — for example, when a particular function is executed, or when a particular variable changes value.
我们的软件断点使用int 3实现,int 3会产生一个SIGTRAP的信号量,当在调试时如果不将该信号pass to program,那么程序就会停在这里作为一个断点。但有时候,程序本身注册了对于SIGTRAP的处理函数,如果不把信号量pass则会产生同一程序在调试与否的情况下会产生不同的运行结果。因此,很多逆向题会用这个方法来隐藏程序真正的逻辑。下面是一个案例:
#include <stdio.h>
#include <signal.h>
#define SPC_DEBUGGER_PRESENT (num_traps == 0)
static int num_traps = 0;
static void int3handler(int signo) {
num_traps++;
}
int spc_trap_detect(void) {
if (signal(SIGTRAP, int3handler) == SIG_ERR) return 0;
raise(SIGTRAP);
return 1;
}
如果上面的程序正常运行,那么会触发int3handler,因此SPC_DEBUGGER_PRESENT为false。而如果调试器在的话,因为SIGTRAP被调试器截获,因此不会触发num_traps++, SPC_DEBUGGER_PRESENT为True。
SIGSEGV
这个信号量相信大家都不陌生,写bug的程序员们最讨厌它了。SIGSEGV
is the signal a program gets from referencing a place in memory far away from all the areas in use.
这个也可以用作反调试,当程序正常运行时,SIGSEGV会被用户写的函数处理,因此不会直接崩溃。但是如果调试时没有pass,则程序会直接崩溃。
void sigsegv_handler(int signo, siginfo_t *info, void *extra) {
ucontext_t* p = (ucontext_t*)extra;
char* pc = (void*)(p->uc_mcontext.gregs[REG_RIP]);//获取触发SIGSEGV时的RIP
char n = pc[5], m = pc[4];
char *d = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
decode_and_run(d, &pc[8], n, m); //对encrypted_func解密并运行
munmap(d, 0x1000);
}
void fun() {
encrypted_func(); //被加密的代码块,里面会触发SIGSEGV
while(1);
}
SIGALRM
SIGALRM
occurs when the alarm clock timer goes off (which happens only if your program has requested an alarm).
在逆向题目中往往用alarm来注册并隐藏用于检查输入是否合法的check函数。因为处理SIGALRM并不会暂停程序(在gdb的info handle中第一个stop是no),所以用alarm可以做出并行处理。下面是一个SIGALRM的样例:
#include <stdio.h>
#include <signal.h>
unsigned long counter;
int MAX;
int ALARMcount;
int SECOND;
void ALARMhandler(int sig)
{
signal(SIGALRM, SIG_IGN);
ALARMcount++;
printf("*** ALARMhandler --> alarm received no. %d.\n", ALARMcount);
printf("*** ALARMhandler --> counter = %ld\n", counter);
printf("*** ALARMhandler --> alarm reset to %d seconds\n", SECOND);
if (ALARMcount == MAX) {
printf("*** ALARMhandler --> Maximum alarm count reached. exit\n");
exit(0);
}
counter = 0; /* otherwise, reset counter */
alarm(SECOND); /* set alarm for next run */
signal(SIGALRM, ALARMhandler); /* reinstall the handler */
}
void main(int argc, char *argv[])
{
if (argc != 3) {
printf("Use: %s seconds max-alarm-count\n", argv[0]);
printf("No. of seconds is set to 1\n");
SECOND = 1;
printf("Max number of alarms set to 5\n");
MAX = 5;
}
else {
SECOND = atoi(argv[1]);
MAX = atoi(argv[2]);
}
counter = 0;
ALARMcount = 0;
signal(SIGALRM,ALARMhandler);
printf("Alarm set to %d seconds and is ticking now.....\n", SECOND);
alarm(SECOND);
while (1)
counter++;
}