前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Pin-in-CTF 学习整理记录

Pin-in-CTF 学习整理记录

作者头像
信安之路
发布2018-08-08 17:14:10
1.8K1
发布2018-08-08 17:14:10
举报
文章被收录于专栏:信安之路信安之路

本文作者:Jeb(信安之路读者首次投稿)

这次打 qctf,做到了一个 ollvm,控制流平坦化的题,虽然不是很明白原理(但这么叫感觉很 6 批)。听师傅们说可以用 pin 解决,于是先学习一下 pin 在 ctf 中的应用,为解决 olvm 铺路。

应用

具体的 pin 和 pintool 我就不说了

0x0 NDH2k13-crackme-500

首先看到这个文件 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 使其能在执行完成后,将输出到终端上。

对于指令数来说,最简单的猜想就是会不会和输入的长度以及输入的字符有关,首先尝试输入不同长度的字符串。确实是存在规律的。

代码如下:

代码语言:javascript
复制
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 时指令数量会出现突变,所以根据这点进行逐字节爆破。

代码如下:

代码语言:javascript
复制
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 有了大致的了解,接下来看下一题

0x01 hxpCTF-2017-main_strip

首先还是基本的识别一下程序,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代码如下:

代码语言:javascript
复制
for(i=0;i<len(input);i++){
   if(main_mapanic(input[i])!=const_array[i]){
       printf('nope');
       exit();
  }
}
print('correct');

按理来说我们应该逆向分析main_mapanic函数,并且动态调试,从内存中dumpconst_array,但是我们大可不必如此做,因为每一次循环必定带来指令数的递增,这不正是使用 pin 的绝佳场合嘛!?

但是仅仅使用原来的 pintool 还远远不够,为了更好的解决问题,我们必须学会对 pintool 进行调整,在之前的分析中我们已经确定.text:000000000047B921 cmp rdx, rax是对输入进行判断的位置,因此我们只需统计该条指令运行的次数即可确定我们的flag是否正确,因此调整inscount0.cpp,有关的调整方法可以参考:

http://www.ic.unicamp.br/~rodolfo/mo801/04-PinTutorial.pdf

中的pintool2itrace,重新编译生成 pintool,再次进行测试。

ok!事已至此,我们复用上一题的脚本,就可以跑出 flag。

代码如下:

代码语言:javascript
复制
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解,都是符号执行,不过我也没有尝试。

0x02ISCC-2018-re250

这题代码的逻辑很清晰。

将 flag fencode得到 v6,在encode得到 s1,最终同lUFBuT7hADvItXEGn7KgTEjqw8U5VQUq进行比较。但是当我们点开fencodeencode时就有点不知所措。其实题目使用了ollvm的控制流平坦化。

不过不用很害怕,我们先简单学习一下什么是控制流平坦化,其实就是打破原有的代码块之间的联系,通过一个分发器进行控制。

查看fencode函数不过 15 个分支,并不算复杂,而且代码中的也还算清晰,因此我直接尝试还原c代码。

以下是简单的分析:emmm 写着写着忘记保存,丢了一部分内容下面就简写了。

手动的跟一遍 fencode 和 encode 的控制流,可以得到伪 C 代码,大致上 fencode 是一个矩阵乘法,encode 是 base64,但我忘了怎么求矩阵了,所以直接引用的夜影师傅的脚本。

代码语言:javascript
复制
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

0x03 AlexCTF-2017-move-350

首先这题加了 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

代码语言:javascript
复制
#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。

完整脚本如下:

代码语言:javascript
复制
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
0x04 csaw-2015-wyvern-500

这题和第一题如出一辙,显示爆破出长度,其次爆破出正确的 flag

代码语言:javascript
复制
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)
代码语言:javascript
复制
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 符号执行的。

0x05 picoctf-2014-baleful-200

题目为 32 位,加了 upx 壳,简单脱壳后丢入 IDA,除去了符号表,但是同样的,和上一题同一个思路,甚至程序逻辑都不需要进行分析。

示例代码:

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 应用
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档