2020.8.21 添加一个遇到的问题以及解决; 修改 start.sh;

环境

  • Windows10 v2004
  • VMware15.6 Pro
  • Ubuntu18.04 LTS 虚拟机

编译内核

kernel 源码下载: https://www.kernel.org/

按需下载版本即可,这里我用的是linux-5.7.8

安装必要依赖

sudo apt update
sudo apt install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc

解压源码后进入目录

make menuconfig

配置选项

  1. 进入kernel hacking
  2. 勾选Kernel debugging
  3. Compile-time checks and compiler options -> Compile the kernel with debug info 和 Compile the kernel with frame pointers
  4. KGDB

保存退出。不过这些选项默认是已选的。

接着make bzImage -j8进行编译,-j表示多线程编译,8即8个线程,

出现以下信息表示编译OK

Setup is 15228 bytes (padded to 15360 bytes).
System is 8888 kB
CRC 2c515b81
Kernel: arch/x86/boot/bzImage is ready (#1)
  • ./arch/x86/boot/bzImage拿到bzImage
  • 从源码根目录拿到vmlinux

问题

Linux 编译内核问题汇总

添加自定 syscall

在源码根目录创建一个新的目录(模块),以经典的helloworld为例。

➜  linux-5.1.7 cd helloworld/
➜ helloworld cd tree
.
├── helloworld.c
└── Makefile

0 directories, 2 files
➜ helloworld bat helloworld.c
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: helloworld.c
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ #include <linux/kernel.h>
2 │
3 │ asmlinkage long sys_helloworld(void){
4 │ printk("hello world\n");
5 │ return 0;
6 │ }
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────
➜ helloworld bat Makefile
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: Makefile
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ obj-y=helloworld.o
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────

编辑源码根目录下的Makefile,加入helloworld模块。

...
PHONY += prepare0

ifeq ($(KBUILD_EXTMOD),)
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ helloworld/

vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
$(core-y) $(core-m) $(drivers-y) $(drivers-m) \
$(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))

vmlinux-alldirs := $(sort $(vmlinux-dirs) $(patsubst %/,%,$(filter %/, \
$(init-) $(core-) $(drivers-) $(net-) $(libs-) $(virt-))))
...

然后编辑include/linux/syscalls.h,添加helloworld函数原型。

asmlinkage long sys_helloworld(void);

添加到文件末尾即可

再修改arch/x86/entry/syscalls/syscall_32.tblarch/x86/entry/syscalls/syscall_64.tbl, 添加自定义的系统调用号。

  • i386: 1000 i386 helloworld sys_helloworld
  • amd64: 1000 common helloworld sys_helloworld

最后再编译生成新的内核即可。

编译 busybox

官网源码: busybox

下载源码后解压进入根目录输入make menuconfig进行配置

在配置时进入Settings,勾上Build static binary (no shared libs),这样就不会依赖libc文件。如果不勾选的话,需要自行配置libc库,这样步骤会很繁琐。

20200817142246

➜  _install ldd bin/busybox
not a dynamic executable

然后输入make install -j8进行编译,busybox 编译要比 kernel 快很多。

编译完成后会生成一个_install的目录,这就是我们需要的环境。

先进行一些简单的初始化:

cd _install
mkdir proc
mkdir sys
mkdir lib64
mkdir -p lib/x86_64-linux-gnu/
mkdir etc
mkdir home
echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
echo "root:x:0:" > etc/group
touch etc/shadow
touch etc/gshadow
touch init
chmod +x init

然后把libcld准备好,否则程序需要静态编译才能运行,则会使得生成的程序调试的时候不太方便。

在生成的init初始化脚本中,加入如下内容:

#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
mount -t devtmpfs devtmpfs /dev

# insmod /xxx.ko # load ko
mdev -s # We need this to find /dev/sda later
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

setsid /bin/cttyhack setuidgid 1000 /bin/sh #normal user
# exec /bin/sh #root

poweroff -d 0 -f

然后在_install目录里运行下面的命令进行打包:

find . | cpio -o --format=newc > ../rootfs.img

qemu

通过上面两步,我们得到了含有helloworld syscallkernel ,bzImage和用busybox打包的fs(附带了ld和libc)

使用 qemu 启动

qemu-system-x86_64  \
-m 256M \
-cpu kvm64 \
-kernel ./bzImage \
-initrd rootfs.img \
-nographic \
-s \
-smp 4,cores=2,threads=2 \
-append "console=ttyS0 quiet root=/dev/sda nokaslr"

这里qemu-system-x86_64代表是x86的64位架构

  • -m 256M 表示分配256M物理内存给该 kernel 运行, 如果内存分配不够, 那么将导致卡死

  • -cpu kvm64 表示采用kvm64的cpu,同时如果要添加保护可以在这里加,如添加smep:-cpu kvm64,smep

  • -kernel ./bzImage 用于使用的内核。

  • -initrd rootfs.img 用于指定选用的文件系统。

  • -nographic 表示不使用图形化界面,只使用串口。

  • -s 表示开启远程gdb调试,端口号为默认的1234.

  • -smp 4,cores=2,threads=2 表示cpu为双核双线程。

  • -append "console=ttyS0 root=/dev/sda quiet nokaslr" 这里代表内核启动参数

    • console=ttyS0 用于说明输出设备
    • nokaslr 代表不开启内核地址随机化,如果要开启则输入 kaslr 即可
    • quiet 表示静默开启
    • root=/dev/sda 告诉内核硬盘上有根文件系统

驱动

register_chrdev

int register_chrdev (unsigned int major, const  char *name, struct file_operations*fops);

在这里,我们指定要注册它的设备的名称和主要编号,之后将链接设备和file_operations结构。 如果我们为主参数指定零,该函数将自己分配一个主设备号(即它返回的值)。 如果返回的值为零,则表示成功,而负数表示错误。 两个设备编号均在0-255范围内指定。

我们将设备名称作为name参数的字符串值传递(如果模块注册单个设备,则此字符串也可以传递模块的名称)。 然后,我们使用此字符串来标识/sys/devices文件中的设备。 读取,写入和保存等设备文件操作由存储在file_operations结构中的函数指针处理。 这些函数由模块实现,并且指向标识该模块的module结构的指针也存储在file_operations结构中。

来自源码:linux-5.8/include/linux/fs.h:1827

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
  • 如果file_operations结构包含一些不需要的函数,您仍然可以使用该文件而不实现它们。指向未实现函数的指针可以简单地设置为零。 之后,系统将负责该功能的实现并使其正常运行

字符设备模块使用insmod加载,加载完毕需要在/dev目录下使用mkmod命令建立相应的文件结点

编写驱动程序

memory.c

#include <linux/init.h>

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/proc_fs.h>
#include <linux/fcntl.h>

#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file *filp);
ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);
ssize_t memory_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos);
void memory_exit(void);
int memory_init(void);

struct file_operations memory_fops = {
read : memory_read,
write : memory_write,
open : memory_open,
release : memory_release
};

module_init(memory_init);
module_exit(memory_exit);

int memory_major = 60;

char *memory_buffer;

int used = 0;

#define LENGTH 0x1000

int memory_init(void)
{
int result;

result = register_chrdev(memory_major, "memory", &memory_fops);
if (result < 0)
{
printk("<1>memory: can't obtain major number %d\n", memory_major);
return result;
}

memory_buffer = kmalloc(LENGTH, GFP_KERNEL);
if (!memory_buffer)
{
result = -ENOMEM;
goto fail;
}
memset(memory_buffer, 0, LENGTH);

printk("<1>Inserting memory module\n");
return 0;

fail:
memory_exit();
return result;
}

void memory_exit(void)
{
unregister_chrdev(memory_major, "memory");

if (memory_buffer)
kfree(memory_buffer);

printk("<1>Removing memory module\n");
}

int memory_open(struct inode *inode, struct file *filp)
{
printk("<1>Open\n");
return 0;
}

int memory_release(struct inode *inode, struct file *filp)
{
printk("<1>Release\n");
return 0;
}

ssize_t memory_read(struct file *filp, char *buf,
size_t count, loff_t *f_pos)
{
int bytes;

if(used > count && used > 0)
{
used -= count;
bytes = count;
copy_to_user(buf, memory_buffer, bytes);
}
else if(used > 0)
{
bytes = used;
used = 0;
copy_to_user(buf, memory_buffer, bytes);
}

return bytes;
}

ssize_t memory_write(struct file *filp, const char *buf,
size_t count, loff_t *f_pos)
{
int bytes = 0;

if(used + count < LENGTH)
{
used += count;
bytes = count;
copy_from_user(memory_buffer, buf, bytes);
}
else if(used < LENGTH)
{
bytes = LENGTH - used;
used = LENGTH;
copy_from_user(memory_buffer, buf, bytes);
}

return bytes;
}

上面的驱动可以看成一个简单的字符仓库,如果放满了字符就放不进去,如果是空的也拿不出来。

驱动源码并不能用gcc直接进行编译,需要生成一个Makefile来进行编译。

Makefile:

ARGET_MODULE:=memorys
PWD:=$(shell pwd)
# KERNELDIR := /lib/modules/$(shell uname -r)/build
KERNELDIR:=./linux-4.15

$(TARGET_MODULE)-objs := memory.o
obj-m := $(TARGET_MODULE).o

all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers

对应的内核要编译相对应的驱动才能载入,否则会失败。

编译好会生成一个memorys.ko的驱动。

时我们可以把驱动复制到_install根目录,然后在我们的init脚本中加入下面两条命令,重新生成镜像。

insmod /memorys.ko
mknod /dev/memorys c 60 0

60 为我们设置的主设备号

动态调试

qemu 内置有 gdb 的接口, 通过 help 命令进行查看

➜  ~ qemu-system-x86_64 --help | grep gdb
-gdb dev wait for gdb connection on 'dev'
-s shorthand for -gdb tcp::1234

即可以通过 -gdb tcp:port 或者 -s 来开启调试端口, 可以写入start.sh中, 接着就可以在gdb使用target remote localhost:1234进行连接了, 注意, 后者会使虚拟机启动时强制终端等待调试器连接 (因为我曾傻傻的加入以后没有连接gdb…..然后在那里傻等😂)

一般来说加nokaslrkaslr关了调试起来会方便一些。否则gdb将找不到ELF基地址(毕竟不是本地)。

-append "console=ttyS0 nokaslr"

但通过 gdb ./vmlinux 启动时,虽然加载了 kernel 的符号表, 但没有加载 LKMs 的.text, .bss,.data等节区的地址, 此时可以通过 add-symbol-file LKMs(模块名) textaddr(text段地址) -s .bss bssaddr(bss段地址) 加载

pwndbg> help add-symbol-file
Load symbols from FILE, assuming FILE has been dynamically loaded.
Usage: add-symbol-file FILE ADDR [-s <SECT> <SECT_ADDR> -s <SECT> <SECT_ADDR> ...]
ADDR is the starting address of the file's text.
The optional arguments are section-name section-address pairs and
should be specified if the data and bss segments are not contiguous
with the text. SECT is a section name to be loaded at SECT_ADDR.

不过要注意的是,我们加载到内核的模块名不一定是模块文件的名字,可以使用 lsmod 命令查看。

一般这些模块的地址都可以通过 /sys/modules/* 来查看,以core模块的.text为例子grep 0x /sys/modules/core/section/.text

一般来说, 查看需要 root 权限, 所以可以修改init以获得 root 权限

setsid /bin/cttyhack setuidgid 0 /bin/sh

重新打包,这样启动的时候就是 root 权限了。

参考

Kernel 环境配置

Linux kernel 初探

kernel环境搭建