前言:没有怎么接触过 KernelPWN 但是又不知道怎么学,在网上偶然发现有这么个学习内核基础利用的网站
也就十二道题目,并且利用链并不长,决定学习一下。其实也不想一上来就学习太难的东西打消自己的积极性,另外没有一个好的基础直接学攻击手法利用链什么的总觉得有点空中楼阁,经不起考验。<指的我自己~~~>
从一开始会非常详细,往后逐渐了解之后会着重点记录 \[^_^]\
下载 pwn.college 附件:
pwn.college 的 kernel 基础关卡在:https://pwn.college/system-security/kernel-security/

首先在 User Setting 添加 SSH key,然后每个关卡开启之后都会给你 ssh 协议链接:

然后复制下来在虚拟机上进行 ssh 连接即可:

每道题目的文件都会放在 /chanllege/ 的目录下,直接去取即可。
由于我的 IDA 不在虚拟机内,我采取的方法是将 kali_share 文件夹连通物理机和虚拟机,把文件复制出来即可:

1 | scp hacker@dojo.pwn.college:/challenge/babykernel_level12.1 /home/kali/Desktop/kali_share/pwn_college/ |
然后再用物理机的 IDA 打开文件即可分析。
LEVEL1_密码校验



先进入到远程环境当中:抓取 flag 结果被告知 1 权限不够,我们当前处于的是用户态。

IDA 分析程序:

init_module():
init_module 是这是内核模块的入口,你要看它注册了什么。通常它会调用 register_chrdev(注册字符设备)或者 proc_create(创建 /proc 接口)。主要是为了找到它为用户态暴露了什么接口(设备名、proc 文件名),并记录下绑定的 file_operations(文件操作结构体)。
1 | int __cdecl init_module() |
读取 Flag 到内核全局变量:
filp_open("/flag", 0, 0):在内核态直接打开根目录下的 /flag 文件。memset(flag, 0, sizeof(flag)):清空一个名为 flag 的内核全局变量缓冲区。kernel_read(v0, flag, 128, ...):从 /flag 文件中读取前 128 个字节,存入到内核全局变量 flag 中filp_close(v0, 0):关闭文件句柄。
交互注册接口:
proc_create("pwncollege", 438, 0, &fops):在 /proc 目录下创建一个名为 pwncollege 的伪文件接口
- 权限
438转成八进制就是0666(即所有用户可读可写)。 - 关键在于
&fops,这是一个file_operations结构体指针。
cleanup_module():
- 模块卸载时的清理工作(如
unregister_chrdev或proc_remove)。 - 粗略看一眼即可,主要检查是否有释放不彻底导致的残余,或者在卸载时是否有潜在的 UAF 漏洞。
1 | void __cdecl cleanup_module() |
- if ( proc_entry ):检查在 init_module 中创建的 /proc/pwncollege 伪文件的入口指针是否不为 NULL
- proc_remove():调用内核 API 移除这个
/proc条目。这样当模块退出后,用户态就无法再通过/proc/pwncollege访问它了。
device_write():
device_read是内核向用户态返回数据的函数。重点看它如何使用_copy_to_user。- ****是否允许读取未初始化的内核内存,或者没有严格限制读取长度,导致内核信息泄露,可用于绕过 KASLR
1 | ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset) |
这个函数没有太多的疑问,但是 strncmp(password, "euxscnoiramxamcf", 0x10u) == 0可以控制 device_state 的值,假如 strncmp = 0,则值为 1;若 !=0,则值为 2;
device_read():
device_write是用户态向内核写入数据的函数。重点看它如何处理_copy_from_user读进来的数据。- 是否没有校验用户传入的长度,直接拷贝到内核栈/堆(导致内核栈溢出或堆溢出)。
1 | ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset) |
很显然我们是需要让 device_state[0]=2 的
最后利用 copy_to_user 将 p_invalid_password_n 指向的字符串,拷贝到你用户态的 buffer 中。
device_open&device_realease
1 | int __fastcall device_open(inode *inode, file *file) |
1 | int __fastcall device_release(inode *inode, file *file) |
没什么用处,都是打印内容,可跳过。
EXP 思路:
此时程序分析清楚大概也就知道 EXP 思路了,令 device_state[0] = 2
1 |
|
1 | 在 |

vm connect 启动环境,将写的 exp 复制到远程环境当中:
1 | scp /home/kali/Desktop/kali_share/pwn_college/exp1 hacker@dojo.pwn.college:~/ |
执行 ./exp1 获取 flag

还看到有的师傅使用 python 解决:
1 | with open("/proc/pwncollege", "rb+") as f: |
LEVEL2_内核日志


这道题目没有 device_read ,其中在 device_write 会把内容输出到内核日志当中 ==> printk
1 | ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset) |

除此之外,其他内容和 LEVEL1 都相同,那我们用相同的脚本,password 改一下,然后去读取日志即可:
EXP
1 |
|
运行 exp 之后查看日志,得到flag:dmesg | tail -n 20

LEVEL3_提权函数
1 | void __cdecl win() |
1 | ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset) |
和 LEVEL1&2 一样,不同的是多了提权函数 win:
1 | v0 = prepare_kernel_cred(0); |
提权核心凭证:commit_creds(prepare_kernel_cred(0))
Linux 内核用一个叫 cred 的结构体来记录每个进程的权限(比如用户的 UID、GID 等)。
普通用户的 UID 通常是大于 1000 的,而超级用户(Root)的 UID 是 0。
prepare_kernel_cred(0):这个内核函数的作用是创建一个新的凭证结构体。当我们传入参数0(也就是 NULL)时,内核会默认以内核 root 的身份作为模板,生成一套 UID=0、GID=0 的最高权限凭证。commit_creds(v0):这个函数的作用是把刚刚生成的凭证,正式应用到当前正在运行的进程上。
exp 的思路是一样的,只是获取 flag 的方式不同了,主要是让理解提权函数的一关。
EXP
1 |
|

LEVEL4_iotcl
ioctl 是 Input/Output Control(输入/输出控制) 的缩写。它是 Linux 和 Unix 系统中一个非常强大且通用的系统调用(System Call)。
- ioctl 就像是发送控制指令。当你除了传输数据外,还想让驱动程序执行某些特定操作、调整设备状态、或者执行某个特定函数时,就会使用 ioctl。
1 | __int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg) |
EXP
1 |
|

LEVEL5_参数指针调用
1 | __int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg) |
1 | void __cdecl win() |
当 cmd 命令号为 1337 时,会调用传入的参数 arg,允许任意内核函数执行
1 | cat /proc/cmdline # 查看当前内核的启动命令行配置 |

未开启内核基地址随机化
kaslr:如果存在,说明开启了内核地址空间布局随机化(Kernel ASLR),每次重启内核基地址都会变nokaslr,关闭了随机化,内核基地址是固定的。pti=on或kpti:说明开启了内核页表隔离 Kernel Page Table Isolation,用于防御熔断Meltdown 漏洞,会极大地限制内核与用户态页表的共享。quiet/loglevel:决定了内核日志输出的详细程度。
内核模块基地址 Module_Base):0xffffffffc0000000
这是 Linux 动态加载模块(LKM,Linux Kernel Modules)的固定起始地址。

那么 win 函数的地址就应该是 0xffffffffc00010AD
如果我们把 arg 的参数传承 win 函数的地址,执行 arg 就会跳转到 win 函数提权
EXP:
1 |
|

LEVEL6
1 | int __cdecl init_module() |
在内核空间里直接申请了 4096 字节,也就是 1个内存页的大小。
1 | ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset) |
在 device_write 当中,v6 = copy_from_user(shellcode, buffer, len);直接把用户输入 copy 给内核,
然后执行 shellcode,意思是只要我们编写一段 shellcode 进行提权即可
内核核心基地址(Kernel Base):0xffffffff81000000
这是 Linux 内核核心(Kernel Core)代码和数据的固定起始地址。
- 内核自带的核心函数,比如你前面看到的
commit_creds、prepare_kernel_cred,以及系统调用的入口等,在未开启 KASLR 时,都是在这个基地址往后的偏移中。
开始实验特权 privileged 获取 prepare_kernel_cred 和 commit_creds 函数地址:
1 | sudo grep -E "commit_creds|kernel_cred" /proc/kallsyms |

编一段提权的 shellcode 并转换为字节码:
1 | from pwn import * |

EXP:
1 |
|

参考资料:
说些什么吧!