1、栈溢出或者栈数据被踩时,继续运行就会出现segmentation fault。可以尝试着接管SIGSEGV信号,在信号处理函数中保存一些出现异常时候的信息。
2、基于栈溢出场景,栈空间被破坏,也就没法使用栈区,当然就没法执行SIGSEGV信号的处理函数。因此需要开辟额外的空间用于栈空间使用,系统提供了sigaltstack接口。
2.1、开辟第二栈区的代码
void create_alt_stack(void)
{
stack_t sigstack;
memset(&sigstack, 0x00, sizeof(stack_t));
sigstack.ss_sp = malloc(SIGSTKSZ);
if( !sigstack.ss_sp)
{
printf("Err: malloc error\n");
exit(EXIT_FAILURE);
}
sigstack.ss_size = SIGSTKSZ;
sigstack.ss_flags = 0;
if(sigaltstack(&sigstack, NULL) == -1)
{
printf("Err: sigaltstack error\n");
exit(EXIT_FAILURE);
}
printf("Now the alternate signal stack is successfully allocated\n");
printf("The address of signal stack is : %10p - %10p\n",sigstack.ss_sp,(char*)sbrk(0)-1);
return;
}
2.2、接管SIGSEGV信号
void sig_action(void)
{
struct sigaction act;
memset(&act, 0x00, sizeof(struct sigaction));
act.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler; //须挂在sa_sigaction 不应该挂在sa_handler
sigaction(SIGSEGV, &act, NULL);
// sigaction(SIGABRT, &act, NULL);
return;
}
信号处理函数handler不应该挂接到sa_handler,应该挂接到sa_sigaction,这样处理函数可以通过参数获取额外的信息,同时sa_flags需要增加SA_SIGINFO标志。
struct sigaction {
union {
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int, struct siginfo *, void *);
} _u;
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
};
2.3、当出现异常时,会运行到处理函数handler
static void handler(int sig, siginfo_t *info, void *cntxt)
{
int i = 0;
int *paddr = NULL;
paddr = (int *)((((ucontext_t *)(cntxt))->uc_mcontext).gregs[REG_RSP]);
printf("Waoh, caught signal %s, Stack addr = %p\n",strsignal(sig), paddr);
for (i = -20; i < 20; i++) {
printf("0x%x\n", *(paddr + i));
}
sleep(2);
_exit(EXIT_FAILURE);
}
其中ucontext_t结构体定义如下:
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link; // 当前上下文执行完了,恢复运行的上下文
stack_t uc_stack; // 该上下文中使用的栈
mcontext_t uc_mcontext; // 保存当前上下文,即各个寄存器的状态
__sigset_t uc_sigmask; // 保存当前线程的信号屏蔽掩码
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
typedef struct
{
gregset_t gregs; //用于装载寄存器
/* Note that fpregs is a pointer. */
fpregset_t fpregs; //所有寄存器的类型
__extension__ unsigned long long __reserved1 [8];
} mcontext_t;
2.4、打印出现异常时,异常栈区地址附近的数据,分析数据。根据被踩空间大小,适当调整打印范围。
其中红框的数据就是被踩的数据,黄框的数据保存着函数的地址。通过cat /proc/$pid/maps查看代码段的基地址,进而使用addr2line命令查看函数在代码中的位置。
从而可以缩小范围,在main->stack_funcAx这层调用路径下。同时通过红框中被踩的数据grep反推这些数据在代码中是否存在。
当然被踩的数据区域大小场景不一致,需要具体问题具体分析。
3、正常情况下会打开栈保护功能,-fstack-protector-all。注释demo程序sleep(8)前面初始化这几行,Makefile中增加-fstack-protector-all编译选项。
3.1、gdb挂载程序
3.2、直接运行程序,程序出现错误,查看当前的堆栈信息
查看__stack_chk_fail函数的上一层函数test_stack_overflow,应该是这一层的函数出现了问题。
3.3、退出gdb,重新进入gdb,给test_stack_overflow打断点,运行。查看附近的汇编指令
其中绿框中显示的汇编指令就是为了栈保护功能在栈区添加的canary魔鬼数字,在函数即将退出时,会比较canary魔鬼数字是否发生改变。假如被改写,会跳转到__stack_chk_fail函数运行。
3.4、使用n命令运行至绿框汇编指令后面,打印%rbp寄存器的值,获取存放canary魔鬼数字的地址。
3.5、查看地址的数据,同时watch命令对这一地址的数据监控起来,当地址数据被改写时,gdb会停留在改写的位置点。 对被改后的值1820209125数值转换成十六进制0x6F792065,对应着e yo字符串,被改后的值也是定位问题的参考证据。
3.6、查看此处的汇编指令和堆栈信息
从中可以看出是test_stack_overflow函数中strcpy代码处出现的问题。
4、增加-fstack-protector-all编译参数后,在函数的栈区会添加canary值,函数即将退出时通过判断canary的值是否被修改确定栈区数据有没有被破坏。
然而,栈区数据存在被破坏,但是偏偏没有在存放canary值的位置,此时也是无法通过上述方法定位。
可以周期性地打印栈区数据,比较栈区数据的差异,综合定位栈区数据被修改问题。
测试demo程序