This commit is contained in:
firmianay 2018-04-20 21:00:41 +08:00
parent 41440f5629
commit d07a110725
12 changed files with 476 additions and 2 deletions

View File

@ -1 +1,159 @@
# 1.5.9 Linux 内核 # 1.5.9 Linux 内核
- [编译安装](#编译安装)
- [系统调用](#系统调用)
- [参考资料](#参考资料)
## 编译安装
我的编译环境是如下。首先安装必要的软件:
```
$ uname -a
Linux firmy-pc 4.14.34-1-MANJARO #1 SMP PREEMPT Thu Apr 12 17:26:43 UTC 2018 x86_64 GNU/Linux
$ yaourt -S base-devel
```
为了方便学习,选择一个稳定版本,比如最新的 4.16.3。
```
$ mkdir ~/kernelbuild && cd ~/kernelbuild
$ wget -c https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.16.3.tar.xz
$ tar -xvJf linux-4.16.3.tar.xz
$ cd linux-4.16.3/
$ make clean && make mrproper
```
内核的配置选项在 `.config` 文件中,有两种方法可以设置这些选项,一种是从当前内核中获得一份默认配置:
```
$ zcat /proc/config.gz > .config
$ make oldconfig
```
另一种是自己生成一份配置:
```
$ make localmodconfig # 使用当前内核配置生成
$ # OR
$ make defconfig # 根据当前架构默认的配置生成
```
为了能够对内核进行调试,需要设置下面的参数:
```
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_REDUCED=n
CONFIG_GDB_SCRIPTS=y
```
如果需要使用 kgdb还需要开启下面的参数
```
CONFIG_STRICT_KERNEL_RWX=n
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
```
`CONFIG_STRICT_KERNEL_RWX` 会将特定的内核内存空间标记为只读,这将阻止你使用软件断点,最好将它关掉。
如果希望使用 kdb在上面的基础上再加上
```
CONFIG_KGDB_KDB=y
CONFIG_KDB_KEYBOARD=y
```
另外如果你在调试时不希望被 KASLR 干扰,可以在编译时关掉它:
```
CONFIG_RANDOMIZE_BASE=n
CONFIG_RANDOMIZE_MEMORY=n
```
将上面的参数写到文件 `.config-fragment`,然后合并进 `.config`
```
$ ./scripts/kconfig/merge_config.sh .config .config-fragment
```
最后因为内核编译默认开启了 `-O2` 优化,可以修改 Makefile 为 `-O0`
```
KBUILD_CFLAGS += -O0
```
编译内核:
```
$ make
```
完成后当然就是安装,但我们这里并不是真的要将本机的内核换掉,接下来的过程就交给 QEMU 了。参考章节4.1
## 系统调用
在 Linux 中,系统调用是一些内核空间函数,是用户空间访问内核的唯一手段。这些函数与 CPU 架构有关x86-64 架构提供了 322 个系统调用x86 提供了 358 个系统调用参考附录9.4)。
下面是一个用 32 位汇编写的例子:
```
.data
msg:
.ascii "hello 32-bit!\n"
len = . - msg
.text
.global _start
_start:
movl $len, %edx
movl $msg, %ecx
movl $1, %ebx
movl $4, %eax
int $0x80
movl $0, %ebx
movl $1, %eax
int $0x80
```
编译执行可以编译成64位程序的
```
$ gcc -m32 -c hello32.S
$ ld -m elf_i386 -o hello32 hello32.o
$ strace ./hello32
execve("./hello32", ["./hello32"], 0x7ffff990f830 /* 68 vars */) = 0
strace: [ Process PID=19355 runs in 32 bit mode. ]
write(1, "hello 32-bit!\n", 14hello 32-bit!
) = 14
exit(0) = ?
+++ exited with 0 +++
```
可以看到程序将调用号保存到 `eax`,并通过 `int $0x80` 来使用系统调用。
虽然软中断 `int 0x80` 非常经典,早期 2.6 及以前版本的内核都使用这种机制进行系统调用。但因其性能较差在往后的内核中使用了快速系统调用指令来替代32 位系统使用 `sysenter` 指令,而 64 位系统使用 `syscall` 指令。
下面是一个 64 位的例子:
```
.data
msg:
.ascii "Hello 64-bit!\n"
len = . - msg
.text
.global _start
_start:
movq $1, %rdi
movq $msg, %rsi
movq $len, %rdx
movq $1, %rax
syscall
xorq %rdi, %rdi
movq $60, %rax
syscall
```
编译执行不能编译成32位程序
```
$ gcc -c hello64.S
$ ld -o hello64 hello64.o
$ strace ./hello64
execve("./hello64", ["./hello64"], 0x7ffe11485290 /* 68 vars */) = 0
write(1, "Hello 64-bit!\n", 14Hello 64-bit!
) = 14
exit(0) = ?
+++ exited with 0 +++
```
在这两个例子中我们直接使用了 `execve`、`write` 和 `exit` 三个系统调用。但一般情况下应用程序通过在用户空间实现的应用编程接口API而不是直接通过系统调用来编程。例如函数 `printf()` 的调用过程是这样的:
```
调用printf() ==> C库中的printf() ==> C库中的write() ==> write()系统调用
```
## 参考资料
- [The Linux Kernel documentation](https://www.kernel.org/doc/html/latest/)
- [linux-insides](https://legacy.gitbook.com/book/0xax/linux-insides/details)

View File

@ -4,6 +4,7 @@
- [安装](#安装) - [安装](#安装)
- [参考资料](#参考资料) - [参考资料](#参考资料)
## 简介 ## 简介
QEMU 是一个广泛使用的开源计算机仿真器和虚拟机。当作为仿真器时,可以在一种架构(如PC机)下运行另一种架构(如ARM)下的操作系统和程序,当作为虚拟机时,可以使用 Xen 或 KVM 访问 CPU 的扩展功能(HVM),在主机 CPU 上直接执行虚拟客户端的代码。 QEMU 是一个广泛使用的开源计算机仿真器和虚拟机。当作为仿真器时,可以在一种架构(如PC机)下运行另一种架构(如ARM)下的操作系统和程序,当作为虚拟机时,可以使用 Xen 或 KVM 访问 CPU 的扩展功能(HVM),在主机 CPU 上直接执行虚拟客户端的代码。

View File

@ -1,10 +1,15 @@
# 4.15 利用 vsyscall 和 vDSO # 4.15 利用 vsyscall 和 vDSO
- [vsyscall](#vsyscall)
- [vDSO](#vdso)
- [CTF 实例](#ctf-实例) - [CTF 实例](#ctf-实例)
- [参考资料](#参考资料) - [参考资料](#参考资料)
## vsyscall
## vDSO
## CTF 实例 ## CTF 实例
例如章节 6.1.6 的 ret2vdso。 例如章节 6.1.6 的 ret2vdso。

View File

@ -1,6 +1,10 @@
# 4.1 Linux 内核调试 # 4.1 Linux 内核调试
- [准备工作](#准备工作) - [准备工作](#准备工作)
- [printk](#printk)
- [QEMU + gdb](#qemu-gdb)
- [kdb](#kdb)
- [参考资料](#参考资料)
## 准备工作 ## 准备工作
@ -102,3 +106,208 @@ block crypto Documentation fs Kbuild MAINTAINERS README sna
certs debian drivers include Kconfig Makefile samples sound usr certs debian drivers include Kconfig Makefile samples sound usr
COPYING debian.hwe dropped.txt init kernel mm scripts spl 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)

View File

@ -1,6 +1,7 @@
# 9.1 更多 Linux 工具 # 9.1 更多 Linux 工具
- [dd](#dd) - [dd](#dd)
- [dmesg](#dmesg)
- [file](#file) - [file](#file)
- [edb](#edb) - [edb](#edb)
- [foremost](#foremost) - [foremost](#foremost)
@ -48,6 +49,20 @@ dump 运行时的内存镜像:
- `dd if=/proc/<pid>/mem of=/path/a.out skip=xxxx bs= 1 count=xxxx` - `dd if=/proc/<pid>/mem of=/path/a.out skip=xxxx bs= 1 count=xxxx`
## dmesg
**dmesg** 命令用于显示 Linux 内核环形缓冲区ring buffer的信息。开机信息和各种错误信息都会放到里面。在调试和故障诊断中非常有用。
#### 重要参数
```
-c, --read-clear
Clear the ring buffer after first printing its contents.
-s, --buffer-size size
Use a buffer of size to query the kernel ring buffer. This is 16392 by default.
-n, --console-level level
Set the level at which printing of messages is done to the console.
```
## file ## file
**file** 命令用来探测给定文件的类型。 **file** 命令用来探测给定文件的类型。

View File

@ -1,3 +1,5 @@
# 9.4 Linux x86-64 系统调用表 # 9.4 Linux x86-64 系统调用表
http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ - [Linux System Call Table for x86 64](http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/)
- [syscall_32](https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl)
- [syscall_64](https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl)

View File

@ -0,0 +1,14 @@
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_REDUCED=n
CONFIG_GDB_SCRIPTS=y
CONFIG_STRICT_KERNEL_RWX=n
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_KDB_KEYBOARD=y
CONFIG_RANDOMIZE_BASE=n
CONFIG_RANDOMIZE_MEMORY=n

View File

@ -0,0 +1,19 @@
.data
msg:
.ascii "hello 32-bit!\n"
len = . - msg
.text
.global _start
_start:
movl $len, %edx
movl $msg, %ecx
movl $1, %ebx
movl $4, %eax
int $0x80
movl $0, %ebx
movl $1, %eax
int $0x80

View File

@ -0,0 +1,19 @@
.data
msg:
.ascii "Hello 64-bit!\n"
len = . - msg
.text
.global _start
_start:
movq $1, %rdi
movq $msg, %rsi
movq $len, %rdx
movq $1, %rax
syscall
xorq %rdi, %rdi
movq $60, %rax
syscall

View File

@ -0,0 +1,8 @@
BUILDPATH := ~/kernelbuild/linux-4.16.3/
obj-m += hello.o
all:
make -C $(BUILDPATH) M=$(PWD) modules
clean:
make -C $(BUILDPATH) M=$(PWD) clean

View File

@ -0,0 +1,4 @@
#!/bin/bash
cd ./initramfs/
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

View File

@ -0,0 +1,20 @@
#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.");