pin-in-CTF
十几个月前,在学习 fuzz 时,我接触了 intel-pin 这个动态插桩工具,当时发现对于一些 CTF reverse 题目,尤其是代码混淆比较严重的题目,可以编写 pintool 统计指令数等信息,多快好省的通过侧信道的方法逐位爆破出 flag,详见 pin-in-ctf: blog, pin-in-ctf: repo。
但这种方法缺点也很多:
- 动态插桩使得程序的 runtime overhead 成倍增长(~%100)
- 对于比较复杂的程序,尤其是系统调用较多的程序,内核态指令数会占总指令数的很大一部分,影响用户判断
- pintool 编写相对复杂
- 只能对用户态的程序插桩,对内核态程序插桩需要较大的改动
intel-pt and perf
半年前在做毕设时,我接触了 intel-pt 这个硬件级别的 trace 工具,其同样具有记录程序指令的功能,并且由于直接在硬件层面实现,runtime overhead 可以缩小到惊人的 5%,同时通过用户态的系统分析工具 perf,用户也可以相当方便的调用 intel pt 的某些功能。而 perf 也就是本篇文章要介绍的主角。
前几个月的 oGeek 线上赛中,我有一道 pwn 一道 reverse 的出题任务,本来想打算用 intel-pt 的特性出一道逆向题,但因为时间紧张,构思尚不成熟,不想浪费想法,最终出了另外一道 NSIS script 的题目 King of KoF。在昨天的 ASIS Final 2019 的比赛中,遇到了一道题目 Cursed app,分析之后觉得使用 perf 解决十分合适,于是终于有了分享 perf-in-ctf 这个想法(并打理要长草的博客)的机会,接下来进入正文。
perf
perf 是 Linux 中分析性能的工具,具有很多强大的功能,详见 linux perf turtorial。本篇文章只对其统计指令数的用法进行介绍。
首先需要安装,以 ubuntu 18.04 为例,直接使用包管理安装:
sudo apt install linux-tools-common
查看功能简介:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
~ perf --version
perf version 5.0.21
~ tldr perf
perf
Framework for linux performance counter measurements.
- Display basic performance counter stats for a command:
perf stat gcc hello.c
- Display system-wide real time performance counter profile:
sudo perf top
- Run a command and record its profile into "perf.data":
sudo perf record command
- Read "perf.data" (created by perf record) and display the profile:
sudo perf report
|
这里只对 perf stat 功能进行介绍,参考 tldr 的例子,对 id 这个命令进行性能分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
~ sudo perf stat id
uid=0(root) gid=0(root) groups=0(root)
Performance counter stats for 'id':
1.71 msec task-clock # 0.710 CPUs utilized
1 context-switches # 0.584 K/sec
0 cpu-migrations # 0.000 K/sec
122 page-faults # 0.071 M/sec
6,244,401 cycles # 3.647 GHz
2,353,996 instructions # 0.38 insn per cycle
459,910 branches # 268.614 M/sec
18,201 branch-misses # 3.96% of all branches
0.002412965 seconds time elapsed
0.000000000 seconds user
0.002342000 seconds sys
~
|
其中有指令数这个结果
1
2
3
|
...
2,353,996 instructions # 0.38 insn per cycle
...
|
通过格式化输出简化显示
1
2
3
4
5
6
7
8
9
10
11
|
~ sudo perf stat -x : id
uid=0(root) gid=0(root) groups=0(root)
1.17:msec:task-clock:1167180:100.00:0.721:CPUs utilized
0::context-switches:1167180:100.00:0.000:K/sec
0::cpu-migrations:1167180:100.00:0.000:K/sec
121::page-faults:1167180:100.00:0.104:M/sec
4243048::cycles:1208711:100.00:3.635:GHz
2388158::instructions:1208711:100.00:0.56:insn per cycle
461917::branches:1208711:100.00:395.755:M/sec
18229::branch-misses:1208711:100.00:3.95:of all branches
~
|
只保留 instructions 这一条结果
1
2
|
~ sudo perf stat -x : -e instructions id 1>/dev/null
2359978::instructions:1819500:100.00::
|
只保留用户态的结果
1
2
|
~ sudo perf stat -x : -e instructions:u id 1>/dev/null
716210::instructions:u:1532634:100.00::
|
看到这,看过 pin-in-CTF 的朋友可能已经知道怎么做了,接下来通过 ASIS Final 2019 的 Cursed app 这个题目进行举例。
perf-in-CTF
binary here
64 位 linux 程序,通过读文件验证结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Desktop check ./cursed_app.elf
./cursed_app.elf: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, BuildID[sha1]=7c0031d8c259457ca7f3bab1c5a5f40269167b5f, stripped
[*] '/home/m4x/Desktop/cursed_app.elf'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Desktop ./cursed_app.elf
please locate license file, run ./app license_key
Desktop echo "ASIS{" > flag && ltrace ./cursed_app.elf ./flag
_setjmp(0x7ffc7e160590, 0x7ffc7e160758, 0x7ffc7e160770, 0x559e6789dbb0) = 0
fopen("./flag", "r") = 0x559e68a2d260
fseek(0x559e68a2d260, 0, 2, 0) = 0
ftell(0x559e68a2d260, 0x559e68a2d340, 0, 6) = 6
fseek(0x559e68a2d260, 0, 0, 0) = 0
malloc(6) = 0x559e68a2e4a0
fread(0x559e68a2e4a0, 1, 6, 0x559e68a2d260) = 6
fclose(0x559e68a2d260) = 0
puts("Bummer! You got the wrong flag!!"...Bummer! You got the wrong flag!!
) = 33
+++ exited (status 0) +++
|
根据题目描述
1
2
3
4
|
Description:
If we want to say 100, should we start at 1 and count until 100?
sha256(flag) = dd791346264fc02ddcd2466b9e8b2b2bba8057d61fbbc824d25c7957c127250d
|
已经可以猜到没必要逆清楚程序的整个逻辑。
使用 IDA 分析程序,发现程序的逻辑很简单
读取文件内容后有多个判断,都通过才会执行到
1
2
3
|
.text:0000000000001F3B lea rdi, s ; "Congratz! You got the correct flag!!"
.text:0000000000001F42 call _puts
.text:0000000000001F47 mov [rsp+0F8h+var_F4]
|
简单调试了几个判断,发现是对文件内容的逐字节判断,因此必然每次输入的正确 flag 长度越长,程序执行的指令数就越多,可以利用这个特性用侧信道的方法得到 flag。
先使用 perf 验证可行性,已经知道 flag 格式为 ASIS{this_is_flag}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Desktop echo -n "0" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167612::instructions:u:1000300:100.00::
Desktop echo -n "1" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167614::instructions:u:1237218:100.00::
Desktop echo -n "2" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167614::instructions:u:967846:100.00::
Desktop echo -n "A" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167628::instructions:u:934691:100.00::
Desktop echo -n "A0" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167631::instructions:u:1034616:100.00::
Desktop echo -n "A1" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167629::instructions:u:1019846:100.00::
Desktop echo -n "A2" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167630::instructions:u:504064:100.00::
Desktop echo -n "AS" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167648::instructions:u:955516:100.00::
Desktop echo -n "AS0" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167645::instructions:u:914409:100.00::
Desktop echo -n "AS1" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167647::instructions:u:2748495:100.00::
Desktop echo -n "AS2" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167646::instructions:u:677381:100.00::
Desktop echo -n "ASI" > ./license && perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167664::instructions:u:564774:100.00::
|
可以看到,当我们猜到下一位正确的 flag 时,指令数会有较为明显的增长(这里只保留用户态的 instructions 提高稳定性)。
把上述过程封装为脚本,运行即可得到 flag
exploit here
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
29
30
31
32
33
34
35
36
37
38
39
40
41
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ASIS{y0u_c4N_s33_7h15_15_34513R_7h4n_Y0u_7h1nk_r16h7?__!!!}
from subprocess import *
import string
import sys
command = "perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null"
flag = 'ASIS{'
def write_file(cont):
with open("./license", "wb") as f:
f.write(cont)
write_file(flag)
'''
Desktop cat license
ASIS{
Desktop perf stat -x : -e instructions:u ./cursed_app.elf ./license 1>/dev/null
167689::instructions:u:377798:100.00::
'''
ins_count = 167689
while True:
delta = 0
count_chr = ''
for i in string.printable:
write_file(flag + i)
target = Popen(command, stdout=PIPE, stdin=PIPE, stderr=STDOUT, shell=True)
target_output = target.communicate()
# import pdb;pdb.set_trace()
# print(target_output)
instructions = int(target_output[0].split(':')[0])
if ins_count + 20 > instructions > ins_count + 10:
count_chr = i
delta = instructions - ins_count
ins_count = instructions
break
flag += count_chr
print(delta, flag)
|
整个过程耗时不到 1 分钟,远远快于使用 pintool 的方法。
这里使用了统计用户态总指令数这种相对粗糙的方法,有兴趣的同学还可以试试精确的只统计某种 pt packet(tnt, tip 等)数目来达到更为精准的结果。
summary
pin-in-CTF is dead, I prefer perf.
使用 perf 来解决这类问题有以下优点:
- 基于硬件,runtime overhead < 5%
- 可以通过只保留用户态指令或者只保留某种特定的 pt 数据包来达到更精确的效果
- 工具较为成熟,直接使用即可
- intel pt 本身就是内核模块,可以用于内核态
前文简单的展示了如何使用 perf 这个工具解决一部分 CTF reverse challenge,但也仅仅展现了 perf/intel pt 这个强大工具的冰山一角,更多的高级用法和 trick 还需要大家一起学习和脑洞。
reference