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

LK01.tar.gz

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