以2021SCTF的Low-RE为主,初探pintool技术,也对RE手段有了新的认识

IntelPin的安装

Pin 是 Intel 公司研发的一个动态二进制插桩框架,可以在二进制程序运行过程中插入各种函数,以监控程序每一步的执行。

条件准备

1
2
3
4
5
6
7
8
9
1、Visual Studio Community 2019 Edition

2、Cygwin's 64-bit
//https://cygwin.com/install.html
//需要再安装时选择make gcc-g++的包,非默认

3、Intel Pin
//https://www.intel.com/content/www/us/en/developer/articles/tool/pin-a-binary-instrumentation-tool-downloads.html
//版本可以在3.18 - 3.20 或 3.11-3.13 有些对应版本的工具已经被编译为dll在git上

VS

不过要记录VS的vcvars32/64.bat的存放路径,找到VS存放的位置,例如。

“F:\visual studio2019\VC\Auxiliary\Build”
build文件夹下有这两个bat文件

Cygwin

Cygwin是能在windows环境下执行linux的指令,不过make,gcc,g++等指令要自己下载。

view 选择FULL 搜索要安装的gcc-g++ 和 make 即可,小箭头选着版本,skip即跳过。

下载完成后,将’D:\Cygwin\bin’添加到环境变量。

cygwin安装新包

1
2
3
4
5
1、重新运行安装程序

2、类似apt-get

apt-cyg install yourPackage

Intel Pin

Intel pin官网下载

版本可以选择3.18-3.20 / 3.10-3.13 有现成的工具

完成下载后,将pin.exe所在的目录添加到环境变量。

例如: “D:\pindir\pin”

完成以上操作后,需要对pin\source\tools\ManualExamples的文件进行编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
x64

pushd D:\pindir\pin\source\tools\ManualExamples
//选择对应位的bat文件

"F:\visual studio2019\VC\Auxiliary\Build\vcvars64.bat"

make all 或 make TARGET=intel64 //等待即可



x86

pushd D:\pindir\pin\source\tools\ManualExamples
//选择对应位的bat文件

"F:\visual studio2019\VC\Auxiliary\Build\vcvars64.bat"

make all 或 make TARGET=ia32 //等待即可

编译成功后,会在ManualExamples目录下生成两个目录,里面的dll 文件即用到的pintools。

使用语法
pin -t inscount0.dll – test.exe 两个-

单独把pintool的dll文件放到与目标PE文件同目录下,打开cmd输入指令即可。

完成上述操作后,Intel Pin的一些常用pindll即可自由使用。

有关pin的更多知识详见:https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/5.2.1_pin.html#pin-在-ctf-中的应用

Pin在CTF中的使用

根据做题经验,往往加密后flag的check是逐个比对,也就是较接近明文的输入执行的指令数目越多,或用比较次数来反映,根据这一特性,有了pin的inscount的辅助,我们便能通过反馈的指令执行数目来爆破flag。

low_re

题目附件在文末。

VM保护壳,64位程序,脱起壳来就比较麻烦,不过x64dbg能直接定位入口点dump出,但脱壳后的程序拖入IDA的逻辑也看不出如何执行。

strings窗口有线索,有些hash和flag的提示输入,也难猜出加密算法如何,起初尝试动调,虽然了解到是在调用py文件执行加密,并且找到了几个关键函数,但是还是发现不了加密过程。

由此引入一种新的RE方式,静态和动态都无感,就尝试pintool来暴破一下。

爆破输入长度

为了让输出效果更直观一些,修改inscount0.cpp编译出mycount64.dll,(自带的是写入到文件)。

inscount1(BB级插桩) 与 inscount0(ins级插桩) 效果相同,但 inscount1 速度更快,实际解题时可以用 inscount1 代替 inscount0

1
2
3
4
5
6
7
8
VOID Fini(INT32 code, VOID *v)
{
// Write to a file since cout and cerr maybe closed by the application
OutFile.setf(ios::showbase);
OutFile << "Count " << icount << endl;
std::cout<<"Count " << icount << endl;//this 结果输出
OutFile.close();
}

根据hash的条数来看输入的位数不会太多,先用mycount爆破一下不同输入位数,因为程序一般都会检测一下输入的长度,如果长度正确反馈的指令数也会更多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from subprocess import Popen,PIPE
from sys import argv
import string
pinInit = lambda tool,pe: Popen(['pin','-t',tool,'--',pe],stdin=PIPE,stdout=PIPE)
pinWrite = lambda cont : pin.stdin.write(cont)
pinRead = lambda : pin.communicate()[0]
if __name__ == "__main__":
last_count = 0
for i in range(1,30):
pin = pinInit("mycount64","low_re.exe")
pinWrite(b"a"*i+b'\n') # 换行前即输入内容
_count = int(pinRead().split(b"Count ")[1])
print("inputlen({:2d}) -> cout({}) -> delta({})".format(i,_count,_count-last_count))
last_count=_count

可见在输入长度为17时返回的指令数目明显多于其他长度,故flag大致长为17。

爆破输入内容

也是根据反馈指令数目最多来估计为正确字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from subprocess import Popen,PIPE
from sys import argv
import string
pinInit = lambda tool,pe: Popen(['pin','-t',tool,'--',pe],stdin=PIPE,stdout=PIPE)
pinWrite = lambda cont : pin.stdin.write(cont)
pinRead = lambda : pin.communicate()[0]
if __name__ == "__main__":
last_count = 0
for i in string.printable: #从可打印字符中爆破
pin = pinInit("mycount64","low_re.exe")
pinWrite(i.encode()+b'*'*16+b'\n') # 换行前即输入内容
_count = int(pinRead().split(b"Count ")[1])
#print("inputlen({:2d}) -> cout({}) -> delta({})".format(i,_count,_count-last_count))
#last_count=_count
print('Count(%s) : %d'%(i,_count))
# ***************** ->624221257
# S**************** ->659084533
# S1*************** ->699517166

如下图,第一个字符为S时指令数目最多,按照如下思路爆破。

单由主线程爆破会比较慢,初涉时跑了1个多小时,了解多线程后,用python实现多线程的爆破。

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
42
43
44
from subprocess import Popen,PIPE
from sys import argv
import threading
import queue
import string
pinInit = lambda tool,pe: Popen(['pin','-t',tool,'--',pe],stdin=PIPE,stdout=PIPE)
last_count=695980376 #当前字符对应的指令数
flag='S1' #测试的字符

def pintool(s):
pin = pinInit("mycount64", "low_re.exe")
pin.stdin.write(s.encode())
_count = int(pin.communicate()[0].split(b"Count ")[1])
return _count

def boom():
global flag,last_count,Tcount

while not q.empty():
nows=q.get()
s=(flag+nows).ljust(17,'*')+'\n' #换行代表输入
cout=pintool(s)
if cout - last_count > 30000000:#设定值来判定输入是否正确
flag += nows
last_count = cout
print('now_str({}) -> count({})'.format(flag, cout))
q.queue.clear()
return
# q.queue.clear() 清除队列再赋值
def setque(q):
for i in string.printable[:-2]:#
q.put(i)

if __name__ == "__main__":
q = queue.Queue(100)
setque(q)

while len(flag)!=17:
if q.empty():
setque(q)
while threading.active_count()<5: #维持活跃的多线程
t=threading.Thread(target=boom)
#print(threading.active_count())
t.start()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
now_str(S1) -> count(695980376)
now_str(S1d) -> count(735896918)
now_str(S1de) -> count(773007592)
now_str(S1deC) -> count(811103179)
now_str(S1deCh) -> count(852675272)
now_str(S1deCh4) -> count(885489938)
now_str(S1deCh4n) -> count(927208181)
now_str(S1deCh4nn) -> count(965470416)
now_str(S1deCh4nne) -> count(1002501465)
now_str(S1deCh4nnel) -> count(1035433103)
now_str(S1deCh4nnelA) -> count(1074349914)
now_str(S1deCh4nnelAt) -> count(1112107131)
now_str(S1deCh4nnelAtt) -> count(1149152834)
now_str(S1deCh4nnelAtt@) -> count(1192837040)
now_str(S1deCh4nnelAtt@c) -> count(1231986313)
//SCTF{S1deCh4nnelAtt@ck}
/*
hello challanger
please input your flag:
S1deCh4nnelAtt@ck
you are right
Count 1227060385*/

在跑到最后一个字符时会有些反常,可以单独再爆破或者根据大意猜出为attack。

反思:本题有着VM壳,或者如果遇到大量的混淆,在flag长度较短的情况下,pintool无疑是一大利器。

有些程序判断flag正误会有congra或wrong!等提示,返回值在pin.communicate()元组中,也可以用于爆破。

check判断计数

有时正确或者错误的输入在执行指令数上没有较大差别,那么第一种方式就不太适用了,但是程序如果是对输入逐个check的话,并且我们IDA中已知判断代码的地址,我们也能通过改写pintool来计数。

例如:

1
2
3
4
5
6
7
8
9
10
for (int i=0; i<length(provided_flag); i++)
{
if (main_mapanic(provided_flag[i]) != constant_binary_blob[i])
{
bad_boy();
exit();
}
goodboy();
}
//节选自前辈博客

可见是逐个对flag进行比较的,即cmp处,我们可以以此为参考,每当程序执行到cmp一次计数加一,因为如果比对错误程序就会退出,由此可以由count的大小来判断输入的正确性。

同样,对inscount0进行修改并编译成新的pintool。

1
2
3
4
5
6
7
8
9
10
更改前:
VOID docount() { icount++; }
更改后:
VOID docount(void *ip)
{
// .text:000000000047B96E cmp al, cl; #代码比较处
if ((long long int)ip == 0x000000000047B96E)
icount++;
}

编写py脚本对程序进行pintool攻击即可。

最近初涉二进制插桩技术和pintool的简单使用,某些地方可能有错误理解,同时多线程的脚本可能写的有点拉跨,还望师傅们指正。


参考:

[pin install]起初配置参考: https://www.cnblogs.com/mgdzy/p/13644475.html

[pin in ctf]https://m4x.fun/post/pin-in-ctf/

[cpu侧信道]https://www.istt.org.cn/NewsDetail/2672118.html

[多线程脚本编写]https://www.cnblogs.com/franknihao/p/6627857.html

low_re链接:https://pan.baidu.com/s/17-1WMhu9g5scBQ-RO6MO9Q
提取码:uuuu