前言
这次打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使其能在执行完成后,将输出到终端上。
对于指令数来说,最简单的猜想就是会不会和输入的长度以及输入的字符有关,首先尝试输入不同长度的字符串。确实是存在规律的。
代码如下:
import subprocess
import os
import logging
import json
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# js = json.dumps(ssst, sort_keys=True, indent=4, separators=(',', ':'))# format json output
class shell(object):
def runCmd(self, cmd):
res = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
sout, serr = res.communicate()
return res.returncode, sout, serr, res.pid
def initPin(self, cmd):
res = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.res = res
def pinWrite(self, input):
self.res.stdin.write(input)
def pinRun(self):
sout, serr = self.res.communicate()
return sout, 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
for i in range(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=''
for i in range(8):
for s in dic:
shell.initPin(cmd)
pwd = cur+s+'?'*(7-len(cur))
print pwd
shell.pinWrite(pwd+'\n')
sout,serr = shell.pinRun()
cout = sout.split("Count ")[1]
cout_sub= int(cout) - cout_old
cout_old = int(cout)
if cout_sub > 2000 and cout_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
中
阅读这篇文章可以帮助你初步的了解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
,有关的调整方法可以参考this中的pintool2
的itrace
,重新编译生成pintool,再次进行测试。
ok!事已至此,我们复用上一题的脚本,就可以跑出flag。
代码如下:
dic = string.letters+'_{}'+string.digits
cur='hxp{'
shell = shell()
cout_old=5
start_time = time.time()
for i in range(0x27):
for s in dic:
pwd = cur+s+'?'*(0x29-len(cur))
print len(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)
if cout_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
解,都是符号执行,不过我也没有尝试。
0x02
ISCC-2018-re250
这题代码的逻辑很清晰。
将flag fencode
得到v6,在encode
得到s1,最终同lUFBuT7hADvItXEGn7KgTEjqw8U5VQUq
进行比较。但是当我们点开fencode
和encode
时就有点不知所措。其实题目使用了ollvm
的控制流平坦化。
不过不用很害怕,我们先简单学习一下什么是控制流平坦化,其实就是打破原有的代码块之间的联系,通过一个分发器进行控制。查看fencode
函数不过15个分支,并不算复杂,而且代码中的也还算清晰,因此我直接尝试还原c
代码。
以下是简单的分析:emmm 写着写着忘记保存,丢了一部分内容
下面就简写了。
手动的跟一遍fencode和encode的控制流,可以得到伪C代码,大致上fencode是一个矩阵乘法,encode是base64,但我忘了怎么求矩阵了,所以直接引用的夜影师傅的脚本。
import numpy
table = "FeVYKw6a0lDIOsnZQ5EAf2MvjS1GUiLWPTtH4JqRgu3dbC8hrcNo9/mxzpXBky7+"
s = "lUFBuT7hADvItXEGn7KgTEjqw8U5VQUq"
def decode(base64_str):
base64_bytes = ['{:0>6}'.format(str(bin(table.index(s))).replace('0b', '')) for s in base64_str]
resp = []#bytearray()
nums = len(base64_bytes) // 4
remain = len(base64_bytes) % 4
integral_part = base64_bytes[0:4 * nums]
while integral_part:
# 取4个6位base64字符,作为3个字节
tmp_unit = ''.join(integral_part[0:4])
tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
for i in tmp_unit:
resp.append(i)
integral_part = integral_part[4:]
if remain:
remain_part = ''.join(base64_bytes[nums * 4:])
tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
for i in tmp_unit:
resp.append(i)
return resp
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] for i in range(6)])
b = numpy.mat([m[4*i:4*i+4] for i in range(4)])
b = b.T.I
flag = (a*b).A
print(flag)
for i in range(24):
print(chr((int(flag[i//4][i%4]+0.5)%256)), end='')
pin在此题中的作用,可能也仅限于求出flag的长度,对解题没有什么实质性的帮助。
腾讯实验室曾写过清洗控制流平坦化的脚本,但是由于angr和barf的版本更新,导致部分api不可用,所以还是有点难受的。
0x03
AlexCTF-2017-move-350
首先这题加了upx壳,很简单的脱出。IDA打开发现使用了movfuscator
,github上有相应的demovfuscator项目,但是环境搭建太麻烦,所以我没弄。
这里使用pin来解决此问题。
这道题我并没有弄懂,它的pintool为什么需要这么写,mov的one-bit-writes
又有何含义?
虽然管不了这么多,但是解决方法还是需要记录一下。参考
首先我们新建一个tracer.cpp
,同样的make obj-i32/itrace.so
,最后将原有的itrace.cpp备份一下,然后将新建的tracer.cpp改名为itrace.cpp,这是为了不违反make的规则,也就省的去修改make.rules的内容了。
itrace.cpp
#include "pin.H"
#include <fstream>
std::ofstream TraceFile;
PIN_LOCK lock;
ADDRINT main_begin;
ADDRINT main_end;
static ADDRINT WriteAddr;
static INT32 WriteSize;
static VOID RecordWriteAddrSize(ADDRINT addr, INT32 size)
{
WriteAddr = addr;
WriteSize = size;
}
static VOID RecordMemWrite(ADDRINT ip)
{
UINT8 memdump[256];
PIN_GetLock(&lock, ip);
PIN_SafeCopy(memdump, (void *)WriteAddr, WriteSize);
if (WriteSize==1)
TraceFile << static_cast<CHAR>(*memdump);
PIN_ReleaseLock(&lock);
}
VOID Instruction_cb(INS ins, VOID *v)
{
ADDRINT ip = 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);
}
}
}
void ImageLoad_cb(IMG Img, void *v)
{
PIN_GetLock(&lock, 0);
if(IMG_IsMainExecutable(Img))
{
main_begin = IMG_LowAddress(Img);
main_end = IMG_HighAddress(Img);
}
PIN_ReleaseLock(&lock);
}
VOID Fini(INT32 code, VOID *v)
{
TraceFile.close();
}
int main(int argc, 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();
return 0;
}
之后我们便可以进行测试,根据实际情况猜测flag。
完整脚本如下:
from string import ascii_lowercase, digits
import os
allChars = digits + '_}' + ascii_lowercase
flag = 'ALEXCTF{'
wrong = '\x01\x01\x00\x00'
right = '\x00\x00\x01\x00'
case = '\x00\x00\x00\x00'
def tryFlag(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
return data[offset - 4:offset]
while flag[:-1] != '}':
for c in allChars:
result = tryFlag(flag + c)
if result == case:
c = c.upper()
result = tryFlag(flag + c)
if result == right:
flag += c
print flag
break
0x04
csaw-2015-wyvern-500
这题和第一题如出一辙,显示爆破出长度,其次爆破出正确的flag
shell = shell()
cout_old=0
for i in range(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=''
for i in range(28):
for s in dic:
shell.initPin(cmd)
pwd = cur+s+'?'*(27-len(cur))
print pwd
shell.pinWrite(pwd+'\n')
sout,serr = shell.pinRun()
cout = sout.split("Count ")[1]
cout_sub= int(cout) - cout_old
cout_old = int(cout)
if cout_sub > 2000 and cout_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,除去了符号表,但是同样的,和上一题同一个思路,甚至程序逻辑都不需要进行分析。
示例代码:
from subprocess import Popen, PIPE
from sys import argv
import IPython
import pdb
import string
pinPath = "/home/m4x/pin-3.6-gcc-linux/pin"
pinInit = lambda tool, elf: Popen([pinPath, '-t', tool, '--', elf], stdin = PIPE, stdout = PIPE)
pinWrite = lambda cont: 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
while True:
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)
if now - last < 0:
print pwd
off += 1
if off >= 30:
break
idx = 0
last = 0
continue
idx += 1
last = now
好了!此次Pin-in-ctf的学习差不多到此为止了,也已经为后续做了很多铺垫,希望当你面对一个混淆的程序一头莫展时能想起此种方法。
总结
差不多花了两天时间写了这篇带有总结性的文章,收货很多。同样的感谢github上的原作者将其整理。参考的链接太多了。这里是原作者github地址。