mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-12-25 11:41:16 +07:00
314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# 4.1 Linux 内核调试
|
||
|
||
- [准备工作](#准备工作)
|
||
- [printk](#printk)
|
||
- [QEMU + gdb](#qemu-gdb)
|
||
- [kdb](#kdb)
|
||
- [参考资料](#参考资料)
|
||
|
||
|
||
## 准备工作
|
||
与用户态程序不同,为了进行内核调试,我们需要两台机器,一台调试,另一台被调试。在调试机上需要安装必要的调试器(如GDB),被调试机上运行着被调试的内核。
|
||
|
||
这里选择用 Ubuntu16.04 来展示,因为该发行版默认已经开启了内核调试支持:
|
||
```
|
||
$ cat /boot/config-4.13.0-38-generic | grep GDB
|
||
# CONFIG_CFG80211_INTERNAL_REGDB is not set
|
||
CONFIG_SERIAL_KGDB_NMI=y
|
||
CONFIG_GDB_SCRIPTS=y
|
||
CONFIG_HAVE_ARCH_KGDB=y
|
||
CONFIG_KGDB=y
|
||
CONFIG_KGDB_SERIAL_CONSOLE=y
|
||
# CONFIG_KGDB_TESTS is not set
|
||
CONFIG_KGDB_LOW_LEVEL_TRAP=y
|
||
CONFIG_KGDB_KDB=y
|
||
```
|
||
|
||
#### 获取符号文件
|
||
下面我们来准备调试需要的符号文件。看一下该版本的 code name:
|
||
```
|
||
$ lsb_release -c
|
||
Codename: xenial
|
||
```
|
||
然后在下面的目录下新建文件 `ddebs.list`,其内容如下(注意看情况修改Codename):
|
||
```
|
||
$ cat /etc/apt/sources.list.d/ddebs.list
|
||
deb http://ddebs.ubuntu.com/ xenial main restricted universe multiverse
|
||
deb http://ddebs.ubuntu.com/ xenial-security main restricted universe multiverse
|
||
deb http://ddebs.ubuntu.com/ xenial-updates main restricted universe multiverse
|
||
deb http://ddebs.ubuntu.com/ xenial-proposed main restricted universe multiverse
|
||
```
|
||
`http://ddebs.ubuntu.com` 是 Ubuntu 的符号服务器。执行下面的命令添加密钥:
|
||
```
|
||
$ wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -
|
||
```
|
||
然后就可以更新并下载符号文件了:
|
||
```
|
||
$ sudo apt-get update
|
||
$ uname -r
|
||
4.13.0-38-generic
|
||
$ sudo apt-get install linux-image-4.13.0-38-generic-dbgsym
|
||
```
|
||
完成后,符号文件将会放在下面的目录下:
|
||
```
|
||
$ file /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
|
||
/usr/lib/debug/boot/vmlinux-4.13.0-38-generic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=f00f4b7ef0ab8fa738b6a9caee91b2cbe23fef97, not stripped
|
||
```
|
||
可以看到这是一个静态链接的可执行文件,使用 gdb 即可进行调试,例如这样:
|
||
```
|
||
$ gdb -q /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
|
||
Reading symbols from /usr/lib/debug/boot/vmlinux-4.13.0-38-generic...done.
|
||
gdb-peda$ p init_uts_ns
|
||
$1 = {
|
||
kref = {
|
||
refcount = {
|
||
refs = {
|
||
counter = 0x2
|
||
}
|
||
}
|
||
},
|
||
name = {
|
||
sysname = "Linux", '\000' <repeats 59 times>,
|
||
nodename = "(none)", '\000' <repeats 58 times>,
|
||
release = "4.13.0-38-generic", '\000' <repeats 47 times>,
|
||
version = "#43~16.04.1-Ubuntu SMP Wed Mar 14 17:48:43 UTC 2018", '\000' <repeats 13 times>,
|
||
machine = "x86_64", '\000' <repeats 58 times>,
|
||
domainname = "(none)", '\000' <repeats 58 times>
|
||
},
|
||
user_ns = 0xffffffff822517a0 <init_user_ns>,
|
||
ucounts = 0x0 <irq_stack_union>,
|
||
ns = {
|
||
stashed = {
|
||
counter = 0x0
|
||
},
|
||
ops = 0xffffffff81e2cc80 <utsns_operations>,
|
||
inum = 0xeffffffe
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 获取源文件
|
||
将 `/etc/apt/sources.list` 里的 `deb-src` 行都取消掉注释:
|
||
```
|
||
$ sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list"
|
||
```
|
||
然后就可以更新并获取 Linux 内核源文件了:
|
||
```
|
||
$ sudo apt-get update
|
||
$ mkdir -p ~/kernel/source
|
||
$ cd ~/kernel/source
|
||
$ apt-get source $(dpkg-query '--showformat=${source:Package}=${source:Version}' --show linux-image-$(uname -r))
|
||
```
|
||
```
|
||
$ ls linux-hwe-4.13.0/
|
||
arch CREDITS debian.master firmware ipc lib net security tools zfs
|
||
block crypto Documentation fs Kbuild MAINTAINERS README snapcraft.yaml ubuntu
|
||
certs debian drivers include Kconfig Makefile samples sound usr
|
||
COPYING debian.hwe dropped.txt init kernel mm scripts spl
|
||
```
|
||
|
||
|
||
## printk
|
||
在用户态程序中,我们常常使用 `printf()` 来打印信息,方便调试,在内核中也可以这样做。内核(v4.16.3)使用函数 `printk()` 来输出信息,在 `include/linux/kern_levels.h` 中定义了 8 个级别:
|
||
```c
|
||
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
|
||
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
|
||
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
|
||
#define KERN_ERR KERN_SOH "3" /* error conditions */
|
||
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
|
||
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
|
||
#define KERN_INFO KERN_SOH "6" /* informational */
|
||
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
|
||
```
|
||
用法是:
|
||
```c
|
||
printk(KERN_EMERG "hello world!\n"); // 中间没有逗号
|
||
```
|
||
|
||
而当前控制台的日志级别如下所示:
|
||
```
|
||
$ cat /proc/sys/kernel/printk
|
||
4 4 1 4
|
||
```
|
||
这 4 个数值在文件定义及默认值在如下所示:
|
||
```c
|
||
// kernel/printk/printk.c
|
||
|
||
int console_printk[4] = {
|
||
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
|
||
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
|
||
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
|
||
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
|
||
};
|
||
|
||
|
||
// include/linux/printk.h
|
||
|
||
/* printk's without a loglevel use this.. */
|
||
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
|
||
|
||
/* We show everything that is MORE important than this.. */
|
||
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
|
||
|
||
/*
|
||
* Default used to be hard-coded at 7, we're now allowing it to be set from
|
||
* kernel config.
|
||
*/
|
||
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
|
||
|
||
#define console_loglevel (console_printk[0])
|
||
#define default_message_loglevel (console_printk[1])
|
||
#define minimum_console_loglevel (console_printk[2])
|
||
#define default_console_loglevel (console_printk[3])
|
||
```
|
||
虽然这些数值控制了当前控制台的日志级别,但使用虚拟文件 `/proc/kmsg` 或者命令 `dmesg` 总是可以查看所有的信息。
|
||
|
||
|
||
## QEMU + gdb
|
||
QEMU 是一款开源的虚拟机软件,可以使用它模拟出一个完整的操作系统(参考章节2.1.1)。这里我们介绍怎样使用 QEMU 和 gdb 进行内核调试,关于 Linux 内核的编译可以参考章节 1.5.9。
|
||
|
||
接下来我们需要借助 BusyBox 来创建用户空间:
|
||
```
|
||
$ wget -c http://busybox.net/downloads/busybox-1.28.3.tar.bz2
|
||
$ tar -xvjf busybox-1.28.3.tar.bz2
|
||
$ cd busybox-1.28.3/
|
||
```
|
||
生成默认配置文件并修改 `CONFIG_STATIC=y` 让它生成的是一个静态链接的 BusyBox:
|
||
```
|
||
$ make defconfig
|
||
$ cat .config | grep "CONFIG_STATIC"
|
||
CONFIG_STATIC=y
|
||
```
|
||
编译安装后会出现在 `_install` 目录下:
|
||
```
|
||
$ make
|
||
$ sudo make install
|
||
$ ls _install
|
||
bin linuxrc sbin usr
|
||
```
|
||
接下来创建 initramfs 的目录结构:
|
||
```
|
||
$ mkdir initramfs
|
||
$ cd initramfs
|
||
$ cp ../_install/* -rf ./
|
||
$ mkdir dev proc sys
|
||
$ sudo cp -a /dev/null /dev/console /dev/tty /dev/tty2 /dev/tty3 /dev/tty4 dev/
|
||
$ rm linuxrc
|
||
$ vim init # 创建启动脚本
|
||
$ cat init
|
||
#!/bin/busybox sh
|
||
mount -t proc none /proc
|
||
mount -t sysfs none /sys
|
||
|
||
exec /sbin/init
|
||
```
|
||
最后把它们打包:
|
||
```
|
||
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
|
||
```
|
||
这样 initramfs 根文件系统就做好了,其中包含了必要的设备驱动和工具,boot loader 会加载 initramfs 到内存,然后内核将其挂载到根目录 `/`,并运行 `init` 脚本,挂载真正的磁盘根文件系统。
|
||
|
||
QEMU 启动!
|
||
```
|
||
$ qemu-system-x86_64 -s -S -kernel ~/kernelbuild/linux-4.16.3/arch/x86_64/boot/bzImage -initrd ~/kernelbuild/busybox-1.28.3/initramfs.cpio.gz -nographic -append "console=ttyS0"
|
||
```
|
||
- `-s`:`-gdb tcp::1234` 的缩写,QEMU 监听在 TCP 端口 1234,等待 gdb 的连接。
|
||
- `-S`:在启动时冻结 CPU,等待 gdb 输入 c 时继续执行。
|
||
- `-kernel`:指定内核。
|
||
- `-initrd`:指定 initramfs。
|
||
- `nographic`:禁用图形输出并将串行 I/O 重定向到控制台。
|
||
- `-append "console=ttyS0`:所有内核输出到 ttyS0 串行控制台,并打印到终端。
|
||
|
||
在另一个终端里使用打开 gdb,然后尝试在函数 `cmdline_proc_show()` 处下断点:
|
||
```
|
||
$ gdb -ex "target remote localhost:1234" ~/kernelbuild/linux-4.16.3/vmlinux
|
||
(gdb) b cmdline_proc_show
|
||
Breakpoint 1 at 0xffffffff8121ad70: file fs/proc/cmdline.c, line 9.
|
||
(gdb) c
|
||
Continuing.
|
||
|
||
Breakpoint 1, cmdline_proc_show (m=0xffff880006701b00, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
|
||
9 seq_printf(m, "%s\n", saved_command_line);
|
||
```
|
||
可以看到,当我们在内核里执行 `cat /proc/cmdline` 时就被断下来了。
|
||
```
|
||
/ # id
|
||
uid=0 gid=0
|
||
/ # echo hello kernel!
|
||
hello kernel!
|
||
/ # cat /proc/cmdline
|
||
console=ttyS0
|
||
```
|
||
|
||
现在我们已经可以对内核代码进行单步调试了。对于内核模块,我们同样可以进行调试,但模块是动态加载的,gdb 不会知道这些模块被加载到哪里,所以需要使用 `add-symbol-file` 命令来告诉它。
|
||
|
||
来看一个 helloworld 的例子:
|
||
```c
|
||
#include <linux/init.h>
|
||
#include <linux/module.h>
|
||
#include <linux/kernel.h>
|
||
|
||
static int hello_init(void)
|
||
{
|
||
printk(KERN_ALERT "Hello module!\n");
|
||
return 0;
|
||
}
|
||
|
||
static void hello_exit(void)
|
||
{
|
||
printk(KERN_ALERT "Goodbye module!\n");
|
||
}
|
||
|
||
module_init(hello_init);
|
||
module_exit(hello_exit);
|
||
|
||
MODULE_LICENSE("GPL");
|
||
MODULE_DESCRIPTION("A simple module.");
|
||
```
|
||
Makefile 如下:
|
||
```makefile
|
||
BUILDPATH := ~/kernelbuild/linux-4.16.3/
|
||
obj-m += hello.o
|
||
|
||
all:
|
||
make -C $(BUILDPATH) M=$(PWD) modules
|
||
|
||
clean:
|
||
make -C $(BUILDPATH) M=$(PWD) clean
|
||
```
|
||
编译模块并将 `.ko` 文件复制到 initramfs,然后重新打包:
|
||
```
|
||
$ make && cp hello.ko ~/kernelbuild/busybox-1.28.3/initramfs
|
||
$ cd ~/kernelbuild/busybox-1.28.3/initramfs
|
||
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
|
||
```
|
||
|
||
最后重新启动 QEMU 即可:
|
||
```
|
||
/ # insmod hello.ko
|
||
[ 7.887392] hello: loading out-of-tree module taints kernel.
|
||
[ 7.892630] Hello module!
|
||
/ # lsmod
|
||
hello 16384 0 - Live 0xffffffffa0000000 (O)
|
||
/ # rmmod hello.ko
|
||
[ 24.523830] Goodbye module!
|
||
```
|
||
三个命令分别用于载入、列出和卸载模块。
|
||
|
||
再回到 gdb 中,`add-symbol-file` 添加模块的 `.text`、`.data` 和 `.bss` 段的地址,这些地址在类似 `/sys/kernel/<module>/sections` 位置:
|
||
```
|
||
/ # cat /sys/module/hello/sections/.text
|
||
0x00000000fa16acc0
|
||
```
|
||
在这个例子中,只有 .text 段:
|
||
```
|
||
(gdb) add-symbol-file ~/kernelbuild/busybox-1.28.3/initramfs/hello.ko 0x00000000fa16acc0
|
||
```
|
||
然后就可以对该模块进行调试了。
|
||
|
||
|
||
## kdb
|
||
|
||
## 参考资料
|
||
- [KernelDebuggingTricks](https://wiki.ubuntu.com/Kernel/KernelDebuggingTricks)
|