0x09. Linux kernel基础:ret2hbp
ret2hbp
[toc]
Linux kernel 之 ret2hbp
0x00. 前言
hbp即Hardware Breakpoint ,实际上是针对cpu_entry_area mapping这一段区域的一种利用方法,个人感觉也是比较trick的一种方法,尤其是数据的泄露部分能在低版本且有任意地址读的情况下获取内核基地址。
该利用点在于cpu_entry_area mapping这一段区域在linux 6.2(不包括)以下的版本中是不会参与随机化的,这意味着kaslr不会对该区域产生影响。而在这段区域中,不但含有内核基地址,而且还可以在一定情况下(通过硬件断点)将用户的寄存器类似于pt_regs一样置入该区域。
而尽管在linux 6.2及以上版本,cpu_entry_area进行了随机化,上方的IDT表仍然不会随机化,因此地址泄露不受影响。
这说明我们在一定情况下可以利用该方法进行数据泄露或者栈迁移到该区域进行rop。
1 | |
0x01. 原理
在cpu_entry_area mapping这段区域,其类似于如下结构:

可以看到,该区域由IDT表和若干个cpu_entry_area组成,其数量等同于cpu的数量。其中cpu_entry_area结构体如下所示:
1 | |
这段区域用于描述CPU的各种状态和布局,尤其是estacks中的DB_stack是我们尤其需要关注的部分。
0x02. 地址泄露
很简单,如下图为我开启kaslr时的cpu_entry_area mapping段泄露的内核地址:

说明该段并未参与随机化,若我们含有一个任意地址读即可在未知其他地址的情况下获得内核基地址。
0x03. 用于栈迁移
在cpu_entry_area的DB_stack,更具体一点来说,在0xfffffe0000010f58这个地址上,存放着内核陷入硬件断点时,用户态的寄存器的值。
我们使用一个poc如下:
1 | |
上述脚本原理比较简单,这里再简单描述一下:
create_hbp函数用于对指定地址添加硬件断点第一个
ptrace表示要为哪个地址添加硬件断点,第二个ptrace表示具体的实现子进程绑定
cpu,这里选择绑定到第0个,生成一个SIGSTOP信号暂停父进程确定子进程已经绑定了核心,此时第一个
waitpid执行完毕父进程使用
create_hbp向全局变量buf下了一个硬件断点,并通过PTRACE_CONT恢复执行子进程执行,直到
mov rdi, [rsi]处,硬件断点被访问到父进程通过
wait得知子进程触发硬件断点,在通过PTRACE_CONT恢复执行子进程
exit,再次触发父进程的wait此时子进程在汇编中写下的寄存器的值已经进入
DB_stack,父进程继续后面的流程
0x04. demo - SCTF2023-sycrop
保护全开不必多说,逻辑也很简单如下:
1 | |
题目有两个功能,每个功能都只能使用一次:
- 读取任意地址上的值
- 直接将栈迁移到用户提供的地址
对于第二个功能要注意,IDA上其实没有体现出来,要看汇编才知道:
1 | |
其实就是为ret2hbp这样的利用方法量身定做的,我们对着写exp即可,脚本如下:
1 | |
0x05. 通过任意地址写+ret2hbp进行提权
上面我们提到了cpu_entry_area mapping这段区域的两个特点:
IDT表不会参与随机化,因此可用于泄露内核基地址DB_stack在低版本内核中不会参与随机化,一定情况下可存放用户寄存器,类似于pt_regs
而实际上这段区域还有另一个特点。用户态通过读写触发hardware breakpoint会进入exec_debug_user()函数,而内核态通过copy_from/to_user触发则会进入exec_debug_kernel()函数。我们关注内核态的exec_debug_kernel()函数,其存在如下情况:
1 | |
注意到此时rip为0xffffffff816e039c,查看这部分代码:
1 | |
这部分代码就是一个复制数据的过程。而复制的长度由rcx决定。
查看其地址:
1 | |
可知实际上该地址即为0xfffffe0000010fb0,就存储于cpu_entry_area mapping这个段中的DB_stack中。根据我们的理解,该地址在linux6.2以下则是不受随机化影响的。因此,我们可以对该值进行攻击,从而在内核态下调用copy_from/to_user时,过多地将数据拷贝到内核或用户地栈上。
通过uname泄露数据
在系统调用uname中,有如下逻辑:
1 | |
可以看到,uname系统调用通过copy_to_user将内核栈上的信息拷贝到用户数据中。
而我们上面提到了,若拷贝到用户的值被下了硬件断点,则会进入到exc_debug_kernel()函数的流程中。此时若我们能够通过任意地址写regs.cx,则可以拷贝意外多的数据到用户态上,从而泄露内核地址。若需要对内核进行栈溢出,同时还可以泄露canary。
通过prctl栈溢出来rop
在系统调用prctl中,我们设置正确的分支,例如如下为prctl_set_mm中的prctl_set_mm_map中:
1 | |
注意上面的1973行,此处有一个从用户态拷贝数据到内核态的操作。若用户态的地址被下了硬件断点,结合对regs.cx的攻击,我们则可以将过多数据拷贝到内核栈上,从而实现栈溢出。
demo:无限任意地址写
逻辑很简单,如下:
1 | |
现在我们默认其版本为linux 5.15,其他保护全开。
则可以采取如下流程:
开启两个子进程,一个为
victim,将其绑定到cpu 0;一个为trigger,将其绑定到cpu 1。对于
trigger进程,无限调用题目功能对regs.cx进行修改,地址为0xfffffe0000010fb0对于
victim进程,对其要用到的某个地址下硬件断点。随后,使用uname系统调用,获得内核基地址和canary获取到内核基地址和
canary后,使用prctl系统调用来栈溢出,rop进行提权。
一个exp如下:
1 | |
0xFF. 地址总结
0xfffffe0000000000,位于cpu_entry_area mapping中的IDT table中,全版本不会参与随机化,可用于泄露内核基地址0xfffffe0000010f58,其类似于pt_regs,在满足一定条件时存放了用户的寄存器,可用于栈迁移打rop0xfffffe0000010fb0,在内核态调用有硬件断点的copy_from/to_user时,会根据这个地址的值决定拷贝长度,修改后可通过uname泄露或prctl溢出。