Pin-in-ctf 学习分析

前言

这次打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函数,并且动态调试,从内存中dumpconst_array,但是我们大可不必如此做,因为每一次循环必定带来指令数的递增,这不正是使用pin的绝佳场合嘛!?
但是仅仅使用原来的pintool还远远不够,为了更好的解决问题,我们必须学会对pintool进行调整,在之前的分析中我们已经确定.text:000000000047B921 cmp rdx, rax是对输入进行判断的位置,因此我们只需统计该条指令运行的次数即可确定我们的flag是否正确,因此调整inscount0.cpp,有关的调整方法可以参考this中的pintool2itrace,重新编译生成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进行比较。但是当我们点开fencodeencode时就有点不知所措。其实题目使用了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地址。

猜你喜欢

转载自blog.csdn.net/qq_33438733/article/details/81108186
今日推荐