以2021SCTF的Low-RE为主,初探pintool技术,也对RE手段有了新的认识
IntelPin的安装
Pin 是 Intel 公司研发的一个动态二进制插桩框架,可以在二进制程序运行过程中插入各种函数,以监控程序每一步的执行。
条件准备
1 2 3 4 5 6 7 8 9 1 、Visual Studio Community 2019 Edition2 、Cygwin' s 64 -bit 3 、Intel Pin
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 "F:\visual studio2019\VC\Auxiliary\Build\vcvars64.bat" make all 或 make TARGET=intel64 x86 pushd D:\pindir\pin\source\tools\ManualExamples "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) { OutFile.setf(ios::showbase); OutFile << "Count " << icount << endl ; std ::cout <<"Count " << icount << endl ; OutFile.close(); }
根据hash的条数来看输入的位数不会太多,先用mycount爆破一下不同输入位数,因为程序一般都会检测一下输入的长度,如果长度正确反馈的指令数也会更多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from subprocess import Popen,PIPEfrom sys import argvimport stringpinInit = 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,PIPEfrom sys import argvimport stringpinInit = 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 ('Count(%s) : %d' %(i,_count))
如下图,第一个字符为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,PIPEfrom sys import argvimport threadingimport queueimport stringpinInit = 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 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) 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 )
在跑到最后一个字符时会有些反常,可以单独再爆破或者根据大意猜出为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) { 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