1 内核漏洞利用的特点

2 调试环境搭建
2.1 启动
########## 1、安装qemu
lzx@ubuntu:~$ sudo apt install qemu-system
########## 2、解压
lzx@ubuntu:~/LKPWN/pawnyable$ tar xzvf LK01.tar.gz
LK01/
LK01/qemu/
LK01/qemu/run.sh
LK01/qemu/rootfs.cpio
LK01/qemu/bzImage
LK01/src/
LK01/src/vuln.c
LK01/src/vuln.ko
LK01/src/Makefile
########## 3、运行run.sh,启动qemu
lzx@ubuntu:~/LKPWN/pawnyable$ cd LK01/qemu/
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ ./run.sh
qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Saving random seed: OK
Starting network: udhcpc: started, v1.34.0
udhcpc: broadcasting discover
udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2
udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400
deleting routers
adding dns 10.0.2.3
OK
Starting dhcpcd...
dhcpcd-9.4.0 starting
DUID 00:01:00:01:2b:94:cc:35:52:54:00:12:34:56
eth0: IAID 00:12:34:56
eth0: soliciting an IPv6 router
eth0: Router Advertisement from fe80::2
eth0: adding address fec0::45db:ad18:6e76:7422/64
eth0: adding route to fec0::/64
eth0: adding default route via fe80::2
eth0: soliciting a DHCP lease
eth0: offered 10.0.2.15 from 10.0.2.2
eth0: leased 10.0.2.15 for 86400 seconds
eth0: adding route to 10.0.2.0/24
eth0: adding default route via 10.0.2.2
forked to background, child pid 105
Boot took 5.67 seconds
[ Holstein v1 (LK01) - Pawnyable ]
/ $ id
uid=1337 gid=1337 groups=1337
/ $ uname -a
Linux zer0pts 5.10.7 #1 SMP PREEMPT Wed Oct 6 21:20:36 JST 2021 x86_64 GNU/Linux
2.2 gdb调试内核
2.2.1 获取root权限
内核启动时会先运行一个程序,此程序根据配置的不同,路径会不同,但在大多数情况下是 /init
和 /sbin/init
。该环境下存在/init
,所以会先执行该脚本,那么先来查看qemu中根目录下的init脚本:
########## 1、创建rootfs目录
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ mkdir rootfs
########## 2、使用cpio命令解压cpio文件
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ cd rootfs; cpio -idv < ../rootfs.cpio
.
sbin
sbin/sulogin
sbin/setconsole
sbin/iptunnel
......
bin/ls
bin/base32
tmp
run
3928 blocks
########## 3、查看/init脚本
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs$ ls
bin dev etc init lib lib64 linuxrc media mnt opt proc root run sbin sys tmp usr var
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs$ cat init
#!/bin/sh
# devtmpfs does not get automounted for initramfs
/bin/mount -t devtmpfs devtmpfs /dev
# use the /dev/console device node from devtmpfs if possible to not
# confuse glibc's ttyname_r().
# This may fail (E.G. booted with console=), and errors from exec will
# terminate the shell, so use a subshell for the test
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi
exec /sbin/init "$@"
$@:所有参数列表。如"$@"用「"」括起来的情况、以"$1" "$2" … "$n" 的形式输出所有参数。
https://www.cnblogs.com/fhefh/archive/2011/04/15/2017613.html
/init
会执行/sbin/init
程序:
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs$ ls sbin/ -al | grep init
lrwxrwxrwx 1 lzx lzx 14 Mar 3 23:27 init -> ../bin/busybox
lrwxrwxrwx 1 lzx lzx 14 Mar 3 23:27 run-init -> ../bin/busybox
最后会执行脚本etc/init.d/rcS
(虽然没搞懂中间的过程,后面再补吧),该脚本又会执行同目录下的所有S开头的脚本:
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs/etc/init.d$ ls
rcK rcS S01syslogd S02klogd S02sysctl S20urandom S40network S41dhcpcd S99pawnyable
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs/etc/init.d$ cat rcS
#!/bin/sh
# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do
# Ignore dangling symlinks (if any).
[ ! -f "$i" ] && continue
case "$i" in
*.sh)
# Source shell script for speed.
(
trap - INT QUIT TSTP
set start
. $i
)
;;
*)
# No sh extension, so fork subprocess.
$i start
;;
esac
done
其中有一个脚本S99pawnyable
,它关系到qemu虚拟机以什么身份启动。所以下面以root身份启动qemu的时候要改这个脚本:
########## 1、修改etc/init.d/S99pawnyable
########## 1.1 将 setsid 命令的参数 1337 改为 0
########## 1.2 将 kptr_restrict 所在行注释掉,理由后面讲
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs/etc/init.d$ vim S99pawnyable
12 #echo 2 > /proc/sys/kernel/kptr_restrict <-- 注释掉
13 #echo 1 > /proc/sys/kernel/dmesg_restrict
14
15 ##
16 ## Install driver
17 ##
18 insmod /root/vuln.ko
19 mknod -m 666 /dev/holstein c `grep holstein /proc/devices | awk '{print $1;}'` 0
20
21 ##
22 ## User shell
23 ##
24 echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
25 echo "[ Holstein v1 (LK01) - Pawnyable ]"
26 #setsid cttyhack setuidgid 1337 sh <-- 注释掉
27 setsid cttyhack setuidgid 0 sh <-- 1337 改为 0
########## 2、重新打包rootfs(因为Host中当前用户不是root,所以在命令中加上了选项 --owner=root 选项)
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu/rootfs$ find . -print0 | cpio -o --format=newc --null --owner=root > ../rootfs_updated.cpio
3928 blocks
########## 3、将 run.sh 中 initrd 选项的参数由 rootfs.cpio 改为 rootfs_updated.cpio
########## 4、重新启动
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ ./run.sh
qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
Starting syslogd: OK
......
[ Holstein v1 (LK01) - Pawnyable ]
/ # id
uid=0(root) gid=0(root) groups=0(root)
/ #
对于命令:setsid cttyhack setuidgid 1337 sh:
cttyhack
:允许使用 Ctrl+C 等输入的命令。
setuidgid
:将用户标识和组标识设置为 1337
sh:并以/bin/sh
启动
2.2.2 attach到gdb
qemu提供了在gdb中调试的功能,通过设置qemu的-gdb
选项,可以指定协议、主机和端口号来监听。比如通过在run.sh中编辑并添加以下选项,就可以在本地主机上的tcp端口1234上监听gdb。
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu qemu64 \
-smp 1 \
-monitor /dev/null \
-initrd rootfs_updated.cpio \
-net nic,model=virtio \
-net user \
-gdb tcp::1234
重新启动qemu虚拟机,然后在Host里用gdb attach上去:
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ gdb
......
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
......
然后就可以使用gdb调试qemu虚拟机了。如果gdb不知道要调试的目标的架构,那么可以用下面的命令来设置(默认是会自动识别的),这次的架构是x86-64:
pwndbg> set arch i386:x86-64:intel
The target architecture is assumed to be i386:x86-64:intel
2.2.3 gdb基础调试
先看一下内核加载基址,并获得commit_creds函数的地址。
前面已经将etc/init.d/S99pawnyable 里的 kptr_restrict 所在行注释掉了,这个操作的目的是为了关闭 KADR(Kernel Address Display Restriction)。否则即使是root权限,内核地址也是不可见的。
- qemu
/ # head -n 3 /proc/kallsyms
ffffffff81000000 T startup_64
ffffffff81000000 T _stext
ffffffff81000000 T _text
/ # grep "commit_creds" /proc/kallsyms
ffffffff8106e390 T commit_creds
- Host
lzx@ubuntu:~/LKPWN/pawnyable/LK01/qemu$ gdb
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
......
此时使用gdb为commit_creds
函数设置断点:b *0xffffffff8106e390
,然后输入c
让系统继续运行。接着,在Host里执行一个命令,比如ls
。那么控制流就会被gdb中断,断在commit_creds
,而后就可以继续进行调试了。
pwndbg> b *0xffffffff8106e390
Breakpoint 1 at 0xffffffff8106e390
pwndbg> c
Continuing.
Breakpoint 1, 0xffffffff8106e390 in ?? ()
......
第一个参数 RDI 包含一个内核空间指针,看看这个指针指向的内存:
pwndbg> p/x $rdi
$2 = 0xffff8880032b7200
pwndbg> x/16gx 0xffff8880032b7200
0xffff8880032b7200: 0x0000000000000001 0x0000000000000000
0xffff8880032b7210: 0x0000000000000000 0x0000000000000000
0xffff8880032b7220: 0x0000000000000000 0x0000000000000000
0xffff8880032b7230: 0x000001ffffffffff 0x000001ffffffffff
0xffff8880032b7240: 0x000001ffffffffff 0x0000000000000000
0xffff8880032b7250: 0xffffffff81e32b00 0xffffffff81e32b80
0xffff8880032b7260: 0xffff888002d16540 0x0000000000000000
0xffff8880032b7270: 0x0000000000000000 0x0000000000000000
2.2.4 gdb调试驱动
LK01 加载了一个名为 vuln 的内核模块。可以在/proc/modules
找到已加载模块及其基址的列表:
/ # cat /proc/modules
vuln 16384 0 - Live 0xffffffffc0000000 (O)
可以看到名为vuln的模块被加载到 0xfffffffc0000000。该模块的源代码和二进制文件都在目录src
中:
lzx@ubuntu:~/LKPWN/pawnyable/LK01/src$ ls
Makefile vuln.c vuln.ko
如果用ida打开vuln.ko
,可以看到其中的函数,比如module_close
,它的相对地址是0x20f。
所以,这个函数应该在内核的 0xffffffffc0000000 + 0x20f 处,在这里打个断点:
pwndbg> x/4i 0xffffffffc0000000+0x20f
0xffffffffc000020f: push rbp
0xffffffffc0000210: mov rbp,rsp
0xffffffffc0000213: sub rsp,0x10
0xffffffffc0000217: mov QWORD PTR [rbp-0x8],rdi
pwndbg> b *(0xffffffffc0000000+0x20f)
Breakpoint 1 at 0xffffffffc000020f
pwndbg> c
Continuing.
从module_initialize
可以看出vuln被映射到一个名为/dev/holstein
的驱动
在虚拟机终端中执行cat /dev/holstein
后控制流将会运行到断点处,触发断点:
如果要在调试时使用驱动程序的符号,则可以使用add-symbol-file 命令,将驱动程序作为第一个参数传递,将基地址作为第二个参数传递,以读取符号信息。然后就可以使用函数名称设置断点了。
pwndbg> add-symbol-file vuln.ko 0xffffffffc0000000
add symbol table from file "vuln.ko" at
.text_addr = 0xffffffffc0000000
Reading symbols from vuln.ko...(no debugging symbols found)...done.
pwndbg> b module_close
Breakpoint 1 at 0xffffffffc0000213
pwndbg> b *module_close
Breakpoint 2 at 0xffffffffc000020f
至此,gdb如何调试内核及内核模块的基操就完了,其他操作与用户态调试没有太大区别。
参考文献
- https://pawnyable.cafe/linux-kernel/introduction/introduction.html
- https://pawnyable.cafe/linux-kernel/introduction/debugging.html