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
解压源码后进入目录
配置选项
进入kernel hacking 勾选Kernel debugging Compile-time checks and compiler options -> Compile the kernel with debug info 和 Compile the kernel with frame pointers 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 │ 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.tbl
和arch/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库,这样步骤会很繁琐。
➜ _install ldd bin/busybox not a dynamic executable
然后输入make install -j8
进行编译,busybox 编译要比 kernel 快很多。
编译完成后会生成一个_install
的目录,这就是我们需要的环境。
先进行一些简单的初始化:
cd _installmkdir 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/passwdecho "root:x:0:" > etc/grouptouch etc/shadow touch etc/gshadow touch init chmod +x init
然后把libc
和ld
准备好,否则程序需要静态编译才能运行,则会使得生成的程序调试的时候不太方便。
在生成的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 mdev -s echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds" setsid /bin/cttyhack setuidgid 1000 /bin/sh poweroff -d 0 -f
然后在_install
目录里运行下面的命令进行打包:
find . | cpio -o --format=newc > ../rootfs.img
qemu 通过上面两步,我们得到了含有helloworld syscall
的kernel
,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:=./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…..然后在那里傻等😂)
一般来说加nokaslr
把kaslr
关了调试起来会方便一些。否则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环境搭建