linux内核学习入门1
前言
记录20250423及之前关于linux内核学习的内容
参考博客:
https://arttnba3.cn/2021/02/21/OS-0X01-LINUX-KERNEL-PART-II/
https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/
由题目开始的内核学习
示例题目为 强网杯2018 - core
开始
解压命令
本题目中core.cpio使用了gzip压缩,解压命令:
mkdir file_folder |
解压后有一堆文件
其中几个文件夹很明显就是linux根目录的文件夹,另外在当前目录下还有core.ko init gen_cpio.sh等文件
core.ko就是有漏洞的内核模块了
gen_cpio.sh
我们先看gen_cpio.sh,这里面的内容是打包命令
find . -print0 \ |
可以看到使用了cpio和gzip
我们在当前文件夹修改之后如何打包呢?
./gen_cpio.sh ../core.cpio |
即可
init
以前只模模糊糊知道init是linux启动后的第一个程序,那init到底是什么?
在 Linux 系统中,init 文件是系统启动过程中的第一个用户空间程序。它的作用非常关键,是操作系统启动时执行的第一个用户进程,它负责初始化系统的其他部分,并最终启动系统的主要服务。
init 文件的作用:
- 启动系统服务:
init是在内核初始化后第一个运行的程序,它通常会启动其他必要的系统服务(如网络、文件系统、用户空间进程等)。这些服务可能包括登录进程、系统守护进程(如syslog)、硬件初始化等。 - 进程树的根:
init进程的进程号(PID)是 1,它是所有其他进程的祖先。所有其他进程最终都是由init启动的,因此它也是进程树的根节点。任何其他用户进程都是由它派生出来的。 - 处理运行级别(Runlevels): 在传统的 Unix 系统中,
init还负责管理不同的运行级别(runlevels)。它通过读取/etc/inittab或其他配置文件来确定系统当前应该处于哪个运行级别,并根据运行级别启动不同的服务。例如,在多用户模式下,init会启动网络服务和登录进程,而在单用户模式下,它可能仅启动最基础的服务。 - 执行用户指定的启动脚本:
init通常会根据配置文件的设置执行一些初始化脚本,例如/etc/rc.d/rc,这些脚本用于启动各种服务。现代的 Linux 系统通常使用systemd或upstart来取代传统的init进程。
在 QEMU 环境中:
当你在 QEMU 中搭建 Linux 环境时,init 文件通常是你自己创建的一个文件,它用于替代或模拟系统启动时的第一个用户进程。你可以自定义这个 init 文件来完成特定的初始化工作,例如:
- 挂载文件系统
- 初始化网络接口
- 启动虚拟机中的关键服务
- 启动一个 shell 或其他应用程序
例如,在一个简化的 Linux 系统中,你的 init 文件可能只是一个简单的 shell 脚本,它执行一些初始化任务,并最终启动一个 shell 让你可以与虚拟机进行交互。
ok,以上是gpt生成的答案,大概还是只需要知道init就是第一个程序就可以了?
init文件内容
下面是init文件内容
|
其中poweroff -d 120 -f &意思是定时2分钟关机,因此我把这句话注释掉了。
insmod /core.ko就是加载core.ko模块。
setsid /bin/cttyhack /bin/sh 原来的指令是setsid /bin/cttyhack setuidgid 1000 /bin/sh
其中setuidgid 1000就导致用户转变为普通用户。我们在调试的时候可以吧这句话删掉,这样我们就可以在进入linux之后查看core.ko模块的基址了。
cat /proc/kallsyms > /tmp/kallsyms |
- 开始时内核符号表被复制了一份到
/tmp/kalsyms中,利用这个我们可以获得内核中所有函数的地址(原博客内容)
start.sh
我们接下来再看一下start.sh内容。
qemu-system-x86_64 \ |
其中 -m 128M 是我改的,原先是64M,导致内存太少一直进不去。
报错信息:
[ 0.038667] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline |
然后append里面有一个nokaslr,这就是关闭内核基址随机化,原来是没有no的,我们加上no就关闭随机化方便调试。
-s:相当于-gdb tcp::1234的简写(也可以直接这么写),后续我们可以通过gdb连接本地端口进行调试(原博客内容)
qemu+gdb调试
ok,我们更改完init(重新打包)和start.sh之后,就可以运行了。
直接./start.sh即可,接下来就会运行linux内核了。
获取基址
我们需要知道core模块的基址,刚好我们之前在init里面直接以root用户登录,因此我们现在可以很轻松地得到。
cat /sys/module/core/sections/.bss |
其中cat /sys/module/core/sections/.text是最重要的。
gdb调试
接下来我们新开一个终端
gdb vmlinux |
这是加载内核符号表
然后
set architecture i386:x86-64 |
其中第二句话因为我们qemu的设置中增加了-s,因此可以这样。
之后我们还需要加载core模块符号表
add-symbol-file ./file_folder/core.ko 0xffffffffc0000000 |
ok,现在我们就成功调试上linux内核了。
我们可以下断点
b core_ioctl |
其中这三个函数都是在core.ko里面的函数,这代表我们成功加载上符号表了。
接下来就可以愉快地进行内核调试了。
用户态与内核态的切换
首先贴一下大佬的博客
主要的一个过程如下:
- 切换 GS 段寄存器:通过
swapgs切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用 - 保存用户态栈帧信息:将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里(由 GS 寄存器所指定的
percpu段),将 CPU 独占区域里记录的内核栈顶放入 rsp/esp - 保存用户态寄存器信息: 通过 push 保存各寄存器值到栈上,以便后续“着陆”回用户态
- 通过汇编指令判断是否为32位
- 控制权转交内核,执行相应的操作
由内核态重新“着陆”回用户态只需要恢复用户空间信息即可:
swapgs指令恢复用户态GS寄存器sysretq或者iretq系列指令让 CPU 运行模式回到 ring 3,恢复用户空间程序的继续运行
这里着重讲一下我的理解,尤其是从用户态转为内核态的。
内核空间中有一段空间是percpu空间,这段空间可以通过gs段寄存器来访问。
当用户态切换为内核态时,首先swapgs,将gs_base和gs_kernel_base交换,然后将用户态栈帧信息保存在percpu空间中,并把之前保存在percpu空间中的内核态rsp切换到现在的rsp里。
那现在的栈就是内核空间中的栈了, 接下来把用户态的几个寄存器保存在内核空间的栈中,接下来就是内核态代码了。
简单来说,内核空间中有一段空间叫做percpu,可以通过gs寄存器来访问。把用户态rsp信息保存在这里面,把其余寄存器信息设置为pt_regs结构体并保存在内核态栈上(内核态栈本来也保存在percpu空间里)(内核态栈在内核态空间里,用户态栈在用户态空间里)
其中还有几个段寄存器,比如cs ss,这几个段寄存器的值表明了当前正在运行在哪一个状态下(ring0还是ring3)
当然目前我所涉猎的知识还非常有限,未来会对这块内容进行再补充的。






