Featured image of post Perf与简单的侧信道攻击

Perf与简单的侧信道攻击

perf

简介

perf 是 Linux 系统性能分析工具集,全称是 Performance Event Counters 它基于 Linux 内核的 perf_events 子系统,能够提供硬件和软件层面的性能分析能力 我们这里会用到它的一些功能以获取一些硬件事件的信息,因此,你必须确保自己的虚拟机支持 PMU 功能

准备perf

事前准备

首先确定你的设备支持 PMU 功能,如果条件允许,实体机上使用 perf 会方便很多,如果你使用的是 wsl2 作为使用的虚拟机,请在设置中开启 性能计数器 ,并使用除 ubuntu 以外的系统,因为 wsl2 对内核的魔改会导致一些兼容性问题,我捣鼓了两天也没能完美解决,可以在 wsl2 上选择其他 Linux 进行尝试,或者换用 VMware ,在使用VMware时,请在硬件设置里,添加上虚拟化 CPU 性能计数器,并禁用所有HyperV相关的设置,并关闭虚拟化保护 包括(仅限VMware使用者,wsl2 不要改):

  • Windows安全中心 -> 设备安全性 -> 内核隔离 -> 内存完整性/本地安全机构保护
  • Windows功能 -> Hyper-V/Virtual Machine Platform/Windows虚拟机监控程序平台

perf安装

安装 perf 可以通过

1
sudo apt-get install linux-tools-generic

相关设置

使用权限

等安装完成后并不能直接使用,你需要先查看

1
cat /proc/sys/kernel/perf_event_paranoid

这个perf_event_paranoid设置了运行时权限相关的内容,建议修改成小于等于1的值

1
sudo sh -c 'echo -1 > /proc/sys/kernel/perf_event_paranoid'
CPU核心与内存分配

此时可以尝试一下使用

1
perf stat whoami

检测perf是否能正常运行,如果你的 CPU 是有大小核区分的话,可能会出现内存分配问题,尝试查看你的 CPU 状态

1
cat /sys/devices/cpu_atom/cpus

这里可以看到你的 CPU 属于 atom 的核心,也就是常说的能效核 确定了 CPU 核心属于 atom 或 core 后,将进程绑定在非 atom 的核心上运行 可以通过 taskset 指令绑定核心

1
taskset -c <core>
手动编译 perf (适用于wsl2用户)

如果你使用的是 wsl2 ,且以上设置后仍不可使用,可以尝试手动编译适合自己的 perf 首先准备编译需要用到的一些工具包

1
2
3
sudo apt update

sudo apt install -y build-essential flex bison libelf-dev libdw-dev libaudit-dev libnuma-dev libssl-dev python3-dev pkg-config libcap-dev

接下来确定内核版本

1
uname -r

记录下完整的版本号 接下来 git clone 得到对应版本的 wsl2 内核源码,只需要 clone 该版本分支下的内容即可 可以前往WSL2-Linux-Kernel手动查看对应分支后再clone

将源码完全解压,并进入 perf 目录

1
cd tools/perf

此时可以开始编译

1
make -j$(nproc)

完成编译后将得到的 perf 复制到 /usr/local/bin 目录

1
sudo cp perf /usr/local/bin/

此时再次重复之前的权限设置和核心内存设置即可

RMKS

如果以上设置都完成了,但你的perf只能使用软件事件或根本无法使用,这里包括使用 perf 输出指令数为0<not supported>,那么恭喜你该尝试其他的设备了,至少我这里虚拟机基本全军覆没,为了方便,最后还是上实体机了

简单使用说明

如果你需要 perf 的完整使用说明,可以前往perf学习

这里我们主要用到 stat

1
perf stat [options] command [command-options]
1
2
3
4
5
6
常用选项
-e:指定要监控的事件
-p:监控指定进程ID
-a:监控所有CPU
-r:重复运行并显示平均值
-d:显示更多详细事件

Side-Chanel-Attack

这里简单介绍使用 perf 指令计数的侧信道攻击,我选择搭配例题讲解 这里使用的是 SwampCTF2019 的题目 future_fun 作为例题,同时我也是在这个题上研究学习的该手法 题目可以从 SwampCTF-2019 寻找下载

简单原理

一个程序如果要判断两个字符串是否相同,最简单的方法就是直接从第一个字符开始,逐个进行比较,如果发现不同的字符,就会停下并返回结果,如果一个字符相同,就会继续比较下去,因此如果字符相同的个数更多,执行的指令数会与不同的情况更多,这样就可以逐个字符的确认完整的字符串,有点像拿铁丝翘一些比较原始的锁,将每个弹簧分别依次复位并成功开锁

例题

拿到 future_fun 的题目文件后,直接运行,可以提前知到flag格式为 flag{} ,事实上不知道flag格式也行,只是多跑两轮计数罢了

1
2
3
4
$ ./future_fun 
Give the key, if you think you are worthy.

1234

此时我们再使用 perf 进行测试

1
$ perf stat -x : -e instructions:u ./future_fun

此时可以看到输出下面多出来了一行

1
2
3
4
Give the key, if you think you are worthy.

1234
8176046::instructions:u:5593443:100.00::

这里的 8176046 就是结束时总共的指令数,如果这一位你的结果是0,那么请回到上一节排查问题 我们尝试将第一个字符改成f

1
2
3
4
5
$ perf stat -x : -e instructions:u ./future_fun
Give the key, if you think you are worthy.

f111
16149892::instructions:u:7950522:100.00::

可以发现指令数的确显著变大了,继续测试

1
2
3
4
5
$ perf stat -x : -e instructions:u ./future_fun
Give the key, if you think you are worthy.

fl11
24123750::instructions:u:9684953:100.00::

结果的确如预想一样,当一个字符对应上后指令数会显著增大

所以我们可以尝试逐字符输入,每个字符处分别尝试所有的可打印字符,并统计指令数,将最大的一个提取出来,最后可以写出一份合适的exp,最后得到以下输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
f
fl
fla
flag
flag{
flag{g
flag{g0
flag{g00
flag{g00d
flag{g00d_
flag{g00d_t
flag{g00d_th
flag{g00d_th1
flag{g00d_th1n
flag{g00d_th1ng
flag{g00d_th1ng5
flag{g00d_th1ng5_
flag{g00d_th1ng5_f
flag{g00d_th1ng5_f0
flag{g00d_th1ng5_f0r_
flag{g00d_th1ng5_f0r_w
flag{g00d_th1ng5_f0r_w4
flag{g00d_th1ng5_f0r_w41
flag{g00d_th1ng5_f0r_w41t
flag{g00d_th1ng5_f0r_w41ti
flag{g00d_th1ng5_f0r_w41tin
flag{g00d_th1ng5_f0r_w41ting
flag{g00d_th1ng5_f0r_w41ting}

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from subprocess import *
import string
import sys

command = "perf stat -x : -e instructions:u " + sys.argv[1] + " 1>/dev/null"
flag = ''
while True:
    ins_count = 0
    count_chr = ''
    for i in (string.ascii_letters + string.digits + string.punctuation):
        target = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT, shell=True)
        target_output, _ = target.communicate(input=('%s\n'%(flag + i)).encode())
        instructions = int(target_output.decode().split(':')[0])
        if instructions > ins_count:
            count_chr = i
            ins_count = instructions
    flag += count_chr
    print(flag)

最后的垃圾话

这种打法还是太超模了,很大一部分简单的 re 题目就这一个exp都能差不多解决了,但是要是加上一些长度检验之类的防范措施,就得再手动修改下exp了

这篇文章还是受 guyinatuxedo 的启发,但是他的文章内容是错的,至少perf部分有问题,不要错把运行时间与指令数搞混淆

如果不使用 instructions:u 而去看运行时间,理论上也能出,但是受限于运行时环境的稳定性,可能需要多次计算取平均

Licensed under CC BY-NC-SA 4.0