本文作者:Jeb(信安之路读者首次投稿)
这次打 qctf,做到了一个 ollvm,控制流平坦化的题,虽然不是很明白原理(但这么叫感觉很 6 批)。听师傅们说可以用 pin 解决,于是先学习一下 pin 在 ctf 中的应用,为解决 olvm 铺路。
具体的 pin 和 pintool 我就不说了
首先看到这个文件 700+k,一看就不好分析,nm 提示内存分配太多?,IDA 打开,提示各种错误,不多我还是强行将其打开了。搜索字符串,无果。此题可能得靠天!
好了,是时候拿出利器 pin 了。
这里尝试用最简单的 pintool,inscount0.so,使用方法如下:
make obj-intel64/inscount0.so TARGET=intel64
编译生成 64 位的 pintool
make obj-ia32/inscount0.so
编译生成 32 位的 pintool
pin -t your_pintool -- your_binary <arg>
使用基本命令
我修改了 inscount0.cpp
使其能在执行完成后,将输出到终端上。
对于指令数来说,最简单的猜想就是会不会和输入的长度以及输入的字符有关,首先尝试输入不同长度的字符串。确实是存在规律的。
代码如下:
importsubprocess
importos
importlogging
importjson
logging.basicConfig(level=logging.INFO)
logger= logging.getLogger(__name__)
# js = json.dumps(ssst, sort_keys=True, indent=4, separators=(',', ':'))# format json output
classshell(object):
defrunCmd(self, cmd):
res= subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
sout, serr= res.communicate()
returnres.returncode, sout, serr, res.pid
definitPin(self, cmd):
res= subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.res= res
defpinWrite(self, input):
self.res.stdin.write(input)
defpinRun(self):
sout, serr= self.res.communicate()
returnsout, serr
filename= "/home/jeb/Documents/pin-in-CTF/examples/NDH2k13-crackme-500/crackme"
cmd= "/opt/pin-3.7-97619-g0d0c92f4f-gcc-linux/pin -t "+\
"/opt/pin-3.7-97619-g0d0c92f4f-gcc-linux/source/tools/ManualExamples/obj-intel64/inscount0.so"+" -- "+filename
# print shell.runCmd(cmd)
cout_old= 0
# for i in range(30):
# res = subprocess.Popen(cmd,shell=True,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
# res.stdin.write("a"*i+'\n')
# sout,serr = res.communicate()
# cout = sout.split("Count ")[1]
# cout_sub= int(cout) - cout_old
# cout_old = int(cout)
# print ("current len ", i,"current count:",cout,"sub_count ",cout_sub)
shell= shell()
shell.initPin(cmd)
cout_old=0
foriinrange(30):
shell.initPin(cmd)
shell.pinWrite("a"*i)
sout,serr= shell.pinRun()
cout= sout.split("Count ")[1]
cout_sub= int(cout) -cout_old
cout_old= int(cout)
print("current len ", i,"current count:",cout,"sub_count ",cout_sub)
发现在 len 为 8 时,指令数出现了跃变,因此判断 flag 的长度为 8 位。继续探究。
知道了长度之后,尝试使用不同的字符,首先遍历第一个字符,发现在输入 A 时指令数量会出现突变,所以根据这点进行逐字节爆破。
代码如下:
cur=''
foriinrange(8):
forsindic:
shell.initPin(cmd)
pwd= cur+s+'?'*(7-len(cur))
printpwd
shell.pinWrite(pwd+'\n')
sout,serr= shell.pinRun()
cout= sout.split("Count ")[1]
cout_sub= int(cout) -cout_old
cout_old= int(cout)
ifcout_sub>2000andcout_sub<10000:
cur=cur+s
break
print("current cur ", cur,"current count:",cout,"sub_count ",cout_sub)
最终得到 flag 为AzI0wBsX
我想通过这题应该对 pin 以及 pintool 有了大致的了解,接下来看下一题
首先还是基本的识别一下程序,1002k 也是蛮大,IDA 识别正常,就是打开有点慢,我的 x1c 也老了啊!除去了符号表,并且使用了静态编译。还是先使用长度进行测试,很无奈,指令数没有任何规律可循。
刚好 IDA 已经分析完成,可以看到这两个段, 这是 go 语言的特征。知道了这一点,我们想办法回复符号表,由于 Go 语言将信息存放在.gopclntab section
中, 阅读下面的文章:
https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/
可以帮助你初步的了解 Go 语言编写的 ELF 程序该如何进行逆向。文章写的很长,完整看下来确实很困难,我就简单说下自己的理解。
首先 Go 语言编写的 ELF 程序是小端序,但是我们却搜索不到程序中出现的字符串信息,这是由于 Go 使用的是拆分 + 小端序的方式进行存储的。Go 会将其符号信息存放在.gopclntab section
中,主函数是main_main
,并且通过runtime_morestack_noctext
机制进行回调,我们可以通过原文中提供的脚本进行清洗.此脚本在 ida6.8 中可以运行。
清洗完成后便可以较为清洗的看到函数符号。
重新搜索字符串,定位到Nope.
好像没什么收获。
可以看到它是通过3次mov
传递的字符串
loc_47b998
为正确的分支,loc_47ba23
为错误分支,向上回溯,寻找 cmp 指令,在主函数中找到以下几处,挨个查看,从而可以理清程序的判断逻辑。
首先判断命令行参数,其次判断输入的长度是否为 0x2a,之后循环判断输入的每个字符是否符合要求,它采用的是逐位判断,众所周知rcx
是用来存放循环计数的,这里也不例外
类似的c
代码如下:
for(i=0;i<len(input);i++){
if(main_mapanic(input[i])!=const_array[i]){
printf('nope');
exit();
}
}
print('correct');
按理来说我们应该逆向分析main_mapanic
函数,并且动态调试,从内存中dump
出const_array
,但是我们大可不必如此做,因为每一次循环必定带来指令数的递增,这不正是使用 pin 的绝佳场合嘛!?
但是仅仅使用原来的 pintool 还远远不够,为了更好的解决问题,我们必须学会对 pintool 进行调整,在之前的分析中我们已经确定.text:000000000047B921 cmp rdx, rax
是对输入进行判断的位置,因此我们只需统计该条指令运行的次数即可确定我们的flag
是否正确,因此调整inscount0.cpp
,有关的调整方法可以参考:
http://www.ic.unicamp.br/~rodolfo/mo801/04-PinTutorial.pdf
中的pintool2
的itrace
,重新编译生成 pintool,再次进行测试。
ok!事已至此,我们复用上一题的脚本,就可以跑出 flag。
代码如下:
dic= string.letters+'_{}'+string.digits
cur='hxp{'
shell= shell()
cout_old=5
start_time= time.time()
foriinrange(0x27):
forsindic:
pwd= cur+s+'?'*(0x29-len(cur))
printlen(pwd)
rcmd= cmd+' '+pwd
shell.initPin(rcmd)
sout,serr= shell.pinRun()
cout= sout.split("Count ")[1]
cout_sub= int(cout) -cout_old
cout_old= int(cout)
ifcout_sub== 1:
cur=cur+s
print("current flag ", pwd,"current count:",cout,"sub_count ",cout_sub)
end_time=time.time()
times= end_time-start_time
print"need times :",times,'s'
据说使用inscount1.cpp
运行起来更快,有兴趣的可以自行去尝试。
至此,我们已经学会了使用 pintool,并且加以调整,接下来让我们更进一步。
不过感觉这题可以用angr
解,都是符号执行,不过我也没有尝试。
ISCC-2018-re250
这题代码的逻辑很清晰。
将 flag fencode
得到 v6,在encode
得到 s1,最终同lUFBuT7hADvItXEGn7KgTEjqw8U5VQUq
进行比较。但是当我们点开fencode
和encode
时就有点不知所措。其实题目使用了ollvm
的控制流平坦化。
不过不用很害怕,我们先简单学习一下什么是控制流平坦化,其实就是打破原有的代码块之间的联系,通过一个分发器进行控制。
查看fencode
函数不过 15 个分支,并不算复杂,而且代码中的也还算清晰,因此我直接尝试还原c
代码。
以下是简单的分析:emmm 写着写着忘记保存,丢了一部分内容下面就简写了。
手动的跟一遍 fencode 和 encode 的控制流,可以得到伪 C 代码,大致上 fencode 是一个矩阵乘法,encode 是 base64,但我忘了怎么求矩阵了,所以直接引用的夜影师傅的脚本。
importnumpy
table= "FeVYKw6a0lDIOsnZQ5EAf2MvjS1GUiLWPTtH4JqRgu3dbC8hrcNo9/mxzpXBky7+"
s= "lUFBuT7hADvItXEGn7KgTEjqw8U5VQUq"
defdecode(base64_str):
base64_bytes= ['{:0>6}'.format(str(bin(table.index(s))).replace('0b', '')) forsinbase64_str]
resp= []#bytearray()
nums= len(base64_bytes) //4
remain= len(base64_bytes) %4
integral_part= base64_bytes[0:4*nums]
whileintegral_part:
# 取4个6位base64字符,作为3个字节
tmp_unit= ''.join(integral_part[0:4])
tmp_unit= [int(tmp_unit[x: x+8], 2) forxin[0, 8, 16]]
foriintmp_unit:
resp.append(i)
integral_part= integral_part[4:]
ifremain:
remain_part= ''.join(base64_bytes[nums*4:])
tmp_unit= [int(remain_part[i*8:(i+1) *8], 2) foriinrange(remain-1)]
foriintmp_unit:
resp.append(i)
returnresp
n= decode(s)
print(n)
m= [2, 2, 4, -5, 1, 1, 3, -3, -1, -2, -3, 4, -1, 0, -2, 2]
a= numpy.mat([n[4*i:4*i+4] foriinrange(6)])
b= numpy.mat([m[4*i:4*i+4] foriinrange(4)])
b= b.T.I
flag= (a*b).A
print(flag)
foriinrange(24):
print(chr((int(flag[i//4][i%4]+0.5)%256)), end='')
pin 在此题中的作用,可能也仅限于求出 flag 的长度,对解题没有什么实质性的帮助。
曾写过清洗控制流平坦化的脚本,但是由于 angr 和 barf 的版本更新,导致部分 api 不可用,所以还是有点难受的,参考文章:
https://security.tencent.com/index.php/blog/msg/112
首先这题加了 upx 壳,很简单的脱出。IDA 打开发现使用了movfuscator
, github 上有相应的 demovfuscator 项目,但是环境搭建太麻烦,所以我没弄。
这里使用 pin 来解决此问题。
这道题我并没有弄懂,它的 pintool 为什么需要这么写,mov 的one-bit-writes
又有何含义?
虽然管不了这么多,但是解决方法还是需要记录一下。参考:
https://github.com/TeamContagion/CTF-Write-Ups/tree/master/AlexCTF-2017/Reversing/RE5%20-%20Packed%20Movement%20%28350%29
首先我们新建一个tracer.cpp
,同样的make obj-i32/itrace.so
,最后将原有的 itrace.cpp 备份一下,然后将新建的 tracer.cpp 改名为 itrace.cpp,这是为了不违反 make 的规则,也就省的去修改 make.rules 的内容了。
itrace.cpp
#include "pin.H"
#include <fstream>
std::ofstreamTraceFile;
PIN_LOCKlock;
ADDRINTmain_begin;
ADDRINTmain_end;
staticADDRINTWriteAddr;
staticINT32WriteSize;
staticVOIDRecordWriteAddrSize(ADDRINTaddr, INT32size)
{
WriteAddr=addr;
WriteSize=size;
}
staticVOIDRecordMemWrite(ADDRINTip)
{
UINT8memdump[256];
PIN_GetLock(&lock, ip);
PIN_SafeCopy(memdump, (void*)WriteAddr, WriteSize);
if(WriteSize==1)
TraceFile<<static_cast<CHAR>(*memdump);
PIN_ReleaseLock(&lock);
}
VOIDInstruction_cb(INSins, VOID*v)
{
ADDRINTip=INS_Address(ins);
if((ip<main_begin) ||(ip>main_end))
return;
if(INS_IsMemoryWrite(ins))
{
INS_InsertPredicatedCall(
ins, IPOINT_BEFORE, (AFUNPTR)RecordWriteAddrSize,
IARG_MEMORYWRITE_EA,
IARG_MEMORYWRITE_SIZE,
IARG_END);
if(INS_HasFallThrough(ins))
{
INS_InsertCall(
ins, IPOINT_AFTER, (AFUNPTR)RecordMemWrite,
IARG_INST_PTR,
IARG_END);
}
}
}
voidImageLoad_cb(IMGImg, void*v)
{
PIN_GetLock(&lock, 0);
if(IMG_IsMainExecutable(Img))
{
main_begin=IMG_LowAddress(Img);
main_end=IMG_HighAddress(Img);
}
PIN_ReleaseLock(&lock);
}
VOIDFini(INT32code, VOID*v)
{
TraceFile.close();
}
int main(intargc, char*argv[])
{
PIN_InitSymbols();
PIN_Init(argc,argv);
TraceFile.open("trace-1byte-writes.bin");
if(TraceFile==NULL)
return-1;
IMG_AddInstrumentFunction(ImageLoad_cb, 0);
INS_AddInstrumentFunction(Instruction_cb, 0);
PIN_AddFiniFunction(Fini, 0);
PIN_StartProgram();
return0;
}
之后我们便可以进行测试,根据实际情况猜测 flag。
完整脚本如下:
fromstringimportascii_lowercase, digits
importos
allChars= digits+'_}'+ascii_lowercase
flag= 'ALEXCTF{'
wrong= '\x01\x01\x00\x00'
right= '\x00\x00\x01\x00'
case= '\x00\x00\x00\x00'
deftryFlag(f):
os.system('(echo "{}" | ../../../pin -t obj-ia32/tracer.so -- ../../../../move) > /dev/null'.format(f))
data= open('trace-1byte-writes.bin', 'rb').read()
offset= len(f) *4
returndata[offset-4:offset]
whileflag[:-1] != '}':
forcinallChars:
result= tryFlag(flag+c)
ifresult== case:
c= c.upper()
result= tryFlag(flag+c)
ifresult== right:
flag+= c
printflag
break
这题和第一题如出一辙,显示爆破出长度,其次爆破出正确的 flag
shell= shell()
cout_old=0
foriinrange(30):
shell.initPin(cmd)
shell.pinWrite("a"*i)
sout,serr= shell.pinRun()
cout= sout.split("Count ")[1]
cout_sub= int(cout) -cout_old
cout_old= int(cout)
print("current len ", i,"current count:",cout,"sub_count ",cout_sub)
shell= shell()
cout_old=0
dic= string.letters+'_+'+string.digits
cur=''
foriinrange(28):
forsindic:
shell.initPin(cmd)
pwd= cur+s+'?'*(27-len(cur))
printpwd
shell.pinWrite(pwd+'\n')
sout,serr= shell.pinRun()
cout= sout.split("Count ")[1]
cout_sub= int(cout) -cout_old
cout_old= int(cout)
ifcout_sub>2000andcout_sub<20000:
cur=cur+s
break
print("current cur ", cur,"current count:",cout,"sub_count ",cout_sub)
确实没有什么好写的,同时此题也是可以利用 angr 符号执行的。
题目为 32 位,加了 upx 壳,简单脱壳后丢入 IDA,除去了符号表,但是同样的,和上一题同一个思路,甚至程序逻辑都不需要进行分析。
示例代码:
fromsubprocessimportPopen, PIPE
fromsysimportargv
importIPython
importpdb
importstring
pinPath= "/home/m4x/pin-3.6-gcc-linux/pin"
pinInit= lambdatool, elf: Popen([pinPath, '-t', tool, '--', elf], stdin= PIPE, stdout= PIPE)
pinWrite= lambdacont: pin.stdin.write(cont)
pinRead= lambda: pin.communicate()[0]
if__name__== "__main__":
# last = 0
# for i in xrange(1, 50):
# pin = pinInit("./myInscount1.so", "./baleful")
# pinWrite('_' * i)
# now = int(pinRead().split(':')[1])
# print "inputLen({}) -> ins({}) -> delta({})".format(i, now, now - last)
# if now - last > 2000 and last:
# exit()
# last = now
pwd= "_"*30
off= 0
idx= 0
# dic = map(chr, xrange(0x20, 0x80))
dic= map(chr, xrange(94, 123))
last= 0
whileTrue:
pin= pinInit("./myInscount1.so", "./baleful_unpacked")
# if off == 1:
# pdb.set_trace()
pwd= pwd[: off] +dic[idx] +pwd[off+1:]
# print pwd
pinWrite(pwd+'\n')
now= int(pinRead().split(':')[1])
print"input({}) -> ins({}) -> delta({})".format(pwd, now, now-last)
ifnow-last<0:
printpwd
off+= 1
ifoff>= 30:
break
idx= 0
last= 0
continue
idx+= 1
last= now
好了!此次 Pin-in-ctf 的学习差不多到此为止了,也已经为后续做了很多铺垫,希望当你面对一个混淆的程序一头莫展时能想起此种方法。
差不多花了两天时间写了这篇带有总结性的文章,收货很多。同样的感谢 github 上的原作者将其整理。参考的链接太多了。这里是原作者 github 地址:
https://github.com/0x01f/pin-in-CTF